程序构建原理
基于《程序员的自我修养—链接、装载与库》的笔记
程序构建过程
宏观视角:四步转换流程
程序构建是一个层层递进的过程,每个阶段都建立在前一阶段的基础上:
源代码(.cpp) → 预处理 → 纯净代码(.i) → 编译 → 汇编代码(.s)
→ 汇编 → 目标文件(.o) → 链接 → 可执行程序
第一阶段:预处理 - 代码的"准备工作"
核心任务:为编译做准备
预处理就像是代码的"后勤工作",处理所有以#开头的指令,为后续编译阶段准备干净的代码。
具体工作内容:
- 模块化处理:通过
#include将头文件内容插入当前位置 - 代码复用:使用
#define进行宏定义和替换 - 条件编译:根据
#ifdef、#ifndef等条件选择性地包含代码 - 环境清理:删除所有注释,减少编译负担
输入输出:.cpp → .i(纯C++代码文件)
第二阶段:编译 - 从高级语言到汇编的"翻译官"
编译器的作用
编译器是构建过程中最复杂的部分,它不仅要"翻译"代码,还要理解代码的语义并进行优化。
六步翻译流程:
1. 词法分析:理解"单词"
将源代码分解成有意义的单元(token):
// 源代码:int result = a + b;
// 分解为:[关键字:int] [标识符:result] [运算符:=] [标识符:a] [运算符:+] [标识符:b]
2. 语法分析:理解"句子结构"
根据语法规则构建抽象语法树(AST),确保代码结构正确:
=
/ \
result +
/ \
a b
3. 语义分析:理解"含义"
- 类型检查:确保变量声明与使用匹配,类型转换合理
- 符号表构建:记录所有变量、函数的信息和作用域
4. 中间代码生成:创建"通用语言"
生成平台无关的中间表示(如三地址码),为优化和跨平台支持做准备。
5. 代码优化:提升"表达效率"
对中间代码进行各种优化,如常量传播、死代码消除等,提升程序性能。
6. 目标代码生成:输出"具体指令"
生成特定CPU架构的汇编代码,完成从高级语言到底层指令的转换。
输入输出:.i → .s(汇编代码文件)
第三阶段:汇编 - 从汇编到机器的"编码器"
核心任务:将人类可读的汇编转换为机器可执行的二进制
具体工作:
- 指令翻译:将汇编指令逐条转换为机器指令
- 目标文件生成:创建标准的可重定位目标文件格式
- 符号管理:记录函数和变量的位置信息
- 重定位信息:标记那些需要在链接时确定的地址引用
输入输出:.s → .o(二进制目标文件)
第四阶段:链接 - 程序的"最终组装"
静态链接
地址和空间分配
任务:建立程序运行时的内存布局
- 段合并:将各目标文件的相同段(.text代码段、.data数据段等)合并
- 空间分配:为每个段分配在虚拟地址空间中的位置
- 布局确定:建立完整的程序内存映射结构
// 链接前:各自为政
File1.o: [.text@0x0000, .data@0x1000, .bss@0x2000]
File2.o: [.text@0x0000, .data@0x1000, .bss@0x2000]
// 链接后:统一王国
Program: [.text@0x00400000, .data@0x00600000, .bss@0x00800000]
符号决议
任务:解决所有符号的引用关系
- 符号绑定:将符号引用与正确的定义关联起来
- 唯一性保证:确保每个全局符号只有一个定义
- 强弱符号处理:按规则处理重复的符号定义
// FileA.cpp
extern int global_var; // 声明(需要解析的引用)
void func() {
global_var = 100; // 链接前:不知道global_var在哪里
}
// FileB.cpp
int global_var = 0; // 定义(提供实际位置)
// 链接后:func()中的global_var指向FileB中的定义
重定位
任务:根据实际内存布局修正所有地址引用
- 绝对地址计算:将相对地址转换为绝对地址
- 指令修正:更新代码中对函数和变量的引用
- 重定位表应用:使用目标文件中的重定位信息
; 重定位前(目标文件中使用相对地址)
call 0xFFFFFFF0 ; 未知函数位置(占位符)
mov [0x00001000], %eax ; 未知变量地址
; 重定位后(可执行文件中使用绝对地址)
call 0x00401050 ; 实际函数地址(如printf)
mov [0x00602000], %eax ; 实际变量地址
动态链接
核心思想:"按需加载,资源共享"
与静态链接在编译时绑定所有符号不同,动态链接将链接过程推迟到运行时。
动态链接的优势:
- 节省内存:多个进程可共享同一个动态库的物理内存
- 便于更新:更新库文件无需重新编译主程序
- 灵活加载:程序可以在运行时决定加载哪些模块
动态链接的关键机制
- 位置无关代码 (PIC - Position Independent Code)
目的:确保共享库可以被加载到任意内存地址
// 非PIC代码(静态链接)
void call_function() {
function(); // 直接调用固定地址
}
// PIC代码(动态链接)
void call_function() {
// 通过GOT间接寻址
call *(%ebx + function_offset)
}
实现方式:
- 使用相对寻址而非绝对地址
- 通过全局偏移表(GOT)间接访问外部变量
- 通过过程链接表(PLT)间接调用外部函数
- 全局偏移表 (GOT - Global Offset Table)
作用:存储外部全局变量的实际地址
GOT结构:
GOT[0]: .dynamic段地址
GOT[1]: 链接器标识信息
GOT[2]: 动态链接器入口点
GOT[3]: printf函数的实际地址
GOT[4]: global_var变量的实际地址
...
- 过程链接表 (PLT - Procedure Linkage Table)
作用:实现延迟绑定,提高性能
; PLT条目示例
.PLT0: pushl GOT[1] ; 链接器信息
jmp *GOT[2] ; 跳转到动态链接器
.PLT1: jmp *GOT[3] ; 第一次调用时指向.PLT0
push $0 ; printf的重定位偏移
jmp .PLT0 ; 调用链接器解析地址
; 使用PLT调用函数
call printf@PLT ; 通过PLT间接调用
动态链接的过程
- 程序启动时
// 动态链接器完成以下工作:
1. 加载程序本身
2. 加载依赖的共享库(如libc.so)
3. 符号解析和重定位
4. 执行初始化代码
5. 将控制权交给程序入口点
- 延迟绑定 (Lazy Binding)
优化策略:函数在第一次被调用时才进行链接
第一次调用printf的流程:
1. call printf@PLT
2. 跳转到PLT[1]
3. PLT[1]跳转到动态链接器
4. 链接器解析printf的实际地址
5. 将GOT[3]更新为printf的真实地址
6. 后续调用直接通过GOT跳转
- 动态符号表
作用:记录需要动态解析的符号信息
// 使用readelf查看动态符号表
$ readelf -d program
Dynamic section at offset 0x1000 contains 10 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x8048000
0x00000019 (INIT_ARRAY) 0x8049000
静态链接 vs 动态链接对比
| 特性 | 静态链接 | 动态链接 |
|---|---|---|
| 链接时机 | 编译时 | 运行时 |
| 文件大小 | 较大(包含所有库代码) | 较小(只包含引用) |
| 内存使用 | 每个进程独立副本 | 多个进程共享库代码 |
| 更新维护 | 需重新编译整个程序 | 只需更新库文件 |
| 加载速度 | 较快(无运行时链接) | 稍慢(需要解析符号) |
| 灵活性 | 低 | 高(支持插件机制) |
实际应用示例
// 编译静态链接程序
g++ -static main.cpp -o program_static
// 编译动态链接程序
g++ main.cpp -o program_dynamic
// 查看程序动态依赖
ldd program_dynamic
// 输出:linux-vdso.so.1 => libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
// 运行时加载特定库
LD_LIBRARY_PATH=/path/to/libs ./program_dynamic
动态链接的挑战
- 版本兼容性:确保程序与库版本匹配
- 符号冲突:不同库可能定义相同符号
- 安全性:防止恶意库被加载
- 性能开销:第一次调用函数时有解析成本
输入输出:多个.o文件 → 单个可执行文件

浙公网安备 33010602011771号