动态链接的共享库是 GNU/Linux® 的一个重要方面。该种库允许可执行文件在运行时动态访问外部函数,从而(通过在需要时才会引入函数的方式)减少它们对内存的总体占用。本文研究了创建和使用静态库的过程,详细描述了开发它们的各种工具,并揭秘了这些库的工作方式。

Linux 支持两种类型的库,每一种库都有各自的优缺点。
静态库包含在编译时静态绑定到一个程序的函数。
动态库则不同,它是在加载应用程序时被加载的,而且它与应用程序是在运行时绑定的。图 1 展示了 Linux 中的库的层次结构。
Linux 链接剖析(动态链接) - 图1
您可以动态地将程序和共享库链接并让 Linux 在执行时加载库(如果它已经在内存中了,则无需再加载)
另外一种方法是使用一个称为动态加载的过程,这样程序可以有选择地调用库中的函数。使用动态加载过程,程序可以先加载一个特定的库(已加载则不必),然后调用该库中的某一特定函数。

Linux 链接剖析(动态链接) - 图2

Linux 链接剖析(动态链接) - 图3

Linux 动态链接原理

当用户启动一个应用程序时,它们正在调用一个可执行和链接格式(Executable and Linking Format,ELF)映像。内核首先将 ELF 映像加载到用户空间虚拟内存中。
然后内核会注意到一个称为.interp 的 ELF 部分,它指明了将要被使用的动态链接器(/lib/ld-linux.so)

  1. mtj@camus:~/dl$ readelf -l dl
  2. Elf file type is EXEC (Executable file)
  3. Entry point 0x8048618
  4. There are 7 program headers, starting at offset 52
  5. Program Headers:
  6. Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
  7. PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
  8. INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1
  9. [Requesting program interpreter: /lib/ld-linux.so.2]
  10. LOAD 0x000000 0x08048000 0x08048000 0x00958 0x00958 R E 0x1000
  11. LOAD 0x000958 0x08049958 0x08049958 0x00120 0x00128 RW 0x1000
  12. DYNAMIC 0x00096c 0x0804996c 0x0804996c 0x000d0 0x000d0 RW 0x4
  13. NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4
  14. GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4

