第二章:编译和链接
第二章:编译和链接
2.1 被隐藏了的过程
预编译(Preprocessing)
预编译阶段主要处理以下内容:
- 处理所有的预处理指令
- 展开所有的宏定义
- 处理条件编译指令
- 删除注释
示例代码:
1 | #include <stdio.h> |
使用以下命令查看预处理后的文件:
1 | gcc -E preprocessing.c -o preprocessing.i |
编译(Compilation)
编译阶段将预处理后的文件转换为汇编代码。主要包括以下步骤:
词法分析
- 将源代码分解成标记(token)
- 识别关键字、标识符、运算符等
语法分析
- 将标记组织成语法树
- 检查语法正确性
语义分析
- 检查类型匹配
- 检查变量声明
- 检查函数调用
中间代码生成
- 生成中间表示(IR)
- 优化中间代码
目标代码生成
- 生成汇编代码
- 寄存器分配
- 指令选择
示例代码:
1 | int add(int a, int b) { |
使用以下命令生成汇编代码:
1 | gcc -S compilation.c -o compilation.s |
汇编(Assembly)
汇编阶段将汇编代码转换为机器码。主要包括:
- 将汇编指令转换为机器指令
- 生成目标文件(.o文件)
- 生成符号表
使用以下命令生成目标文件:
1 | gcc -c compilation.s -o compilation.o |
链接(Linking)
链接阶段将多个目标文件合并成一个可执行文件。主要包括:
- 地址和空间分配
- 符号决议
- 重定位
示例代码(多文件):
1 | #include <stdio.h> |
1 | int add(int a, int b) { |
编译和链接命令:
1 | gcc -c link_main.c -o link_main.o |
2.2 编译器做了什么
词法分析
词法分析器将源代码分解成标记(token)。例如:
1 | int a = 10; |
会被分解为:
- 关键字:int
- 标识符:a
- 运算符:=
- 常量:10
- 分号:;
语法分析
语法分析器将标记组织成语法树。例如:
1 | if (a > b) { |
会生成类似这样的语法树:
1 | if |
语义分析
语义分析器检查程序的语义正确性。例如:
1 | int a = "hello"; // 类型不匹配错误 |
中间代码生成
中间代码生成器生成中间表示。例如:
1 | int a = 10; |
可能生成类似这样的中间代码:
1 | t1 = 10 |
目标代码生成
目标代码生成器生成最终的汇编代码。例如:
1 | int add(int a, int b) { |
可能生成类似这样的汇编代码:
1 | add: |
实践练习
- 预编译实验
1 | # 创建预编译示例文件 |
- 编译实验
1 | # 生成汇编代码 |
- 链接实验
1 | # 编译多个源文件 |
思考题
- 预编译阶段为什么要处理条件编译指令?
- 词法分析和语法分析的区别是什么?
- 为什么需要中间代码生成阶段?
- 链接阶段的主要任务是什么?
- 编译器优化在哪个阶段进行?
参考资料
- 《程序员的自我修养:链接、装载与库》
- 《编译原理》(龙书)
- GCC 在线文档
- LLVM 文档
- GNU Binutils 文档