读大牛Ulrich Drepper关于如何写动态库的大作心得。
原文地址:
https://www.akkadia.org/drepper/dsohowto.pdf
一些术语
DSO, Dynamic Shared Objects
PLT,Procedure Linkage Table
关键点
- section 1
回顾a.out的历史,阐述它的优缺点, 引入COFF, 再到ELF. 相关描述也可以参照有一定年头的Linker and Loader一书. 其中有更多不同平台上的文件格式的发展的介绍:)
简述ELF的结构及组织:- 系统中同时存在static linker和dynamic linker, DSO要依靠dynamic linker来相互合作. 经由dynamic linker的一些动作, 如reloaction之后, 控制流才会转到相应的可执行文件.
- ELF header提供了所有ELF section的相关信息, 它的结构可以参照Dwarf3.pdf, 本章简单的介绍了一下ELF header中的各个结构成员的用途. 它会指向各个program header.
- program header中有一个关键的PT INTERP tag, 它表示了应该用什么样的dynamic linker, 利用它来指定具体的dynamic linker. 当它被kernel 映射到进程空间之后, 还需要为它准备auxiliary vector的数据结构(以便绕过一些不必要的系统调用, 使得dynamic linker更易工作, AT_开头的tag,都是与auxiliary vector相关的), 之后才能把控制权转交到dynamic linker.
- dynamic linker的三个任务:确认并加载相关依赖, 重定位应用程序和所有的依赖, 正确的初始化应用程序和其依赖.
最繁重的任务就是重定位, 它的时间复杂度是O(R +nr), 其中R是相关重地位的个数, r是命名重地位的个数, n是主程序所用到的DSO的个数
有不少针对上诉任务的优化方案, 其中有的能使得时间复杂度变为O(R + rn log s), 其中s是符号的个数. 从中可见,最终要的就是尽可能的减少重定位和符号的个数.
而重地位中最复杂的就是symbol relocation.
- 禁止lazy relocation: 对linker 使用-z now参数. 但是, 这依赖于重新link DSO或者更改binary, 所以, 慎用.
- 传统的ELF方式
- 计算symbol name的hash值
- 在lookup scope内,对各个object进行以下操作
2.1 使用该object的hash值和hash table的大小来决定hash bucket
2.2 获得该symbol名称的offset, 并用它作为一个以NUL结尾的名称
2.3 拿该symbol name与relocation name比较
2.4 如果名称相符, 再比较它们的version name, 两者都相同, 则找到所要的.
2.5 如果名称不相符, 在hash bucket中选取下一个
2.6 如果当前的object所包含的链中都没有找到, 就选取下一个lookup scope - 如果在所有的lookup scope中都没有找到, 则搜寻失败
从中可以看出, 关键的效率考量在如何选取lookup scope中的object数及hash chain的长度. GNU 通过优化这两者提供了另外一种更有效的方式.
- GNU 方式, 优化symbol name comparison, symbol entry link list更好的利用了CPU cache
- 计算symbol name的hash值
- 在lookup scope内,对各个object进行以下操作
2.1 该hash值用来判断是否该当前的object中已经有该值了. 这是通过2-bit的Bloom filter来完成的(它使得比较操作被显著减少了). 如果没有, 就在该scope中选取下一个object进行查找.
2.2 使用该object的hash值和hash table的大小来决定hash bucket. 该值是一个symbol index
2.3 从object chain中拿到这个symbol index所对应的entry, 用该entry中的值与1中的值比较. 忽略bit 0