剖析共享程序库

来源:互联网 发布:2018高考倒计时软件 编辑:程序博客网 时间:2024/04/30 22:18

来源:http://www.ibm.com/developerworks/cn/linux/l-shlibs.html

共享程序库通过版本号来完成对应用程序所使用的程序库的升级,同时保留了对原有应用程序的兼容。本文将讨论此方法的实际内幕,以及在常规 Linux™系统上的 /usr/lib 中有很多符号链接的原因。

共享程序库是现代 UNIX®系统中有效利用空间和资源的基础。SUSE 系统中的 C 程序库大约有 1.3 MB。为 /usr/bin 中每一个程序(我有 2,569个)制作副本将占去几个 G 的空间。

当然这个数字有一些夸张 ——静态链接程序只合并它们使用的那部分程序库。尽管如此, printf()的所有副本所占用的空间数量也会让系统显得非常臃肿。

共享程序库不仅可以节省磁盘空间,而且还可以节省内存。内核可以在内存中保持某个共享程序库的一个惟一副本,并在多个应用程序间共享这个副本。所以,我们不但可以在磁盘上只有 printf()的一个副本,而且在内存中也只需要一个副本。这对性能有很大的影响。

      在本文中,我们将讨论共享程序库所使用的底层技术,以及在共享程序库版本号帮助下预防兼容性难题的方法,过去,本机共享程序库实现也曾遇到过这些难题。首先来看一下共享程序库的工作原理。

 

共享程序库的工作原理

     这个概念理解起来非常简单。拥有一个程序库;然后共享这个程序库。但是,当您的程序尝试调用printf()时,也就是说实际操作的时候,具体发生的事情却稍微有点复杂。

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

但是大部分共享程序库都是动态链接的。这具有一些更深层次的意义。其中一方面是,您不能事先预计某个函数在调用时的确切地址!(以及静态链接的共享程序库模式,比如 BSD/OS中的,但是它们不在本文讨论范围之内。)

动态链接器可以为每个被链接的函数做相当多的工作,所以大部分链接器都是不积极的。只有在函数被调用时,它们才实际做一些工作。C程序库中有一千多个外部可见的符号,有大约三千多个本地符号,因此这种方法可以节省非常多的时间。

    实现此奇妙功能的是一个称为过程链接表(Procedure Linkage TablePLT)的数据块,它是程序中的一个表,列出了程序所调用的每一个函数。当程序开始运行时,PLT包含每个函数的代码,以便查询运行期链接器,从而获得已加载某个函数的地址。然后它会在表中填入这个条目并跳转到那个已加载函数。当每个函数被调用时,它的 PLT中的条目就会被简化为一个到那个已加载函数的直接跳转。

    不过,重要的是,要注意到还有一个间接的额外层次 ——可以通过跳转到某个表来解析每个函数调用。

 

兼容性不仅是为了关联

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

     当出现新版本的程序库时会怎样?特别是新版本改变了某个给定函数的调用次序时,又会怎样?

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

不过,动态链接的可能优势之一在于修正缺陷。如果可以修正程序库中的缺陷,而且不必重新编译上千个程序,就可以利用这一修正功能,这将是非常令人愉快的。有时,需要链接到某个较新的版本。

不幸的是,这会导致在某些情况下,您希望链接到较新的版本,而在另外一些情况下,您宁愿坚持使用较老的版本。不过,有一个解决方案——使用两类版本号:

  • 主版本号表明程序库版本之间的潜在不兼容性。
  • 次要版本号表明只是修正了缺陷。

这样,在大部分情形下,加具有相同主版本和更高次要版本的程序是安全的;而加主版本更高的程序是不安全的行

(和程序)不必追踪程序版本和更新,系提供了大量的符号链接。通常,其模式是:

libexample.so

是一指向

libexample.so.N

接,其中N 是在系中可以找到的最高的版本

受支持的每一主版本而言,

libexample.so.N

将是一个指向

libexample.so.N.M

的链接,其中M 是最高的次要版本号。

这样,如果为链接器指定了-lexample,那么它会去寻找libexample.so,这是一个符号链接,指向某个指向最新版本的符号链接。另一方面,当加载某个现有程序时,它将尝试去加载libexample.so.N,其中N 是它先前链接的版本。各得其所!

 

为了进行调试,首先必须知道如何编译

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

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

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

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

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

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

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

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

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

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

 

 
原创粉丝点击