ld-linux.so 本身就是一个 ELF 共享库,但它是静态编译的并且不具备共享库依赖项。当需要动态链接时,内核会引导动态链接(ELF 解释器),该链接首先会初始化自身,然后加载指定的共享对象(已加载则不必。
接着它会执行必要的重定位,包括目标共享对象所使用的共享对象LD_LIBRARY_PATH 环境变量定义查找可用共享对象的位置。定义完成后,控制权会被传回到初始程序以开始执行。
重定位是通过一个称为 Global Offset Table(GOT)Procedure Linkage Table(PLT)的间接机制来处理的。这些表格提供了 ld-linux.so 在重定位过程中加载的外部函数和数据的地址。这意味着无需改动需要间接机制(即,使用这些表格)的代码:只需要调整这些表格。一旦进行加载,或者只要需要给定的函数,就可以发生重定位(稍候在用 Linux 进行动态加载 小节中会看到更多的差别)。
Linux 链接剖析(动态链接) - 图4
重定位完成后动态链接器就会允许任何加载的共享程序来执行可选的初始化代码。该函数允许库来初始化内部数据并备之待用。这个代码是在上述 ELF 映像的 .init 部分中定义的。在卸载库时,它还可以调用一个终止函数(定义为映像的 .fini 部分)。当初始化函数被调用时,动态链接器会把控制权转让给加载的原始映像
Linux 链接剖析(动态链接) - 图5

Linux 进行动态加载(dlopen)

Linux 并不会自动为给定程序加载和链接库,而是与应用程序本身共享该控制权。这个过程就称为动态加载。使用动态加载,应用程序能够先指定要加载的库,然后将该库作为一个可执行文件来使用(即调用其中的函数)。但是正如您在前面所了解到的,用于动态加载的共享库与标准共享库(ELF 共享对象)无异。事实上,**ld-linux** 动态链接器作为 ELF 加载器和解释器,仍然会参与到这个过程中
动态加载(Dynamic Loading,DL)API 就是为了动态加载而存在的,它允许共享库对用户空间程序可用。尽管非常小,但是这个 API 提供了所有需要的东西,而且很多困难的工作是在后台完成的。表 1 展示了这个完整的 API。

函数 描述
dlopen 使对象文件可被程序访问
dlsym 获取执行了
dlopen
函数的对象文件中的符号的地址
dlerror 返回上一次出现错误的字符串错误
dlclose 关闭目标文件

该过程首先是调用 dlopen,提供要访问的文件对象和模式。调用 dlopen 的结果是稍候要使用的对象的句柄。**mode** 参数通知动态链接器何时执行再定位。有两个可能的值。第一个是RTLD_NOW,它表明动态链接器将会在调用dlopen 时完成所有必要的再定位。第二个可选的模式是RTLD_LAZY,它只在需要时执行再定位。这是通过在内部使用动态链接器重定向所有尚未再定位的请求来完成的。这样,动态链接器就能够在请求时知晓何时发生了新的引用,而且再定位可以正常进行。后面的调用无需重复再定位过程。
还可以选择另外两种模式,它们可以按位 ORmode 参数中。RTLD_LOCAL 表明其他任何对象都无法使加载的共享对象的符号用于再定位过程。如果这正是您想要的的话(例如,为了让共享的对象能够调用原始进程映像中的符号),那就使用RTLD_GLOBAL 吧。
dlopen 函数还会自动解析共享库中的依赖项。这样,如果您打开了一个依赖于其他共享库的对象,它就会自动加载它们。函数返回一个句柄,该句柄用于后续的 API 调用。dlopen 的原型为:

  1. #include <dlfcn.h>
  2. void *dlopen( const char *file, int mode );

有了 ELF 对象的句柄,就可以通过调用 dlsym 来识别这个对象内的符号的地址了。该函数采用一个符号名称,如对象内的一个函数的名称。返回值为对象符号的解析地址:

  1. void *dlsym( void *restrict handle, const char *restrict name );

如果调用该 API 时发生了错误,可以使用 dlerror 函数返回一个表示此错误的人类可读的字符串。该函数没有参数,它会在发生前面的错误时返回一个字符串,在没有错误发生时返回 NULL:

  1. char *dlerror();

最后,如果无需再调用共享对象的话,应用程序可以调用 dlclose 来通知操作系统不再需要句柄和对象引用了。它完全是按引用来计数的,所以同一个共享对象的多个用户相互间不会发生冲突只要还有一个用户在使用它,它就会待在内存中)。任何通过已关闭的对象的dlsym 解析的符号都将不再可用。

  1. char *dlclose( void *handle );

选项 -rdynamic 用来通知链接器将所有符号添加到动态符号表中(目的是能够通过使用dlopen 来实现向后跟踪)。-ldl 表明一定要将dllib 链接于该程序


动态链接器工作流程

动态链接器可以为每个被链接的函数做相当多的工作,所以大部分链接器都是不积极的。只有在函数被调用时,它们才实际做一些工作(懒加载)。C 程序库中有一千多个外部可见的符号,有大约三千多个本地符号,因此这种方法可以节省非常多的时间。
实现此奇妙功能的是一个称为 过程链接表(Procedure Linkage Table)(PLT)的数据块,它是程序中 的一个表,列出了程序所调用的每一个函数。当程序开始运行时,PLT 包含每个函数的代码,以便查询运行期链接器,从而获得已加载某个函数的地址。然后它会在表中填入这个条目并跳转到那个已加载函数。当每个函数被 调用时,它的 PLT 中的条目就会被简化为一个到那个已加载函数的直接跳转。
首先链接器将所有对符号的引用进行分类,标识出它们是在哪个程序库中找到的,将静态程序库的符号添加到最终的可执行文件中;然后将共享程序库的符号放入 PLT 中最后创建对 FLT 的引用。在完成这些任务之后,生成的可执行文件会拥有一个列表,该列表列出了计划从运行期将加载的程序库中找出的那些符号。
在运行期间应用程序将加载动态链接器。实际上,动态链接器本身使用与共享程序库相同种类的版本号。 例如,在 SUSE Linux 9.1 中, /lib/ld-linux.so.2 文件是一个指向 /lib/ld-linux.so.2.3.3 的符号链接。另一方面,寻找 /lib/ld-linux.so.1 的程序不会尝试使用新的版本。
然后动态链接器开始进行所有有趣的工作。它会查明某个程序先前链接到了哪些程序库(以及哪个版本), 然后加载它们。加载程序库的步骤包括:

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

