简体   繁体   English

LD_PRELOAD 未按预期工作

[英]LD_PRELOAD does not work as expected

Consider the following library which can be preloaded before any program execution:考虑以下可以在任何程序执行之前预加载的库:

// 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;

The problem is that, while the constructor of the global variable goodbye is always called, the destructor is not called for some programs, like ls :问题是,虽然始终调用全局变量goodbye的构造函数,但对于某些程序,例如ls ,不会调用析构函数:

$ LD_PRELOAD=./preload.so ls
Hello

For some other programs, the destructor is called as expected:对于其他一些程序,析构函数按预期调用:

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

Can you explain why the destructor is not called in the first case?你能解释为什么在第一种情况下不调用析构函数吗? EDIT: the above question has been already answered, that is a program might well use _exit(), abort() to exit.编辑:上面的问题已经回答了,那就是程序很可能使用 _exit(), abort() 退出。

However:然而:

Is there a way to force a given function being called when a preloaded program exits?有没有办法在预加载的程序退出时强制调用给定的函数?

ls has atexit (close_stdout); lsatexit (close_stdout); as its initialisation code.作为它的初始化代码。 When it finishes, it closes stdout (ie close(1) ), so your cout , printf or write(1, ... operations will not print anything. It doesn't mean destructor isn't called. You can verify this by eg creating a new file in your destructor.完成后,它会关闭 stdout(即close(1) ),因此您的coutprintfwrite(1, ...操作不会打印任何内容。这并不意味着不会调用析构函数。您可以通过以下方式验证这一点例如在析构函数中创建一个新文件。

http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/ls.c#n1285 here is the line in GNU coreutils ls. http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/ls.c#n1285这里是 GNU coreutils ls 中的行。

It is not just ls , most of coreutils do that.不仅仅是ls ,大多数 coreutils 都这样做。 Unfortunately, I don't know exact reason why they prefer to close it.不幸的是,我不知道确切的原因,他们更愿意将其关闭。

Side note on how this could be found (or at least what I did) - may help next time or with program with no source code access:关于如何找到它(或至少我做了什么)的旁注 - 可能有助于下次或没有源代码访问的程序:

Destructor message is printed with /bin/true (simplest program I could think of), but isn't printed with ls or df .析构函数消息用/bin/true打印(我能想到的最简单的程序),但不是用lsdf打印的。 I started with strace /bin/true and strace /bin/ls and compared latest system calls.我从strace /bin/truestrace /bin/ls并比较了最新的系统调用。 It shown close(1) and close(2) for ls , but none for true .它显示了ls close(1)close(2) ,但没有显示true After that things started to make sense and I just had to verify that destructor is called.在那之后事情开始变得有意义了,我只需要验证析构函数是否被调用。

If the program exits via _exit (POSIX) or _Exit (C99) or abnormal program termination ( abort , fatal signals, etc.) then there's no way destructors could be called.如果程序通过_exit (POSIX) 或_Exit (C99) 或异常程序终止( abort 、致命信号等) abort ,则无法调用析构函数。 I don't see any way around this.我看不出有什么办法解决这个问题。

Like others said, a program might call via _exit() , _Exit() or abort() and your destructors won't even notice.就像其他人说的那样,程序可能会通过_exit()_Exit()abort() _Exit()而您的析构函数甚至不会注意到。 To solve these cases you could override these functions by just writing a wrapper like the following the example below:要解决这些情况,您可以通过编写如下示例的包装器来覆盖这些函数:

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);
}

But this wouldn't solve all the possibilities of a program escaping without your library's knowledge, because those are not the only exit points an application could have.但这并不能解决程序在您的库不知情的情况下逃逸的所有可能性,因为这些并不是应用程序可能拥有的唯一退出点。 It could also trigger the exit system call via the syscall() function and to avoid it you would have to wrap it too.它还可以通过syscall()函数触发exit系统调用,为了避免它,您也必须包装它。

Another way a program could exit is by receiving an unhandled signal, so you should also handle (or wrap?) all signals that could trigger the death of a program.程序退出的另一种方式是接收未处理的信号,因此您还应该处理(或包装?)所有可能触发程序死亡的信号。 Read the signal(2) man page for more information but please be aware that signals like SIGKILL (9) cannot be handled and an application could destroy itself by calling kill() .阅读signal(2)手册页了解更多信息,但请注意像SIGKILL (9) 这样的信号无法处理,应用程序可能会通过调用kill()来破坏自身。 With that being said and unless you don't expect to handle insane applications written by crazy monkeys you should wrap kill() too.话虽如此,除非您不希望处理由疯狂猴子编写的疯狂应用程序,否则您也应该包装kill()

Another system call you'd have to wrap is execve() .您必须包装的另一个系统调用是execve()

Anyway, a system call (like _exit ) could also be triggered directly via an assembly int 0x80 instruction or the obsolete _syscallX() macro .无论如何,系统调用(如_exit )也可以通过assembly int 0x80指令或过时的_syscallX()直接触发。 How'd you wrap it if not from outside the application (like strace or valgrind )?如果不是从应用程序外部(如stracevalgrind ),你如何包装它? Well, if you expect this kind of behaviour in you programs I suggest you drop the LD_PRELOAD technique and start thinking about doing like strace and valgrind do (using ptrace() from another process) or creating a Linux kernel module to trace it .好吧,如果你希望在你的程序中出现这种行为,我建议你放弃LD_PRELOAD技术并开始考虑像stracevalgrind那样做(使用另一个进程的ptrace() )或创建一个 Linux 内核模块来跟踪它

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM