【问题标题】:LD_PRELOAD does not work as expectedLD_PRELOAD 未按预期工作
【发布时间】:2014-07-14 01:49:50
【问题描述】:

考虑以下可以在任何程序执行之前预加载的库:

// g++ -std=c++11 -shared -fPIC preload.cpp -o preload.so
// LD_PRELOAD=./preload.so <command>
#include <iostream>

struct Goodbye {
    Goodbye() {std::cout << "Hello\n";}
    ~Goodbye() {std::cout << "Goodbye!\n";}
} goodbye;

问题是,虽然全局变量goodbye的构造函数总是被调用,但有些程序没有调用析构函数,比如ls

$ LD_PRELOAD=./preload.so ls
Hello

对于其他一些程序,析构函数按预期调用:

$ LD_PRELOAD=./preload.so man
Hello
What manual page do you want?
Goodbye!

你能解释一下为什么在第一种情况下不调用析构函数吗? 编辑:上面的问题已经回答了,那就是一个程序很可能使用_exit(), abort() 来退出。

但是:

有没有办法在预加载的程序退出时强制调用给定的函数?

【问题讨论】:

  • This question 似乎表明应该总是调用析构函数。你能做更多的研究来缩小哪些类型的程序最终会调用析构函数,哪些不会?
  • “类型”是什么意思?我似乎找不到区分“好”程序和“坏”程序的方法。请注意,当返回码为 0(无错误,无 abort())时也会出现问题
  • 尝试用 C 语言编写一个预加载模块,使用 GCC 的 __attribute__((constructor)) 在启动时运行一个函数;让该函数使用atexit 注册一个函数以在拆卸时运行。这有什么不同吗? (它不应该,但它可能。)
  • R.. 已回答您的第二个问题,但不是您的第一个问题。由于_exitabort 以及其他各种“异常程序终止”机制,其合同中包含它们不',因此不可能强制调用函数t 执行任何析构函数、atexit 函数等。但是,我发现 /bin/ls 通常会以这种方式退出是不可信的,所以为什么你的预加载模块的析构函数没有运行仍然是个谜.
  • 请注意,您在这里使用了相当高级的功能:std::cout 是一个与 stdio 流同步的缓冲流。 ls 很可能在退出时做了一些破坏这种机制的事情。如果你改用 ::write(2, "Goodbye\n", 8); 会发生什么?

标签: c++ linux gcc ld ld-preload


【解决方案1】:

