`__attribute__((destructor))` 在某些情况下不是 运行?

`__attribute__((destructor))` not running in some cases?

我正在编写一个带有 LD_PRELOAD 的玩具 malloc(3) 实现作为练习。我有一个用 __attribute__((destructor)) 注释的函数,用于在退出时转储分配列表及其状态以进行调试,但我发现在某些情况下它不会 运行。具体来说,它对本地编译的代码执行 运行,但不针对系统二进制文件,如 /bin/ls(在 Arch 上 Linux)。不过,constructor 标记的函数确实适用于这两种情况。

问题的简单重现是:

main.c:

#include <stdio.h>

// compile with: clang -o main main.c

int main() {
    printf("main\n");
}

wrap.c:

#include <stdio.h>

// compile with: clang -o wrap -shared -fPIC wrap.c

void __attribute__((constructor)) say_hi() {
    printf("hi y'all\n");
}

void __attribute__((destructor)) say_bye() {
    printf("bye y'all\n");
}

析构函数 运行s 与 main.c:

$ LD_PRELOAD=./wrap ./main
hi y'all
main
bye y'all

析构函数不 运行 与 /bin/ls:

$ LD_PRELOAD=./wrap /bin/ls
hi y'all
main  main.c  wrap  wrap.c

我不知道如何调试它。 LD_DEBUG=all 没有显示任何有用的信息。 ldd mainldd /bin/ls 看起来相当。两个二进制文件都是动态链接的。据我所知,GCC docs 没有列出任何我应该注意的注意事项。

出于某种原因,GNU ls closes stdout before it exits,通过一个 atexit 处理程序,它可能在你的析构函数之前得到 运行。

所以可能您的析构函数 运行 没问题,但不会打印任何内容,因为您正在写入已关闭的流。

您可能需要让析构函数和一般的包装器以更稳健的方式进行日志记录。打开原始 fd 并 write()ing 到它会更好,但即便如此,一些程序也会执行 for (i = 0; i < 1024; i++) close(i); 这也会关闭你的日志 fd。唯一真正安全的方法可能是每次都 open/write/close,或者在内存中的某个地方维护自己的日志缓冲区,并在满时手动将其写出。


根据评论,可能的原因是 stdout 可能被定向到一个文件,并且由于它被缓冲,一些数据可能在流关闭之前不会被写出。此时可能会发生错误(磁盘已满、I/O 错误等)。如果 ls 像您的简单 main.c 那样将 stdout 的结束留给 C 库 startup/shutdown 代码,那么这个错误将不会被检测到并且 ls 会仍然以状态 0 退出;父进程无法知道输出文件不完整。因此,通过显式调用 fclose(stdout)ls 可以处理失败,如果可能的话报告它,并以非零状态退出以确保父级知道不信任输出。

正如 ShadowRanger 指出的那样,所有 GNU coreutils 都有这种行为。