解决Linux中依赖库文件版本过低的问题

去年上半年我偶然间在网上看到了一篇介绍Manjaro-Deepin的文章,当时就被那漂亮的UI给吸引住了——原来Linux还可以这么好看。以至于后来换笔记本时,就执意要买X1 Carbon装Manjaro用。其实当时也不是很清楚Debian和Arch的区别,但就这么误打误撞地用上了Arch。如今使用Manjaro-Deepin作为日常工作娱乐的系统已经一年有余了,不得不说,强大的AUR和滚动更新是真的爽。不过由于滚动更新而造成的故障也成了家常便饭。比如我这个系统,因为更新而导致无法进入图形界面的事情就出现了四五次了。从第一次遇到宕机时还慌得要命,到现在再遇到这种情况时淡定地处理,这种故障处理的经验让我更加了解Linux,相比之下用Ubuntu反而显得死气沉沉。今天来谈谈Linux软件依赖库文件不兼容的问题。


背景

一直以来我都使用SendAnywhere(一款很好用的跨平台文件传输软件)跨越不同的设备来传输文件。后来有一次随着pango版本更新到1.44后,SendAnywhere就再也无法启动了。原因是SendAnywhere依赖低于1.44版本的pango库文件。这时候最理想的解决办法应该是SendAnywhere也随之做出更新,但谁让Linux用户太小众,开发商愿意做出Linux版本的软件就已经很善良了,保持更新的确要求过高了。

一开始我选择了一个最直接的办法——降级pango——简单却一点都不优雅,因为还有许多其他的软件依赖新版本的pango,降级难免埋下一些潜在的问题。后来参考了SendAnywhere的package page(这里就要吹一波arch的文档和社区之完整了,用户可以就软件出现的问题在软件的package page下留言讨论),有人找到了导致该bug的关键库文件,修改了PKGBUILD文件,重新编译后解决问题。

这是我第一次遇到库文件不兼容的例子。虽然当时解决了问题,但其实并不是很理解背后的原理。这也就引出了昨天再一次遇到的类似问题——

这两天在写论文,看到有人推荐writefull这个工具,于是就想下载来用用。但启动应用后又遇到了pango不兼容的问题。第一反应还是去package page看看有没有解决办法,总不能再去降级pango吧。遗憾的是只看到有人也遇到了同样的问题,却没人给出现成的解决办法。幸运的是后来搜到了其他软件也因为pango更新而出现异常的问题,于是依葫芦画瓢再一次顺利解决了问题。

不过这一次我没有得过且过。一方面是好奇为什么人家的方法能work,另一方面是不愿意再吃没技术的亏了,毕竟这样的问题已经出现两次了,事不过三,很有必要把原理搞清楚。

共享函数库

其实这个问题涉及到Linux共享函数库的加载。

首先要知道程序函数库可分为3种类型:静态函数库(static libraries)、共享函数库(shared libraries)、动态加载函数库(dynamically loaded libraries):

  1. 静态函数库是在程序执行前就加入到了目标程序中 。静态函数库的好处是允许程序员把程序链接起来而不用重新编译代码。虽然节省了一些编译时间,但代价是程序的体积较大,而且如今计算机的速度越来越快,重新编译也花不了多少时间。所以静态库以前用的比较多,现在很少用了;
  2. 动态函数库同共享函数库是一个东西(在linux上叫共享对象库,shared object, 文件后缀是.so ,windows上叫动态加载函数库, 文件后缀是.dll)。共享函数库在可执行程序启动时才被加载。共享的意思就是所有的程序在重新运行的时候都可以自动加载最新的函数库中的函数。

共享库的命名

每个共享函数库都有三个名字,分别称作“realname”、“soname”、“linkname”。我以下面这个共享库为例进行说明:

[harlin@harlin-pc ~]$ ls /usr/lib/libpango* -lh
lrwxrwxrwx 1 root root   17  2月 11 11:20 /usr/lib/libpango-1.0.so -> libpango-1.0.so.0
lrwxrwxrwx 1 root root   24  2月 11 11:20 /usr/lib/libpango-1.0.so.0 -> libpango-1.0.so.0.4400.7
-rwxr-xr-x 1 root root 306K  2月 11 11:20 /usr/lib/libpango-1.0.so.0.4400.7

共享库的命名规则如图所示:

  • realname是共享函数库真正的名字,在本例中是libpango-1.0.so.0.4400.7,它是包含真正库函数代码的文件。realname由一个主版本号,一个次版本号,以及一个发行版本号组成(最后一个发行版本号是可选的)。主版本号代表当前共享库的版本,如果共享库的接口有变化,那么这个版本号就要加1;后面的次版本号和发行版本号则给出了详细的信息。
  • soname是程序加载时寻找共享库所用到的文件名,在本例中是libpango-1.0.so.0。相比于realname,soname只包含主版本号。
  • linkname顾名思义就是在编译过程链接阶段用到的文件名,在本例中是libpango-1.0.so。它不包含任何版本信息,其作用是将soname 和real name关联起来。

