为啥 ld 在将可执行文件链接到 a 时需要 -rpath-link 以便需要另一个 so ?
Posted
技术标签:
【中文标题】为啥 ld 在将可执行文件链接到 a 时需要 -rpath-link 以便需要另一个 so ?【英文标题】:Why does ld need -rpath-link when linking an executable against a so that needs another so?为什么 ld 在将可执行文件链接到 a 时需要 -rpath-link 以便需要另一个 so ? 【发布时间】:2014-08-27 04:40:48 【问题描述】:我只是好奇。我创建了一个共享对象:
gcc -o liba.so -fPIC -shared liba.c
还有一个与前一个链接的共享对象:
gcc -o libb.so -fPIC -shared libb.c liba.so
现在,当创建一个链接到 libb.so
的可执行文件时,我必须指定 -rpath-link 到 ld 以便它可以在发现 liba.so
依赖它时找到 liba.so
:
gcc -o test -Wl,-rpath-link,./ test.c libb.so
否则我会抱怨的。
为什么在链接test
时,ld 必须能够找到liba.so
?因为在我看来,除了确认liba.so
的存在之外,ld 似乎并没有做太多其他事情。例如,运行readelf --dynamic ./test
只根据需要列出libb.so
,所以我猜动态链接器必须自己发现libb.so -> liba.so
依赖,然后自己搜索liba.so
。
我在 x86-64 GNU/Linux 平台上,test
中的 main() 例程调用 libb.so
中的一个函数,该函数又调用 liba.so
中的一个函数。
【问题讨论】:
【参考方案1】:为什么在链接
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.
【讨论】:
学到了很多。谢谢。不过链接已经失效了。 @SurajeetBharati 一旦我的编辑被批准,链接应该被修复(基本上,用.html
替换最后的斜线)
我现在无法访问该链接,但可以在 archive.org 上找到它:web.archive.org/web/20161025105929/http://www.kaizou.org/2015/…【参考方案2】:
我猜你需要知道何时使用-rpath
选项和-rpath-link
选项。
首先我引用man ld
指定的内容:
-rpath 和-rpath-link 的区别在于-rpath 指定的目录 选项包含在可执行文件中并在运行时使用,而 -rpath-link 选项仅在链接时有效。搜索 这种方式的 -rpath 仅由已配置 --with-sysroot 选项的本机链接器和交叉链接器支持。
您必须区分链接时和运行时。根据您接受的 anton_rh 的回答,在编译和链接共享库或静态库时未启用检查未定义符号,但在编译和链接可执行文件时启用。 (但是,请注意存在一些共享库和可执行文件,例如,ld.so
。键入man ld.so
来探索这个,我不知道是否启用了检查未定义符号时编译这些“双重”类型的文件)。
所以-rpath-link
用于链接时间检查,-rpath
用于链接时间和运行时间,因为rpath
嵌入到 ELF 标头中。但是你应该注意-rpath-link
选项将在链接时覆盖-rpath
选项,如果它们都被指定的话。
但是,为什么要选择-rpath-option
和-rpath
?我认为它们用于消除“过度链接”。看到这个Better understanding Linux secondary dependencies solving with examples.,只需使用ctrl + F
导航到与“overlinking”相关的内容。你应该关注为什么“overlinking”不好,因为我们采用了避免“overlinking”的方法,ld
选项-rpath-link
和-rpath
的存在是合理的:我们在命令中故意省略了一些库编译和链接以避免“过度链接”,并且由于省略,ld
需要-rpath-link
或-rpath
来定位这些省略的库。
【讨论】:
【参考方案3】:你的系统通过ld.so.conf
、ld.so.conf.d
和系统环境LD_LIBRARY_PATH
等提供system-wide库搜索路径,通过安装库补充pkg-config
针对标准库构建时的信息等。当库位于定义的搜索路径中时,将自动遵循标准库搜索路径,从而允许找到所有必需的库。
对于您自己创建的自定义共享库,没有标准的运行时库搜索路径。在编译和链接期间,您可以通过 -L/path/to/lib
指定库的搜索路径。对于非标准位置的库,可以选择在编译时将库搜索路径放在可执行文件的标头(ELF 标头)中,以便您的可执行文件可以找到所需的库。
rpath
提供了一种在 ELF 标头中嵌入自定义运行时库搜索路径的方法,以便您也可以找到自定义库,而无需在每次使用时指定搜索路径。这也适用于依赖库的库。正如您所发现的,不仅在命令行上指定库的顺序很重要,您还必须为您链接的每个依赖库提供运行时库搜索路径或 rpath 信息,以便标头包含运行所需的所有库的位置。
来自评论的补充
我的问题主要是为什么 ld 必须“自动尝试定位 共享库”(liba.so)和“将其包含在链接中”。
这就是ld
的工作方式。来自man ld
“-rpath 选项也用于定位 共享对象需要的共享对象 明确包含在链接中...如果在链接 ELF 可执行文件时未使用 -rpath,则如果已定义,将使用环境变量“LD_RUN_PATH”的内容。在您的情况下,liba
不在LD_RUN_PATH
中,因此ld
需要在编译可执行文件期间使用rpath
(如上所述)或通过提供显式搜索路径来定位liba
给它。
其次,“将其包含在链接中”的真正含义。在我看来 它只是意味着:“确认它的存在”(liba.so's),因为 libb.so 的 ELF 标头没有被修改(它们已经有一个 NEEDED 标签 针对 liba.so),并且 exec 的标头仅将 libb.so 声明为 需要。为什么ld关心找到liba.so,它不就可以离开吗 运行时链接器的任务?
不,回到ld
的语义。为了生成“良好链接”,ld
必须能够找到所有 依赖库。 ld
否则无法确保良好的链接。运行时链接器必须查找并加载,而不仅仅是查找程序所需的共享库。 ld
不能保证会发生这种情况,除非 ld
本身可以在链接程序时找到所有需要的共享库。
【讨论】:
是的,但是 -rpath-link 选项不会在任何目标文件中插入任何 RPATH 标记。文档说:使用 ELF 或 SunOS 时,一个共享库可能需要另一个共享库。当 ld -shared 链接包含一个共享库作为输入文件之一时,就会发生这种情况。当链接器在做一个非共享的、不可重定位的链接时遇到这样的依赖时,它会自动尝试定位所需的共享库并将其包含在链接中,如果它没有显式包含的话。在这种情况下,-rpath-link 选项指定要搜索的第一组目录 我猜链接一个可执行文件是一个“非共享、不可重定位的链接”。我的问题主要是为什么 ld 必须“自动尝试查找共享库”(liba.so)并“将其包含在链接中”。其次,“将其包含在链接中”的真正含义是什么。对我来说,它似乎只是意味着:“确认它的存在”(liba.so),因为 libb.so 的 ELF 标头没有被修改(它们已经有一个针对 liba.so 的 NEEDED 标签),并且 exec 的标头仅声明 libb。所以需要。为什么 ld 关心找到 liba.so,它不能把任务留给运行时链接器吗?【参考方案4】:您实际上并没有告诉 ld(当链接 libb
与 liba
时)在哪里 liba
是 - 只是它是一个依赖项。快速的ldd libb.so
会告诉你它找不到liba
。
由于这些库可能不在您的链接器搜索路径中,因此您在链接可执行文件时会收到链接器错误。请记住,当您链接 liba 本身时,libb 中的函数仍然未解析,但 ld
的默认行为是在您链接最终可执行文件之前不关心 DSO 中未解析的符号。
【讨论】:
以上是关于为啥 ld 在将可执行文件链接到 a 时需要 -rpath-link 以便需要另一个 so ?的主要内容,如果未能解决你的问题,请参考以下文章
Maven:编译时将可执行文件(.exe)移动到目标文件夹中
在 RAMDisk 上执行二进制文件是不是会将可执行文件重新加载到内存中?