CSAPP
Chapter 4: 链接
编译和汇编
- 预处理:展开头文件、宏替换、删注释,(.c -> .i),还是 C 语言文件
- 编译:.c -> .s,将 C 翻译为汇编
- 汇编:.x -> .o,汇编转换为机器码
可执行目标文件的生成
每个 .c 文件生成一个 .o 文件,再通过链接合并为一个可执行目标文件
-
符号解析
- 符号:全局静态变量名、函数名
作用是将每一个符号的引用与其定义建立关联
-
重定位
将不同的 .o 文件的相同 section 合并
目标文件格式
可重定位目标文件
| 文件位置 | 节 (Section) 名称 | 说明 (链接器关注的信息) |
|---|---|---|
| 头部 | ELF Header | 包含魔数、机器类型、字节序。 关键点: 入口点地址 (Entry point) 为 0。 |
| 中间 | .text | 已编译的机器代码。 注意: 这里的指令地址都是从 0 开始的相对偏移。 |
| .rodata | 只读数据(如 printf 里的格式化字符串、switch 跳转表)。 |
|
| .data | 已初始化的全局变量和静态变量 (int x = 1;)。 |
|
| .bss | 未初始化的全局变量 (int x;)。 注:在文件中仅占位标记,不占实际磁盘空间。 |
|
| 重定位信息 | .rel.text | 代码重定位表。 记录了 .text 中哪些指令的地址(如 call 后的地址)需要修改。 |
| .rel.data | 数据重定位表。 记录了 .data 中哪些指针的值需要修改。 |
|
| 符号信息 | .symtab | 符号表。 存放函数和全局变量的名字、大小、类型。 |
| .strtab | 字符串表。 存放 .symtab 中用到的符号名称的字符串实体。 |
|
| 尾部 | Section Header Table | **节头表。**目录索引,记录了上面所有节的偏移量、大小和属性。 链接器通过它来查找各个节。 |
可执行目标文件
| 文件位置 | 结构名称 | 说明 (加载器/OS 关注的信息) |
|---|---|---|
| 头部 | ELF Header | 关键点: 入口点地址不再是 0,而是 _start 的虚拟地址 (e.g. 0x8048000)。 |
| 核心索引 | Program Header Table (程序头表) | 段的地图。 告诉操作系统:把文件的哪一部分映射到内存的哪个区域。 它是可执行文件的标志。 |
| 代码段 (Read-Only) | .init | 程序初始化代码(在 main 之前运行)。 |
| .text | 合并后的机器代码。地址已修正为绝对虚拟地址。 | |
| .rodata | 合并后的只读数据。 | |
| 数据段 (Read/Write) | .data | 合并后的已初始化数据。 |
| .bss | 合并后的未初始化数据。 | |
| 调试信息 (可选) | .symtab | 符号表(通常保留用于 gdb 调试,但程序运行不需要它)。 |
| .strtab | 字符串表。 | |
| 尾部 | Section Header Table | 节头表,虽然还在,但在程序加载运行时不再关键,主要供调试工具使用。 |
符号表和符号解析
包含在符号表中的符号:
- 全局符号:非静态函数名和非静态全局变量名
- 外部符号:外部函数名和外部变量名
- 本地符号:静态函数名和静态变量名
符号类型:
| 类型宏名 | 中文含义 | 详细描述 |
|---|---|---|
| NOTYPE | 未指定 | 符号类型未指定 |
| OBJECT | 变量 | 表示该符号是一个数据对象(变量) |
| FUNC | 函数 | 表示该符号是一个函数 |
| SECTION | 节 | 主要用于重定位 |
符号绑定属性:
| 属性名称 | 可见性范围 | 冲突与优先级规则 |
|---|---|---|
| LOCAL (本地) | 不可见:在定义它的目标文件外部不可见 | 互不干扰:名称相同的本地符号可存在于多个文件中 |
| GLOBAL (全局) | 可见:对合并的所有目标文件都可见 | (通常作为强符号参与链接) |
| WEAK (弱) | 可见:与全局符号类似 | 低优先级:定义具有较低的优先级(会被同名 GLOBAL 覆盖) |
符号的强弱特性:
- 强符号:函数名、已初始化的全部变量名
- 弱符号:未初始化的全局变量名
当链接器在多个文件中发现同名符号时,它遵循以下逻辑:
- 强强冲突:若存在多个同名的强符号,链接器将无法判定,直接报“多重定义”错误。
- 强弱共存:若一个强符号与多个弱符号同名,链接器以强符号为准,所有引用皆指向该强符号的地址。
- 弱弱共存:若只有多个同名弱符号而无强符号,链接器将在其中任意选择一个作为最终定义。
重定位
重定位类型:
- R_386_PC32 采用相对寻址,计算的是目标位置与当前指令的相对位置,主要用于函数调用和代码跳转。
- R_386_32 采用绝对寻址,直接填入目标在内存中的绝对位置,主要用于全局变量引用和指针赋值(如
int *p = &var)。
main.c
int sum = 1;
int add(int x, int y) {
sum = x + y;
}
int main() {
add(1, 2);
return 0;
}
main.o -> main.s
00000000 <add>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 55 08 mov 0x8(%ebp),%edx # EDX = x
6: 8b 45 0c mov 0xc(%ebp),%eax # EAX = y
9: 01 d0 add %edx,%eax # EAX = EAX + EDX
# --- sum = x + y; ---
# 这是一个写内存操作,需要知道 sum 的确切绝对地址。
# 当前机器码中地址是 0x00000000 (占位符)。
# 重定位类型: R_386_32
b: a3 00 00 00 00 mov %eax,0x0
# [解析]: 链接器将来会把这个 0x0 修改为 sum 变量在数据段中的真实物理地址 (例如 0x0804a018)。
18: 5d pop %ebp # 恢复旧的栈底指针
19: c3 ret # 返回主调函数
0000001a <main>:
# --- 函数序言 ---
1a: 55 push %ebp
1b: 89 e5 mov %esp,%ebp
1d: 6a 02 push $0x2 # 压入第二个参数 (y = 2)
1f: 6a 01 push $0x1 # 压入第一个参数 (x = 1)
# --- add(1, 2); ---
# call 指令的操作码是 e8,后面跟着 4 字节的“相对偏移量”。
# 当前机器码 fc ff ff ff 是十进制的 -4 (指向 call 指令自己的下一条指令之前)。
# 重定位类型: R_386_PC32
21: e8 fc ff ff ff call 22 <main+0x8>
# [解析]: 链接器会计算 (add函数的最终地址 - 下一条指令的地址),算出一个偏移量填在这里。
# 比如,如果 add 在 main 前面很远,这个值就是个负数;在后面,就是正数。
26: 83 c4 08 add $0x8,%esp # 清理堆栈
29: b8 00 00 00 00 mov $0x0,%eax # return 0;
2e: c9 leave
2f: c3 ret
动态链接
注意到例如printf这样的操作十分常用,如果使用静态链接的方式,每个程序都需要在链接的时候打包一份完整的printf,同样加载到内存中时也会有多份 printf 程序。这样显然不是最优。
通过动态链接的方法,运行静态链接好的程序之后,加载到内存之前,再调用动态链接器进行一次动态链接,就可以节省硬盘和内存资源。
动态链接主要分为两张表、三个过程:
GOT 表:用于记录动态链接的全局变量;
- 编译时:记录需要动态链接的资源
- 启动时:动态链接器加载程序和依赖库,通过 GOT 表记录全局变量位置
- 运行时:计算函数地址供调用(PLT表)
位置无关代码:将跳转和符号的地址都使用相对地址的代码
上面的代码开启位置无关代码生成之后:
00000000 <add>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: e8 fc ff ff ff call 4 <add+0x4> # 调用 Thunk 函数,获取当前 PC 值存入 EAX
8: 05 01 00 00 00 add $0x1,%eax # EAX += 偏移量 (R_386_GOTPC),计算出 GOT 表基地址
d: 8b 4d 08 mov 0x8(%ebp),%ecx
10: 8b 55 0c mov 0xc(%ebp),%edx
13: 01 ca add %ecx,%edx
15: 89 90 00 00 00 00 mov %edx,0x0(%eax) # 访问全局变量:使用 GOT基址 + 变量偏移 (R_386_GOTOFF)
1b: 90 nop
1c: 5d pop %ebp
1d: c3 ret
0000001e <main>:
1e: 55 push %ebp
1f: 89 e5 mov %esp,%ebp
21: e8 fc ff ff ff call 22 <main+0x4> # main 函数也需要获取 PC 来定位 GOT 表
26: 05 01 00 00 00 add $0x1,%eax # 计算 GOT 表基地址 (EAX 现在指向 GOT)
2b: 6a 02 push $0x2
2d: 6a 01 push $0x1
2f: e8 fc ff ff ff call 30 <main+0x12> # 调用函数:通常通过 PLT 表间接跳转 (R_386_PLT32)
34: 83 c4 08 add $0x8,%esp
37: b8 00 00 00 00 mov $0x0,%eax
3c: c9 leave
3d: c3 ret
Disassembly of section .text.__x86.get_pc_thunk.ax:
00000000 <__x86.get_pc_thunk.ax>:
0: 8b 04 24 mov (%esp),%eax # 读取栈顶值 (即 Call 之后那条指令的地址) 到 EAX
3: c3 ret

Comments | NOTHING