【问题标题】:LD_PRELOAD doesn't affect dlopen() with RTLD_NOWLD_PRELOAD 不影响带有 RTLD_NOW 的 dlopen()
【发布时间】:2016-10-14 00:09:57
【问题描述】:

如果我直接使用共享库中的函数,即通过在我的代码中声明它并在编译时链接,LD_PRELOAD 可以正常工作。但是如果我改用dlopen()/dlsym() LD_PRELOAD 就没有效果了!

问题是我想调试一个使用dlopen()加载一些插件的程序,并且它使用绝对文件名,所以简单地使用LD_LIBRARY_PATH是行不通的。

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

./libfoo.so

void foo() {
    printf("version 1\n");
}

./preload/libfoo.so

void foo() {
    printf("version 2\n");
}

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

命令行

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

输出

版本 2
版本 1

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

【问题讨论】:

  • 您永远不会调用pfoo 以使main 无法产生指定的输出。此外,为了重现问题,您应该指定构建命令(因为它们很重要,请参阅我的回答)。
  • 这是一个复制/粘贴错误,抱歉。这就是您多次编辑和重新编辑时发生的情况。现已修复。

标签: c linux shared-libraries dlopen ld-preload


【解决方案1】:

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

因此,在您的示例中,对 foo() 的调用使用预加载模块中的 符号,如果您使用 NULL 句柄调用它,dlsym 将返回相同的符号。

但是,对dlopen 的调用没有考虑到您正在寻找的符号(原因很明显)。它只是加载指定的共享对象或返回已缓存的共享对象版本的句柄。如果需要,它不会将模块添加到要加载的模块列表中;它只是加载模块。当您将返回的句柄传递给dlsym 时,dlsym 会精确地查看该模块以解析符号,而不是搜索可执行文件中存在的外部符号集。 [注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;
}

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

gcc -Wall -o main main.c -ldl

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

gcc -Wall -o libfoo.so -shared -fPIC libfoo.c
gcc -Wall -o preload/libfoo.so -shared -fPIC preload/libfoo.c

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

$ LD_PRELOAD=preload/libfoo.so LD_LIBRARY_PATH=. ./main libfoo.so
version 2
version 1

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

$ 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

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

$ LD_PRELOAD=preload/libfoo.so LD_LIBRARY_PATH=. ./main ./libfoo.so
version 2
version 1

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

$ 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

恕我直言,这有点小题大做,但它对于调试来说是可以接受的。 [注4]

注意事项:

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

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

  3. 这不太正确,但动态符号命名空间超出了此答案的范围。

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

【讨论】:

    【解决方案2】:

    根据http://linux.die.net/man/3/dlopen

    dlopen()、dlsym()、dlclose()、dlerror()四个函数实现 动态链接加载器的接口。

    LD_PRELOAD 只影响动态链接器本身——即:ld.so (http://linux.die.net/man/8/ld.so)。我能想到的唯一方法是通过chroot 强制dlopen 解决您想要的问题。

    后续思考:

    我刚刚想到的另一个想法是,如果您编写一个包装器,该包装器首先加载正确的*.so,然后调用您尝试重定向的程序。这是否会导致子进程使用重定向的*.so

    【讨论】:

    • 2 个问题 - 动态链接器和动态链接加载器有什么区别?在调用dlopen() 时,LD_PRELOAD 还没有加载库吗?实际上,这可以通过更改顺序并在dlopen() 之前调用foo() 来轻松检查,我会尽快检查。编辑:我发现了不同之处 - stackoverflow.com/questions/10052464/…
    • 回答第一个问题——调用dlopen() 是您将特定*.so 文件作为动态链接文件打开的方式。这是ld.so使用的底层机制。
    【解决方案3】:

    问题是我想调试一个使用 dlopen() 加载一些插件的程序,并且它使用绝对文件名,所以简单地使用 LD_LIBRARY_PATH 是行不通的。

    是的,dlopen 不会在 LD_LIBRARY_PATH 搜索带有斜杠的路径。

    您可以覆盖/叠加 dlopen 本身来搜索那些特定的插件路径。

    【讨论】:

      猜你喜欢
      • 2011-03-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-28
      • 2013-01-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多