Ubuntu18下GCC-7使用Runpath处理二级依赖的问题咨询
我完全明白你遇到的这个困扰——在Ubuntu 18.04的GCC 7环境下,明明二级依赖库和主共享库在同一个目录,而且可执行文件的runpath已经指向了这个目录,但加载时就是找不到二级依赖。这本质上是rpath和runpath的行为差异导致的,而GCC 7默认启用了新的dtags(也就是默认用runpath代替rpath),这和Ubuntu 16.04的GCC 5行为不一样。
问题根源:Runpath的查找规则
先搞清楚两者的核心区别:
- rpath:是全局继承的——可执行文件的rpath会被所有依赖的共享库继承,用来查找它们自己的依赖项。这就是GCC 5默认的行为,所以你的例子在Ubuntu 16上能正常工作。
- runpath:是"私有"的——只有当前ELF文件的runpath会被用来查找它的直接依赖。也就是说,可执行文件的runpath只负责找它直接链接的
libprimary.so,而libprimary.so要找它的依赖libsecondary.so时,只会用libprimary.so自己的runpath(如果有的话),不会继承可执行文件的runpath。
在你的测试案例中,编译libprimary.so时只加了-L$(pwd)(编译时的查找路径),但没有设置任何runpath/rpath。所以libprimary.so本身的动态段里没有RUNPATH或RPATH标签,当加载器需要它找libsecondary.so时,只会去系统默认路径或LD_LIBRARY_PATH里找,完全不会用到可执行文件的runpath。
正确使用Runpath处理二级依赖的方案
要解决这个问题,我们需要给每个共享库也设置对应的runpath,让它们知道去哪里找自己的依赖。这里有两种靠谱的方式:
1. 编译共享库时添加自身的Runpath
在编译libprimary.so的时候,加上-Wl,-rpath=$(pwd)参数,让它的runpath指向自己所在的目录(也就是libsecondary.so的位置):
# 编译libsecondary.so gcc -shared -o libsecondary.so -fPIC ../secondary.cpp # 编译libprimary.so时添加runpath gcc -shared -o libprimary.so -fPIC ../primary.cpp -lsecondary -L$(pwd) -Wl,-rpath=$(pwd)
这样libprimary.so的动态段里会有自己的RUNPATH标签,当加载器加载它时,就会用这个路径去查找libsecondary.so,而两者在同一目录,自然能找到。
2. 使用$ORIGIN实现相对路径的Runpath(更推荐)
如果你的程序需要在不同目录移动,硬编码绝对路径的runpath会失效。这时候可以用链接器的$ORIGIN变量,它表示当前ELF文件所在的目录:
- 编译
libprimary.so时,设置runpath为当前目录(即$ORIGIN):
gcc -shared -o libprimary.so -fPIC ../primary.cpp -lsecondary -L$(pwd) -Wl,-rpath='$ORIGIN'
注意这里的单引号很重要,避免shell解析$ORIGIN变量。
- 编译可执行文件时,用
$ORIGIN指向相对路径的库目录:
gcc -o app ../main.cpp -L../build -lstdc++ -lsecondary -lprimary -Wl,-rpath='$ORIGIN/../build'
这样不管你把整个项目目录移动到哪里,只要库和可执行文件的相对关系不变,加载器都能正确找到所有依赖。
验证解决方案
修改编译命令后,你可以用readelf -d libprimary.so查看它的动态段,会看到类似这样的RUNPATH条目:
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN]
再执行ldd app,就能看到libsecondary.so被正确找到了:
libsecondary.so => <some_path>/build/libsecondary.so (0x00007f9e86fe7000)
为什么不推荐--disable-new-dtags?
虽然这个参数可以让链接器回到rpath的行为,但rpath确实已经被标记为弃用,未来的链接器版本可能会逐步移除对它的支持。而且runpath的设计更符合模块化的思想——每个共享库自己管理自己的依赖查找路径,避免全局路径带来的冲突问题。
内容的提问来源于stack exchange,提问作者Stefan K.

