# ELF原理四
动态链接(Dynamic Linking)
程序解释器(Program Interpreter)
一个可执行文件可以有一个PT_INTERP程序头部信息。在exec(操作系统提供的执行程序的系统调用)函数执行时,操作系统通过解析ELF文件的程序头部表,然后从PT_INTERP段中获取路径名,并从解释器文件的段创建初始进程映像。也就是说,操作系统不是使用原始的可执行文件的段映像,而是先为解释器创建一个内存映像。然后,操作系统通过执行解释器的代码给应用程序提供执行环境。
解释器通过以下两种方式来创建应用程序环境:
- 接收一个文件描述符来读取可执行文件的头部信息,也即使用这个文件描述符来读取或映射可执行文件的段到内存
- 根据可执行文件格式,操作系统可以将可执行文件直接加载到内存中,然后将可执行文件信息直接传递给解释器使用,而不是通过打开的文件描述符来操作文件
除了传递可能的文件描述符之外,解释器进程的初始状态与可执行文件被加载执行时的状态相匹配。解释器本身不需要第二个解释器。解释器的文件类型可以是共享对象文件,也可以是可执行文件:
- 作为一个共享文件对象(解释器通常为该类型),由于该动态链接库是地址无关的,所以其地址可能因进程而异。操作系统通过吗mmap(操作系统提供的系统调用,用于将进程的虚拟地址空间与真实物理内存进行映射关联)和其他相关函数,在进程的动态段虚拟内存中创建自己的段(详细可参考混沌学堂:程序内存分布,这里也即进程的堆区)。因此,共享文件类型的解释器通常不会与原始可执行文件的原始段地址冲突
- 作为一个可执行文件被加载在固定地址,操作系统使用程序头表中的虚拟地址创建解释器的段。因此,可执行文件解释器的虚拟地址可能与可执行文件的虚拟地址发生冲突,此时解释器将负责解决冲突
动态链接器(Dynamic Linker)
当构建一个需要动态链接的可执行文件时,链接编辑器将一个PT_INTERP类型的程序头元素添加到一个可执行文件中,并通知操作系统将动态链接器作为程序解释器调用(可以看到:动态链接器为一个特殊的程序解释器)(注意:操作系统提供的动态链接器的位置与特定处理器平台相关)。
Exec函数和动态链接器一起为程序创建进程映像,执行步骤如下:
- 将可执行文件的内存段添加到进程映像(通过可执行文件的程序头部表)
- 向进程映像添加所需共享对象内存段
- 对可执行文件及其共享对象执行重定位
- 关闭用于读取可执行文件的文件描述符
- 将控制转移到程序,使其看起来像是程序好像直接从exec函数直接开始执行
链接编辑器还为可执行和共享对象文件构建各种数据来辅助动态链接器。如前面介绍的程序头部信息,这些数据驻留在可加载段中,使它们在执行期间可用。(回想一下,确切的段内容是特定于处理器的。有关完整信息,请参阅相关处理器补充):
- SHT_DYNAMIC类型的.dynamic section保存着各种数据,其位于段开头并保存着其他动态链接信息的地址
- SHT_HASH类型的.hash section保存着一个符号哈希表
- SHT_PROGBITS类型的section .got和.plt保存着两个表:全局偏移表(GOT)和过程链接表(PLT),随后将说明动态链接器如何使用这两个表来为对象文件创建内存映像
因为每个符合ABI规范的程序都会使用共享对象库中提供的功能函数和数据,所以动态链接器会参与每个符合ABI规范的程序的执行。正如”程序加载”在特定处理器补充中解释的那样,共享对象文件可能会占用与记录在文件程序头表中的地址不同的虚拟内存地址(也即进程的堆区),此时动态链接器需要重新定位共享目标文件的内存映像,在应用程序获得控制权之前将它们更新为绝对地址(所以,地址无关代码如此重要)。
如果进程环境包含一个名为LD_BIND_NOW的非空值变量(由操作系统提供的exec函数来指明),动态链接器在执行程序代码之前将处理所有重定位。例如,以下所有环境参数用于决定此行为:
- LD_BIND_NOW=1
- LD_BIND_NOW=on
- LD_BIND_NOW=off
否则,LD_BIND_NOW变量要么不出现在环境中,要么为空值。动态连接器将延迟计算过程链接表项,从而避免了为那些没有被调用的函数进行符号解析和重定位开销。相关过程将会在后面描述程序链接表时详细描述。
动态节(Dynamic Section)
如果一个对象文件参与动态链接,它的程序头表将有一个PT_DYNAMIC类型的表项,也即包含一个.dynamic 段,它使用一个特殊的符号:_DYNAMIC 表示包含以下结构数组的节信息:
对于每个具有此类型的表项,d_tag控制对d_un联合体的解释:
- d_val:使用 Elf32_Word 对象表示所需的整数值信息
- d_ptr:使用 Elf32_Addr 对象表示程序虚拟地址。如前所述,在执行期间文件的虚拟地址可能与内存虚拟地址不匹配,当解释包含在动态结构中的地址时,动态链接器根据原始文件值和内存基地址计算实际虚拟地址
下表总结了可执行和共享文件的d_tag与d_un的解释。如果一个d_tag被设置为 mandatory,那么符合ABI的文件的动态链接数组必须包含该类型的数组项。同样,optional 意味着可能会出现数组项,但并不是必需的:
DT_NULL:带有DT_NULL标记的数组项标志着_DYNAMIC数组的结束
DT_NEEDED:此标记表示的数组项保存以空白符结尾的字符串的字符串表偏移量,其中指明了所需共享库的名称。这里偏移量指的是 DT_STRTAB 表项中记录表的索引。_DYNAMIC数组可以包含多个此类型的数组项,但这些数组项的相对顺序很重要,因为它决定了动态链接库的加载顺序
DT_PLTRELSZ:此标记表示的数组项保存着与过程链接表相关联的重定位表项的总大小(以字节为单位)。如果存在一个DT_JMPREL类型的数组项,则必须包含一个DT_PLTRELSZ类型的数组项
DT_PLTGOT:此标记表示的数组项保存着一个与过程链接表(PLT)或全局偏移表(GOT)相关联的地址
DT_HASH:此标记表示的数组项保存着符号哈希表的地址,这个哈希表指向DT_SYMTAB元素所引用的符号表
DT_STRTAB:此标记表示的数组项保存前面描述的字符串表的地址。符号名称、库名称和其他字符串都保存在该表中
DT_SYMTAB:此标记表示的数组项保存前面描述的符号表的地址
DT_RELA:此标记表示的数组项保存着重定位表的地址。表中的项具有显式加数,例如:32位的Elf32_Rela,一个目标对象文件可以有多个重定位节。当为一个可执行或共享目标对象文件构建重定位表时,链接编辑器将这些部分串联起来形成一个单独的表,虽然这些节在目标对象文件中保持独立,但动态连接器看到的是由它们组成的一张表。当动态连接器为一个可执行文件创建进程映像或向进程映像中添加一个动态链接库时,它读取对象文件的重定位表并执行相关的操作。如果 DT_RELA 存在,动态结构中还必须有 DT_RELASZ 和 DT_RELAENT 类型的数组项
DT_RELASZ:此标记表示的数组项保存着 DT_RELA 重定位表的总大小(以字节为单位)
DT_RELAENT:此标记表示的数组项保存着 DT_RELA 重定位表项的字节大小(注意:DT_RELASZ为总长度,这里为内部的表项大小)
DT_STRSZ:此标记表示的数组项保存字符串表的大小(以字节为单位)
DT_SYMENT:此标记表示的数组项保存着符号表项的字节大小
DT_INIT:此标记表示的数组项保存着初始化函数的地址,将会在下面的“初始化和终止函数”一节中讨论
DT_FINI:此标记表示的数组项保存终止函数的地址,将在下面的“初始化和终止函数”一节中讨论
DT_SONAME:此标记表示的数组项保存一个以空白符结尾的字符串的字符串表偏移量,其中保存着共享对象的名称,这里的偏移量是 DT_STRTAB 表项中记录的表的索引下标
DT_RPATH:此标记表示的数组项保存一个以空白符结尾的搜索库的搜索路径字符串信息的字符串表偏移量。这里的偏移量是DT_STRTAB表项中记录的表的索引下标
DT_SYMBOLIC:此标记表示的数组项保存存在于动态链接库中用于更改符号解析的算法信息。由于动态链接器不是从可执行文件开始符号搜索,而是从共享对象本身开始,如果共享对象不能提供引用的符号,那么动态链接器默认搜索可执行文件和其他共享对象信息
DT_REL:该数组项类似于 DT_RELA,除了DT_RELA的表项有隐式加数,比如:32位的Elf32_Rel信息。如果该数组项存在,动态数组中还必须有 DT_RELSZ 和 DT_RELENT 类型的元素
DT_RELSZ:此标记表示的数组项保存着DT_REL重定位表的总大小(以字节为单位)
DT_RELENT:此标记表示的数组项保存着DT_REL重定位表项的字节大小
DT_PLTREL:此标记表示的数组项指定过程链接表引用的重定位表项的类型。d_val 成员根据情况保存 DT_REL 或DT_RELA ,注意:过程链接表中的所有重定位都必须使用相同的重定位
DT_DEBUG:此标记表示的数组项用于调试,它的内容没有在ABI规范中指定,访问此数组项的程序不符合ABI规范
DT_TEXTREL:此标记表示的数组项用于表示是否有重定位表项会引起对非可写段(如:代码段)的修改,详细权限描述可以参考程序头部表中的段权限描述。如果该数组项存在,那么表示一个或多个重定位表项可能会请求修改一个非可写段,动态连接器可以为此做出相应准备
DT_JMPREL:此标记表示的数组项的 d_ptr 成员保存着重定位表项的地址,该重定位表项仅与过程链接表相关联。如果启用了惰性绑定(调用该plt所指向的函数时才要求动态链接器完成实际地址绑定操作),分离这些重定位项可以让动态连接器在进程初始化期间忽略它们。如果这个表项存在,DT_PLTRELSZ 和 DT_PLTREL 类型的相关表项也必须存在。
DT_LOPROC 到 DT_HIPROC:范围内的值为特定于处理器的语义保留
除了位于数组末尾的DT_NULL类型的数组项以及DT_NEEDED类型数组项的相对顺序外,其他类型的数组项可以以任何顺序出现。表中没有出现的标记值是保留的。
共享对象的依赖关系(Shared Object Dependencies)
当链接编辑器处理归档库(.a 库,参考混沌学堂描述)时,它提取其中链接库并将它们复制到输出对象文件中。这些静态链接的服务在执行期间不需要动态链接器参与便可直接使用,也即静态链接。如果涉及到引用的动态链接库共享对象,动态链接器必须将适当的共享对象文件附加到进程映像以执行,因此,可执行和共享对象文件描述了它们特定的依赖关系。
当动态链接器为一个对象文件创建内存段时,依赖关系(记录在动态结构的DT_NEEDED类型表项)表示需要哪些共享对象来提供程序的服务。通过不断链接被引用的共享对象及其依赖项,动态链接器最终将构建一个完整的进程映像。当解析符号引用时,动态链接器使用广度优先搜索来使用符号表,也就是说,它首先查看可执行程序本身的符号表,然后是DT_NEEDED类型表项的符号表(注意:这里是按顺序搜索,所以 DT_NEEDED 数组项顺序非常重要),然后是第二级DT_NEEDED类型表项的符号表,以此类推,此时,动态链接库文件必须是进程可读的,不需要其他权限。
注:即使一个共享对象在依赖项列表中被引用多次,动态链接器也只会将该对象链接到进程一次。
依赖项列表中的名称要么是 DT_SONAME 字符串的副本,要么是用于构建对象文件的共享对象的路径名称的副本。例如,如果链接编辑器使用一个 DT_SONAME 数组项为lib1的共享对象和另一个共享对象库以/usr/lib/lib2路径名构建一个可执行文件,可执行文件将包含lib1和/usr/lib/lib2的依赖列表。
如果共享对象名称中有一个或多个斜杠( / )字符,例如:/usr/lib/lib2 或 directory / file,动态连接器直接使用该字符串作为路径名。如果名称没有斜杠,例如上面的 lib1,则有三种方式指定共享对象路径搜索,优先级如下:
- 首先,动态数组标记 DT_RPATH 可以给出一个包含目录列表的字符串,用冒号(:)分隔。例如:字符串 /home/dir/lib : /home/dir2/lib: 告诉动态连接器首先搜索目录/home/dir/lib,然后搜索/home/dir2/lib,然后搜索当前目录来查找依赖项
- 其次,进程环境变量中一个名为LD_LIBRARY_PATH的变量(参考Linux 该环境变量的描述)可以保存上面的目录列表,后面可以有一个分号(;)和另一个目录列表。下面的值等价于前面的例子:
- LD_LIBRARY_PATH=/home/dir/lib:/home/dir2/lib:
- LD_LIBRARY_PATH=/home/dir/lib;/home/dir2/lib:
- LD_LIBRARY_PATH=/home/dir/lib:/home/dir2/lib:;
- 所有LD_LIBRARY_PATH目录都在DT_RPATH目录之后搜索。虽然有些程序(如链接编辑器)对分号前后的列表有不同的处理方式,但动态链接器没有,且动态链接器接受分号表示法,具有上面描述的语义 。
- 最后,如果其他两组目录没有找到所需的库,动态连接器将搜索 /usr/lib 目录下的动态链接库
注:为了安全,对于设置用户ID和设置组ID的程序,动态链接器忽略环境搜索规范(例如:不使用LD_LIBRARY_PATH变量值)。但是,它会继续搜索 DT_RPATH 目录和 /usr/lib 。