为啥 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.solibb.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

虽然 readelfliba.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.solibb.so)位于您当前的工作目录 (./) 中,则现在可以运行 test。如果您只是使用 -rpath-linktest 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.confld.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(当链接 libbliba 时)在哪里 liba 是 - 只是它是一个依赖项。快速的ldd libb.so 会告诉你它找不到liba

由于这些库可能不在您的链接器搜索路径中,因此您在链接可执行文件时会收到链接器错误。请记住,当您链接 liba 本身时,libb 中的函数仍然未解析,但 ld 的默认行为是在您链接最终可执行文件之前不关心 DSO 中未解析的符号。

【讨论】:

以上是关于为啥 ld 在将可执行文件链接到 a 时需要 -rpath-link 以便需要另一个 so ?的主要内容,如果未能解决你的问题,请参考以下文章

Maven:编译时将可执行文件(.exe)移动到目标文件夹中

编译汇编链接加载

在 RAMDisk 上执行二进制文件是不是会将可执行文件重新加载到内存中?

为啥在将 liblas 链接到 Qt 后出现未定义的参考错误?

为啥链接器链接了错误的函数?

所以我们需要站在用户的场景去考虑整体的内容