LD_PRELOAD 不影响 RTLD_NOW 的 dlopen()
LD_PRELOAD doesn't affect dlopen() with RTLD_NOW
如果我直接使用共享库中的函数,即通过在我的代码中声明它并在编译时进行链接,LD_PRELOAD 工作正常。但是如果我用 dlopen()
/dlsym()
代替 LD_PRELOAD 没有效果!
问题是我想调试一个使用dlopen()
加载一些插件的程序,它使用绝对文件名,所以简单地使用LD_LIBRARY_PATH
是行不通的。
下面是说明问题的示例代码。
./libfoo.so
void foo() {
printf("version 1\n");
}
./preload/libfoo.所以
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
输出
version 2
version 1
为什么 LD_PRELOAD 不影响 dlopen()
,有什么方法可以重定向 dlopen()
,尤其是在使用绝对路径时?
根据http://linux.die.net/man/3/dlopen
The four functions dlopen(), dlsym(), dlclose(), dlerror() implement
the interface to the dynamic linking loader.
而 LD_PRELOAD
只影响动态链接器本身——即:ld.so (http://linux.die.net/man/8/ld.so)。我能想到的强制 dlopen
解决的唯一方法是通过 chroot
.
跟进想法:
我刚才的另一个想法是,如果您编写一个首先加载正确 *.so
然后调用您尝试重定向的程序的包装器,会怎样呢?这会导致子进程使用重定向的 *.so
吗?
The problem is that I want to debug a program that loads some plugins using dlopen(), and it uses absolute file names at that, so simply using LD_LIBRARY_PATH
won't work.
是的,dlopen
不会 LD_LIBRARY_PATH
搜索带斜杠的路径。
您可以 override/superimpose dlopen
本身来搜索那些特定的插件路径。
指定 LD_PRELOAD
将导致加载程序在加载主可执行文件之前无条件地加载(并初始化)指定的共享库。这使得预加载库中定义的符号在链接 main
之前可用,允许插入符号。 [注1]
因此,在您的示例中,对 foo()
的调用使用预加载模块中的 符号 ,而 dlsym
将 return如果您使用 NULL 句柄调用它,则相同的符号。
但是,对 dlopen
的调用并未考虑您要查找的符号(原因很明显)。它只是加载指定的共享对象或 returns 已缓存版本的共享对象的句柄。如果需要,它不会将模块添加到要加载的模块列表中;它只是加载模块。当您将 returned 句柄传递给 dlsym
时,dlsym
会准确地查看该模块以解析符号,而不是搜索可执行文件中存在的外部符号集。 [注2]
正如我提到的,如果 dlopen
已经加载了对象,则它不会多次加载 "same" 共享对象。 [注3]。但是,您 LD_PRELOAD
中的共享对象称为 preload/libfoo.so
,而不是 libfoo.so
。 (与某些其他操作系统不同,ELF 不会从共享对象名称中删除目录路径。)因此,当您调用 dlopen("libfoo.so")
时,动态加载程序不会在缓存中找到任何名为 libfoo.so
的共享对象加载共享对象,因此它将使用库搜索路径在文件系统中查找该对象,因为提供的文件名不包含 /
.
事实证明,ELF 确实允许您指定共享对象的名称。所以你可以将预加载模块的名称设置为稍后动态加载的名称,然后dlopen
将return预加载模块的句柄。
我们首先修正原题中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]
备注:
因此,注释"call foo() first so we are sure ./preload/libfoo.so is loaded"不正确;预加载模块已 pre 加载,未添加到模块列表中以在必要时加载。
如果你想让dlsym
只是寻找一个符号,你可以传递一个NULL
句柄。在这种情况下,dlsym
将在 dlopen
加载的模块中搜索(包括 dlopen
加载的模块所需的模块)。但这很少是您想要的,因为使用 dlsym
加载插件的应用程序通常指定插件必须定义的特定符号(或多个符号),并且这些符号将出现在每个加载的插件中,通过符号名称进行查找不精确。
这不太正确,但动态符号命名空间超出了此答案的范围。
当然,其他黑客攻击也是可能的。例如,您可以插入您自己的 dlopen
版本来覆盖共享对象名称查找。但这可能比必要的工作要多得多。
如果我直接使用共享库中的函数,即通过在我的代码中声明它并在编译时进行链接,LD_PRELOAD 工作正常。但是如果我用 dlopen()
/dlsym()
代替 LD_PRELOAD 没有效果!
问题是我想调试一个使用dlopen()
加载一些插件的程序,它使用绝对文件名,所以简单地使用LD_LIBRARY_PATH
是行不通的。
下面是说明问题的示例代码。
./libfoo.so
void foo() {
printf("version 1\n");
}
./preload/libfoo.所以
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
输出
version 2 version 1
为什么 LD_PRELOAD 不影响 dlopen()
,有什么方法可以重定向 dlopen()
,尤其是在使用绝对路径时?
根据http://linux.die.net/man/3/dlopen
The four functions dlopen(), dlsym(), dlclose(), dlerror() implement the interface to the dynamic linking loader.
而 LD_PRELOAD
只影响动态链接器本身——即:ld.so (http://linux.die.net/man/8/ld.so)。我能想到的强制 dlopen
解决的唯一方法是通过 chroot
.
跟进想法:
我刚才的另一个想法是,如果您编写一个首先加载正确 *.so
然后调用您尝试重定向的程序的包装器,会怎样呢?这会导致子进程使用重定向的 *.so
吗?
The problem is that I want to debug a program that loads some plugins using dlopen(), and it uses absolute file names at that, so simply using
LD_LIBRARY_PATH
won't work.
是的,dlopen
不会 LD_LIBRARY_PATH
搜索带斜杠的路径。
您可以 override/superimpose dlopen
本身来搜索那些特定的插件路径。
指定 LD_PRELOAD
将导致加载程序在加载主可执行文件之前无条件地加载(并初始化)指定的共享库。这使得预加载库中定义的符号在链接 main
之前可用,允许插入符号。 [注1]
因此,在您的示例中,对 foo()
的调用使用预加载模块中的 符号 ,而 dlsym
将 return如果您使用 NULL 句柄调用它,则相同的符号。
但是,对 dlopen
的调用并未考虑您要查找的符号(原因很明显)。它只是加载指定的共享对象或 returns 已缓存版本的共享对象的句柄。如果需要,它不会将模块添加到要加载的模块列表中;它只是加载模块。当您将 returned 句柄传递给 dlsym
时,dlsym
会准确地查看该模块以解析符号,而不是搜索可执行文件中存在的外部符号集。 [注2]
正如我提到的,如果 dlopen
已经加载了对象,则它不会多次加载 "same" 共享对象。 [注3]。但是,您 LD_PRELOAD
中的共享对象称为 preload/libfoo.so
,而不是 libfoo.so
。 (与某些其他操作系统不同,ELF 不会从共享对象名称中删除目录路径。)因此,当您调用 dlopen("libfoo.so")
时,动态加载程序不会在缓存中找到任何名为 libfoo.so
的共享对象加载共享对象,因此它将使用库搜索路径在文件系统中查找该对象,因为提供的文件名不包含 /
.
事实证明,ELF 确实允许您指定共享对象的名称。所以你可以将预加载模块的名称设置为稍后动态加载的名称,然后dlopen
将return预加载模块的句柄。
我们首先修正原题中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]
备注:
因此,注释"call foo() first so we are sure ./preload/libfoo.so is loaded"不正确;预加载模块已 pre 加载,未添加到模块列表中以在必要时加载。
如果你想让
dlsym
只是寻找一个符号,你可以传递一个NULL
句柄。在这种情况下,dlsym
将在dlopen
加载的模块中搜索(包括dlopen
加载的模块所需的模块)。但这很少是您想要的,因为使用dlsym
加载插件的应用程序通常指定插件必须定义的特定符号(或多个符号),并且这些符号将出现在每个加载的插件中,通过符号名称进行查找不精确。这不太正确,但动态符号命名空间超出了此答案的范围。
当然,其他黑客攻击也是可能的。例如,您可以插入您自己的
dlopen
版本来覆盖共享对象名称查找。但这可能比必要的工作要多得多。