Chap 7: Linking
约 1658 个字 113 行代码 3 张图片 预计阅读时间 11 分钟
1. Compilation
广义上的编译分为以下几个步骤:
- C pre-processor:对源文件进行预处理,主要为库、宏定义替换,得到
.i文件 - Compiler:将
.i文件翻译成汇编.s文件 - Assembler:将
.s文件翻译成可重定位目标文件.o - Linker:将可重定位目标文件链接成可执行文件
2. Sections
ELF:Executable and Linkable Format(可执行可链接格式)
每一个 ELF 分为三个部分:
- ELF Header
- Sections
- Section Header Table(SHT)
以下面的 C 语言代码为例:
1 2 3 4 5 | |
ELF Header:在 x86/64 下通常为 64 字节,主要包含 SHT 的起始位置、SHT 的表项数量、每个表项的大小.根据这些可以推断出 SHT 的位置.
SHT:SHT 的每个表项对应一个 Section.
Sections:
.text已编译程序的机器代码.data已初始化的全局和静态变量.bss未初始化或初始化为 0 的全局或静态变量.其不占用可执行文件中的实际数据空间,但运行时会占用内存空间(SHT 会记录.bss应该有多大,但这些空间由于最后会被初始化为 0,所以不需要记录那么多 0 占用空间)- 对于未初始化的全局变量,由于其可能被外部引用而被赋值,因此其是暂定定义 tentative.在编译阶段,其符号为 COMMON.经过链接后,若外部将其赋值为非零,则存放于
.data中,反之存放于.bss中.
- 对于未初始化的全局变量,由于其可能被外部引用而被赋值,因此其是暂定定义 tentative.在编译阶段,其符号为 COMMON.经过链接后,若外部将其赋值为非零,则存放于
3. Symbol
3.1 Symbol Table
linux> readelf -s main.o 可以查看 ELF 的符号表.符号表包含了所有的符号,包括全局 / 静态变量、函数,每个符号有三个重要参数:
- Bind:一般为 local / global / weak;只有
static变量是 local(因为局部变量在栈存储,不会记录在符号表),只有用__attribute__((weak))修饰的函数 / 变量为 weak,属于弱符号(但弱符号不一定都是 weak bind) - Type:一般为 object / func / notype;如果函数声明而未定义、
extern变量声明而未定义则为 notype - Section:所在的 Section,或者 COMMON
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
3.2 Strong / Weak Symbol
强符号:函数和已初始化的全局变量
弱符号:未初始化的全局变量
规则:不允许出现多个同名强符号,重定位时优先选择强符号.
4. Static Libraries
可以将多个 .o 文件打包成一个静态库 .a 文件.静态库本质上是若干可重定位目标文件的集合,链接器在需要某个符号时,才会从静态库中抽取相应的成员目标文件.
使用静态库时,链接器会从左到右按照命令行出现的顺序扫描可重定位目标文件和静态库文件.在扫描过程中,链接器会维护三个集合:
- \(E\):已经被加入最终可执行文件的目标文件集合;
- \(U\):当前还没有解析的未定义符号集合;
- \(D\):已经定义的符号集合.
对于普通 .o 文件,链接器会无条件将其加入 \(E\);对于静态库 .a 文件,链接器只会抽取其中能够解析 \(U\) 中未定义符号的成员目标文件.因此,静态库的顺序非常重要.
简单来说,命令行中文件的输入顺序应满足“先需求,后供给”:
- 普通目标文件
.o通常放在前面,因为它们会先产生未定义符号; - 静态库
.a通常放在后面,因为它们负责提供符号定义; - 如果多个库相互独立,它们可以以任意顺序放在命令行结尾;
- 如果库之间存在依赖关系,也需要满足“先需求,后供给”的顺序;
- 如果库之间存在循环依赖,可以在命令行中重复出现某些库,或者使用链接器的分组机制.
静态库链接顺序
Case 1:无循环依赖
假设 foo.o 调用了 libx.a 和 libz.a 中的函数,而 libx.a 和 libz.a 又调用了 liby.a 中的函数,即:
1 2 3 4 | |
此时应先放产生需求的文件,再放提供符号的库:
1 | |
其中 libx.a 和 libz.a 都依赖 liby.a,所以 liby.a 应放在它们之后.libx.a 和 libz.a 彼此独立,二者顺序可以互换.
Case 2:存在循环依赖
假设依赖关系为:
1 2 3 4 | |
此时 libx.a 和 liby.a 之间存在循环依赖.一种可行写法是:
1 | |
也可以写成:
1 | |
这里重复出现某个库,是为了让链接器在扫描后面的库产生新未定义符号后,能够再次扫描前面的库来解析这些新需求.
Case 3:CS:APP Practice Problem 7.3 C
依赖关系为:
1 2 | |
一个最小可行命令是:
1 | |
解释如下:
1 2 3 4 | |
这里不需要在最后再次写 p.o.因为普通 .o 文件一旦出现,就会被无条件加入链接;而且 p.o 已经在最前面加入过了.后面真正需要重复扫描的是静态库 libx.a,不是目标文件 p.o.
5. Relocation
考虑这两个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
分别对应可重定位目标文件 main.o 和 sum.o.
第一步:链接器会把相同类型的 section 合并为一个新的 section.这一步完成后,程序中每条指令、全局变量都有了唯一的运行时地址.
第二步:重定位 section 中的符号引用.此时外部符号的目的地址为全 0,需要进行重定位..text 段起始位置一般为 0x4004d0.
1 2 3 4 5 6 7 8 9 | |
.data 和 .text 内的数据需要重定位,对应的重定位条目分别放在 .rel.data 和 .rel.text 中.
重定位条目的结构如图所示:
1 2 3 4 5 6 | |
callq sum 为例:
offset表示该需要重定位的项相对其所在函数的起始地址.对于sum而言,offset = 0xftype中最重要的两种类型是R_X86_64_PC32相对地址重定位和R_X86_64_32绝对地址重定位,此处为前者symbol表示重定位的目标符号名,此处为sumaddend表示用于修正的偏移量常数,由于使用 PC 相对寻址,并且在执行指令callq sum时%rip指向下一条指令地址0x4004e3,而目标为0x4004df,因此addend = -4
链接器会通过当前函数地址和 offset 计算需要重定位的项的地址:
1 2 3 | |
再根据 sum 的地址、ref_addr 和 addend 计算得到其相对 PC 的地址:
1 2 3 | |
当 CPU 执行 callq 命令时,PC 的值为下一条指令地址 0x4004e3:
- 将 PC 压栈
PC <- PC + 0x5 = 0x4004e3 + 0x5 = 0x4004e8恰好为sum第一条命令地址


