为什么在链接test 时,ld 必须能够定位liba.so?因为在我看来,除了确认liba.so 的存在之外,ld 似乎并没有做太多其他事情。例如,运行readelf --dynamic ./test 只根据需要列出libb.so,所以我猜动态链接器必须自己发现libb.so -> liba.so 依赖,并自己搜索liba.so。
如果我正确理解链接过程,ld 实际上甚至不需要定位libb.so。它可以忽略test 中所有未解析的引用,希望动态链接器在运行时加载libb.so 时能够解析它们。但是如果 ld 这样做的话,很多“未定义的引用”错误在链接时不会被检测到,而是会在运行时尝试加载test 时被发现。所以 ld 只是做额外的检查,以确保在 test 本身中找不到的所有符号都可以在 test 所依赖的共享库中真正找到。因此,如果test 程序出现“未定义引用”错误(在test 本身和libb.so 中都没有找到某些变量或函数),这在链接时会变得很明显,而不仅仅是在运行时。因此,这种行为只是额外的健全性检查。
但 ld 走得更远。当您链接 test 时,ld 还会检查 libb.so 中所有未解析的引用是否在 libb.so 所依赖的共享库中找到(在我们的例子中,libb.so 依赖于 liba.so,所以它需要liba.so 在链接时定位)。好吧,实际上 ld 在链接libb.so 时已经完成了这项检查。为什么它会第二次进行这种检查...也许 ld 的开发人员发现这种双重检查对于检测损坏的依赖关系很有用,当您尝试将您的程序与可能在它的时间加载的过时库链接时已链接,但现在无法加载,因为它所依赖的库已更新(例如,liba.so 后来被重新设计并从中删除了一些函数)。
UPD
只是做了一些实验。看来我的假设 “实际上 ld 在链接 libb.so 时已经完成了这项检查” 是错误的。
假设liba.c有以下内容:
int liba_func(int i)
{
return i + 1;
}
libb.c 有下一个:
int liba_func(int i);
int liba_nonexistent_func(int i);
int libb_func(int i)
{
return liba_func(i + 1) + liba_nonexistent_func(i + 2);
}
和test.c
#include <stdio.h>
int libb_func(int i);
int main(int argc, char *argv[])
{
fprintf(stdout, "%d\n", libb_func(argc));
return 0;
}
链接libb.so时:
gcc -o libb.so -fPIC -shared libb.c liba.so
链接器不会生成任何liba_nonexistent_func 无法解析的错误消息,而是默默地生成损坏的共享库libb.so。其行为与您使用 ar 创建静态库 (libb.a) 的行为相同,它也无法解析生成库的符号。
但是当你尝试链接test:
gcc -o test -Wl,-rpath-link=./ test.c libb.so
你得到错误:
libb.so: undefined reference to `liba_nonexistent_func'
collect2: ld returned 1 exit status
如果 ld 没有递归扫描所有共享库,则无法检测到此类错误。所以看来问题的答案和我上面说的一样:ld需要-rpath-link以确保链接的可执行文件可以稍后加载通过动态加载。只是一个健全的检查。
UPD2
尽早检查未解析的引用是有意义的(在链接libb.so 时),但ld 出于某些原因不这样做。这可能是为了允许对共享库进行循环依赖。
liba.c 可以有以下实现:
int libb_func(int i);
int liba_func(int i)
{
int (*func_ptr)(int) = libb_func;
return i + (int)func_ptr;
}
所以liba.so 使用libb.so 而libb.so 使用liba.so(最好不要这样做)。这成功编译并工作:
$ gcc -o liba.so -fPIC -shared liba.c
$ gcc -o libb.so -fPIC -shared libb.c liba.so
$ gcc -o test test.c -Wl,-rpath=./ libb.so
$ ./test
-1217026998
虽然 readelf 说liba.so 不需要libb.so:
$ readelf -d liba.so | grep NEEDED
0x00000001 (NEEDED) Shared library: [libc.so.6]
$ readelf -d libb.so | grep NEEDED
0x00000001 (NEEDED) Shared library: [liba.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
如果 ld 在链接共享库期间检查未解析的符号,则无法链接 liba.so。
请注意,我使用 -rpath 键而不是 -rpath-link。不同之处在于 -rpath-link 在链接时仅用于检查最终可执行文件中的所有符号是否可以解析,而 -rpath 实际上嵌入了您指定的路径作为 ELF 的参数:
$ readelf -d test | grep RPATH
0x0000000f (RPATH) Library rpath: [./]
因此,如果共享库(liba.so 和 libb.so)位于您当前的工作目录 (./) 中,则现在可以运行 test。如果您只是使用 -rpath-link,test ELF 中将没有这样的条目,您必须将共享库的路径添加到 /etc/ld.so.conf 文件或 @987654381 @环境变量。
UPD3
实际上可以在链接共享库期间检查未解析的符号,必须使用--no-undefined 选项:
$ gcc -Wl,--no-undefined -o libb.so -fPIC -shared libb.c liba.so
/tmp/cc1D6uiS.o: In function `libb_func':
libb.c:(.text+0x2d): undefined reference to `liba_nonexistent_func'
collect2: ld returned 1 exit status
我还发现了一篇很好的文章,它阐明了链接依赖于其他共享库的共享库的许多方面:
Better understanding Linux secondary dependencies solving with examples.