为什么ld在将可执行文件与需要另一个so的so链接时需要-rpath-link?

46

我只是好奇。我已经创建了一个共享对象:

gcc -o liba.so -fPIC -shared liba.c

还有一个共享对象,它链接到前面的对象:

gcc -o libb.so -fPIC -shared libb.c liba.so

现在,当创建一个链接到 libb.so 的可执行文件时,我必须指定 -rpath-link 给 ld,以便在发现 libb.so 依赖于 liba.so 时能够找到它:

gcc -o test -Wl,-rpath-link,./ test.c libb.so
否则,ld会抱怨。
为什么在链接test时,ld必须能够找到liba.so呢?因为对我来说,ld似乎除了确认liba.so的存在之外没有做其他太多的事情。例如,运行readelf --dynamic ./test只列出了需要的libb.so,所以我猜测动态链接器必须自己发现libb.so -> liba.so的依赖关系,并对liba.so进行自己的搜索。
我在x86-64 GNU/Linux平台上,test中的main()函数调用libb.so中的一个函数,该函数又调用liba.so中的一个函数。
4个回答

34
为什么链接test时,ld必须能够找到liba.so?因为在我看来,ld除了确认liba.so的存在之外,并没有做其他很多事情。例如,运行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的开发人员发现这个双重检查有用,可以检测到在尝试将程序链接到过时的库时出现的损坏的依赖项,该库可能在它被链接的时候可以加载,但现在由于它所依赖的库已更新(例如,从中删除了一些函数),因此无法加载。 更新 刚刚做了一些实验。似乎我的假设“实际上,在链接libb.so时,ld已经完成了这个检查”是错误的。
让我们假设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以确保被动态加载的链接可执行文件可以稍后加载。只是一个健全性检查。
更新2
尽早检查未解决的引用可能是有意义的(在链接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.solibb.so)位于您当前的工作目录(./),则现在可以运行test。 如果您只是使用-rpath-link,则在test ELF中没有这样的条目,您必须将共享库路径添加到/etc/ld.so.conf文件或LD_LIBRARY_PATH环境变量中。

更新3

实际上可以在链接共享库时检查未解析的符号,必须使用--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

我找到了一篇好的文章,可以澄清许多关于链接依赖其他共享库的共享库的相关方面:通过示例更好地理解Linux次要依赖关系的解决方法


1
学到了很多,谢谢。不过链接已经失效了。 - Surajeet Bharati
2
@SurajeetBharati,一旦我的编辑被批准(基本上是用.html替换最后一个斜杠),链接就应该会修复。 - Edward
1
我现在无法访问该链接,不过你可以在archive.org上找到它:https://web.archive.org/web/20161025105929/http://www.kaizou.org/2015/01/linux-libraries.html - Horstinator

11
我想你需要知道何时使用 -rpath 选项和 -rpath-link 选项。首先引用一下 man ld 的说明:
  1. -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选项呢? 我认为它们用于消除 "过度链接"。查看此 通过示例更好地理解Linux二次依赖关系解决方案 ,只需使用ctrl + F 即可导航到与“过度链接”有关的内容。您应该专注于为什么“过度链接”是不好的,因为我们采取的方法可以避免“过度链接”,所以存在ld选项-rpath-link-rpath是合理的:我们故意省略一些库用于编译和链接的命令,以避免“过度链接”,由于省略了这些库, 所以ld需要-rpath-link-rpath来定位这些被省略的库。


你写道“但是,为什么是'-rpath-option'”,但我认为你指的是'-rpath-link',但由于有太多待处理的编辑,我目前无法进行修改。 - frog.ca

7
您的系统通过 ld.so.conf, ld.so.conf.d 和系统环境变量 LD_LIBRARY_PATH 等,提供了系统范围内的库搜索路径,当您构建标准库时,已安装的库会通过 pkg-config 信息等进行补充。当库位于定义的搜索路径中时,将自动按照标准库搜索路径进行搜索,从而找到所有所需的库。
对于您自己创建的自定义共享库,不存在标准的运行时库搜索路径。您需要在编译和链接时通过 -L/path/to/lib 参数指定库的搜索路径。对于位于非标准位置的库,可以在可执行文件的头部(ELF 头)中选择性地放置库搜索路径,以便可执行文件能够找到所需的库。 rpath 提供了一种在 ELF 头中嵌入自定义运行时库搜索路径的方法,以便能够找到自定义库,而不必每次使用时都指定搜索路径。这也适用于依赖其他库的库。如您所发现的那样,不仅需要按照命令行上指定的库的顺序进行链接,还必须为每个链接的依赖库提供运行时库搜索路径或 rpath 信息,以使头部包含运行所需的 所有 库的位置。 ld 的工作方式就是自动查找共享对象。在 man ld 中可以看到,"-rpath" 选项还用于定位链接中显式包含的共享对象所需的共享对象…如果在链接 ELF 可执行文件时没有使用 -rpath 选项,则会使用环境变量 "LD_RUN_PATH" 的内容(如果已定义)。在您的情况下,由于 liba 不在 LD_RUN_PATH 中,因此在编译可执行文件时,ld 需要一种能够找到 liba 的方式,可以使用上文提到的 rpath,或提供对其的显式搜索路径。
对于第二个问题,“include it in the link” 实际上意味着将库加入链接过程中。这是因为编译器需要检查库是否存在及其依赖关系,并将这些信息嵌入到最终的可执行文件头部中,在运行时进行链接。如果编译器检查不到库的存在,将无法生成可执行文件。因此,ld 确认库的存在并将其包含到链接中,以便在运行时正确地链接库和可执行文件。

不,回到ld的语义。为了生成一个"好的链接"ld必须能够找到所有的依赖库。否则ld无法保证良好的链接。运行时链接器必须查找和加载,而不仅仅是查找程序所需的共享库。ld不能保证这将发生,除非在程序链接时ld本身可以找到所有需要的共享库


1
是的,但-rpath-link选项不会在任何对象文件中插入任何RPATH标签。文档说:当使用ELF或SunOS时,一个共享库可能需要另一个共享库。当ld-shared链接将共享库作为输入文件之一时,就会发生这种情况。当链接器在进行非共享、非可重定位链接时遇到这样的依赖关系时,如果未显式包含它,则会自动尝试定位所需的共享库并将其包含在链接中。在这种情况下,-rpath-link选项指定要搜索的第一组目录。 - Troels Folke
1
我猜链接可执行文件是一个“非共享的、不可重定位的链接”。我的问题主要是为什么ld必须“自动尝试定位共享库”(liba.so)并“将其包含在链接中”。其次,“将其包含在链接中”真正意味着什么。对我来说,它似乎只是意味着:“确认它的存在”(liba.so),因为libb.so的ELF头没有被修改(它们已经有了针对liba.so的NEEDED标记),而exec的头只声明了libb.so作为NEEDED。为什么ld要关心找到liba.so,它不能把这个任务留给运行时链接器吗? - Troels Folke

1
您实际上并没有告诉ld(在将libbliba链接时) liba在哪里 - 只是它是一个依赖项。快速运行ldd libb.so会显示找不到liba
由于这些库可能不在您的链接器搜索路径中,因此在链接可执行文件时会出现链接器错误。请记住,当您链接liba本身时,libb中的函数仍未解析,但是ld的默认行为是不关心DSO中未解析的符号,直到您链接最终的可执行文件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接