那么为什么程序加载时依赖的是soname,而不是linkname或realname?这主要是出于方便兼容的考虑。

因为Linux的动态库的命名格式是libname.so.x.y.z,最后一个z版本的变动一定是兼容的,y版本升级一般向前兼容,所以这个y和z不能写死。x版本变动一般是不兼容升级。所以使用soname是最为合理的。

举个例子: 有一个程序app,它依赖于的库是libtest.so.1.0,即app启动的时候需要libtest.so.1.0。 如果链接的时候直接把libtest.so.1.0传给app,那么将来库升级为libtest.so.1.1的时候,app仍然只能使用libtest.so.1.0的代码,并不能得到升级的好处。 为了解决这个问题,我们在开始时我们建立一个链接:ln -sf libtest.so.1.0 libtest.so.1,指定soname为libtest.so.1。于是app在启动时会通过soname(libtest.so.1)找到realname(libtest.so.1.0)。 在库升级后,我们重新链接:ln -sf libtest.so.1.1 libtest.so.1 ,这时app启动时通过soname查找到的就是libtest.so.1.1。 这样app不需要任何变动就能享受升级后的库的特性了。而libtest.so.1.0,libtest.so.1.1可以同时存在于系统内,不必非得把libtest.so.1.1的名字改成libtest.so.1.0,因为其他应用也许用到的依然是libtest.so.1.0。

简言之,soname的便利之处在于,当共享库进行小的版本升级时,只要将新的库文件链接到原来的soname即可,而相关程序无需任何修改,继续使用原有的soname就行。只有在遇到大的版本升级时才需要修改程序需要加载的共享库的soname。

共享库的搜索路径

知道了共享库是什么之后,还剩下的一个问题就是程序该去哪里找需要的共享库。

  • 如果设置了环境变量LD_LIBRARY_PATH,那么最先查找的是就是LD_LIBRARY_PATH指定的查找路径;
  • 可执行文件中硬编码的运行时搜索路径rpath,这是写在可执行文件header中的路径;
  • 缺省搜索路径是在配置文件/etc/ld.so.conf中指定动态库搜索路径和/lib/usr/lib

温故知新

所以现在再回过头来看这两次库文件不兼容的故障,其实问题出在了pango更新到1.44这一版本后接口发生了变化,导致一些像SendAnywhere、WriteFull这些没有及时做出更新的软件所依赖的pango共享库被链接到1.44版本(也有可能是所链接的共享库被新的所覆盖),从而无法启动。 当时我知其然而不知其所以然的解决办法,其实所做的就是让程序依然链接到旧版的pango库文件。

例如SendAnywhere中pango库文件版本过旧的问题,当时那位用户给出的解决办法是修改PKGBUILD文件,其中关键的修改是:

+    # fix core dump cause by pango 1.44+
+    install -Dm644 "$srcdir/usr/lib/libpango-1.0.so.0.4300.0" "$pkgdir/opt/SendAnywhere/libpango-1.0.so.0"
+    install -Dm644 "$srcdir/usr/lib/libpangocairo-1.0.so.0.4300.0" "$pkgdir/opt/SendAnywhere/libpangocairo-1.0.so.0"
+    install -Dm644 "$srcdir/usr/lib/libpangoft2-1.0.so.0.4300.0" "$pkgdir/opt/SendAnywhere/libpangoft2-1.0.so.0"

他其实是把旧版pango的相关库文件复制到SendAnywhere的安装路径中。这样一来尽管/usr/lib路径中的相关pango库文件是最新的,但当程序启动时(这里应该是共享库搜索路径写在了rpath中的情况),将首先在安装路径中搜索相关库文件,于是程序实际上加载的是旧版的pango库文件。

所以Linux软件依赖库文件不兼容问题很好解决,只要有旧版兼容的库文件,然后再设置好库文件的搜索路径即可。

沿着这个思路,还可以这么做:

  1. 直接在终端中通过LD_LIBRARY_PATH=/path/to/old/lib /path/to/app启动应用。 第一个路径是旧版本库文件的位置,第二个路径是应用位置。其含义是让应用启动时加载指定的旧版库文件,即搜索LD_LIBRARY_PATH的情况;
  2. 在程序的安装路径中创建/lib文件夹,再将旧版库文件文件放入其中,就可直接启动应用,这里应该也是共享库文件写在了rpath中的情况;
  3. 稍微麻烦一些,将旧版库文件放入缺省搜索路径中,如/usr/lib,然后将库文件的realname与程序的soname建立链接。

    后来我赶紧注册了一个Arch User Repository的账号,把解决writefull依赖库不兼容的方法写到了comments里,为社区做一点微小的贡献:)




    参考资料
    [1]. Shared Libraries
    [2]. linux下动态库中的soname
    [3]. Linux动态库soname的使用
    [4].Linux共享库、静态库、动态库详解
    [5].Where do executables look for shared objects at runtime?