LD_PRELOAD不会影响带有RTLD_NOW的dlopen()

sas*_*alm 6 c linux shared-libraries dlopen ld-preload

如果我直接使用共享库中的函数,即在我的代码中声明它并在编译期间链接,LD_PRELOAD工作正常.但如果我使用dlopen()/ dlsym()而不是LD_PRELOAD没有效果!

问题是我想调试一个加载一些插件的程序dlopen(),并且它使用绝对文件名,因此简单地使用LD_LIBRARY_PATH将无法工作.

这是一个说明问题的示例代码.

./libfoo.so

void foo() {
    printf("version 1\n");
}
Run Code Online (Sandbox Code Playgroud)

./preload/libfoo.so

void foo() {
    printf("version 2\n");
}
Run Code Online (Sandbox Code Playgroud)

main.c中

#include <stdio.h>
#include <dlfcn.h>
void foo();
int main(int argc, char *argv[]) {
    void (*pfoo)();
    foo(); // call foo() first so we are sure ./preload/libfoo.so is loaded when we call dlopen()
    pfoo = dlsym(dlopen("libfoo.so", RTLD_NOW), "foo");
    pfoo();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

命令行

LD_PRELOAD=preload/libfoo.so LD_LIBRARY_PATH=. ./a.out

产量

version 2
version 1

为什么LD_PRELOAD不会影响dlopen(),有没有办法重定向dlopen(),特别是在使用绝对路径时?

ric*_*ici 7

指定LD_PRELOAD将导致加载程序在加载主可执行文件之前无条件加载(并初始化)指定的共享库。这使得在链接之前可以使用预加载库中定义的符号main,从而可以插入符号。[注1]

所以,在你的榜样,该呼叫foo()使用的符号从预加载的模块,并且dlsym将返回相同的符号,如果你用了一个空手柄的说法。

但是,对的调用dlopen没有考虑您要查找的符号(出于明显的原因)。它只是加载指示的共享对象或返回该共享对象已缓存版本的句柄。如有必要,它不会将模块添加到要加载的模块列表中;它只是加载模块。并且,当您将返回的句柄传递给时dlsymdlsym正是为了解析符号而不是搜索可执行文件中存在的一组外部符号,才精确地查看该模块。[笔记2]

如前所述dlopen,如果已经加载了“相同”共享对象,则不会多次加载。[注3]。但是,您中的共享库LD_PRELOAD称为preload/libfoo.so,而不是libfoo.so。(与某些其他操作系统不同,ELF不会从共享对象名称中删除目录路径。)因此,当您调用时dlopen("libfoo.so"),动态加载程序将不会libfoo.so在已加载的共享对象的高速缓存中查找任何命名为共享对象,因此它将寻找该文件系统中的对象,使用库搜索路径,因为提供的文件名不包含/

事实证明,ELF确实允许您指定共享对象的名称。因此,您可以将预加载的模块的名称设置为稍后动态加载的名称,然后dlopen将句柄返回到预加载的模块。

我们首先纠正main.c原始问题的版本:

#include <stdio.h>
#include <dlfcn.h>
void foo();
int main(int argc, char *argv[]) {
    const char* soname = argc > 1 ? argv[1] : "libfoo.so";
    void (*pfoo)();
    pfoo = dlsym(NULL, "foo"); // Find the preloaded symbol, if any.
    if (pfoo) pfoo(); else puts("No symbol foo before dlopen.");
    void* handle = dlopen(soname, RTLD_NOW);
    if (handle) {
      pfoo = dlsym(handle, "foo"); // Find the symbol in the loaded SO
      if (pfoo) pfoo(); else puts("No symbol foo after dlopen.");
    }
    else puts("dlopen failed to find the shared object.");
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

可以在不指定libdl以外的任何库的情况下进行构建:

gcc -Wall -o main main.c -ldl
Run Code Online (Sandbox Code Playgroud)

如果我们构建两个没有指定名称的共享库,则可能是您所做的:

gcc -Wall -o libfoo.so -shared -fPIC libfoo.c
gcc -Wall -o preload/libfoo.so -shared -fPIC preload/libfoo.c
Run Code Online (Sandbox Code Playgroud)

然后我们观察到dlopen/dlsym发现在加载的模块中找到了符号:

$ LD_PRELOAD=preload/libfoo.so LD_LIBRARY_PATH=. ./main libfoo.so
version 2
version 1
Run Code Online (Sandbox Code Playgroud)

但是,如果将要查找的名称分配给预加载的共享库,则会得到不同的行为:

$ gcc -Wall -o preload/libfoo.so -Wl,--soname=libfoo.so -shared -fPIC preload/libfoo.c
$ LD_PRELOAD=preload/libfoo.so LD_LIBRARY_PATH=. ./main libfoo.so
version 2
version 2
Run Code Online (Sandbox Code Playgroud)

之所以有效,dlopen是因为正在寻找名为的共享对象libfoo.so。但是,加载插件的应用程序更有可能使用文件名而不是使用库搜索路径。这将导致不考虑预加载的共享库,因为名称不再匹配:

$ LD_PRELOAD=preload/libfoo.so LD_LIBRARY_PATH=. ./main ./libfoo.so
version 2
version 1
Run Code Online (Sandbox Code Playgroud)

碰巧的是,我们可以通过使用实际要查找的名称构建共享库来完成此工作:

$ gcc -Wall -o preload/libfoo.so -Wl,--soname=./libfoo.so -shared -fPIC preload/libfoo.c
$ LD_PRELOAD=preload/libfoo.so LD_LIBRARY_PATH=. ./main libfoo.so
version 2
version 2
Run Code Online (Sandbox Code Playgroud)

有点麻烦,恕我直言,但调试是可以接受的。[注4]

笔记:

  1. 因此,注释“首先调用foo(),以便我们确定./preload/libfoo.so已加载”是不正确的;预加载的模块是加载的,如果需要,不会添加到要加载的模块列表中。

  2. 如果dlsym只想查找符号,可以传递一个NULL手柄。在这种情况下,dlsym将搜索加载的dlopen模块(包括加载的模块所需的模块dlopen)。但是,这几乎不是您想要的,因为加载插件的应用程序dlsym通常会指定插件必须定义的特定符号(或多个符号),并且这些符号将出现在每个加载的插件中,从而使得按符号名称进行的查找不精确。

  3. 这不是很正确,但是动态符号名称空间不在此答案的范围内。

  4. 当然,其他黑客也是可能的。例如,您可以插入自己的版本,dlopen以覆盖共享库名称查找。但这可能比必要的工作多得多。