7.1 为什么要动态链接
- 静态链接浪费内存和磁盘空间, 模块更新也比较困难
- 而解决空间浪费和更新困难这两个问题最简单的方法就是将程序模块相互分割开来, 形成独立的文件。也就是说, 把链接这个过程推迟到了运行时候再进行, 这就是动态链接的基本思想
- 通过动态链接不仅可以节省内存, 也可以减少物理页面的换入和换出, 还可以增加CPU缓存的命中率。
- 动态链接还可以增强程序的兼容性
- 动态链接的基本思想是把程序按照模块拆分成各个相对独立的部分, 在程序运行时才将他们链接在一起形成一个完整的程序, 而不是像静态链接一样把所有的程序模块都链接成一个单独的文件
7.2 简单的动态链接的例子
- 动态链接下的可执行文件和共享对象都可以看做是程序的一个模块
- 如果foobar 是一个定义于静态目标模块中的函数, 那么连接器就会按照静态链接的规则将program中的foobar的地址进行重定位, 而如果他是定义在某个动态共享中的函数, 那么连接器就会将这个引用标记为一个动态链接符号, 不对他进行重定位, 把这个过程留到装载的时候再进行。
- 共享对象的最终装配地址在编译的时候是不确定的, 而是在装载的时候, 装载器根据当前空间地址的空闲情况动态分配一块足够大小的虚拟地址空间给相应的共享对象
7.3 地址无关代码
- 为了实现动态链接,我们需要确定共享对象在进行虚拟地址空间中的位置, 采用固定状态地址的时候, 首先遇到的问题就是, 共享对象地址的冲突问题, 另外静态共享库的升级也很成问题
- 共享对象在编译的时候不能假设自己在进程虚拟空间中的其实位置
- 基本思想: 在链接的时候, 对所有绝对地址的引用不作重定位, 而把这一步推迟到装载的时候再完成
- 基址重置: 整个程序按照一个整体被加载, 程序中的指令和数据的相对位置不会改变,因此加载的时候只需要一个整体的偏移即可
- 装载时候重定位可以有效解决动态模块中 存在绝对地址引用的问题, 但是他的缺点也很明显:* 指令部分无法再多个进程之间进行共享*
- 解决方法是: 将指令中那些需要被修改的部分分离出来, 跟数据部分放在一起, 这样指令部分就可以保持不变, 而数据部分可以再每个进程中保留一个副本, ie, 地址无关代码 PIC, (有点类似设计模式里面将变化隔离的思想)
- 小结:
- ELF 中全局变量也是通过GOT(全局偏移表) 进行访问
- 两类需求:
- 多进程共享全局变量, 被称为 共享数据段
- 多个线程访问不同的全局变量的副本, 称为 线程私有存储
- 小结:
7.4 延迟绑定 PLT
- 动态链接比静态链接慢, 主要原因: 动态链接下对于全局和静态的数据访问都要进行复杂的GOT定位, 然后间接寻址, 对模块间的调用也要先定位 GOT, 然后间接跳转。
- PLT, 通过在传统的利用GOT调用的方式之上加入了一个中间层进行跳转, 第一次访问的时候, 会通过GOT进行查找, 找到的数据将直接给到PLT, 使得下次调用的时候, 就不需要再次访问这个PLT了。
7.5 动态链接相关的结构
- 基本流程: OS 加载动态连接器, 动态连接器动态链接程序, 可执行文件执行
- .interp 段 用来标识动态连接器的路径
- .dynamic 段 用来记录动态连接器所需要的基本信息, 如共享对象, 动态链接符号表位置, 动态链接重定位表的位置, 共享对象初始化代码等信息
- .symtab 动态符号表
- 辅助信息数组, 保存了参数个数, 参数指针, 环境变量指针等信息
7.6 动态链接的步骤和实现
- 启动动态连接器本身, 装载所有需要的共享对象, 最后重定位和初始化
- 动态连接器自举(动态连接器本身不可以依赖于其他任何共享对象, 其次动态连接器本身需要的全局和静态变量的重定位工作需要由他本身完成, 不能使用任何系统库, 运行库, 全局和静态变量, 包括本身定义的函数(涉及GOT/PLT 调用))
- 装载对象的过程一般通过类似图遍历的方式进行
- .init 段用来初始化, .finit 段用来类似析构操作
- windows 中通过rundll32.exe 工具可以实现将一个dll 当做可执行文件进行执行
7.7 显式运行时链接
- 运行时候装载, 这种共享对象往往被称为是动态装载库
- 动态库和一般的共享对象没有区别, 主要区别在于共享对象是由动态连接器在程序启动之前负责装载和链接的, 这一系列步骤都是由动态连接器自动完成, 而动态库的装载则是通过一系列由动态连接器提供的API进行实现的
7.8 小结
- 动态链接可以更加有效的利用内存和磁盘资源, 方便的升级程序
- 通过装载时候重定位和地址无关代码可以解决绝对地址引用问题 , 但是装载时重定位无法共享代码段, 运行速度快, 而 地址无关代码 运行速度稍慢, 但是可以实现代码段在各个进程之间的共享问题。