链接器主要是干三件件事:空间地址分配,符号解析,符号重定位,文章也会围绕这三部分来进行展开说明
空间地址分配
对于多个输入文件,链接器需要将其对应的段先进行合并,将符号表中符号引用和符号定义合并称为一张全局符号表,计算输出文件各个段合并后的长度和 位置,并且与VMA(虚拟内存地址
)建立关联。
objdump -h xxxx.o
1
2
3
4
5
6
Idx Name Size VMA LMA File off Algn
0 .text 00000027 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000067 2**0
CONTENTS, ALLOC, LOAD, DATA
......
File off
这一列就是段的偏移量,再加上linux中默认给elf分配的开始位置,能计算出每个段对应的虚拟内存地址,
链接之前VMA都是0,链接后VMA就是各段所对应的虚拟地址。
1
2
3
4
5
6
Idx Name Size VMA LMA File off Algn
0 .text 00000071 00000000004000e8 00000000004000e8 000000e8 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
2 .data 00000004 0000000000601000 0000000000601000 00001000 2**2
CONTENTS, ALLOC, LOAD, DATA
......
在段的虚拟地址分配完之后,就开始计算各个符号的虚拟地址了。符号在段内为相对位置是固定的。所以比较好计算,通过符号表里的Value值结合对应的段地址就可以计算出来。
所以在链接器完成地址和空间分配之后,就可以确定了所有符号的虚拟地址了。
符号解析与重定位
上一篇文章符号表格式中我们知道有的符号引用是在其他目标文件中,在链接前的Ndx都是UNDEF并且它的虚拟符号地址都是0,等到了链接这步才会将这些指令的地址给重新定位上,那链接器是如何知道这些符号引用的具体位置呢?
在上面空间地址分配这块已经说明了链接器在完成地址和空间分配后,就可以知道所有符号的虚拟地址了。剩下的工作就是将这些虚拟地址对应到符号上呢?
在前面提过rel.data和rel.text这两个段,这两个段就是定位的关键,它们称为是重定位段,如果.text中需要有重定位的地方,就会有一个相对应的rel.text的段保存了代码段的重定位段中。
readelf -s xxxx.o
1
2
3
4
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000014 R_X86_64_32 shared
0000000000000021 R_X86_64_PC32 swap-0x0000000000000004
里面的offset这一列就是表示代码段中要调整的位置,而TYPE分绝对寻址修正和相对寻址修正,就是结合OFFSET用来计算符号具体位置。(具体计算请查阅《程序员的自我修养链接、装载与库》)
COMMON与.BBS
正是由于弱类型符号可以存在多个文件,所以在一开始并不知道弱类型符号的所占用的空间大小,因为其他模块中的这个弱符号所占用的空间可能更大,所以无法分配到.bbs段中。把它的决定权留给链接器处理,等链接过程中若符号的空间大小已经可以确认了,就可以再最终输出文件的BBS段为其分配空间,总体上看未初始化的全局变量最终还是在BBS段上分配。另外如果已经符号初始化为0 说明是强符号,所以放在bss。
对于一个弱类型符号(未初始化的全局变量)在链接之前是不知道具体地址的,编译器无法为弱符号在.bbs分配具体大小,等到了链接阶段,弱符号的最终大小已经确定了。它可以在最终输出文件bbs段中为其分配空间。所以总体上未初始化全局变量最终还是放在bbs段。
静态库链接
静态库就看成多组目标文件的集合。编译系统将所有相关目标文件打包为一个单独的文件,称为静态库。可以用作链接器的输入,当链接器构造一个输出的可执行文件,它只复制静态库中被应用程序用到的目标模块。 这块如果深入也是有不少东西,不过暂时就想了解下概念,毕竟我不是专业的C/C++开发。 😏(摸鱼~~~)
资料引用
- 《深入理解计算机系统(第三版)》第七章
- 《程序员的自我修养链接、装载与库》第四章