您的位置首页>企业动态>

linux共享程序库剖析

导读大家好,我是极客范的本期栏目编辑小友,现在为大家讲解linux共享程序库剖析问题。共享库通过版本号升级应用程序使用的库,同时保持与原始

大家好,我是极客范的本期栏目编辑小友,现在为大家讲解linux共享程序库剖析问题。

共享库通过版本号升级应用程序使用的库,同时保持与原始应用程序的兼容性。本文将讨论这种方法的实际内幕,以及在传统Linux系统上的/usr/lib中存在许多符号链接的原因。共享库是现代UNIX系统中有效利用空间和资源的基础。SUSE系统中的C库约为1.3 MB。复制/usr/bin中的每个程序(我有2569个)会占用几个G的空间。

当然,这个数字有些夸张。——静态链接器只合并它们使用的那部分库。然而,printf()的所有副本占用的空间量会使系统看起来臃肿。

共享库不仅可以节省磁盘空间,还可以节省内存。内核可以在内存中保存共享库的唯一副本,并在多个应用程序之间共享该副本。因此,我们不仅可以在磁盘上有一个printf()的副本,而且在内存中也只需要一个副本。这对表演影响很大。

在本文中,我们将讨论共享库使用的底层技术,以及借助共享库版本号防止兼容性问题的方法。过去,本机共享库实现也遇到过这些问题。让我们首先看看共享库是如何工作的。

共享图书馆的工作原理

这个概念很容易理解。有图书馆;然后共享这个库。但是,当您的程序试图调用printf(),也就是说,在实际操作中,发生的事情稍微复杂一些。

这个过程在静态链接系统中比在动态链接系统中更简单。在静态链接系统中,生成的代码将保存对函数的引用。链接器用加载函数的真实地址替换这个引用,这样生成的二进制代码将在正确的位置有正确的地址。然后,在运行代码时,只需跳转到相应的地址。对于管理员来说,这是一项简单的任务,因为它允许您链接那些实际上只在程序的某个位置被引用的对象。

但是大多数共享库是动态链接的。这有一些更深层次的意义。一方面,当一个函数被提前调用时,你无法预测它的确切地址!(还有静态链接的共享库模式,比如BSD/OS,但它们都超出了本文的范围。)

动态链接器可以为每个链接的功能做很多工作,所以大多数链接器都是不活动的。只有当函数被调用时,它们才真正做一些工作。C库中有1000多个外部可见的符号和大约3000个局部符号,所以这种方法可以节省很多时间。

这个奇妙的功能是通过一个名为Procedure Linkage Table(PLT)的数据块来实现的,PLT是程序中的一个表,列出了程序调用的每个函数。当程序开始运行时,PLT包含每个函数的代码,以便查询运行时链接器,获取某个函数已经加载的地址。然后,它将在表中填写这个条目,并跳转到加载的函数。当调用每个函数时,其PLT中的条目被简化为直接跳转到该加载的函数。

但是,需要注意的是,有一个间接的额外级别——,可以通过跳转到一个表来解析每个函数调用。

兼容性不仅仅是关联。

这意味着您最终想要链接到的库最好与调用它的代码兼容。使用静态链接的可执行文件可以保证在某种程度上不会有任何变化。如果使用动态链接,就无法得到这样的保证。

当新版本的库出现时会发生什么?尤其是当新版本改变了给定函数的调用顺序时会发生什么?

版本号可以解决这个问题。——的共享库将有一个版本号。当程序链接到库时,它计划支持的版本号存储在程序中。如果库被更改,版本号将不匹配,并且程序将不会链接到较新版本的库。

然而,动态链接的一个可能的优点在于纠正缺陷。如果库里面的缺陷可以纠正,成千上万的程序不需要重新编译,可以使用这个纠正功能,那会非常愉快。有时,您需要链接到更新的版本。

不幸的是,在某些情况下,你想链接到新版本,而在其他情况下,你更喜欢坚持旧版本。但是,有一个解决方案——使用两种类型的版本号:

