02-从代码到执行的旅程¶
重要性:⭐⭐⭐⭐⭐ 实用度:⭐⭐⭐⭐⭐ 学习时间:2-3天 必须掌握:是
为什么学这一章?¶
上一章我们了解了计算机的基本工作原理,这一章我们将聚焦于一个核心问题:你写的代码是如何一步步变成可执行程序,最终在计算机上运行的?
学完这一章,你将能够: - ✅ 画出从源代码到程序执行的完整流程图 - ✅ 解释编译过程的每个阶段 - ✅ 理解不同类型的编程语言(编译型vs解释型) - ✅ 使用工具观察代码的每个转换阶段
📖 核心概念¶
1. 编程语言的分类¶
编译型语言 vs 解释型语言¶
┌─────────────────────────────────────────────────────────────┐
│ 编程语言的执行方式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 编译型语言(C/C++) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 源代码 │───→│ 编译器 │───→│ 机器码 │ │
│ │ .c/.cpp │ │ │ │ .exe │ │
│ └─────────┘ └─────────┘ └────┬────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ 直接执行 │ │
│ │ 在CPU上运行 │ │
│ └─────────────┘ │
│ │
│ 特点:执行前需要完整编译,执行速度快 │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ 解释型语言(Python) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 源代码 │───→│ 解释器 │───→│ 逐行执行 │ │
│ │ .py │ │ │ │ 不生成独立 │ │
│ └─────────┘ └─────────┘ │ 可执行文件 │ │
│ └─────────┘ │
│ │
│ 特点:边解释边执行,灵活性高,执行速度相对慢 │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ 混合型(Java) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 源代码 │───→│ 编译器 │───→│ 字节码 │ │
│ │ .java │ │ │ │ .class │ │
│ └─────────┘ └─────────┘ └────┬────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ JVM虚拟机 │ │
│ │ 解释执行字节码│ │
│ └─────────────┘ │
│ │
│ 特点:一次编译,到处运行(跨平台) │
│ │
└─────────────────────────────────────────────────────────────┘
对比总结¶
| 特性 | C/C++ | Python | Java |
|---|---|---|---|
| 执行方式 | 编译执行 | 解释执行 | 编译+解释 |
| 编译时机 | 运行前 | 运行时 | 运行前(到字节码) |
| 执行速度 | 快 | 较慢 | 中等 |
| 跨平台 | 需重新编译 | 直接运行 | 字节码跨平台 |
| 调试难度 | 需重新编译 | 即时修改 | 中等 |
| 适用场景 | 系统编程、游戏 | 脚本、AI | 企业应用 |
2. 代码的完整生命周期¶
让我们追踪一段C++代码从编写到执行的完整旅程:
// hello.cpp - 第1步:编写源代码
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
完整流程图¶
┌─────────────────────────────────────────────────────────────────────┐
│ 从代码到执行的完整旅程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 阶段1:编写源代码 │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ // hello.cpp │ │
│ │ #include <iostream> │ │
│ │ int main() { │ │
│ │ std::cout << "Hello"; │ │
│ │ return 0; │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段2:预处理(Preprocessing) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 处理:#include, #define, 条件编译 │ │
│ │ 输出:hello.i(展开的源代码) │ │
│ │ 命令:g++ -E hello.cpp -o hello.i │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段3:编译(Compilation) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 处理:词法分析 → 语法分析 → 语义分析 → 优化 → 代码生成 │ │
│ │ 输出:hello.s(汇编代码) │ │
│ │ 命令:g++ -S hello.i -o hello.s │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段4:汇编(Assembly) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 处理:汇编代码 → 机器码 │ │
│ │ 输出:hello.o(目标文件/机器码) │ │
│ │ 命令:g++ -c hello.s -o hello.o │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段5:链接(Linking) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 处理:合并多个目标文件,解析符号,重定位 │ │
│ │ 输出:hello(可执行文件) │ │
│ │ 命令:g++ hello.o -o hello │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段6:加载与执行(Loading & Execution) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 处理:操作系统加载程序,创建进程,分配内存,开始执行 │ │
│ │ 命令:./hello │ │
│ │ 输出:Hello, World! │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
一键完成所有阶段¶
# 通常我们使用一条命令完成所有阶段
g++ hello.cpp -o hello
# 等价于
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 # 链接
3. 各阶段详解¶
阶段2:预处理(Preprocessing)¶
是什么:在真正编译之前,对源代码进行文本替换
主要任务: 1. 头文件展开:#include <iostream> → 插入iostream的内容 2. 宏替换:#define PI 3.14 → 所有PI替换成3.14 3. 条件编译:根据条件保留或删除代码块 4. 注释删除:删除所有注释
示例:
// 源代码
#include <iostream>
#define GREETING "Hello"
int main() {
std::cout << GREETING << std::endl; // 输出问候语
return 0;
}
// 预处理后(简化示意)
// ... iostream的数千行代码 ...
namespace std {
// ... cout的定义 ...
}
int main() {
std::cout << "Hello" << std::endl;
return 0;
}
查看预处理结果:
生活化类比:
就像写论文时的"复制粘贴"。你把参考文献的内容复制到论文里,替换掉引用标记,这样读者就能看到完整内容了。
阶段3:编译(Compilation)¶
是什么:将高级语言翻译成汇编语言
内部过程:
┌─────────────────────────────────────────────────────────────┐
│ 编译器内部流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 源代码 (.i) │
│ ↓ │
│ ┌─────────────┐ │
│ │ 词法分析 │ → 将字符流分割成Token │
│ │ Lexical │ int, main, (, ), {, return, ... │
│ │ Analysis │ │
│ └──────┬──────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ 语法分析 │ → 构建抽象语法树(AST) │
│ │ Syntax │ │
│ │ Analysis │ [main] │
│ └──────┬──────┘ ↓ │
│ ↓ [return] │
│ ┌─────────────┐ ↓ │
│ │ 语义分析 │ [0] │
│ │ Semantic │ │
│ │ Analysis │ → 类型检查、作用域分析 │
│ └──────┬──────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ 中间代码生成 │ → 生成与机器无关的中间表示 │
│ │ IR Gen │ (LLVM IR, GIMPLE等) │
│ └──────┬──────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ 优化 │ → 常量折叠、死代码删除、循环优化等 │
│ │ Optimize │ │
│ └──────┬──────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ 目标代码生成 │ → 生成汇编代码 │
│ │ Code Gen │ │
│ └──────┬──────┘ │
│ ↓ │
│ 汇编代码 (.s) │
│ │
└─────────────────────────────────────────────────────────────┘
示例:
; 生成的汇编代码(x86-64)
add:
push rbp ; 保存基址指针
mov rbp, rsp ; 设置新的基址指针
mov DWORD PTR [rbp-4], edi ; 参数a存入栈
mov DWORD PTR [rbp-8], esi ; 参数b存入栈
mov edx, DWORD PTR [rbp-4] ; 加载a
mov eax, DWORD PTR [rbp-8] ; 加载b
add eax, edx ; 执行加法
pop rbp ; 恢复基址指针
ret ; 返回
查看编译结果:
阶段4:汇编(Assembly)¶
是什么:将汇编代码翻译成机器码(二进制)
过程: - 每条汇编指令对应一个或多个字节 - 生成目标文件(.o或.obj) - 目标文件包含机器码,但还不能直接执行
示例:
目标文件的内容:
┌─────────────────────────────────────────────────────────────┐
│ 目标文件结构(ELF格式) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ELF头部 │
│ ├── 文件类型、架构、入口点等信息 │
│ │
│ 代码段 (.text) │
│ ├── 机器指令(可执行代码) │
│ │
│ 数据段 (.data) │
│ ├── 已初始化的全局变量 │
│ │
│ BSS段 (.bss) │
│ ├── 未初始化的全局变量 │
│ │
│ 符号表 (.symtab) │
│ ├── 函数名、变量名及其地址 │
│ │
│ 重定位表 (.rel) │
│ ├── 需要链接器修正的地址引用 │
│ │
└─────────────────────────────────────────────────────────────┘
查看目标文件:
阶段5:链接(Linking)¶
是什么:将多个目标文件合并成可执行文件
主要任务: 1. 符号解析:找到所有符号(函数、变量)的定义 2. 重定位:修正地址引用,确定最终地址 3. 合并段:将相同类型的段合并
为什么需要链接?
// main.cpp
#include <iostream>
void sayHello(); // 声明外部函数
int main() {
sayHello(); // 调用外部函数
return 0;
}
编译过程:
g++ -c main.cpp -o main.o # 编译main.cpp
g++ -c hello.cpp -o hello.o # 编译hello.cpp
g++ main.o hello.o -o program # 链接成可执行文件
链接器的工作: 1. 发现main.o中调用了sayHello,但不知道地址 2. 在hello.o中找到sayHello的定义 3. 修正main.o中的调用地址,指向hello.o中的sayHello
静态链接 vs 动态链接:
┌─────────────────────────────────────────────────────────────┐
│ 链接方式对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 静态链接 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ main.o │ │ hello.o │ │ libc.a │ ← 静态库 │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ └─────────────┴─────────────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │ program │ ← 包含所有代码 │
│ │ (大文件) │ │
│ └─────────────┘ │
│ │
│ 特点:独立运行,不依赖外部库,文件大 │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ 动态链接 │
│ ┌─────────┐ ┌─────────┐ │
│ │ main.o │ │ hello.o │ │
│ └────┬────┘ └────┬────┘ │
│ └─────────────┘ │
│ ↓ │
│ ┌─────────────┐ 运行时加载 │
│ │ program │ ←──── libc.so │
│ │ (小文件) │ 动态库 │
│ └─────────────┘ │
│ │
│ 特点:文件小,共享库代码,需要运行时环境 │
│ │
└─────────────────────────────────────────────────────────────┘
阶段6:加载与执行¶
是什么:操作系统将可执行文件加载到内存并运行
过程:
┌─────────────────────────────────────────────────────────────┐
│ 程序加载与执行过程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 创建进程 │
│ ├── 操作系统创建进程控制块(PCB) │
│ └── 分配进程ID(PID) │
│ │
│ 2. 分配虚拟地址空间 │
│ ├── 代码段:存储程序指令 │
│ ├── 数据段:存储全局变量 │
│ ├── 堆:动态内存分配 │
│ └── 栈:函数调用和局部变量 │
│ │
│ 3. 加载程序 │
│ ├── 从磁盘读取可执行文件 │
│ ├── 将代码段映射到内存 │
│ └── 设置程序入口点(main函数) │
│ │
│ 4. 初始化运行时环境 │
│ ├── 设置命令行参数(argc, argv) │
│ ├── 初始化标准输入输出 │
│ └── 执行全局构造函数(C++) │
│ │
│ 5. 开始执行 │
│ ├── 跳转到main函数 │
│ ├── CPU开始执行指令 │
│ └── 程序正式运行 │
│ │
│ 6. 程序结束 │
│ ├── main函数返回 │
│ ├── 执行全局析构函数(C++) │
│ └── 操作系统回收资源 │
│ │
└─────────────────────────────────────────────────────────────┘
4. Python代码的执行过程¶
作为对比,让我们看看Python代码是如何执行的:
┌─────────────────────────────────────────────────────────────┐
│ Python代码执行过程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 阶段1:源代码 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ print("Hello, World!") │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段2:词法分析和语法分析 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 生成抽象语法树(AST) │ │
│ │ [Module] │ │
│ │ ↓ │ │
│ │ [Expr] │ │
│ │ ↓ │ │
│ │ [Call] │ │
│ │ / \ │ │
│ │ [Name] [Str] │ │
│ │ print "Hello..." │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段3:编译为字节码 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 0 LOAD_NAME 0 (print) │ │
│ │ 2 LOAD_CONST 1 ('Hello, World!') │ │
│ │ 4 CALL_FUNCTION 1 │ │
│ │ 6 POP_TOP │ │
│ │ 8 LOAD_CONST 2 (None) │ │
│ │ 10 RETURN_VALUE │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段4:虚拟机解释执行 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Python虚拟机(PVM)逐条执行字节码指令 │ │
│ │ 调用C函数完成实际操作(如输出到屏幕) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
查看Python字节码:
🧪 动手实验¶
实验1:观察编译的每个阶段¶
目的:亲身体验代码的完整编译过程
步骤:
-
创建测试文件
-
分阶段编译
-
观察变化
- 对比test.cpp和test.i,看头文件展开
- 对比test.i和test.s,看高级语言到汇编
- 对比test.s和test.o,看汇编到机器码
实验2:观察符号和重定位¶
目的:理解链接器的工作原理
步骤:
- 创建两个文件
// main.cpp
#include <iostream> // 引入头文件
int add(int a, int b); // 声明
int main() {
int result = add(3, 5);
std::cout << "Result: " << result << std::endl;
return 0;
}
-
查看符号表
-
观察未定义符号
-
链接后查看
实验3:使用Compiler Explorer对比¶
目的:理解不同优化级别的影响
步骤:
-
输入代码:
-
对比不同优化级别:
- 选择编译器:x86-64 gcc
- 编译器选项:
-O0(无优化) - 观察汇编代码
- 改为
-O3(最高优化) -
观察变化
-
思考问题:
- 优化后的代码有什么不同?
- 递归函数被优化成了什么?
实验4:观察Python字节码¶
目的:理解Python的执行过程
步骤:
-
创建Python文件
-
查看字节码
-
生成.pyc文件
-
查看.pyc文件(二进制,不可读)
💡 核心要点总结¶
必须记住的流程¶
C++代码执行流程:
源代码 (.cpp)
↓ 预处理 (g++ -E)
预处理后的代码 (.i)
↓ 编译 (g++ -S)
汇编代码 (.s)
↓ 汇编 (g++ -c)
目标文件 (.o)
↓ 链接 (g++)
可执行文件
↓ 运行
程序执行
关键概念¶
- 编译型语言:C/C++,先编译后执行,执行速度快
- 解释型语言:Python,边解释边执行,灵活性高
- 预处理:头文件展开、宏替换、条件编译
- 编译:词法→语法→语义→优化→代码生成
- 汇编:汇编代码→机器码
- 链接:符号解析、重定位、合并段
- 加载:创建进程、分配内存、初始化、执行
❓ 常见问题¶
Q1:为什么C++需要编译,而Python不需要?
A:C++是编译型语言,源代码需要转换成机器码才能执行,这样执行速度快。Python是解释型语言,源代码在运行时由解释器逐行执行,这样更灵活但速度较慢。
Q2:头文件里到底有什么?
A:头文件包含函数声明、类定义、宏定义等。预处理器会将头文件的内容插入到#include的位置。例如,iostream头文件包含了cout、cin等对象的定义。
Q3:为什么链接时会报"undefined reference"错误?
A:这意味着链接器找不到某个函数或变量的定义。可能原因: - 忘记链接包含该函数的目标文件或库 - 函数名拼写错误 - 函数声明了但没有实现
Q4:静态链接和动态链接怎么选择?
A: - 静态链接:适合独立分发、不依赖外部环境的程序 - 动态链接:适合节省空间、需要共享库更新的场景 - 现代系统通常混合使用:系统库动态链接,自定义库静态链接
Q5:为什么优化后的代码更难调试?
A:优化器会重排指令、删除"无用"代码、内联函数等,这使得汇编代码与源代码的对应关系变得复杂。调试时建议使用 -O0(无优化)选项。
📚 扩展阅读¶
- 《程序员的自我修养》 - 链接、装载与库
-
深入讲解链接过程和可执行文件格式
-
《编译原理》(龙书) - Aho等
-
编译器设计的权威教材
-
《深入理解计算机系统》 - Bryant & O'Hallaron
-
第7章:链接
-
在线资源:
- Compiler Explorer (godbolt.org)
- LLVM文档(了解现代编译器架构)
🎯 下一步¶
完成本章后,你已经了解了代码从编写到执行的完整流程。接下来进入 02-编译原理基础 阶段,深入学习编译器的各个阶段:
准备好深入编译器的世界了吗?