4

GNU Linker 和 RPATH

 2 years ago
source link: https://blog.theerrorlog.com/the-gnu-linker-and-rpath-zh.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近都在折腾 Python ,免不了要安装几个不同版本的执行环境。Python 的主程 序可以选择使用静态链接或者动态链接——默认是静态链接的;本来静态链接的 Python 也没什么问题,但是在别的程序里嵌入 Python 的时候用动态链接就会比 较方便。

我的系统里有其他版本的 Python ,所以我希望每个版本都有自己的 prefix , 这样不同版本之间就不会有干扰。但是动态链接的 Python 使用自定义安装路径 时有个问题: Python 的主程序找不到 libpython*.so ,因为这个 so 不在标准 的 linker 搜索路径里。

Linker 寻找动态库的顺序

既然问题是 linker 找不到动态库,我们就先看看它寻找的过程。我这用的是 Linux ,默认用的是 GNU linker ,而根据 manpage , GNU linker 的搜索 顺序是酱的:

  1. 搜索 ELF 文件内 .dynamic 区段的 RPATH 字段指定的目录;
  2. 搜索环境变量 LD_LIBRARY_PATH 指定的目录;
  3. 搜索 ELF 文件内 .dynamic 区段的 RUNPATH 字段指定的目录;
  4. 搜索 /etc/ld.so.cache 文件中的索引——这个索引是由 ldconfig 根据 /etc/ld.so.conf 的内容生成的;
  5. 搜索默认目录 /lib 和 /usr/lib (又或者 /lib64 和 /usr/lib64 )。

一般的程序或者库文件都是安装到 /lib 或者 /usr/lib (又或者 lib64 )下, 不然就是在 /etc/ld.so.conf 里添加路径,所以 linker 肯定能找到。而我 习惯将 Python 执行环境安装到 $HOME 下,在全局配置里加入我的用户目录似 乎怪怪的,而每次执行程序都要加上 LD_LIBRARY_PATH 又很麻烦而且可能会对 其他程序造成影响……剩下的选项就只有 1 和 3 了。

设置 RPATH

这是一个 ld 命令的选项,只需要在用 gcc 编译的时候像这样加入 --rpath :

gcc -L/foo/lib/location -lfoo -Wl,--rpath='/foo/lib/location' source.c -o binary

可以用 readelf 检查 RPATH 的值是否正确:

readelf -d binary

输出是这样的:

Dynamic section at offset 0xe08 contains 26 entries:
Tag        Type                         Name/Value
0x0000000000000001 (NEEDED)             Shared library: [libfoo.so]
0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
0x000000000000000f (RPATH)              Library rpath: [/foo/lib/location]
0x000000000000000c (INIT)               0x400548
0x000000000000000d (FINI)               0x400744
....

可见 Library rpath 的值已经被设置为 /foo/lib/location 了。这时如果执行 binary 的话, linker 就会先到 /foo/lib/location 寻找 libfoo.

另外,对于已经编译好的二进制文件,可以用一个叫 patchelf 的小工具修改 RPATH.

在 RPATH 中使用相对路径

上面我们设置的 RPATH 是绝对路径,如果我们想要打包一个“绿色版”的程序,扔到 任何位置都能执行呢?那当然是把所有动态库和可执行文件打包到一起,然后将 RPATH 设置为相对路径了。不幸的是,动态连接器在解析 RPATH 中相对路径的时 候,并不是以可执行文件所在目录为准,而是以当前目录为准的。我们总不能在 每次执行程序之前都 cd 到程序目录去吧?

为此, GNU linker 提供了针对 RPATH 的变量替换。例如,要指定相对于可执行 文件的路径,可以这样干:

gcc -L/foo/lib/location -lfoo -Wl,--rpath='$ORIGIN/../lib' source.c -o binary

执行 binary 时,这里的 $ORIGIN 变量会被 linker 替换为 binary 所 在的路径;如果将 binary 和动态库一起移动到别的位置再执行, linker 也能 通过相对位置找到对应的动态库。

在各种编译环境下设置 RPATH

Configure 脚本

回到 Python 动态链接的问题上, cpython 编译使用 configure 脚本,所以可以 用 LDFLAGS 传递 linker 选项:

export PY_PREFIX=$HOME/py/py35
./configure --prefix=$PY_PREFIX --enable-shared LDFLAGS="-Wl,--rpath='\$\$ORIGIN/../lib'"

这里的 --rpath 选项会被写入到 Makefile 里,所以 $ORIGIN 前面要再加一个 $ 以 进行转义.

CMake

cmake 命令提供了专门的变量 CMAKE_INSTALL_RPATH 用以指定安装后二进制文 件的 RPATH:

cmake /path/to/project -DCMAKE_INSTALL_RPATH="'\$ORIGIN/../lib'" ....

为什么说是“安装后”呢?因为 cmake 很贴心地提供了两种 RPATH 设置: 1)为了让编译好的程序能直接在编译目录执行, cmake 会根据编译时的选项 自动设置 RPATH ,使得处于编译目录中的动态库能被找到; 2)在执行 make install 时,如果有指定 CMAKE_INSTALL_RPATH , cmake 会将 RPATH 更改为相应的值,让程序转而使用安装好的动态库。

当然第一种 RPATH 也是可以手动设置的,详见 cmake 文档

Boost.Build

呃, 这个东西 除了 Boost 大概也没有别的项目在用了吧……问题是你总会 遇到那么几个情况是需要自己编译 Boost 库的……

跟 cmake 类似, b2 命令本身提供了指定 RPATH 的选项:

b2 dll-path="'\$ORIGIN/../lib'" ....

RPATH 和 RUNPATH 的区别

前面说的都是 RPATH ,那 RUNPATH 呢?呃,它俩的功能基本上是一样的,除 了上面提到的搜索顺序: RUNPATH 指定的路径可以被 LD_LIBRARY_PATH 覆盖, 但是 RPATH 指定的路径是优先级最高的。在 RUNPATH 字段存在的情况下, RPATH 字段会被忽略。

貌似 RPATH 的 历史 比 RUNPATH 要久远,所以支持 RPATH 的工具比较多。 但是 RUNPATH 比 RPATH 灵活,因为可以被环境变量覆盖。

GNU linker 在只指定 --rpath 选项的情况下默认只设置 RPATH 字段,要设置 RUNPATH 字段的话,还需要指定 --enable-new-dtags 选项:

gcc -L/foo/lib/location -lfoo -Wl,--rpath='$ORIGIN/../lib',--enable-new-dtags source.c -o binary

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK