Random Tech Thoughts

The title above is not random

为 Linux 程序打包

最近有个项目需要把编译好的 Linux 程序打包后安装到多种 Linux 发行版上执行。由于是不同的发行版,所以不适合使用各个发行版自己的软件包格式。即使针对特定发行版,还是会因为不同版本的系统库版本不同而无法创建通用的软件包。(程序既需要安装到 Fedora 6 这样“古老”的版本上,也需要安装到这两年发布的发行版上。)比较几种解决方案后最终选择了打包开发系统上的动态链接库,用脚本指定 dynamic linker 和动态链接库目录的方式来执行打包后二进制文件的方案。

最先考虑的是静态链接,但开发用的 Ubuntu 并没有提供所有依赖库的静态链接版本。自己重新编译所有的库比较麻烦,所以没有用这个方案。

LD_LIBRARY_PATH 和 dynamic linker

之后尝试过 CDE (后面会具体介绍),由于一些限制最后还是决定自己打包,这样不依赖其他工具,可以获得完全的控制。最开始的时候以为只要简单的把程序依赖的动态链接库找出来全部放到一个目录下,在其他系统上指定 LD_LIBRARY_PATH 即可正常使用,但实际上这么做还差了一步。

只指定 LD_LIBRARY_PATH 时遇到了两种错误:

  • 为了让编译得到的 binary 文件同时工作在 32/64-bit 的系统上,我在 32-bit Ubuntu 上编译。拷贝 binary 到 64-bit 的系统上执行时遇到了如下错误:

    /lib/ld-linux.so.2: bad ELF interpreter: No such file or directory

  • 32-bit 的 Fedora 6 上在设置了 LD_LIBRARY_PATH 之后,执行任何程序都会段错误

Google 之后发现 Stack Overflow 上也有人遇到过第二个问题。其实这两个错误的原因都跟 dynamic linker (dynamic linking loader) 有关。

在使用 GNU C 库的系统上,运行 ELF 文件时会自动执行 dynamic linker (在 32-bit Linux 上一般是 /lib/ld-linux.so.2,64-bit 上是 /lib/ld-linux-x86-64.so.2),dynamic linker 会读取 LD_LIBRARY_PATH 然后在指定的目录下找 shared library。但 dynamic linker 自身的路径是硬编码在 ELF 文件头中的。

所以 64-bit 系统上报的错误真正的含义是找不到 dynamic linker,因为该系统上并没有安装 32-bit 的 C 库。第二个错误应该是由于 Fedora 6 上的 dynamic linker 和我打包的 Ubuntu 的 C 库不兼容。(Stack Overflow 上的回答里说 loader 是 C 库的一部分,不过不兼容的具体原因不清楚。)

解决这个问题的办法是指定程序启动使用的 loader,调用方式如下:

LD_LIBRARY_PATH=<lib path> /lib/ld-linux.so.2 <executable>

/lib/ld-linux.so.2 由于是 dynamic linker,所以支持这样的调用方式。但其实通过 LD_LIBRARY_PATH 环境变量来指定 library search path 有一个缺点:进程会带有这个环境变量。因此进程在尝试执行系统上的其他命令时可能遇到上述的第二个问题。事实上应该尽可能避免使用 LD_LIBRARY_PATH 这个环境变量,参考 Why LD_LIBRARY_PATH is bad

好在 dynamic linker 可以使用参数来指定 library search path,如下:

/lib/ld-linux.so.2 --library-path <lib path> <executable>

给 dynamic linker 指定参数后就会忽略 LD_LIBRARY_PATH

知道 dynamic linker 的问题之后,打包剩下的工作就是修改脚本来用 dynamic linker 启动可执行文件了。

编译时指定 library path 和 dynamic linker

如果发布的程序经常要在 shell 里交互使用,每次执行时指定 dynamic linker 还是会很不方便。实际上 dynamic linker 和 library path 信息都保存在 ELF header 中的,程序在链接时可以指定这些信息。

library path 可以通过传递 --rpath=<path list> 参数给链接器来指定,dynamic loader 则是通过 --dynamic-linker=<path to loader> 来指定。如果是由 gcc 来调用 linker,可以通过 -Wl, 来把选项传递给 linker,如下:

gcc ... -Wl,--rpath=<path list> -Wl,--dynamic-linker=<path to loader>

可以使用 readelf -a <executable> 来查看得到的 ELF 文件头信息,其中 INTERP 指定的就是 dynamic linker,而 RPATH 就是 library search path。

因为不想修改 configure 脚本,所以我最后没有采用这种方式。

CDE

其实 Linux 下打包程序的问题以前就有人尝试解决过。前段时间读了 Philip J. Guo 的 The Ph.D. Grind,他博士期间的一项工作 Code Data Environment (CDE) 就是用来为应用程序创建一个自包含的包,然后发布到其他 Linux 系统上去执行。扫过一眼 CDE 的论文,其最主要的工作机制是使用 ptrace 来拦截系统调用,从而捕获程序依赖的文件以及系统环境(例如环境变量)。简单起见只考虑程序依赖的文件,CDE 工作其原理如下:

  • 在创建包的时候,执行程序同时拦截 open 这样的系统调用来看应用程序使用到了哪些文件,拷贝或者链接到打包目录下
  • 在部署的系统上执行时,同样拦截 open 调用,然后将其尝试打开的文件用打包目录下的文件来替代

这个原理简单有效,CDE 实际使用下来我觉得也非常方便,10 分钟内我就成功的把一台 Fedora 6 上的 vim 安装到另一台 Debian 6 上并且正常使用了。对某些需求来说,CDE 的确可以作为解决 Linux dependency hell 一个方便的工具。

然而 CDE 打包后的程序是在 sandbox 中执行,不能访问 sandbox 之外的文件,除非使用 seamless execution mode。但要让 seamless execution mode 正常工作比较 tricky,CDE 对我来说毕竟是新工具,担心了解不够需要花太多时间 debug 所以最后没有采用。(CDE 执行应用程序时需要使用 ptrace 监控程序,对程序执行性能会有一点影响,不过影响不大,对我的应用来说性能不是主要问题。)

Shared Library 相关资料

Program Library HOWTO,这是一篇不错的介绍 shared library 的文章。这里是我做的一些笔记

Comments