动态链接(Dynamic Linking):一个单一个可执行文件模块被拆分成若干模块,在程序运行时进行链接的一种方式
欲扬先抑:静态链接饱受 内存磁盘空间浪费(同一个目标文件存在多份供使用),更新发布需要重新链接 等问题,而动态链接解决了上面的问题,提供了 可扩展性(动态选择加载),但饱受 兼容性 (如不兼容,程序将崩溃无法运行),性能损耗(装载时重新链接)困扰
基本思想:把程序按照模块 拆分 成各个相对 独立 部分,在程序 运行时 才将它们链接在一起形成一个 完整 的程序,而不是像静态链接一样把所有的程序模块都链接成一个单独的可执行文件
普及: Linux 下,ELF 动态链接文件叫 动态共享对象(DSO,Dynamic Shared Objects),以 .so 结尾;Windows 下,动态链接文件叫 动态链接库(Dynamical Linking Library),以 .dll 结尾
普及:动态链接器 才完成以上操作,策略:延迟绑定(Lazy Binding)
普及:PIC(Position-independent Code)地址无关代码 技术,GOT(Global Offset Table)全局偏移量表
动态链接的地址空间分配 奥秘
共享对象的 最终装载地址 在编译时是 不确定 的,而是在 装载 时,装载器根据当前地址空间的空闲情况,动态分配一块 足够大小 的虚拟地址空间给相应的共享对象
普及:静态链接时提到的重定位叫做 链接时重定位(Link Time Relocation),而动态链接提到的重定位叫做 装载时重定位(Load Time Relocation),也成 基址重置(Rebasing)
共享对象的虚拟地址空间分配技术离不开:地址无关代码(PIC机制),共享模块的全局变量(线程私有存储 Thread Local Storage),数据段地址无关性,也正是这些无关性技术,才保证了共享对象被多个程序引用时,能够发挥各自的作用,确保程序不出现崩溃
延迟绑定(Lazy Binding)的 艺术
性能的事实:动态链接是以 牺牲性能 为代价的,具体表现在:一方面 对于全局和静态数据的访问、模块间的调用都要进行复杂的 GOT定位,然后 间接寻址,另一方面动态链接的工作是在 运行时 完成的,不是事先链接好的
优化的手段:延迟绑定,理解为当函数第一次被用到时才进行绑定(符号查找、重定位等),使用 PLT (Procedure Linkage Table) 来实现,通常以 .plt 作为段名,保存在 ELF 文件中
实现原理:通过 PLT 中的待跳转指令,将各个函数的待跳转偏移量链接到地址栏,等运行时使用的时候,直接读取偏移量进行跳转,这里不是直接读取GOT链接到真正的目标函数地址,而是读取偏移量,进行jump,再到目标函数
原话:而是将上面代码中第二条指令"push n"的地址填入到 bar@GOT 中,这个步骤不需要查找任何符号
动态链接 相关结构
.interp 段:保存可执行文件需要的 动态链接器 的路径,内容就是个字符串
.dymanic 段:保存了动态链接器所需要的 基本信息:依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等,位于经典的头文件 elf.h (Elf32_Dyn) 中
动态符号表(Dynamic Symbol Table)[.dynsym 段]:只保留动态链接这些模块之间的符号导入导出关系
动态链接重定位表:.rel.dyn 是对数据引用的重定位修正,它所修正的位置位于 .got 及 数据段,.rel.plt 是对函数引用的修正,它所修正的位置位于 .rel.plt
动态链接时进程 堆栈初始化信息
堆栈里保存了关于进程 执行环境:程序执行入口 和 命令行参数 等信息,还保存了动态链接器所需要的一些 辅助信息数组(Auxiliary Vector)
int main(int argc, char argv[]) ,argc 表示参数的个数,*argv数组 是参数
动态链接器的 步骤 和 实现
步骤:
启动动态链接器本身
动态链接器本身也是一个共享对象,因此它的编写除了不能依赖于其他任何共享对象,而且本身所需要的全局和静态变量的重定位工作也由它本身来完成,因此有一段精致的启动代码称之为 自举(Bootstrap)
装载所有需要的共享对象
动态链接器会根据 .dynamic 段中所依赖的共享对象,使用 广度优先的图遍历 算法,来按顺序状态共享对象;全局符号表(Global Symbol Table) 的介入,为了解决相同符号名的链接冲突,如已存在,则后加入的符号被忽略
重定位和初始化
动态链接器根据进程的全局符号表,对需要重定位的位置进行修正,如果某个共享对象有 .init 段,那么动态链接器会执行,实现共享对象 特有的 初始化过程(常见的,C++全局/静态对象的构造就在此初始化)
实现:
普及:内核在装载完 ELF 可执行文件以后,就返回到用户空间,将控制权交给程序的入口。对于静态链接,入口地址是 e_entry 制定的地址,对于动态链接,入口地址是将动态链接器映射至进程地址空间,然后把控制权交给动态链接器
普及:Linux 动态链接器:/lib/ld-linux.so.2 -> /lib/ld-x.y.z.so
动态链接器在实现中的 几个问题
动态链接器本身是动态链接还是静态链接? 答:静态链接,不依赖于任何共享对象
动态链接器本身必须是 PIC 吗?答:不一定,如果是 PIC 会简单些
动态链接器可以被当作可执行文件运行,装载地址是多少?答:0x00000000 无效的,内核 会装载它时 分配 一个 有效 的
灵活技术:显示运行时链接(Explicit Run-time Linking)
顾名思义:让程序自己在运行时控制加载制定的模块,并且可以在不需要该模块时将其卸载,操作对象是 DLL
动态链接库提供了 4 个 API 来实现,它们分别是:dlopen(),dlsym(),dlerror(),dlclose()
总结一波:
动态链接可以更加有效地利用内存和磁盘资源,可以更加方便的维护升级程序,可以让程序的重用变得更加有效和可行
装载时重定位和地址无关代码是解决绝对地址引用的两个方法,装载时重定位的缺点是无法共享代码段,但是它的运行速度较快;而地址无关代码的缺点是运行速度较慢,但它可以实现代码段在各个进程之间的共享,还介绍了 ELF 的延迟绑定 PLT 技术
.interp、.dynamic、动态符号表、重定位表等接口,它们是实现 ELF 动态链接的关键结构。动态链接器实现自举,装载共享对象,实现重定位和初始化的过程,实现动态链接,最后关键技术:显示运行时链接