dynamic_link
dynamic_link
动态链接解决了静态链接带来的内存、磁盘、程序开发和发布的问题;
简单说,动态链接就是推迟了链接的进行,等到程序要运行时才链接;如果内存中已加载了目标文件副本,则不必再重新加载一份;
延迟绑定
重要的是延迟绑定的概念,延迟绑定优化了动态链接的部分性能;
动态链接导致了大量的函数引用,因此执行前会耗费时间解决函数引用符号和重定位;
延迟绑定是指,函数第一次用到时才进行绑定(符号查找和重定位),如果不用到就不用耗费开销,这借助了PLT表;
例如当libc.so
第一次调用bar()
函数时,需要调用动态链接器中的某个函数来完成绑定工作,在glibc
中,这个函数就是__dl_runtime_resolve1()
;
第一次调用bar()
函数时,通过一个PLT项
的结构来进行跳转;
bar@plt
的实现:
bar@plt: |
在第一次调用中,*(bar@got)
的内容并非真正的bar()
地址,而是push n
的地址,这个n
事实上是bar
符号引用在重定位表.rel.plt
的下标;
接着再次push moduleID
,将模块的ID压入到栈中,然后跳转到__dl_runtime_resolve
;
而__dl_runtime_resolve
的工作就是将bar
真正的地址填入到bar@got
中;
第二次调用bar()
函数的时候,就可直接跳入真正的bar
地址中;
ELF将GOT拆分成了两个表:
.got
:全局变量引用地址
.got.plt
:保存的是函数引用的地址
这里的.got.plt
中保存了.dynamic
段地址、模块的ID、__dl_runtime_resolve
的地址;
为了减少代码重复使用,上述的bar@plt
的实现为:
PLT0: |
动态链接相关结构
.interp
段保存一个字符串,是可执行文件所需链接器的路径
.dynamic
段保存链接器的基本信息,比如依赖于哪些共享对象,动态链接符号表位置、动态链接重定位表位置、共享对象初始化代码等;
.dynsym
段是动态符号表,保存动态链接相关的符号,可以简单地将导入函数看作对其他目标文件中函数的引用,导出函数看作是本目标文件定义的函数;
.rel.xxx
段是重定位表,.rel.dyn
实际上是对数据引用的修正,所修正的位置位于.got
和数据段。.rel.plt
是对函数引用的修正,修正的位置是.got.plt
段。例如,当调用printf
后,需要进行重定位,找到printf
重定位的入口,这个入口在.got.plt
中,假设.got.plt
的基址为0x000015c8
,而printf
在该表第5项。.got.plt
的前三项是由系统占据的,第四项才开始放导入函数的地址,所以可以得到printf
的偏移为0x000015c8 + 4 * 4 = 0x000015d8
;这是libc.so
的.got.plt
的结构;
当链接器要进行重定位的时候,先查找printf
的地址,printf
位于libc.so
中。链接器会在全局符号表中找到pritnf
的地址,将这个地址填入.got.plt
中偏移为``0x000015d8`的位置中,从而实现了地址的重定位;
动态链接的步骤和实现
动态链接器可以不依赖于其他任何共享对象;
本身所需要的全局和静态变量的重定位工作由其本身完成;
动态链接器的入口地址为自举代码的入口,操作系统把控制权交给链接器后,则进入自举过程;
自举
- 首先找到自己的GOT表,GOT的第一个入口保存的是
.dynamic
段的偏移地址,以此通过.dynamic
段的信息获得本身的重定位表和符号表位置,从而得到动态链接器自身的程序入口; - 接着考虑自己的全局变量和静态变量,在GOT/PLT没有被重定位前,自举代码不可以使用任何全局变量并且调用任何函数;
装载共享对象
- 将可执行文件和链接器本身的符号变合并到一个符号表,即全局符号表;
- 寻找可执行文件依赖的共享对象,通过
.dynamic
的DT_NEEDED
入口,其指出依赖的共享对象; - 找到共享对象文件并打开,读取ELF header和
.dynamic
段,将对应的代码段和数据段映射到进程空间,会涉及到图遍历的问题; - 最后全局符号表中应当有所有依赖共享对象的符号信息;
全局符号介入问题
当一个共享对象里面的全局符号被另一个共享对象的全局符号覆盖时,则称为共享对象全局符号介入;
链接器在往全局符号表里加入符号信息时,如果发现全局符号表中已经存在,那么后来加入的符号会被忽略;
重定位和初始化
链接器重新遍历所有可执行文件和每个共享对象的重定位表,根据全局符号表,将他们的GOT/PLT中每一个需要重定位的位置进行修正;