参考:《程序员的自我修养:链接、装载和库》Chapter 2
一、从源代码到可执行文件
学习任何语言都是先写 Hello World,但它从源代码到输出结果中间发生了太多事情,但我们一般会笼统地说一句 “编译器将源代码编译成更底层的机器语言”。但对于进行系统编程的人来说这是远远不够的,我们必须拨开这层迷雾,仔细瞅瞅里面的门道。
从源代码到输出大致可以分为四个步骤:预处理、编译、汇编和链接。
1.1 预处理
在学习 C 语言都会学到预处理器指令(或预编译指令),如 #define
、#if
和 #pragma
等。它们就是在预处理阶段被处理的指令,常见处理规则如下:
- 删除所有的
#define
,展开所有宏定义; - 处理所有条件预编译指令,如
#if
、#elif
、#endif
等; - 递归处理
#include
指令,插入相应文件到预编译指令位置上; - 删除所有注释;
- 添加文件名和行号标识;
- 保留所有的
#pragma
编译器指令,它用于指导编译器活动。
1.2 编译和汇编
编译阶段的处理参考编译原理,按顺序进行词法分析、语法分析、语义分析、源码优化、目标代码生成和目标代码优化。
词法分析是将源码切分为 Token,类型有标识符、常数、操作符等,在 Unix 系统中可以用 lex 程序实现。
语法分析通过上下文无关文法对 Token 构建语法树,在 Unix 系统中可以使用 yacc 工具。
语义分析进行静态语义分析,包括声明和类型的匹配、类型的转换,完成后语法树上被标识了类型。
源码优化若在语法树上操作通常较难,因此需要将其转为一种中间表示(IR),常见的有三地址码和P-代码。
目标代码生成和目标代码优化在后端处理。
引入中间表示后,可以将编译器分为前端和后端,前端处理与目标机器无关的中间表示,后端将中间表示转为目标代码。
编译的产物是汇编语言程序,汇编阶段再将汇编语言程序转为机器语言程序,即 .o
文件,它是二进制文件。
经过上述复杂活动后,我们将源代码文件编译成了目标代码文件,但该文件存在一个问题,目标代码中的某些变量定义在了其他模块,我们如何确定它们的地址呢?此时就需要引入链接。
1.3 链接
链接分为动态链接和静态链接,这里先讨论静态链接。链接其实就是模块间的通信问题,最常见的就是(1)模块间的函数调用;(2)模块间的变量访问。静态链接就像拼图,把不同的模块组装起来,形成一个完整的画面(功能)。
链接过程主要是两个步骤:
- 地址与空间分配;
- 符号解析与重定向。
其中重点是符号解析与重定向。以下面代码为例,foo 是一个外部函数,在处理时会先将 foo 函数地址置为 0,在链接时查找重定向表中 foo 的地址,再修改 0 地址为查到的地址。
int main() {
foo();
return 0;
}