第七章 动态链接<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

 

 (续)

 
4     延迟绑定

动态链接速度慢的原因有:
(1)       对于全局变量和静态数据的访问都要通过GOT表定位,再间接寻址。
(2)       对于模块间的调用也要先定位GOT,然后再进行间接跳转。
(3)       动态链接的链接工作在运行时完成。
主要的原因是(2)和(3),因为模块间的全局变量访问比较少,多了耦合性就强。

 

延迟绑定是指函数在第一次用到时才进行绑定,包括符号查找、重定位等。如果没有用到则不进行绑定。这样就节省了(3)的时间。
ELF采用PLTProcedure Linkage Table)的方法来实现延迟绑定。这种方法是在GOT之上增加一层间接跳转来实现的。每个外部函数在PLT中都有一个对应的项,调用函数并不是直接跳转的GOT中进行定位,而是先跳转到PLT中的对应项,如果是第一次调用该函数,PLT则先进行符号绑定,填充GOT表,再跳转过去。否则直接跳转到GOT进行定位。

 

在实际中,ELFGOT拆分成表.got.got.plt。前者负责全局变量引用的地址,后者负责函数引用的地址。即实际上PTL中的代码是跳转到.got.plt中所指的位置。

 

PLT没一项16个字节。段名为.plt。表的第0项为两条push指令,因为每个PLT项都有这两条指令,将本模块ID和绑定处理函数地址(_dl_runtime_resolve())压入栈。其余项则为本符号处理代码。
PLT0:

Push           *.got.plt + 4
Push           *.got.plt + 4

 

……

 

bar@plt:

jmp            *(bar@.got.plt)
Push           n
Jump         PLT0
注:.got.plt中第一项为.dynamic地址,第二项为本模块ID,第三项为绑定处理函数地址。

 

5     动态链接相关结构

在动态链接情况下,可执行文件的装载与静态链接情况基本相同。首先操作系统读取可执行文件头部,检查文件的合法性,然后从程序头部表读取每个Segment的虚拟地址、文件地址和属性,将他们映射到进程虚拟空间。
这时,静态链接会把控制权交给可执行文件,而动态链接还不能在装载完可执行文件后把控制权交给可执行文件,因为可执行文件依赖的很多共享对象还没有被装载链接。操作系统会启动动态链接器,把控制权交给它。

 

.interp

动态链接器的地址信息存储在该段,操作系统根据该段的值找到动态链接器。
Linux下动态链接器的位置为/lib/ld-linux.so.2,是一个软链接.
动态链接器是Glibc的一部分,所以跟glibc版本是一致的。

 

.dynamic

这里面保存了动态链接器所需要的基本信息,段是一个结构数组,结构定义如下:
Typedef struct{
           Elf32_Sword              d_tag;
           Union{
         Elf32_Word      d_val;
         Elf32_Addr       d_ptr;
};
}Elf32_Dyn;
根据d_tag的类型,d_vald_ptr表示不同是值。
DT_SYMTAB      符号表地址
DT_STRTAB       字符串表地址
DT_STRSZ          字符串表大小
DT_HASH           哈希表地址
DT_INIT              初始化代码位置
DT_FINIT            结束代码位置
DT_NEED           依赖的共享对象文件
DT_REL               重定位表地址

 

.dynsym动态符号表

.dynsym表只保存了与动态链接相关的符号,而.symtab则保存了所有的符号,包括动态链接符号。动态链接符号表结构与静态链接符号表几乎一样。
导入符号 vs 导出符号
.symtab类似,动态符号表也需要一些辅助表,如:
.dynstr动态符号字符串表

.hash符号哈希表

 

动态链接重定位表

共享对象需要重定位的主要原因是导入符号的存在。因为导入符号需要在运行时才能确定其值。
对于使用PIC技术的可执行文件和共享对象来说,虽然代码段不需要进行重定位,但数据段还包含了绝对地址的引用,因为代码段中绝对地址相关的部分被分离出来到了数据段,变成了GOT。除了GOT,数据段还可能包含其他绝对地址的引用,如数据段的地址无关性那一节。
动态链接重定位相关结构

