将此玩具代码用作可执行文件和共享库:
// main.c
void foo() {}
int main() { return 0; }
// bar.c
void foo();
void bar() { foo(); }
让我们在不进行优化的情况下构建,首先使用 gcc:
$ gcc -fpic -shared -o libbar_gcc.so bar.c
$ gcc -L. -lbar_gcc -o prog_gcc main.c
并观察到
foo
没有出现在 .dynsyms
部分 - 即,没有从可执行文件中导出:
$ readelf --dyn-syms prog_gcc | grep foo
$
现在尝试用 clang 进行同样的操作,看看它确实导出
foo
:
$ clang -fpic -shared -o libbar_clang.so bar.c
$ clang -L. -lbar_clang -o prog_clang main.c
$ readelf --dyn-syms prog_clang | grep foo
6: 0000000000001130 6 FUNC GLOBAL DEFAULT 13 foo
如果我们不链接到
libbar_clang
,foo
isn't 导出:
$ clang -fpic -shared -o libbar_clang.so bar.c
$ clang -o prog_clang main.c
$ readelf --dyn-syms prog_clang | grep foo
$
更令人惊讶的是,如果我们恢复针对 libbar 的链接,但将
bar
的源更改为 not 调用 foo
:
// bar.c
void bar() { }
我们会再次看到
foo
未导出!
$ clang -fpic -shared -o libbar_clang.so bar.c
$ clang -L. -lbar_clang -o prog_clang main.c
$ readelf --dyn-syms prog_clang | grep foo
$
因此,修改依赖共享库中的源代码 - 不更改可执行文件的源代码或链接 - 会改变 clang 关于从可执行文件导出内容的决定。这是什么法术??
我使用的是 Ubuntu 20、clang 14.0.6 和 gcc 9.4.0。两者都使用ld
链接器。
--as-needed
而 clang 使用
--no-as-needed
。即,gcc 驱动程序引导链接器仅链接到实际定义所需符号(尚未定义)的共享库,而 clang 驱动程序引导链接器愉快地链接到命令行上出现的任何共享库。但这如何导致问题中出现明显的差异呢?
我目前的理解是,在链接可执行文件时,默认链接器逻辑仅导出链接器
可以看到实际需要的符号以满足其他地方的链接。如果链接器可以看到某些依赖库依赖于可执行文件来实现某些符号,则该符号将被导出而无需进一步的用户干预。
有时,链接器无法看到库需要某个符号 - 比如说它是否在运行时由dlopen
加载,并且首先对链接器不可见。在这种情况下,用户必须进行干预,可能是将
-rdynamic
添加到链接行 - 从而强制从可执行文件导出所有公共符号。无论如何,在原始案例中:
libbar.so
不直接从可执行文件中使用(直到我们 添加来自
bar()
的
main()
呼叫!),所以 -
--as-needed
,并且
libbar.so
需要符号
foo
并且不会导出它。