如何摆脱 C++ 中的非确定性。尝试ADDR_NO_RANDOMIZE,其他事情

How to get rid of nondeterminism in C++. Tried ADDR_NO_RANDOMIZE, other things

提问人:jyelon 提问时间:2/18/2022 最后编辑:jyelon 更新时间:11/11/2023 访问量:159

问:

我有一个不确定的 C++ 程序。我使用输入文件运行它,它运行正常。我第二次使用相同的输入文件运行它,它崩溃了。我想摆脱非确定性,以使崩溃可重现。

我扔了一些 print-statements 来打印某些数据结构的地址。每次执行时,数据结构都位于不同的地址(在 Linux 下)。

malloc 返回不可预测地址的一个明显原因是 ASLR。我把它关掉了。我可以验证它是否关闭 - 共享库始终在相同的地址加载,堆栈始终在同一地址,依此类推。但即使关闭了 ASLR,malloc 仍然不可重现 - 它会在不同的连续运行中返回不同的地址。

我正在绞尽脑汁寻找非确定论的可能来源:

  • 我已经使用“strace”运行了该程序。我可以“比较”两次连续执行的跟踪,除了打印我的数据结构地址(位于不同地址)的 print-statements 之外,没有其他差异。

  • 据我所知,它没有使用线程,除非glibc或C++在幕后使用线程。我确实注意到 ptmalloc 使用了__thread变量......这是否相关?

  • 除默认信号处理程序外,没有其他信号处理程序。我不是故意使用信号。

  • 从理论上讲,glibc 中的某些东西可能会获得一个 CPU 性能计数器,并将其用作非确定性的来源。但我对此持怀疑态度。

有谁知道 malloc 在连续执行时返回不同地址的原因是什么?

更新:

这是我发现的最小的程序,即使关闭了 ASLR,它也表现出非确定性:


int main(int argc, char **argv) {
    // Turn off ASLR address space layout randomization.
    const int old_personality = personality(ADDR_NO_RANDOMIZE);
    if (!(old_personality & ADDR_NO_RANDOMIZE)) {
       const int new_personality = personality(ADDR_NO_RANDOMIZE);
       if (new_personality & ADDR_NO_RANDOMIZE) {
           execv(argv[0], argv);
       }
    }
    
    // Create a lua engine, then free it.
    lua_State *L = luaL_newstate();
    lua_close(L);

    // Allocate a big block of RAM.
    malloc(4*1024*1024);
    
    // Now print the hash of some mallocs.
    int hash = 0;
    for (int i = 0; i < 100; i++) {
        int n = (int)(ptrdiff_t)malloc(1);
        hash = (hash * 17) + n;
    }
    fprintf(stderr, "%08x\n", hash);
}

下面是三次运行的输出:

$ ./foo
c75ba620
$ ./foo
0e2e5210
$ ./foo
7c38ba10

我不知道为什么 lua 分配是相关的,但如果没有 luaL_newstate 和 lua_close,它就无法做到这一点。如果没有中间的 4 兆字节 malloc,它也无法做到这一点。

更新2:

我找到了非决定论的根源。lua 库调用 time(0) 来获取当前时间,然后将其用作随机种子,影响它所做的内存分配。花了这么长时间才发现这个问题的原因是“strace”没有向“time(0)”报告系统调用。我假设所有系统调用都是由 strace 报告的。

C++ malloc 非确定性 ASLR

评论

0赞 Sami Kuhmonen 2/18/2022
如果你去掉非确定性,你可能会让它永远不会崩溃。我不认为它会帮助你找出问题所在,调试器并确保计算机上的最大呜呜声设置为零警告应该有助于找到实际问题
0赞 K.R.Park 2/18/2022
为了能够回答您的问题,我们需要您的代码。
0赞 K.R.Park 2/18/2022
但是,我认为格式良好的 C++ 程序不应该依赖于 malloc 返回的实际地址,除了硬件控制和驱动程序,我认为您应该检查程序的有效性。
0赞 Raildex 2/18/2022
对于调试器来说,这是一个很好的用例。逐行浏览代码,并通过检查变量的值来检查发生不希望的事情的位置。
1赞 n. m. could be an AI 2/18/2022
尝试使用 valgrind 或地址消毒剂。

答:

-3赞 ufok 2/18/2022 #1

您可能是内存泄漏问题的受害者。您的程序使用不是由您分配的内存。如果此内存地址位于您分配了某些内容的同一内存块中,则程序不会崩溃。但是,如果程序访问了块中的内存,您没有正确读取/写入,操作系统就会杀死您的程序 - 您会将其视为崩溃。

评论

1赞 user17732522 2/18/2022
首先,这不是内存泄漏,其次,问题不在于为什么会发生崩溃,而在于如何使其具有确定性。
0赞 Igor G 2/18/2022 #2

有谁知道 malloc 在连续执行时返回不同地址的原因是什么?

假设该过程实际上是单线程的,并且没有块地址随机化完成,我猜:malloc

您的程序可能在某个时间点分配了随机数量的内存,例如,在计算要分配的某个块的大小时使用不可靠的垃圾值。所有后续分配返回的地址都可能受其影响(因此是随机的)。

当然,这将取决于分配的实现和实际大小:如果它作为每线程存储桶分配器实现,并且随机块大小不超过存储桶大小,那么对后续分配的影响几乎为零。malloc

评论

0赞 jyelon 2/18/2022
确实,使用随机参数调用 malloc 会影响所有未来的分配。但是我的程序没有使用任何“真正的”随机性(它没有读取 /dev/random)。因此,如果它在你第一次运行它时对 malloc 进行“随机”调用,它应该在第二次对 malloc 进行完全相同的“随机”调用。
0赞 Igor G 2/18/2022
随机性不一定是“真实的”:- ) 一个糟糕的意外随机性也会做同样的事情。例如,它可能来自读取未初始化的变量或其他未定义的行为。看,我不是在坚持。我只是在猜测是什么导致了你看到的行为。对不起,如果这没有帮助。
0赞 user17732522 2/18/2022
@IgorG 读取未初始化的内存不应该引入熵,因为我很确定 linux 在将所有页面交给进程之前将其归零,除非为嵌入式设备设置了特定的配置标志。
0赞 jyelon 2/18/2022 #3

在这种情况下,非确定性来自 Lua 运行时内部。Lua 使用 'time(0)' 生成一个随机种子,然后影响 Lua 发出的 malloc 调用。

这种非确定性的来源对我隐藏的原因是 linux 'strace' 没有报告对 “time(0)” 的系统调用。我假设 strace 会向我显示任何在连续执行时返回不同值的系统调用。

评论

0赞 user17732522 2/18/2022
time可能通过 VDSO 实现,以避免系统调用开销。 无法检测到这一点。在这种情况下,查看输出可能会有所帮助。straceltrace
0赞 Yage Hu 11/11/2023 #4

如果您使用的是 Linux,则需要做两件事:

  1. 按照您所做的方式进行设置。ADDR_NO_RANDOMIZATION
  2. 使 malloc 实现具有确定性。例如,对于 dlmalloc,这与重新编译一样简单。wasi-libc 就是这样做的。如果已定义,dlmalloc 将只使用固定的堆栈地址进行种子设定,而不是 time(0) 或 /dev/urandom: https://github.com/WebAssembly/wasi-libc/blob/main/dlmalloc/src/malloc.c#L3181#define LACKS_TIME_H 1LACKS_TIME_H