主要版本号表示库版本之间的潜在不兼容性。次要版本号表示只有缺陷被修复。这样,在大多数情况下,加载具有相同主版本号和更高次版本号的库是安全的;加载主版本号更高的程序是不安全的。

为了防止用户(和程序员)跟踪库的版本号和更新,系统提供了大量的符号链接。一般来说,模式是:

lib范例.所以

会是一个重点

lib范例. so.N

其中n是系统中能找到的最高主版本号。

对于支持的每个主要版本号,

lib范例. so.N

会是一个重点

lib范例. so.N.M

其中m是最高的次要版本号。

这样,如果为链接器指定了-lae example,它将查找libexample.so,它是指向最新版本的符号链接的符号链接。另一方面,当加载一个现有程序时,它将尝试加载libexample.so.N,其中n是它以前链接的版本。去你的地方!

为了调试,你必须首先知道如何编译。

用于调试

使用共享程序库的问题,对它们如何编译有更多一些了解会对您有所帮助。

在传统的静态程序库中,生成的代码通常封装在一个程序库文件中(其名称以 .a 结尾),然后传递给链接器。在动态程序库中,程序库文件的名称通常以 .so 结尾。文件结构稍有不同。

常规的静态程序库的格式是 ar 工具(一个非常简单的存档程序,类似于 tar,但是更简单)所创建的那种格式。相反,共享程序库通常以更复杂的文件格式存储。

在现代 Linux 系统中,这一格式通常是 ELF 二进制格式(可执行与可链接格式(Executable and Linkable Format))。在 ELF 中,每个文件的组成包括:一个 ELF 头,随后是零或者一些段(segments),以及零或者一些区段(secTIons)。 段 中包含文件的运行时执行所需要的信息,而 区段 中包含用于链接和重定位的重要数据。整个文件中的每个字节每次只能由一个区段使用,不过可以存在不被任何区段所包含的孤立字节。通常,在 UNIX 可执行文件中,一个或多个区段会封装在一个段内。

ELF 格式中包含用于应用程序和程序库的规范。但程序库格式要复杂得多,不仅仅是对象模块的简单存档。

链接器将所有对符号的引用进行分类,标识出它们是在哪个程序库中找到的。将静态程序库的符号添加到最终的可执行文件中;然后将共享程序库的符号放入 PLT 中,最后创建对 FLT 的引用。在完成这些任务之后,生成的可执行文件会拥有一个列表,该列表列出了计划从运行期将加载的程序库中找出的那些符号。

在运行期间,应用程序将加载动态链接器。实际上,动态链接器本身使用与共享程序库相同种类的版本号。例如,在 SUSE Linux 9.1 中, /lib/ld-linux.so.2 文件是一个指向 /lib/ld-linux.so.2.3.3 的符号链接。另一方面,寻找 /lib/ld-linux.so.1 的程序不会尝试使用新的版本。

然后动态链接器开始进行所有有趣的工作。它会查明某个程序先前链接到了哪些程序库(以及哪个版本),然后加载它们。加载程序库的步骤包括:

找到程序库(它可能在系统中若干个目录中的任意一个目录中)。 将程序库映射到程序的地址空间。 分配程序库可能需要的由零填充的内存块。 添加程序库的符号表。

调试这一过程可能会比较困难。您可能会遇到多种问题。例如,如果动态链接器不能找到某个给定的程序库,那么它将停止加载程序。如果它找到了所有需要的程序库,但却无法找到某个符号,那么它也可能会因此而停止加载操作(但是可能直到真正尝试去引用那个符号时才会发生这种情形) —— 这是一种很少见的情况,因为通常如果不存在某个符号,那么在初始化链接的时候就会被警告。

 

修改动态链接器的搜索路径

