为什么说LD_LIBRARY_PATH不好

对于一个Unix系统管理员来说,频繁使用LD_LIBRARY_PATH是不好的。

LD_LIBRARY_PATH有什么用

LD_LIBRARY_PATH是一个环境变量,它的作用是让动态链接库加载器(ld.so)在运行时(run-time)有一个额外的选项,即增加一个搜索路径列表。这个环境变量中,可以存储多个路径,用冒号分隔。它的厉害之处在于,搜索LD_LIBRARY_PATH所列路径的顺序,先于嵌入到二进制文件中的运行时搜索路径,也先于系统默认加载路径(如/usr/lib)。

出于安全原因,对于已经设置setuid或setgid的可执行文件,LD_LIBRARY_PATH则被完全忽略。从而让LD_LIBRARY_PATH的用处大打折扣。

为什么会有LD_LIBRARY_PATH

LD_LIBRARY_PATH的需求有很多:

  • 测试新链接库与原版的差别(比如后向兼容或新功能测试)
  • 当你想将链接库转移到别的位置时,暂时地测试一下

然而,一个并不招人喜欢的副作用是,LD_LIBRARY_PATH在链接阶段也会被搜索,顺序位于-L所指定的目录之后(没有-L选项当然也会搜索)。

所以,好的使用LD_LIBRARY_PATH的例子应该是:

  • 升级共享库时,替换之前先测试一下
  • 类似的,升级后的某个程序可能依赖于一些动态链接库,如果你将某个链接库替换了,程序可能就无法工作了。这时候,你可以使用LD_LIBRARY_PATH指向存有备份的一个目录,然后,你可以没有顾忌地替换系统版本了。万一出错,拷贝回去就是了。
  • X11在构建过程中使用LD_LIBRARY_PATH。X11将其字体发布为bdf格式,而构建过程中,则需要将bdf编译成pcf格式。LD_LIBRARY_PATH用来指向构建时的lib目录,使得bdftopcf程序能够顺利运行,直到动态链接库安装。
  • Perl将其大部分核心代码封装为一个共享库。这使得Perl便于嵌入到其它程序中去,因为应用程序在链接时可以使用动态库,从而在运行时节约了空间。但是Perl构建和安装时频繁使用Perl脚本,而作为脚本解析器的可执行文件"perl"找不到动态链接库是无法执行的,这时候就需要用到LD_LIBRARY_PATH来引导这个过程。

LD_LIBRARY_PATH是如何犯错的

当已经失误(未能在编译时嵌入路径),人们常用LD_LIBRARY_PATH来补救。一些程序(甚至是商业程序)常常在编译时没有考虑任何运行时目录,于是用户不得不使用LD_LIBRARY_PATH,否则程序就运行不了。

LD_LIBRARY_PATH就是这样一个狡猾的家伙,一旦它成了一个全局的变量,用户就会一直依赖于它。然后,当LD_LIBRARY_PATH需要修改或删除时,大规模破坏就会发生。

共享库加载器是怎样工作的

SunOS 4.x使用主(major)版本和次(minor)版本。比如,软件库Xt的名字会类似于libXt.so.4.10,它的主版本是4,次版本是10。如果你更新这个库(比如,修正了一个Bug),你可能会安装库libXt.so.4.11,这时程序会自动使用新的版本。为了实现这一点,加载器必须对每一个加载目录都执行一次readdir()函数,然后解析正确的文件名。如果目录中文件很多,或包含链接,或访问NFS网络文件系统,这个操作是非常费时的。

Linux,SunOS 5.x和大部分其它SYSV变种仅使用主版本。软件库Xt的名字类似于libXt.so.4。Linux的库文件名迷惑性地包含了次版本,但是,总会有一个链接指向真正的库文件,该链接名是不包含次版本的。比如,库libXt.so.6实际上是libXt.so.6.0的链接。链接器/加载器实际上搜索的是libXt.so.6。

除了不考虑基于次版本的更新(管理员自己负责更新),加载器的工作本质上还是一样的,只不过,只执行一次stat()函数,因而速度更快。

编译时和运行时路径分家前的黑暗岁月

现在,你可以在链接时为一个可执行文件指定运行时路径,语法是为ld指定-R(或-rpath)选项。你还可以使用LD_RUN_PATH环境变量,功能和-R一样。

但是,此前,你只能使用-L,它不仅影响编译时,还影响运行时。没有"编译时使用一个路径而在运行时使用另一个路径"的说法。因为这个限制,当时存在些一些相当有名的失败模式。比如,你在自动挂载的NFS目录中构建X11R6(/home/snoopy/src)。X11R6由共享库和可执行文件构成。这些可执行文件是在构建树中而非在最终安装位置决定依赖于哪些库的。由于链接器必须在编译时解析代码符号,因此必须使用-L选项同时指定链接时路径和最终的运行时路径(比如/usr/local/X11R6/lib)。结果是,使用这些共享库的程序必须先在/home/snoopy/src搜索,然后再在正确的位置(比如,/usr/local/X11R6/lib)搜索。不是每次启动X11R6,NFS都会自动挂载构建目录(/home/snoopy/src)。很有可能,多年以前那个临时构建目录就已经被删除了,加载器仍然却尝试去那儿搜索。更糟糕的是,若snoopy用户挂了或不复存在,那么所有X11R6程序都无法再运行。太无赖了!!! 令人高兴的是,对于拥有现代链接器/加载器的操作系统的你来说,这些都已成往事。此外,解决这个问题的另外一个方法是在-L选项中将运行时目录置于链接时目录之前。

不幸案例1

根据真实故事改编。

