编译器工作原理的显微镜级拆解

面向读者

  • 写过 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 三件事

  1. 符号表管理:登记每个标识符的类型、作用域、存储类别。
  2. 类型检查 :确保 int x = "hello"; 不被放行。
  3. 作用域与生命周期:决定变量何时生、何时死。

比喻:语义分析是给乐高城堡贴门牌号、安全规范及消防通道图。

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 三大难题

  1. 指令选择:从 IR 节点到具体指令的模式匹配(动态规划、树覆盖)。
  2. 寄存器分配:图着色算法,把无限虚拟寄存器塞进有限物理寄存器。
  3. 指令调度:重排指令顺序,减少流水线气泡。

6.2 输出示例(x86-64)

复制代码
mov    eax,0
ret

七、 链接:把"分镜剧本"合成"整部电影"

  • 重定位 :把 .o 里的相对地址改成最终内存地址。
  • 符号决议 :找到 printf 在 libc 中的真正位置。
  • 静态 vs 动态链接
    • 静态:把所有台词提前打包,体积大但无运行时依赖。
    • 动态:演出当天再去借道具(.so/.dll),体积小但需环境存在。

八、实战:一条语句的 0→1 全链路

源码

c 复制代码
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
│   └── 优化:常量折叠、循环、寄存器
├── 后端(生成)
│   ├── 指令选择、调度、分配
│   └── 汇编/机器码
└── 链接
    ├── 重定位
    └── 静态/动态链接

相关推荐
SixHateSeven12 天前
🚀 TSX动态编译的黑科技,快如闪电!
前端·编译器
矮油0_o2 个月前
第一部分 -- ①语法分析的概要
java·编译器·解释器·语法分析
Jooolin2 个月前
【编程史】IDE 是谁发明的?从 punch cards 到 VS Code
ai编程·visual studio code·编译器
huangyuchi.2 个月前
【Linux】编译器gcc/g++及其库的详细介绍
linux·运维·服务器·笔记·编译器·gcc·g++
胡乱儿起个名3 个月前
Relay算子注册(在pytorch.py端调用)
c++·人工智能·tvm·编译器·ai编译器
胡乱儿起个名3 个月前
PrimExpr 与 RelayExpr 的区别
人工智能·tvm·编译器·ai编译器
胡乱儿起个名3 个月前
Relay IR的核心数据结构
人工智能·tvm·编译器·ai编译器
航哥4 个月前
Go语言编译器的正确打开方式(二)- 通过Debug理解Go的编译过程
go·编译器
航哥4 个月前
Go语言编译器的正确打开方式(一)- 从源码编译 go
go·编译器