TL;DR:如果您想防止已经加载的全局符号在您 dlopen() 时劫持您的库,请始终使用 RTLD_DEEPBIND。
当您使用 dlopen 加载库时,您可以使用 dlsym 访问其中的所有符号,这些符号将是该库中的正确符号,并且不会污染全局符号空间(除非您使用了 RTLD_GLOBAL)。 但即使库本身定义了符号,它的依赖关系仍会使用已加载的全局符号(如果可用)来解决。
考虑将第三方库称为 libexternal.so、external.c:
#include <stdio.h>
void externalFn()
{
printf("External function from the EXTERNAL library.\n");
}
然后考虑liba.so,它在不知不觉中私下实现了一个(注意指示内部链接的静态关键字)。 liba.c:
#include <stdio.h>
static void externalFn()
{
printf("Private implementation of external function from A.\n");
}
void hello()
{
printf("Hello from A!\n");
printf("Calling external from A...\n");
externalFn();
}
然后考虑libb.so,它不知不觉地实现了一个并将其导出,libb.c:
#include <stdio.h>
void externalFn()
{
printf("External implementation from B\n");
}
void hello()
{
printf("Hello from B!\n");
printf("Calling external from B...\n");
externalFn();
}
然后链接到 libexternal.so 的主应用程序会动态加载上述两个库并调用其中的东西,main.c:
#include <stdio.h>
#include <dlfcn.h>
void externalFn();
int main()
{
printf("Calling external function from main app.\n");
externalFn();
printf("Calling libA stuff...\n");
void *lib = dlopen("liba.so", RTLD_NOW);
void (*hello)();
hello = dlsym(lib, "hello");
hello();
printf("Calling libB stuff...\n");
void *libB = dlopen("libb.so", RTLD_NOW);
void (*helloB)();
helloB = dlsym(libB, "hello");
helloB();
printf("Calling externalFn via libB...\n");
void (*externalB)() = dlsym(libB, "externalFn");
externalB();
return 0;
}
构建命令是:
#!/bin/bash
echo "Building External..."
gcc external.c -shared -fPIC -o libexternal.so
echo "Building LibA..."
gcc liba.c -shared -fPIC -o liba.so
echo "Building LibB..."
gcc libb.c -shared -fPIC -o libb.so
echo "Building App..."
gcc main.c libexternal.so -ldl -Wl,-rpath,\$ORIGIN -o app
当您运行 app 时,它会打印:
Calling external function from main app.
External function from the EXTERNAL library.
Calling libA stuff...
Hello from A!
Calling external from A...
Private implementation of external function from A.
Calling libB stuff...
Hello from B!
Calling external from B...
External function from the EXTERNAL library.
Calling externalFn via libB...
External implementation from B
您可以看到,当 libb.so 调用 externalFn 时,将调用来自 libexternal.so 的那个!但是您仍然可以通过 dlsym 访问 libb.so 的 externalFn() 实现。
你什么时候会遇到这个问题?在我们的案例中,当我们为 Linux 发布库时,我们会尽量使其自包含,因此如果可以的话,我们会静态链接每个第三方库依赖项。但是仅仅添加 libwhatever.a 会导致你的库导出 libwhatever.a 中的所有符号
因此,如果消费者应用程序还使用系统预装的 libwhatever.so,那么您的库对 libwhatever 符号的符号引用将链接到已加载的库,而不是您静态链接的库。如果两者不同,结果是崩溃或内存损坏。
解决方法是使用链接器脚本来防止导出不需要的符号以避免混淆动态链接器。
但不幸的是,问题并不止于此。
LibA 的供应商决定在一个插件目录中提供多个库。所以他们将 externalFn() 的实现移到他们自己的库 external2.c 中:
#include <stdio.h>
void externalFn()
{
printf("External function from the EXTERNAL2 library.\n");
}
然后构建脚本更改为构建新的外部库并将所有东西移动到插件目录中:
#!/bin/bash
echo "Building External..."
gcc external.c -shared -fPIC -o libexternal.so
echo "Building External2..."
gcc external2.c -shared -fPIC -o libexternal2.so
echo "Building LibA..."
gcc liba.c libexternal2.so -shared -fPIC -Wl,-rpath,\$ORIGIN,--disable-new-dtags -o liba.so
echo "Building LibB..."
gcc libb.c -shared -fPIC -o libb.so
echo "Installing plugin"
mkdir -p plugins
mv liba.so plugins/
mv libexternal2.so plugins/
echo "Building App..."
gcc main.c libexternal.so -ldl -Wl,-rpath,\$ORIGIN,--disable-new-dtags -o app
很明显,liba.c 依赖于 libexternal2.so,因为我们链接它,我们甚至设置 RPATH 以使链接器在它所在的文件夹中查找它,因此即使 ldd 也显示它没有引用 libexternal.so全部,只有 libexternal2.so:
$ ldd liba.so
linux-vdso.so.1 (0x00007fff75870000)
libexternal2.so => /home/calmarius/stuff/source/linking/plugins/./libexternal2.so (0x00007fd9b9bcd000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd9b97d5000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd9b9fdd000)
所以更改应用程序以从插件目录加载 liba.so。
所以它应该可以正常工作,对吧?错误的!运行应用程序,你会得到:
Calling external function from main app.
External function from the EXTERNAL library.
Calling libA stuff...
Hello from A!
Calling external from A...
External function from the EXTERNAL library.
Calling libB stuff...
Hello from B!
Calling external from B...
External function from the EXTERNAL library.
Calling externalFn via libB...
External implementation from B
您可以看到,现在甚至 libA 都调用了应用程序链接的库,而不是 lib 链接的库!
解决办法是什么?自 glibc 2.3.4(自 2004 年以来存在)有一个选项 RTLD_DEEPBIND 如果你想避免与已经全局符号冲突,你必须始终需要在 dlopen-ing 库时指定此标志。因此,如果我们将标志更改为 RTLD_NOW | RTLD_DEEPBIND,我们会在运行应用程序时得到预期结果:
Calling external function from main app.
External function from the EXTERNAL library.
Calling libA stuff...
Hello from A!
Calling external from A...
External function from the EXTERNAL2 library.
Calling libB stuff...
Hello from B!
Calling external from B...
External implementation from B
Calling externalFn via libB...
External implementation from B