源代码 → 预处理 → 汇编代码 → 目标文件 → 链接 → 可执行程序
一、预处理
预处理器本质上是个文本替换器 ,不懂 C++ 语法,只认 # 指令。
展开宏
cpp
#define PI 3.14
所有 PI 直接替换成 3.14
头文件展开
cpp
#include <iostream>
把整个头文件内容原地复制进来
条件编译
cpp
#ifdef DEBUG
决定哪些代码保留
删除注释
二、编译
编译是 狭义编译 :.i / .cpp → .s,不包含预处理、不包含链接。
hello.cpp
|
| 预处理(文本替换)
v
hello.i
|
| 编译(语法 + 语义 + 优化)
v
hello.s
输入 :已经展开好的 .i
输出 :汇编代码 .s
编译器内部的 6 个核心阶段
词法分析
↓
语法分析
↓
语义分析
↓
中间代码
↓
优化
↓
生成汇编
词法分析
干什么
把字符流 变成 Token
cpp
int a = b + 3;
会被拆成:
int | a | = | b | + | 3 | ;
编译器此时:
不关心 a 是什么
不关心 b 定没定义
只管"像不像合法单词"
语法分析
干什么
判断 Token 组合是不是符合 C++ 语法
cpp
int a = b + 3;
会生成一棵 抽象语法树
=
/ \
a +
/ \
b 3
语义分析
干什么
干什么
变量是否声明
类型是否匹配
作用域是否合法
函数参数是否正确
const / 引用 / 指针规则
生成中间表示
为什么要 IR?
如果直接从 C++ → 汇编:
架构绑定(x86 / ARM)
不利于优化
所以编译器会生成一种 中间语言(LLVM IR / GIMPLE)
IR 是什么样的?
类似"高级汇编":
t1 = load a
t2 = load b
t3 = add t1, t2
store t3 -> c
这一层:
与 CPU 架构无关
是优化的主战场
优化阶段
常见优化手段
常量折叠
cpp
int x = 3 * 4;
直接变成:
cpp
int x = 12;
死代码消除
cpp
if (false) {
foo();
}
foo() 被直接删掉
内联展开
cpp
inline int add(int a, int b) {
return a + b;
}
直接展开函数体
生成汇编代码
干什么
把 IR → 特定架构汇编
分配寄存器
生成函数调用约定
三、汇编
从汇编 → 机器码(二进制)
了什么
把 .s 翻译成 .o
生成 ELF 目标文件
符号表 在这里产生
.o 文件 不能运行,但已经是机器码了
目标文件里有什么
.text:函数机器码
.data:已初始化全局变量
.bss:未初始化全局变量
.symtab:符号表
.rel.text:重定位信息
函数地址此时还没定
四、链接
把所有 .o + 库 拼成一个可执行文件
链接器干了什么
符号解析
编译时只知道:printf 是个外部符号
链接时:去 libc 里找真正实现
地址重定位
.o 里:不知道 printf 在哪
链接后:把地址填进去
静态链接 vs 动态链接
静态链接(.a)
库代码直接拷进可执行文件
文件大
不依赖系统库
动态链接(.so)
程序运行时才加载
节省内存
依赖系统环境
五、g++ 一步到位 vs 分步
一步到位
cpp
g++ hello.cpp -o hello
分步
cpp
g++ -E hello.cpp -o hello.i
g++ -S hello.i -o hello.s
g++ -c hello.s -o hello.o
g++ hello.o -o hello