静态链接的重定位在链接时完成,而共享对象的重定位在装载时完成。
.rel.dyn是对数据引用的修正,所修正的位置位于.got以及数据段。
.rel.plt是对函数函数引用的修正,所修正的位置位于.got.plt

 

静态链接的重定位类型:R_386_32P_386_PC32
动态链接新的重定位类型:R_386_RELATIVER_386_GLOB_DATR_386_JUMP_SLOP

 

 

动态链接时进程堆栈初始化信息

地址从高到低:
环境变量
参数
辅助信息数组
环境变量指针
参数指针
参数数量
每一项都用0隔开。
辅助信息数组保存了动态链接器所需要的一些信息。辅助信息的格式是一个结构数组,结构定义如下:
Typedef struct{
           Uint32_t           a_type;
           Union{
         Uint32_t  a_val;
}a_un;
}Elf32_auxv_t;

 

6     动态链接的步骤和实现

动态链接器自举

动态链接器也是一个共享对象,其重定位工作必须由自己完成。所以,动态链接器不可以依赖其他任何对象,而且全局和静态变量的重定位工作要自己完成。
动态链接器有一段精巧的自举代码,完成上面的工作。

 

装载共享对象

完成基本自举后,动态链接器将可执行文件和链接器本身的符号表归并到一个全局符号表中(Global Symbol Table)。
然后根据.dynamic段中的DT_NEEDED装载所依赖的共享对象,并将共享对象的符号表也归并入全局符号表。
这里有一个符号有限的问题:当两个共享对象符号表中有一个重名的符号时,后装载的符号会被忽略,这就是全局符号介入问题。
由于全局符号介入的存在,在共享对象中的第一类地址引用中,即模块内部的函数调用、跳转,并不会产生相对地址调转的代码,因为假如该符号有全局符号介入问题,则代码需要重定位。于是通过GOT/PLT方法产生最终代码。
当确定改函数不会产生全局符号介入问题时,可以在函数前加static关键字,表示模块内部函数,生成相对地址转移代码,加快跳转速度。

 

重定位和初始化

当完成上面的步骤后,链接器开始重新遍历可执行文件和每个共享对象的重定位表,将它们的GOT/PLT中需要重定位的位置进行修正。由于有了装载时合并的全局符号表,确定了各个符号的虚拟地址,地址修正比较简单。
重定位完成后,如果某个共享对象有.init段,则执行之,如果有.finit段,则进程结束时执行之。可执行文件的.init段,动态链接器不会执行,程序初始化时执行。
这是所有的准备工作完成,进程的控制权交给程序入口地址执行。

 

Linux动态链接器的实现

动态链接器是Glibc的一部分,远吗位于elf目录。起始地址_start位于elf/rtld.c中的_dl_start()函数,对ld.so自己进行重定位,即自举。这是可以使用全局变量了。然后调用_dl_start_final()收集一些运算数值,再调用_dl_sysdep_start()进行平台相关的处理,最后进入_dl_main()进入真正意义上的动态链接器的主函数了,

 

7     显式运行时链接

支持动态链接的系统往往支持显式运行时链接,也叫运行时加载。一般共享对象不需要进行任何修改就可以进行运行时加载,这种共享对象往往叫做动态装载库
共享对象和动态库没有什么区别,主要区别是:共享对象是由动态链接器在程序启动前负责装载和链接的,而动态库的装载则是通过一系列动态链接器提供的API完成的。这几个API的实现是在/lib/libdl.so.2中,声明和相关常量定义在标准头文件<dlfcn.h>中。
1         void* dlopen(const char *filename, int flag)
如果filename这个参数为0,那么dlopen返回全局符号表的句柄,否则返回加载模块的句柄。
2         void* dlsym(void *handle, char *symbol)
查找加载模块的符号,如果找到,则返回符号值,否则返回NULL。返回值对于不同的符号,意义是不同的。
这里有一个符号优先级的问题:如果查找的符号是全局符号表中的符号(dlopen的第一个参数为NULL),则按装载序列。如果是新打开的一个共享文件,则按依赖序列。
依赖序列:本共享对象所依赖的的共享对象的深度优先
3         char* dlerr()
4         dlclose(const char *file)