《程序员的自我修养》—— 静态链接
第四章-静态链接
总结
一个c语言程序需要经过预处理、编译、汇编、链接四个过程后才会生成可执行文件。预处理做了什么?编译做了什么?这里记得复习,汇编之后生成了目标文件。目标文件中包括文件头以及不同的段,比如代码段存放二进制代码、数据段存放初始化的变量等等很多段。为什么要有链接这一个过程呢?因为目标文件可能引用了其他目标文件中的符号,比如引用了全局变量或者函数,这些符号在目标文件中是没有准确的地址的,或者是一个假的地址,这需要链接器来确定这些符号的地址。整个链接步骤分为两步:首先是空间与地址分配,获取每个目标文件各个段的长度和位置,然后按照段合在一起,就是相似段合并,然后分配每一个段的虚拟地址。然后根据重定位表(也是目标文件中的一个段)找到重定位入口(找到哪一些符号是需要重新分配地址的),因为每一个符号在所在段中相对位置是确定的,然后现在每个段的虚拟地址也确定了,因此可以得到每个符号的虚拟地址。
当我们有两个目标文件,如何把他们链接起来呢?
整个连接步骤分为两步
空间与地址分配
- 获得每个目标文件各个段的长度、属性和位置
- 符号表中的所有符号定义和符号引用收集起来,统一放到一个全局符号表
符号解析与重定位
- 读取输入文件中段的数据、重定位信息
- 进行符号解析与重定位、调整代码中的地址
1 | ld a.o b.o -e main -o ab |
空间与地址分配
对于多个输入目标文件,链接器如何将他们的各个段合并到输出文件?
相似段合并
上图展示了链接前后各个段的情况
- 链接前VMA(虚拟地址)都是0,因为虚拟空间还没有被分配,链接后都被分配了相应的虚拟地址
- 链接后各段的大小都进行了合并
符号地址的确定
在第一步的扫描和空间分配阶段,链接器完成了空间分配,这个时候各个段的虚拟地址已经确定了
首先,各个符号在段内的相对位置是固定的,比如a.o中的main函数相对于.text段的偏移是X,而各个段的虚拟地址也已经确定,所以main的虚拟地址就是在.text的虚拟地址+X
符号解析与重定位
对于下面两个文件,a中调用了外部的一个全局变量shared以及一个函数swap
对于单独的a的目标文件,编译器并不能确定shared和swap的地址究竟是什么,所以用的都是假地址,真正的地址计算工作留给了链接器
在第一步中已经知道了每个符号的虚拟地址,因此链接器可以根据符号的地址对每个需要重定位的指令进行地址修正,下图是修正后的结果
很自然的一个问题,链接器是怎么知道shared和swap这两条指令需要被调整呢?
重定位表
对于可重定位的ELF文件来说,必须包含有重定位表,用来描述如何修改相应的段里的内容,一个重定位表就是目标文件中的一个段,比如.rel.text段保存了代码段的重定位表,.rel.data保存了数据段的重定位表
下面是一个代码段的重定位表,偏移表示代码段中必须要重定位的位置。每一行都是一个重定位入口
符号解析
之所以要链接是因为我们目标文件中用到的符号被定义在其他目标文件中
所有未定义的符号都应该能够在全局符号表中找到,否则链接器就报符号未定义错误。
静态库链接
一个静态库可以看做是一组目标文件的集合,比如Linux中有C语言静态库libc位于/usr/lib/libc.a
把零散的目标文件直接给库的使用者比较麻烦,通常使用ar压缩程序将多个目标文件压缩到一起,libc.a 就是这么形成的
我们可以看到libc中是包含有printf这个函数所在的目标文件的
那是不是我们写的helloword.c代码只要和printf.o以及printf.o中用到的目标文件,全部链接起来就可以了呢,就像下图一样?
而真实在链接的过程其实更加复杂,不仅仅会涉及到printf.o,甚至不仅会用到libc.a C语言库,还有其他一些辅助性质的目标文件和库, 使用-verbose会将整个编译链接过程的中间步骤打印出来
具体步骤如下:
- 第一步调用cc1程序编译成汇编语言,再用as程序汇编成目标文件,最后调用collect2程序完成链接
- collect2是ld链接器的一个包装,不仅会调用ld链接器,还会对链接结果做一些处理
- 至少有这么多库被链接了起来