操作系统:动态链接/动态加载

静态链接带来的问题

  1. 像是libc这种几乎每个程序都要用到的库, 如果是静态的, 那么不仅意外着每个程序的 可执行文件很大, 浪费磁盘空间. 并且当程序加载到内存时, 可能许多程序都会用到printf , 使得内存中会存在好多份的printf源码.

  2. 维护和更新难. 一旦静态链接的其中一个目标文件更新, 所有的可执行程序都要重新链接.

  3. 不满足局部性原理. 上面提到, 内存中同时存在多份的printf源码会破坏局部性原理的. 显然如果所有的程序共享一份printf源码的想法更好. 即动态加载.

  4. 可移植性差. 静态链接, 只要有一个依赖目标文件的实现不同, 软件厂商就得专门发布一个 版本. 而动态链接则信赖客户电脑上的动态库, 相当于一个中间层.

动态链接的过程

对比静态链接使用ld链接器在编译后即执行链接, 动态链接则是将链接过程推迟到运行时, 即装载到内存时.

这样, 链接器在链接产生可执行文件时就有两种做法:

  • 对于静态符号, 按照静态链接的规则进行地址引用重定位
  • 对于动态符号, 链接器则仅标记其为动态链接中的符号, 不进行处理. 而是等到装载时由 专门的动态链接器来完成动态符号的链接工作.

⁉️ 链接器如何确定一个符号是静态的 or 动态的?

在动态共享对象(.so)中保存了完整的动态符号表*, 表中存在的符号即为动态的, 否则为静态.

Linux 的 C 语言运行库glib的动态链接版本叫libc.so. 它在外存上只保存一份, 所有的程序 都可以在运行时使用它. 所以千万不要删掉它.

动态链接有一定的性能损失, 因为每次运行程序时都要重新链接, 并不像静态链接是一劳永逸的. 也有例如延迟绑定对性能进行优化的方法, 大概仅有 5%的损耗, 与带来的便利相比可以忽略不计.

地址无关代码 PIC

GCC 生成动态库时需要添加参数-fPIC, 含义就是生成地址无关码

地址无关码的含义是代码中不包含任何的绝对地址引用, 全都是相对地址.

  • 对于模块内的跳转/数据引用, 使用相对的跳转/加载指令. 例如 ARM 指令集中的B, ADR, 经过汇编器之后, 目标的地址都会转为相对于该指令(PC)的偏移.
  • 对于模块间的跳转/数据引用, 借用GOT 表来间接实现地址无关.

而如果该动态库中全部使用相对地址, 那么加载时也就不需要进行重定位, 即所有的程序都可以 共享这些地址无关代码.

上面说 PIC 的动态库不需要重定位其实是错误的, 只不过它的重定位过程不需要修改代码段, 而是设置了一个放置在数据段的GOT表来实现代码段部分的地址无关特性.

现在貌似 GCC ARM 版本在编译动态库时强制使用-fPIC 选项, 否则会报错. 对此我不是 非常确定!

一般来说, 不将主程序编译为地址无关码. 因为主程序不需要共享, 而且地址无关码的调用 需要两个指令: 计算地址 + 跳转. 多了一步根据偏移得到绝对地址.

动态加载

上面说的,有了动态链接之后,库文件的重定位到运行时。运行时一定是库加载到内存时候吗? 看你怎么理解“运行时”,是主程序运行的时刻,还是库中的代码运行的时刻。所以我可以回答的是: 主程序运行时,动态库不一定立马加载到内存,也就不一定发生动态链接

动态加载这种思想类似于 COW,可以提升主程序启动的速度,启动消耗的内存更小。 缺点就是第一次调用库的速度变慢,因为加载、重定位都需要发生。

有一个需要注意的点,一般的动态链接过程(不考虑动态加载)就是所有的库一股脑加载到内存, 然后进行重定位工作。重定位完所有符号后,会将主程序的 GOT 表设置为只读,避免 got 表被恶意修改, 这个过程是动态链接器ld.so做的。

这个说法可以通过看 elf 的 section 布局确定,got 表都会和一些其他的只读 section 放在一起, 而与总是 RW 的 section 分开,这样布局以后设置 got 只读的时候更加方便。

这里图中的 got 虽然是可写入的,只是为了可以被重定位修改,改完之后由 ld.so 重映射为只读。

加入动态加载之后,其实 got 表不能在主程序加载完后就设为只读,因为链接延后到了动态库加载到内存时, 现在的做法时,将 got 表进行拆分==> .got 和.got.plt, .got.plt 保存的是以后运行中随时需要重定位的符号, 就不能被设为只读,而为了兼容,.got 就会在主程序加载后被设为只读,因为里面没有需要执行时重定位的符号了。

主程序编译时添加选项 -Wl,-z,lazy 启用动态加载

动态链接和动态加载关系

动态加载和动态链接并不进行绑定

  • 静态链接-动态加载:因为是静态链接,所以库的地址已经确定,但加载时只分配虚拟地址空间
  • 动态链接-动态加载:用到库时才加载到内存,此时才进行动态重定位
  • 动态链接-静态加载:运行主程序时库同时被加载到内存,然后立马执行动态重定位

创建于: 2022-06-26T19:50:45, Lastmod: 2024-01-17T16:38:18