我遇到的第一次这一类状况来自于SunOS 4.x下的OpenWindows。 由于一些混帐原因,几个OpenWindows程序在编译时没有嵌入正确的运行时加载器路径,用户被迫一直使用LD_LIBRARY_PATH。强调一下,是一直在使用。也就是在全局的OpenWindows的启动脚本中系统自动将LD_LIBRARY_PATH设置为($OPENWINHOME/lib)。

那好,这有什么大不了的? 但是,碰巧的是,同一台机器上刚好曾经编译过X11R4,位于/usr/local/X11R4中。这样,用户会摸头不知脑,因为运行的X11R4程序使用的居然是OpenWindows在/usr/openwin/lib中的共享库,而非/usr/local/X11R4/lib/。X11R5以及随后的X11R6问世之后,用户也只会变得更迷惑,因为此时已经有同一个共享库的4个不同版本,它们很可能相互不兼容。

呵呵,对于这种情况,你该怎么办? 如果你让LD_LIBRARY_PATH首先指向OpenWindows,最好的情况是拖慢系统速度(因为大多数人使用的是X11R5和X11R6,搜索/usr/openwin/lib只是浪费时间)。最坏的情况是生成莫名其妙的警告("ld.so: warning: libX11.x.y has older revision than expected z"),或者导致程序由于不兼容而崩溃。如果你尝试编译X应用程序而忘记使用-L选项,你同样会感到迷惑。

当时,我是怎么办的呢? 我用emacs打开几个OpenWindows的程序,将其错误的运行时路径修改为/usr/openwin/lib(事实上,这些程序是用系统补丁修改的,而打补丁的同志们的系统环境和FCS那帮人不一样)。然后,我修改了所有的启动脚本,删除了setenv LD_LIBRARY_PATH语句。我甚至在自己的.cshrc中添加unsetenv LD_LIBRARY_PATH来做测试。

不幸案例2

根据真实故事改编。

由于版权问题,商业程序通常会带有一个二进制Motif库的副本。Motif是一个商业软件库,并不是每一个操作系统都附带,但却是很多商业程序的常用工具。因为Motif是一个不断更新的产品,得到很好的维护,也常有新功能加入。

WidgetMan就是这样一个使用了Motif的软件。在它的启动脚本里,将LD_LIBRARY_PATH指向它的Motif库的副本,以便在运行时使用。但是,WidgetMan的设计用途是启动其它程序。不幸的是,当WidgetMan启动其它应用时,LD_LIBRARY_PATH的值会被继承。所以从WidgetMan启动的程序就杯具了,因为它们使用的是WidgetMan所包含的Motif库,而非系统的Motif库,这两者版本相同却互不兼容。我了个去!!!

想象一下,按照无良商业软件的指示去设置全局的LD_LIBRARY_PATH是一件多么危险的事。

无益的改进

一些操作系统(比如Linux)支持一个可配置的加载器。你可以通过修改/etc/ld.so.conf来配置运行时的搜索路径。这个办法几乎和修改LD_LIBRARY_PATH一样差劲。这个文件绝对不要随便修改,它里面只应该包含操作系统标准的库目录。

规范的LD_LIBRARY_PATH使用方法

  • 绝不要设置全局的LD_LIBRARY_PATH
  • 如果你一定要发布(ship)一个依赖于标准库的程序,而且要把它安装到非标准位置,按下面方法之一实施:
    • 发布二进制.o文件,在安装过程中使用正确的已安装的库来重新链接。
    • 发布可执行文件时,嵌入一个很长的虚假运行时库路径,在安装过程中,使用二进制编辑器将其替换为正确的库路径。
  • 如果你必须使用LD_LIBRARY_PATH,将其封装起来(如在局部的脚本中使用),而不至于影响其它程序。

一些软件包在标准目录中生成一些链接,指向真正的位置。这个办法行得通,但是并没有解决这个问题。如果你安装了两个版本怎么办? 更不用说很多厂商自作主张选择一些很愚蠢的位置作为"标准"路径(比如/或/usr)。这个办法也使得网络安装变得困难,因为即使你安装了一个程序到某个网络目录中,你还需要到此网络中每一台计算机中生成这样一个链接。

改进UNIX中LD_LIBRARY_PATH的实现的一些想法

  • 取消链接时对LD_LIBRARY_PATH的依赖。(Solaris中ld可以通过-i选项做到这一点)。人们懒于设置-L选项而直接使用LD_LIBRARY_PATH的事情太频繁发生了,这就导致了其它程序运行时的杯具。当然,一些人设置LD_LIBRARY_PATH是为了让某些程序正常运行,但是这却会影响那些没有正确使用-L选项者的正确链接。如果LD_LIBRARY_PATH只能影响运行时,则关系显得清晰多了。如果有必要的话,发明一些其它的环境变量来专门负责链接时路径的设置(类似于LD_LINK_PATH?)。
  • 操作系统发布时自带安全修改可执行文件运行库路径的工具。
  • 为ldd实现-s选项,使其能够打印某个程序的运行时路径。(Solaris的dump -Lv可以实现这个功能)。
  • Solaris 7有一个比较干净的想法。你可以在链接时指定一个运行时路径表达式,该表达式在运行时解析,然后得到最终的运行时路径。比如在链接时指定rpath为$ORIGIN/../lib,而$ORIGIN在运行时才会展开为安装路径。这样,即使你将安装树完全转移,它仍然可以正常工作。强烈要求其它操作系统也这样做。遗憾的是,至少在Solaris 7中,$ORIGIN被视为是一个相对路径(你可以破坏它,只要在同一个文件系统中有一个可写目录,因为UNIX甚至允许硬链接到一个setuid可执行文件),所以对于setuid/setgid二进制文件,$ORIGIN会被忽略。Sun公司在Solaris 8中修复了这个问题,你可以指定"可靠的"crle(1)路径。

你可能感兴趣的:(为什么说LD_LIBRARY_PATH不好)