调试这一过程可能会比较困难。您可能会遇到多种问题。
例如,如果动态链接器不能找到某个给定的 程序库,那么它将停止加载程序。如果它找到了所有需要的程序库,但却无法找到某个符号,那么它也可能会因此而停止加载操作但是可能直到真正尝试去引用那个符号时才会发生这种情形) —— 这是一种很少见的情况,因为通常如果 不存在某个符号,那么在初始化链接的时候就会被警告
GNU 编译器和链接器工具链(linker tool chain)文档
 那么,如果动态可执行程序不包含运行所需的所有函数,Linux 的哪部分负责将这些程序和所有必需的共享库一起装入,以使它们能正确执行呢?答案是动态装入器(dynamic loader),它实际上是您在 ln 的 ldd 清单中看到的作为共享库相关性列出的 ld-linux.so.2 库。动态装入器负责装入动态链接的可执行程序运行所需的共享库。现在,让我们迅速查看一下动态装入器如何在系统上找到适当的共享库。
ld.so.conf
动态装入器找到共享库要依靠两个文件 — /etc/ld.so.conf 和 /etc/ld.so.cache。如果您对 /etc/ld.so.conf 文件进行 cat 操作,您可能会看到一个与下面类似的清单:
ld.so.cache
但是在动态装入器能“看到”这一信息之前,必须将它转换到 ld.so.cache 文件中。可以通过运行 ldconfig 命令做到这一点:
当 ldconfig 操作结束时,您会有一个最新的 /etc/ld.so.cache 文件,它反映您对 /etc/ld.so.conf 所做的更改。从这一刻起,动态装入器在寻找共享库时会查看您在 /etc/ld.so.conf 中指定的所有新目录。
还有另一个方便的技巧可以用来配置共享库路径。有时候您希望告诉动态装入器在尝试任何 /etc/ld.so.conf 路径以前先尝试使用特定目录中的共享库。在您运行的较旧的应用程序不能与当前安装的库版本一起工作的情况下,这会比较方便。

多个进程都链接同一个so动态库

代码段共享,数据段不共享

动态库运行时符号表加载顺序

动态链接在ELF specfication中指定。 (请注意,有一些非常旧的PDF和Postscript文件浮动,但这些文件通常非常过时。)符号查找在Shared Object Dependencies部分中描述:

解析符号引用时,动态链接器使用广度优先搜索来检查符号表。也就是说,它首先查看可执行程序本身的符号表,然后查看DT_NEEDED条目的符号表(按顺序),然后查看第二级DT_NEEDED条目,依此类推。 (有各种扩展可以改变这种行为.ELF规范本身定义了[DF_SYMBOLIC](http://www.sco.com/developers/gabi/latest/ch5.dynamic.html#df_symbolic) flag。)

这意味着您的问题无法解答,因为您的图表未显示主可执行文件,并且不清楚搜索多个依赖项的顺序(从上到下或从下到上)。
查找顺序是否与对象加载顺序匹配是实现定义的,因为仅加载对象(不执行其初始化函数)不是根据ELF规范具有可观察效果的东西。
Initialization order(执行初始化函数的顺序)比符号查找顺序的约束更少,因为DT_NEEDED条目的顺序与此无关。所以在理论上,有可能实现在d.so之前加载初始化b.so,但b.so的符号插入d.so的符号,因为它首先出现在符号搜索顺序中(由于DT_NEEDED条目的排序方式)。

深入剖析Linux动态库在内存的装载

3.1 可执行链接格式

可执行链接格式(Executable and Linking Format)最初是由 UNIX 系统实验室(UNIX System Laboratories,USL)开发并发布的,作为应用程序二进制接口(Application Binary Interface,ABI)的一部分。
目标文件有三种类型:

  • 可重定位文件(Relocatable File) .o)包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
  • 可执行文件(Executable File) .exe) 包含适合于执行的一个程序,此文件规定了exec() 如何创建一个程序的进程映像。
  • 共享目标文件(Shared Object File) .so) 包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理, 生成另外一个目标文件。其次动态链接器(Dynamic Linker)可能将它与某 个可执行文件以及其它共享目标一起组合,创建进程映像。

目标文件全部是程序的二进制表示,目的是直接在某种处理器上直接执行
ELF文件格式:

Linux 链接剖析(动态链接) - 图6

linux系统的动态库有两种使用方法:运行时动态链接库,动态加载库并在程序控制之下使用

浅谈链接器

浅谈链接器