深入动态链接
深入动态链接
上一篇博客:动态链接简单介绍了动态链接的原理,这篇博客将要介绍动态链接的具体实现。
动态链接相关结构
动态链接情况下,可执行文件的装载和静态链接基本一样。首先操作系统会读取可执行文件的头部,检查文件的合法性,然后从头部中的“program header”中读取每个“segment”的虚拟地址、文件地址和属性,并且将他们映射到进程虚拟空间的相应位置。
但是由于动态链接下,可执行文件中很多外部符号的引用还处于无效地址状态,所以操作系统会先启动一个动态链接器。
操作系统在加载完动态链接器后,就将控制权交给动态链接器的入口地址,当所有动态链接工作完成之后,动态链接器会将控制权转交给可执行文件的入口地址,程序正式开始执行。
“.interp段”
在动态链接的ELF可执行文件中,有一个专门的段叫做“.interp”
,这个段内容很简单,里面保存的就是一个字符串,这个字符串保存了动态链接的路径。
操作系统在对可执行文件进行加载的时候,它会去寻找装载可执行文件所需要相应的动态链接器。
“.dynamic段”
“dynamic”
是ELF动态链接中最重要的结构,这个段里面保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表位置、动态链接重定位表的位置、共享对象初始化代码的地址等。
typedef struct{
Elf32_Sword d_tag;
union{
Elf32_Word d_val;
Elf32_Addr d_ptr;
}d_un;
}Elf32_Dyn;
动态符号表
为了完成动态链接,最关键的还是所依赖的符号和相关文件的信息。
为了表示动态链接这些模块之间的符号关系, ELF专门有一个叫做动态符号表的段用来保存这些信息,这个段的段名通常叫“.dynsym”
。与“.symtab"
不同的是,“.dynsym”
只保存了与动态链接相关的符号,对于那些模块内部的符号,比如模块私有变量则不保存。很多时候动态链接的模块同时拥有“.dynsym"
和“.symtab”
两个表,“.symtab"
中往往保存了所有符号,包括“.dynsym"
中的符号。
与“.syrmtab"
类似,动态符号表也需要一一些辅助的表,比如用于保存符号名的字符串
表。静态链接时叫做符号字符串表“.strtab"
,在这里就是动态符号字符串表“.dynstr”
;由于动态链接下,我们需要在程序运行时查找符号,为了加快符号的查找过程,往往还有辅助的符号哈希表。
动态链接重定位表
共享对象需要重定位的主要原因就是导入符号的存在。
共享对象的重定位与我们在前面“静态链接”中分析过的目标文件的重定位十分类似,
唯-有区别的是目标文件的重定位是在静态链接时完成的,而共享对象的重定位是在装载时
完成的。在静态链接中,目标文件里面包含有专门用于表示重定位信息的重定位表,比如
“rel.text”表示是代码段的重定位表,“.rel.data"是数据段的重定位表。
动态链接的文件中,也有类似的重定位表分别叫做“.rel.dyn"和“rel.plt",它们分别
相当于“ .rel.text"和“.rel.data".“.rel.dyn"实际上是对数据引用的修正,它所修正的位置
位于“.got"以及数据段:而“.rel.plt"是对函数引用的修正,它所修正的位置位于“.got.plt"。
动态链接时进程堆栈初始化信息
当操作系统把控制权交给动态链接器时,它开始做链接工作。他需要知道关于可执行文件和本进程的一些信息,这些信息往往由操作系统传递给动态链接器,保存在进程的堆栈里面。实际上,堆栈里面还保存了动态链接器所需要的一些辅助信息数组。它的结构如下:
typedef struct{
uint32_t a_type;
union{
uint32_t a_val
}a_un;
}Elf32_auxv_t;
动态链接的步骤
动态链接器自举
其实,动态链接器本身也是一个共享对象。那么它的重定位工作有谁完成?是否依赖其他对象?
动态链接器本身不可以依赖其他共享对象
动态链接器本身所需的全局和静态变量的重定位工作由它自己完成。
动态链接器必须在启动时有一段非常精巧的代码可以完成这项艰巨的工作而同时又不能用到全局和静态变量。这就是自举。
动态链接器入口地址即是自举代码的入口,当操作系统将进程控制权交给动态链接器时,
动态链接器的自举代码就开始执行。自举代码会先找到它的GOT,找到其本身的.dynamic段,通过其中的信息,自举代码便可以获得动态链接器本身的重定位表和符号表。
装载共享对象
完成自举后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表之中,我们称它为全局符号表。通过“.dynamic”
,链接器列出可执行文件所需的共享对象,并将共享对象的名字放到装载集合中。然后通过这个集合中的名字,找到相应的共享对象。
当一个新共享对象被装载进来时,其符号表被合并到全局符号表。
重定位和初始化
当完成上面的步骤之后,链接器开始重新遍历可执行文件和每个共享对象的重定位表,将它们的GOT/PLT中每个需要重定位的位置进行修正。
重定位完成之后,如果共享对象有“.init”
段,那么动态链接会执行其中的内容,用以实现共享对象特有的初始化过程。
如果进程的可执行文件也有“.init”段,那么动态链接器不会执行它,因为可执行文件的由程序初始化部分代码负责执行。
参考文献
[1] 俞甲子 石凡 潘爱明.程序员的自我修养.电子工业出版社,2009.4.