最近都在折腾 Python ,免不了要安装几个不同版本的执行环境。Python 的主程 序可以选择使用静态链接或者动态链接——默认是静态链接的;本来静态链接的 Python 也没什么问题,但是在别的程序里嵌入 Python 的时候用动态链接就会比 较方便。
我的系统里有其他版本的 Python ,所以我希望每个版本都有自己的 prefix , 这样不同版本之间就不会有干扰。但是动态链接的 Python 使用自定义安装路径 时有个问题: Python 的主程序找不到 libpython*.so ,因为这个 so 不在标准 的 linker 搜索路径里。
Linker 寻找动态库的顺序
既然问题是 linker 找不到动态库,我们就先看看它寻找的过程。我这用的是 Linux ,默认用的是 GNU linker ,而根据 manpage , GNU linker 的搜索 顺序是酱的:
- 搜索 ELF 文件内 .dynamic 区段的 RPATH 字段指定的目录;
- 搜索环境变量 LD_LIBRARY_PATH 指定的目录;
- 搜索 ELF 文件内 .dynamic 区段的 RUNPATH 字段指定的目录;
- 搜索 /etc/ld.so.cache 文件中的索引——这个索引是由 ldconfig 根据 /etc/ld.so.conf 的内容生成的;
- 搜索默认目录 /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 :
1 | gcc -L/foo/lib/location -lfoo -Wl,--rpath='/foo/lib/location' source.c -o binary |
可以用 readelf 检查 RPATH 的值是否正确:
1 | readelf -d binary |
输出是这样的:
1 2 3 4 5 6 7 8 | 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 的变量替换。例如,要指定相对于可执行 文件的路径,可以这样干:
1 | 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 选项:
1 2 | 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:
1 | 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 的选项:
1 | 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 选项:
1 | gcc -L/foo/lib/location -lfoo -Wl,--rpath='$ORIGIN/../lib',--enable-new-dtags source.c -o binary |