第4集:编译器的工作流程概览
学习目标
通过本集的学习,你将能够:
- 理解编译器的前端、中端、后端划分
- 说出编译过程的7个主要阶段
- 用图解法描述完整的编译流程
- 区分黑箱和白箱视角
4.1 前端、中端、后端
现代编译器通常被划分为三个部分:前端、中端和后端。这种设计让编译器更容易移植和扩展。
前端(Front End)
- 任务:分析源代码,理解程序的意思
- 工作:词法分析、语法分析、语义分析
- 特点:与源语言相关,与目标机器无关
- 目标:生成中间表示(IR)
中端(Middle End / 优化器)
- 任务:对中间表示进行优化
- 工作:代码优化、循环优化、内联等
- 特点:既不依赖源语言,也不依赖目标机器
- 目标:让程序跑得更快、更小
后端(Back End)
- 任务:把中间表示转换成目标机器码
- 工作:指令选择、寄存器分配、指令调度
- 特点:与目标机器相关,与源语言无关
- 目标:生成高效的机器码
让我们用一个工厂流水线来类比:
┌─────────────────────────────────────────────────────────────────┐
│ 编译器工厂 │
├──────────────────┬──────────────────┬───────────────────────────┤
│ 前端车间 │ 中端车间 │ 后端车间 │
│ │ │ │
│ ┌──────────┐ │ ┌──────────┐ │ ┌──────────┐ │
│ │源代码 │ → │ │中间表示 │ → │ │目标代码 │ │
│ └──────────┘ │ └──────────┘ │ └──────────┘ │
│ │ │ │
│ 词法分析 │ 常量折叠 │ 指令选择 │
│ 语法分析 │ 死代码消除 │ 寄存器分配 │
│ 语义分析 │ 循环优化 │ 指令调度 │
│ 生成IR │ 内联展开 │ 生成机器码 │
└──────────────────┴──────────────────┴───────────────────────────┘这种划分的好处:
- 可移植性好:想支持新的源语言?只需写新前端
- 可扩展性强:想支持新的目标机器?只需写新后端
- 优化共享:中端优化可以被所有前端和后端共享
4.2 编译过程的7个阶段
让我们详细看看编译器内部的7个阶段:
源代码
│
▼
┌──────────────┐
│ 1. 词法分析 │ ← 把字符流转换成 Token 流
└──────┬───────┘
│
▼
┌──────────────┐
│ 2. 语法分析 │ ← 把 Token 流转换成语法树
└──────┬───────┘
│
▼
┌──────────────┐
│ 3. 语义分析 │ ← 检查语义错误,生成 IR
└──────┬───────┘
│
▼
┌──────────────┐
│ 4. 中间代码 │ ← 生成中间表示(IR)
│ 生成 │
└──────┬───────┘
│
▼
┌──────────────┐
│ 5. 机器无关 │ ← 优化中间代码
│ 优化 │
└──────┬───────┘
│
▼
┌──────────────┐
│ 6. 目标代码 │ ← 把 IR 转换成目标代码
│ 生成 │
└──────┬───────┘
│
▼
┌──────────────┐
│ 7. 机器相关 │ ← 优化目标代码
│ 优化 │
└──────┬───────┘
│
▼
目标代码让我们用一个具体的例子来追踪:int result = 1 + 2;
阶段 1:词法分析
输入:字符流
'int result = 1 + 2;'输出:Token 流
[INT, ID("result"), ASSIGN, NUMBER(1), PLUS, NUMBER(2), SEMICOLON]阶段 2:语法分析
输入:Token 流
输出:语法树
=
/ \
result +
/ \
1 2阶段 3:语义分析
检查:
result是否已声明?1和2都是整数,可以相加- 赋值的类型匹配
输出:带语义信息的 AST
阶段 4:中间代码生成
输出:三地址码
t1 = 1 + 2
result = t1阶段 5:机器无关优化
优化后:
result = 3(常量折叠,直接算出结果)
阶段 6:目标代码生成
输出:汇编(x86)
mov $3, %rax
mov %rax, result阶段 7:机器相关优化
可能优化成:
mov $3, result(直接存储,省去寄存器)
4.3 图解完整编译流程
让我们用一张大图来展示完整的编译流程:
┌──────────────────────────────────────────────────────────────┐
│ 源程序文件 │
│ (hello.c 等) │
└────────────────────┬─────────────────────────────────────────┘
│
▼
┌───────────────────────────┐
│ 预处理器 │ ← 处理 #include、#define 等
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ 词法分析器 │ ← 字符 → Token
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ 语法分析器 │ ← Token → 语法树
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ 语义分析器 │ ← 检查错误、添类型
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ 中间代码生成器 │ ← 生成 IR
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ 机器无关优化器 │ ← 优化 IR
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ 目标代码生成器 │ ← IR → 汇编
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ 汇编器 │ ← 汇编 → 机器码
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ 链接器 │ ← 合并多个文件
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ 可执行文件 │
│ (a.out, hello.exe) │
└───────────────────────────┘4.4 黑箱 vs 白箱
黑箱视角(Black Box)
- 只看输入输出:源代码 → 编译器 → 可执行文件
- 不关心内部:编译器是怎么工作的?我不知道
- 适合使用者:大部分程序员只需要会用编译器就行
源代码
│
▼
┌──────────┐
│ 编译器 │ ← 黑箱,不知道里面在干嘛
└────┬─────┘
│
▼
可执行文件白箱视角(White Box)
- 了解内部结构:打开编译器,看每个阶段在做什么
- 理解原理:词法分析、语法分析、优化...
- 适合本课程:我们就是要打开这个黑箱!
源代码
│
▼
┌─────────────────────────┐
│ ┌─────┐ ┌─────┐ ┌─────┐│
│ │词法 │ │语法 │ │优化 ││ ← 白箱,看清每一步
│ │分析 │ │分析 │ │器 ││
│ └─────┘ └─────┘ └─────┘│
└───────────┬─────────────┘
│
▼
可执行文件4.5 用 GCC 实际看看
让我们用 GCC 来看看实际的编译过程。假设我们有一个文件 hello.c:
#include <stdio.h>
int main() {
printf("Hello, Compiler!\n");
return 0;
}只预处理(-E)
gcc -E hello.c -o hello.i输出 hello.i:展开了头文件,处理了宏
只编译到汇编(-S)
gcc -S hello.c -o hello.s输出 hello.s:汇编语言代码
只编译到目标文件(-c)
gcc -c hello.c -o hello.o输出 hello.o:机器码目标文件
完整编译
gcc hello.c -o hello输出 hello:可执行文件
4.6 自测一下
问题 1
编译器的前端主要负责什么?
A) 生成机器码
B) 分析源代码,理解程序含义
C) 优化目标代码
D) 以上都是
问题 2
编译过程的正确顺序是?
A) 词法分析 → 语法分析 → 语义分析 → 代码生成
B) 语法分析 → 词法分析 → 语义分析 → 代码生成
C) 代码生成 → 语义分析 → 语法分析 → 词法分析
D) 随便什么顺序都可以
问题 3
把编译器分成前端、中端、后端有什么好处?
问题 4
什么是"黑箱视角"和"白箱视角"?
答案:
- B
- A
- 可移植性好,支持新语言或新机器只需改动一部分;优化可以共享
- 黑箱视角只看输入输出,不关心内部;白箱视角了解内部结构和每个阶段的工作
4.7 下集预告
下一集,我们将聆听:历史上的编译器传奇!
我们会了解:
- 第一个编译器是怎么来的
- Fortran 诞生的故事
- C语言与 UNIX 的革命
- LLVM 是如何崛起的
准备好了吗?我们下集见!
参考资料
- 《编译原理》(龙书)第 2 章
- GCC 官方文档
- LLVM 架构文档