当链接某个程序时,在运行期您可以指定另外的搜索路径。在 gcc 中,其语法是 -Wl,-R/path。如果程序已经被链接,那么您也可以设置环境变量 LD_LIBRARY_PATH 来改变这一行为。通常只是在应用程序需要搜索的路径不是系统级默认路径的一部分时才需要这样做,对大部分 Linux 系统来说,这种情况很少见。理论上,Mozilla 用户可以发布某个使用这个路径设置所编译的二进制程序,但是他们更倾向于发布包装器(wrapper)脚本,在启动可执行程序之前正确地设置程序库路径。

设置程序库路径可以为两个应用程序需要同一程序库的不兼容版本的这种罕见情况提供一个迂回解决方案。可以使用包装器脚本使某一应用程序在使用特殊版本程序库的目录中进行搜索。这称不上是一个完美的解决方案,但是在某些情况下,这是您能采用的最佳方法。

如果出于不得已的原因需要为很多程序添加某个路径,那么也可以修改系统的默认搜索路径。通过 /etc/ld.so.conf 控制动态链接器,该文件包含默认搜索路径的列表。对 LD_LIBRARY_PATH 中指定的任何路径的搜索都要先于 ld.so.conf 中列出的路径,所以用户可以覆盖这些设置。

大部分用户没有理由修改系统默认程序库搜索路径;通常环境变量更适用于修改搜索路径,比如连接某个工具包中的程序库,或者使用某个程序库的较新版本的测试程序。

使用 ldd

ldd 是调试共享程序库问题的一个实用工具。其名称来自 list dynamic dependencies。这个程序会查看某个给定的可执行程序或者共享程序库,并指出它需要加载哪些共享程序库以及要使用哪些版本。输出类似如下:

清单 1. /bin/sh 的依赖