lsatexit (close_stdout); 作为其初始化代码。完成后,它会关闭标准输出(即close(1)),因此您的coutprintfwrite(1, ... 操作不会打印任何内容。这并不意味着不调用析构函数。您可以通过例如验证这一点在你的析构函数中创建一个新文件。

http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/ls.c#n1285 这是 GNU coreutils ls 中的一行。

不仅仅是ls,大多数coreutils 都这样做。不幸的是,我不知道他们喜欢关闭它的确切原因为什么

关于如何找到它(或至少我做了什么)的旁注 - 下次可能会有所帮助,或者对无法访问源代码的程序有帮助:

析构函数消息用/bin/true(我能想到的最简单的程序)打印,但没有用lsdf 打印。我从strace /bin/truestrace /bin/ls 开始,比较了最新的系统调用。它显示close(1)close(2)ls,但没有为true。之后事情开始变得有意义,我只需要验证析构函数是否被调用。

【讨论】:

  • 噢!您是否介意添加一个指向源存储库的链接,以便我们自己查看?
  • 我根据您的结果进行了更多挖掘:似乎这样做是为了检测stdout 上的写入失败,以免为时已晚而无法成功退出。不幸的是,close_stdout 的定义隐藏在 gnulib 中,但是那里的 cmets 非常清楚:git.savannah.gnu.org/cgit/gnulib.git/tree/lib/closeout.c#n83
【解决方案2】:

如果程序通过_exit (POSIX) 或_Exit (C99) 或异常程序终止(abort、致命信号等)退出,则无法调用析构函数。我看不出有什么办法。

【讨论】:

  • 为什么/bin/ls 会这样做?
  • 猜的不错,但lingrok.org/xref/coreutils/src/ls.cexit (exit_status); 结尾(OP 指定现在他在 linux 上)
  • @Zack,为什么/bin/ls会这样做?除非有正常关闭所需的外部副作用(例如磁盘数据结构需要更新或发送网络消息以说“再见”),否则为什么不只是 _exit?如果整个堆即将消失,则无需 free() 最后几个块。
  • @Ben 我知道它看起来只是一个不同的字符,但作为成语,_exit不寻常的。只有当您有特定原因需要它时,您才会使用它。 exit / 从main 返回是在 C 中结束程序的正常方式,对于像 ls 这样的基本实用程序以这种方式异常是令人惊讶的。此外,如果ls 使用了_exit,则需要手动刷新stdout
  • @Ben(请注意,exit 不会逐个释放malloc 堆。)
【解决方案3】:

就像其他人说的那样,程序可能会通过_exit()_Exit()abort() 调用,而您的析构函数甚至不会注意到。要解决这些情况,您可以通过编写一个包装器来覆盖这些函数,如下例所示:

void
_exit(int status)
{
    void (*real__exit)(int) __attribute__((noreturn));
    const char *errmsg;

    /* Here you should call your "destructor" function. */
    destruct();

    (void)dlerror();
    real__exit = (void(*)(int))dlsym(RTLD_NEXT, "_exit");
    errmsg = dlerror();
    if (errmsg) {
        fprintf(stderr, "dlsym: _exit: %s\n", errmsg);
        abort();
    }

    real__exit(status);
}

但这并不能解决程序在库不知情的情况下逃逸的所有可能性,因为这些并不是应用程序可能拥有的唯一退出点。它还可以通过syscall() 函数触发exit 系统调用,为了避免它,您也必须对其进行包装。

程序退出的另一种方式是接收未处理的信号,因此您还应该处理(或包装?)所有可能触发程序死亡的信号。阅读signal(2) 手册页以获取更多信息,但请注意SIGKILL (9) 之类的信号无法处理,并且应用程序可能会通过调用kill() 自行破坏。话虽如此,除非您不希望处理由疯狂猴子编写的疯狂应用程序,否则您也应该将 kill() 包装起来。

您必须包装的另一个系统调用是execve()

无论如何,系统调用(如_exit)也可以直接通过assembly int 0x80 instruction 或过时的_syscallX() 触发。如果不是从应用程序外部(如stracevalgrind),你会如何包装它?好吧,如果您希望在您的程序中出现这种行为,我建议您放弃LD_PRELOAD 技术并开始考虑像stracevalgrind do(使用来自另一个进程的ptrace())或creating a Linux kernel module to trace it

【讨论】:

  • 这很聪明,但是当程序调用_exitabort 时,它们会这样做,因为有一些具体而令人信服的理由说明析构函数、atexit 函数等等不应该 运行。例如,在fork 的子端,如果execve 失败,使用_exit 至关重要,这样I/O 就不会刷新两次。图书馆不应该对这类事情事后猜测应用程序。
  • ...,我希望你从我的评论中吸取的教训是not“你也需要包装execve” ,它是“这是一种的聪明。”
  • ...?你是什​​么意思?你以为我不知道这是一个可怕的黑客吗?很明显它很丑。我将execve 添加到列表中只是因为我应该这样做,而不是因为我不理解您的评论。
  • 投反对票:想解释一下吗?我正在回答 OP AND 的最后一个问题,给出了一个最小且有效的示例。
猜你喜欢
  • 2021-06-04
  • 2022-01-24
  • 2015-05-11
  • 2020-05-15
  • 2014-10-31
  • 2018-02-12
  • 2014-01-20
  • 2015-01-13
相关资源
最近更新 更多