编译器工作原理的显微镜级拆解
面向读者
- 写过
main(){printf("hello");}
,却没见过它如何变成 0x48 0x65 0x6c 0x6c 0x6f 的你- 想知道“语法糖”究竟被谁“脱糖”的同学
- 想自己写 DSL、脚本引擎、甚至 toy compiler 的勇士
编译器是一群“翻译官”接力写剧本
人类翻译流程 | 编译器对应阶段 | 关键产出 |
---|---|---|
1. 把中文稿拆成单词 | 词法分析 | Token 流 |
2. 按语法拼成句子 | 语法分析 | 抽象语法树 AST |
3. 理解句子含义 | 语义分析 | 带类型注释的 AST + 符号表 |
4. 润色、改病句 | 中间代码优化 | 更精简的中间代码 IR |
5. 定稿为英文剧本 | 目标代码生成 | 汇编/机器码 |
6. 剧组合并台词 | 链接 | 可执行文件 |
带着这张“翻译官分工表”,我们一步步走进编译器的内心。
一、词法分析:把字符流切成 “Token 羊肉串”
1.1 工作细节
- 输入:纯文本
int main() {return 0;}
- 输出:Token 序列
(INT, "int"), (ID, "main"), (LPAREN, "("), ... , (RETURN, "return"), (INT_LIT, "0"), ...
1.2 内部机制
- 有限自动机 (DFA):像地铁闸机,每读一个字符就“咔哒”一次切换状态,直到认出完整单词。
- 正则→NFA→DFA:先用正则表达式描述关键字、标识符、字面量,再经 Thompson 算法→子集构造算法得到 DFA,最后最小化状态数,减少闸机数量。
比喻:
词法分析器就是高速收费站,字符是车流,Token 是一张张刷过闸机的高速通行卡。
二、 语法分析:把 Token 拼成“语法乐高城堡”
2.1 产出:抽象语法树 AST
FunctionDecl├── type: int├── name: main├── params: []└── body:ReturnStmt└── value: 0
2.2 常用算法
算法 | 特点 | 比喻 |
---|---|---|
递归下降 | 手写直观 | 乐高说明书一页页照着拼 |
LR(1) | 自动生成,能处理左递归 | 机械臂按状态表自动拼装 |
语法错误例子:
int 123abc;
→ 分析器发现123abc
不是合法标识符,抛syntax error
。
三、 语义分析:给树贴“标签”和“说明书”
3.1 三件事
- 符号表管理:登记每个标识符的类型、作用域、存储类别。
- 类型检查:确保
int x = "hello";
不被放行。 - 作用域与生命周期:决定变量何时生、何时死。
比喻:语义分析是给乐高城堡贴门牌号、安全规范及消防通道图。
3.2 典型错误
- 未声明变量
- 函数参数不匹配
- 违反 const/volatile 语义
四、 中间代码生成:AST 的“骨架替身”
4.1 为什么需要 IR?
- 机器无关:前后端解耦,同一前端可配多种后端。
- 便于优化:树结构不利于全局数据流分析,IR 通常是线性三地址码或 SSA 形式。
4.2 三地址码示例
t1 = 0
return t1
4.3 SSA(静态单赋值)
每个变量只赋值一次,便于做“死代码删除”“常量传播”等手术。
比喻:把乐高城堡拍扁成平面图,方便工程师画优化路线图。
五、 代码优化:把“病句”变“金句”
优化类别 | 例子 | 效果 |
---|---|---|
局部 | 常量折叠 2+3→5 | 减少指令数 |
循环 | 循环不变外提 | 减少重复计算 |
全局 | 公共子表达式删除 | 减少冗余 |
机器相关 | 指令调度、寄存器分配 | 利用 CPU 并行 |
比喻:编辑把“我非常非常非常非常高兴”润色成“我欣喜若狂”,既省字数又增强可读性。
六、 目标代码生成:把 IR 翻译成“地道方言”
6.1 三大难题
- 指令选择:从 IR 节点到具体指令的模式匹配(动态规划、树覆盖)。
- 寄存器分配:图着色算法,把无限虚拟寄存器塞进有限物理寄存器。
- 指令调度:重排指令顺序,减少流水线气泡。
6.2 输出示例(x86-64)
mov eax,0
ret
七、 链接:把“分镜剧本”合成“整部电影”
- 重定位:把
.o
里的相对地址改成最终内存地址。 - 符号决议:找到
printf
在 libc 中的真正位置。 - 静态 vs 动态链接:
- 静态:把所有台词提前打包,体积大但无运行时依赖。
- 动态:演出当天再去借道具(.so/.dll),体积小但需环境存在。
八、实战:一条语句的 0→1 全链路
源码
int square(int x){return x * x;
}
8.1 各阶段产物
阶段 | 关键产物片段 |
---|---|
词法 | (INT,"int") (ID,"square") (LPAREN,"(") ... |
语法 | FuncDef(type=int,name=square,params=[x],body=Return(Mul(x,x))) |
语义 | 符号表:x:int,register:%v0 |
中间(IR) | entry: %t1 = mul %x, %x; ret %t1 |
优化 | 若 x 是常量 3,直接 ret 9 |
汇编 | mov eax, edi; imul eax, eax; ret |
九、 拓展:JIT vs AOT,两条不同的“出版路线”
AOT(提前编译) | JIT(即时编译) | |
---|---|---|
何时翻译 | 程序运行前 | 程序运行时 |
优点 | 启动快、无运行时开销 | 可按实际热点再优化 |
缺点 | 编译慢、需全平台重编 | 启动慢、占用内存 |
代表 | GCC/Clang 生成 ELF/EXE | Java HotSpot、V8、ART |
思维导图
编译器全景图
├── 前端(分析)
│ ├── 词法:字符→Token
│ ├── 语法:Token→AST
│ └── 语义:AST + 符号表
├── 中端(优化)
│ ├── IR 生成:AST→三地址/SSA
│ └── 优化:常量折叠、循环、寄存器
├── 后端(生成)
│ ├── 指令选择、调度、分配
│ └── 汇编/机器码
└── 链接├── 重定位└── 静态/动态链接