$ ldd /bin/sh linux-gate.so.1 => (0xffffe000) libreadline.so.4 => /lib/libreadline.so.4 (0x40036000) libhistory.so.4 => /lib/libhistory.so.4 (0x40062000) libncurses.so.5 => /lib/libncurses.so.5 (0x40069000) libdl.so.2 => /lib/libdl.so.2 (0x400af000) libc.so.6 => /lib/tls/libc.so.6 (0x400b2000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

 

看到一个“简单的”的程序使用了这么多个程序库,可能会有些令人惊讶。或许是 libhistory 需要 libncurses。为了查明真相,我们只需要运行另一个 ldd 命令:

清单 2. libhistory 的依赖

$ ldd /lib/libhistory.so.4 linux-gate.so.1 => (0xffffe000) libncurses.so.5 => /lib/libncurses.so.5 (0x40026000) libc.so.6 => /lib/tls/libc.so.6 (0x4006b000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)

 

在某些情况下,可能需要为应用程序指定另外的程序库路径。例如,对 Mozilla 二进制程序尝试运行 ldd 所得到输出的前几行如下所示:

清单 3. 运行 dll 查找不在搜索路径中的 程序库的结果

$ ldd /opt/mozilla/lib/mozilla-binlinux-gate.so.1 => (0xffffe000)libmozjs.so => not foundlibplds4.so => not foundlibplc4.so => not foundlibnspr4.so => not foundlibpthread.so.0 => /lib/tls/libpthread.so.0 (0x40037000)

 

为什么找不到这些程序库?因为它们不在常见的程序库搜索路径中。实际上,它们在 /opt/mozilla/lib 中,所以,解决方案之一是将这个目录添加到 LD_LIBRARY_PATH 中。

另一个选项是将路径设置为 .,并在这个目录下运行 ldd,尽管这样做更危险 —— 将当前目录添加到程序库路径中与将它添加到可执行程序路径中一样有着潜在的危险。

在这种情况下,将这些程序库所在的目录添加到系统级搜索路径中显然不是一个好办法。只有 Mozilla 需要这些程序库。

链接 Mozilla

说起 Mozilla,如果您觉得自己从未见过超过几行的程序库,那么在某种程度上,Mozilla 是一个更为典型的大型应用程序。现在您可以明白为什么 Mozilla 的启动需要那么长时间了吧!

清单 4. mozilla-bin 的依赖性

linux-gate.so.1 => (0xffffe000)libmozjs.so => ./libmozjs.so (0x40018000)libplds4.so => ./libplds4.so (0x40099000)libplc4.so => ./libplc4.so (0x4009d000)libnspr4.so => ./libnspr4.so (0x400a2000)libpthread.so.0 => /lib/tls/libpthread.so.0 (0x400f5000)libdl.so.2 => /lib/libdl.so.2 (0x40105000)libgtk-x11-2.0.so.0 => /opt/gnome/lib/libgtk-x11-2.0.so.0 (0x40108000)libgdk-x11-2.0.so.0 => /opt/gnome/lib/libgdk-x11-2.0.so.0 (0x40358000)libatk-1.0.so.0 => /opt/gnome/lib/libatk-1.0.so.0 (0x403c5000)libgdk_pixbuf-2.0.so.0 => /opt/gnome/lib/libgdk_pixbuf-2.0.so.0 (0x403df000)libpangoxft-1.0.so.0 => /opt/gnome/lib/libpangoxft-1.0.so.0 (0x403f1000)libpangox-1.0.so.0 => /opt/gnome/lib/libpangox-1.0.so.0 (0x40412000)libpango-1.0.so.0 => /opt/gnome/lib/libpango-1.0.so.0 (0x4041f000)libgobject-2.0.so.0 => /opt/gnome/lib/libgobject-2.0.so.0 (0x40451000)libgmodule-2.0.so.0 => /opt/gnome/lib/libgmodule-2.0.so.0 (0x40487000)libglib-2.0.so.0 => /opt/gnome/lib/libglib-2.0.so.0 (0x4048b000)libm.so.6 => /lib/tls/libm.so.6 (0x404f7000)libstdc++.so.5 => /usr/lib/libstdc++.so.5 (0x40519000)libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x405d5000)libc.so.6 => /lib/tls/libc.so.6 (0x405dd000)/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)libX11.so.6 => /usr/X11R6/lib/libX11.so.6 (0x406f3000)libXrandr.so.2 => /usr/X11R6/lib/libXrandr.so.2 (0x407ef000)libXi.so.6 => /usr/X11R6/lib/libXi.so.6 (0x407f3000)libXext.so.6 => /usr/X11R6/lib/libXext.so.6 (0x407fb000)libXft.so.2 => /usr/X11R6/lib/libXft.so.2 (0x4080a000)libXrender.so.1 => /usr/X11R6/lib/libXrender.so.1 (0x4081e000)libfontconfig.so.1 => /usr/lib/libfontconfig.so.1 (0x40826000)libfreetype.so.6 => /usr/lib/libfreetype.so.6 (0x40850000)libexpat.so.0 => /usr/lib/libexpat.so.0 (0x408b9000)

 

深入了解共享程序库

有兴趣深入了解 Linux 中的动态链接的用户有很多选择。GNU 编译器和链接器工具链(linker tool chain)文档都非常好,虽然其内容是以 info 格式存储的,而且也没有在标准手册页中提及。

ld.so 的手册页包含有一个非常详尽的列表,列出了改变动态链接器行为的变量,以及对过去曾经使用的不同版本的动态链接器的说明。

大部分 Linux 文档都假定所有共享程序库都是动态链接的,因为在 Linux 系统上,它们通常是这样的。实现静态链接的共享程序库需要做的工作非常多,而且大部分用户不会因此获得任何好处,尽管支持这个特性的系统的性能会有显著改变。

如果您正在使用现成的预先包装好的系统,那么您可能不会遇到太多的共享程序库版本 —— 系统可能只附带它要链接的那些共享程序库版本。另一方面,如果您做过很多次更新和源代码构建,那么您可能最终得到多个版本的共享程序库,因为老版本依然会被保留,“以防万一”。

像平时一样,如果想了解更多,那么就去亲自实践吧。记住,在某个系统上,几乎所有程序都会引用一些相同的共享程序库,所以,如果破坏了系统的某个核心共享程序库,那么您就得去求助系统恢复工具了。

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。