跳转至

02-从代码到执行的旅程

重要性:⭐⭐⭐⭐⭐ 实用度:⭐⭐⭐⭐⭐ 学习时间:2-3天 必须掌握:是


为什么学这一章?

上一章我们了解了计算机的基本工作原理,这一章我们将聚焦于一个核心问题:你写的代码是如何一步步变成可执行程序,最终在计算机上运行的?

学完这一章,你将能够: - ✅ 画出从源代码到程序执行的完整流程图 - ✅ 解释编译过程的每个阶段 - ✅ 理解不同类型的编程语言(编译型vs解释型) - ✅ 使用工具观察代码的每个转换阶段


📖 核心概念

1. 编程语言的分类

编译型语言 vs 解释型语言

Text Only
┌─────────────────────────────────────────────────────────────┐
│                   编程语言的执行方式                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  编译型语言(C/C++)                                          │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐                 │
│  │ 源代码   │───→│ 编译器   │───→│ 机器码   │                 │
│  │ .c/.cpp │    │         │    │ .exe    │                 │
│  └─────────┘    └─────────┘    └────┬────┘                 │
│                                     ↓                       │
│                              ┌─────────────┐               │
│                              │  直接执行    │               │
│                              │  在CPU上运行 │               │
│                              └─────────────┘               │
│                                                             │
│  特点:执行前需要完整编译,执行速度快                         │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  解释型语言(Python)                                         │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐                 │
│  │ 源代码   │───→│ 解释器   │───→│ 逐行执行 │                 │
│  │  .py    │    │         │    │ 不生成独立 │                 │
│  └─────────┘    └─────────┘    │ 可执行文件 │                 │
│                                └─────────┘                 │
│                                                             │
│  特点:边解释边执行,灵活性高,执行速度相对慢                  │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  混合型(Java)                                               │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐                 │
│  │ 源代码   │───→│ 编译器   │───→│ 字节码   │                 │
│  │  .java  │    │         │    │  .class │                 │
│  └─────────┘    └─────────┘    └────┬────┘                 │
│                                     ↓                       │
│                              ┌─────────────┐               │
│                              │  JVM虚拟机   │               │
│                              │ 解释执行字节码│               │
│                              └─────────────┘               │
│                                                             │
│  特点:一次编译,到处运行(跨平台)                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

对比总结

特性 C/C++ Python Java
执行方式 编译执行 解释执行 编译+解释
编译时机 运行前 运行时 运行前(到字节码)
执行速度 较慢 中等
跨平台 需重新编译 直接运行 字节码跨平台
调试难度 需重新编译 即时修改 中等
适用场景 系统编程、游戏 脚本、AI 企业应用

2. 代码的完整生命周期

让我们追踪一段C++代码从编写到执行的完整旅程:

C++
// hello.cpp - 第1步:编写源代码
#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

完整流程图

Text Only
┌─────────────────────────────────────────────────────────────────────┐
│                    从代码到执行的完整旅程                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  阶段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!                                            │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

一键完成所有阶段

Bash
# 通常我们使用一条命令完成所有阶段
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. 注释删除:删除所有注释

示例

C++
// 源代码
#include <iostream>
#define GREETING "Hello"

int main() {
    std::cout << GREETING << std::endl;  // 输出问候语
    return 0;
}
C++
// 预处理后(简化示意)
// ... iostream的数千行代码 ...
namespace std {
    // ... cout的定义 ...
}

int main() {
    std::cout << "Hello" << std::endl;
    return 0;
}

查看预处理结果

Bash
g++ -E hello.cpp -o hello.i
# 查看hello.i,你会发现代码膨胀了很多倍!

生活化类比

就像写论文时的"复制粘贴"。你把参考文献的内容复制到论文里,替换掉引用标记,这样读者就能看到完整内容了。


阶段3:编译(Compilation)

是什么:将高级语言翻译成汇编语言

内部过程

Text Only
┌─────────────────────────────────────────────────────────────┐
│                      编译器内部流程                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  源代码 (.i)                                                 │
│     ↓                                                       │
│  ┌─────────────┐                                            │
│  │  词法分析    │  → 将字符流分割成Token                      │
│  │  Lexical    │     int, main, (, ), {, return, ...        │
│  │  Analysis   │                                            │
│  └──────┬──────┘                                            │
│         ↓                                                   │
│  ┌─────────────┐                                            │
│  │  语法分析    │  → 构建抽象语法树(AST)                    │
│  │  Syntax     │                                            │
│  │  Analysis   │       [main]                               │
│  └──────┬──────┘         ↓                                   │
│         ↓              [return]                              │
│  ┌─────────────┐         ↓                                   │
│  │  语义分析    │       [0]                                  │
│  │  Semantic   │                                            │
│  │  Analysis   │  → 类型检查、作用域分析                      │
│  └──────┬──────┘                                            │
│         ↓                                                   │
│  ┌─────────────┐                                            │
│  │  中间代码生成 │  → 生成与机器无关的中间表示                 │
│  │  IR Gen     │     (LLVM IR, GIMPLE等)                    │
│  └──────┬──────┘                                            │
│         ↓                                                   │
│  ┌─────────────┐                                            │
│  │  优化        │  → 常量折叠、死代码删除、循环优化等          │
│  │  Optimize   │                                            │
│  └──────┬──────┘                                            │
│         ↓                                                   │
│  ┌─────────────┐                                            │
│  │  目标代码生成 │  → 生成汇编代码                            │
│  │  Code Gen   │                                            │
│  └──────┬──────┘                                            │
│         ↓                                                   │
│  汇编代码 (.s)                                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

示例

C++
// C++代码
int add(int a, int b) {
    return a + b;
}
GAS
; 生成的汇编代码(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                      ; 返回

查看编译结果

Bash
g++ -S hello.cpp -o hello.s
# 查看hello.s中的汇编代码


阶段4:汇编(Assembly)

是什么:将汇编代码翻译成机器码(二进制)

过程: - 每条汇编指令对应一个或多个字节 - 生成目标文件(.o或.obj) - 目标文件包含机器码,但还不能直接执行

示例

GAS
; 汇编代码
mov eax, 1      ; 将1放入寄存器eax
add eax, 2      ; eax = eax + 2
Text Only
机器码(十六进制表示):
B8 01 00 00 00    ; mov eax, 1
83 C0 02          ; add eax, 2

目标文件的内容

Text Only
┌─────────────────────────────────────────────────────────────┐
│                    目标文件结构(ELF格式)                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ELF头部                                                     │
│  ├── 文件类型、架构、入口点等信息                              │
│                                                             │
│  代码段 (.text)                                              │
│  ├── 机器指令(可执行代码)                                    │
│                                                             │
│  数据段 (.data)                                              │
│  ├── 已初始化的全局变量                                        │
│                                                             │
│  BSS段 (.bss)                                                │
│  ├── 未初始化的全局变量                                        │
│                                                             │
│  符号表 (.symtab)                                            │
│  ├── 函数名、变量名及其地址                                    │
│                                                             │
│  重定位表 (.rel)                                             │
│  ├── 需要链接器修正的地址引用                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

查看目标文件

Bash
g++ -c hello.cpp -o hello.o

# 查看目标文件结构
objdump -h hello.o

# 反汇编查看机器码
objdump -d hello.o


阶段5:链接(Linking)

是什么:将多个目标文件合并成可执行文件

主要任务: 1. 符号解析:找到所有符号(函数、变量)的定义 2. 重定位:修正地址引用,确定最终地址 3. 合并段:将相同类型的段合并

为什么需要链接?

C++
// main.cpp
#include <iostream>
void sayHello();  // 声明外部函数

int main() {
    sayHello();   // 调用外部函数
    return 0;
}
C++
// hello.cpp
#include <iostream>

void sayHello() {
    std::cout << "Hello!" << std::endl;
}

编译过程

Bash
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 动态链接

Text Only
┌─────────────────────────────────────────────────────────────┐
│                     链接方式对比                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  静态链接                                                    │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐                     │
│  │ main.o  │  │ hello.o │  │ libc.a  │  ← 静态库            │
│  └────┬────┘  └────┬────┘  └────┬────┘                     │
│       └─────────────┴─────────────┘                         │
│                   ↓                                         │
│            ┌─────────────┐                                 │
│            │  program    │  ← 包含所有代码                  │
│            │  (大文件)    │                                 │
│            └─────────────┘                                 │
│                                                             │
│  特点:独立运行,不依赖外部库,文件大                         │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  动态链接                                                    │
│  ┌─────────┐  ┌─────────┐                                  │
│  │ main.o  │  │ hello.o │                                   │
│  └────┬────┘  └────┬────┘                                   │
│       └─────────────┘                                       │
│             ↓                                               │
│      ┌─────────────┐      运行时加载                         │
│      │  program    │ ←──── libc.so                          │
│      │  (小文件)    │      动态库                            │
│      └─────────────┘                                        │
│                                                             │
│  特点:文件小,共享库代码,需要运行时环境                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

阶段6:加载与执行

是什么:操作系统将可执行文件加载到内存并运行

过程

Text Only
┌─────────────────────────────────────────────────────────────┐
│                  程序加载与执行过程                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 创建进程                                                 │
│     ├── 操作系统创建进程控制块(PCB)                          │
│     └── 分配进程ID(PID)                                     │
│                                                             │
│  2. 分配虚拟地址空间                                          │
│     ├── 代码段:存储程序指令                                   │
│     ├── 数据段:存储全局变量                                   │
│     ├── 堆:动态内存分配                                       │
│     └── 栈:函数调用和局部变量                                 │
│                                                             │
│  3. 加载程序                                                  │
│     ├── 从磁盘读取可执行文件                                   │
│     ├── 将代码段映射到内存                                     │
│     └── 设置程序入口点(main函数)                             │
│                                                             │
│  4. 初始化运行时环境                                          │
│     ├── 设置命令行参数(argc, argv)                           │
│     ├── 初始化标准输入输出                                     │
│     └── 执行全局构造函数(C++)                                │
│                                                             │
│  5. 开始执行                                                  │
│     ├── 跳转到main函数                                        │
│     ├── CPU开始执行指令                                        │
│     └── 程序正式运行                                          │
│                                                             │
│  6. 程序结束                                                  │
│     ├── main函数返回                                          │
│     ├── 执行全局析构函数(C++)                                │
│     └── 操作系统回收资源                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

4. Python代码的执行过程

作为对比,让我们看看Python代码是如何执行的:

Python
# hello.py
print("Hello, World!")
Text Only
┌─────────────────────────────────────────────────────────────┐
│                   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字节码

Bash
python -m dis hello.py


🧪 动手实验

实验1:观察编译的每个阶段

目的:亲身体验代码的完整编译过程

步骤

  1. 创建测试文件

    C++
    // test.cpp
    #include <iostream>
    #define VALUE 42
    
    int main() {
        int x = VALUE;
        std::cout << "Value: " << x << std::endl;
        return 0;
    }
    

  2. 分阶段编译

    Bash
    # 预处理
    g++ -E test.cpp -o test.i
    wc -l test.i  # 查看行数,你会发现有数万行!
    
    # 编译
    g++ -S test.i -o test.s
    cat test.s    # 查看汇编代码
    
    # 汇编
    g++ -c test.s -o test.o
    objdump -d test.o  # 查看机器码
    
    # 链接
    g++ test.o -o test
    ./test
    

  3. 观察变化

  4. 对比test.cpp和test.i,看头文件展开
  5. 对比test.i和test.s,看高级语言到汇编
  6. 对比test.s和test.o,看汇编到机器码

实验2:观察符号和重定位

目的:理解链接器的工作原理

步骤

  1. 创建两个文件
    C++
    // math.cpp
    int add(int a, int b) {
        return a + b;
    }
    
C++
// 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;
}
  1. 查看符号表

    Bash
    g++ -c main.cpp -o main.o
    g++ -c math.cpp -o math.o
    
    # 查看符号表
    nm main.o
    nm math.o
    

  2. 观察未定义符号

    Bash
    # main.o中应该有未定义的add符号(标记为U)
    nm main.o | grep add  # grep文本搜索:按模式匹配行
    

  3. 链接后查看

    Bash
    g++ main.o math.o -o program
    nm program | grep add  # |管道:将前一命令的输出作为后一命令的输入
    

实验3:使用Compiler Explorer对比

目的:理解不同优化级别的影响

步骤

  1. 打开 https://godbolt.org/

  2. 输入代码:

    C++
    int factorial(int n) {
        if (n <= 1) return 1;
        return n * factorial(n - 1);
    }
    

  3. 对比不同优化级别:

  4. 选择编译器:x86-64 gcc
  5. 编译器选项:-O0(无优化)
  6. 观察汇编代码
  7. 改为 -O3(最高优化)
  8. 观察变化

  9. 思考问题:

  10. 优化后的代码有什么不同?
  11. 递归函数被优化成了什么?

实验4:观察Python字节码

目的:理解Python的执行过程

步骤

  1. 创建Python文件

    Python
    # test.py
    def add(a, b):
        return a + b
    
    result = add(3, 5)
    print(result)
    

  2. 查看字节码

    Bash
    python -m dis test.py
    

  3. 生成.pyc文件

    Bash
    python -m compileall test.py
    

  4. 查看.pyc文件(二进制,不可读)

    Bash
    ls -la __pycache__/
    


💡 核心要点总结

必须记住的流程

Text Only
C++代码执行流程:

源代码 (.cpp)
    ↓ 预处理 (g++ -E)
预处理后的代码 (.i)
    ↓ 编译 (g++ -S)
汇编代码 (.s)
    ↓ 汇编 (g++ -c)
目标文件 (.o)
    ↓ 链接 (g++)
可执行文件
    ↓ 运行
程序执行

关键概念

  1. 编译型语言:C/C++,先编译后执行,执行速度快
  2. 解释型语言:Python,边解释边执行,灵活性高
  3. 预处理:头文件展开、宏替换、条件编译
  4. 编译:词法→语法→语义→优化→代码生成
  5. 汇编:汇编代码→机器码
  6. 链接:符号解析、重定位、合并段
  7. 加载:创建进程、分配内存、初始化、执行

❓ 常见问题

Q1:为什么C++需要编译,而Python不需要?

A:C++是编译型语言,源代码需要转换成机器码才能执行,这样执行速度快。Python是解释型语言,源代码在运行时由解释器逐行执行,这样更灵活但速度较慢。

Q2:头文件里到底有什么?

A:头文件包含函数声明、类定义、宏定义等。预处理器会将头文件的内容插入到#include的位置。例如,iostream头文件包含了cout、cin等对象的定义。

Q3:为什么链接时会报"undefined reference"错误?

A:这意味着链接器找不到某个函数或变量的定义。可能原因: - 忘记链接包含该函数的目标文件或库 - 函数名拼写错误 - 函数声明了但没有实现

Q4:静态链接和动态链接怎么选择?

A: - 静态链接:适合独立分发、不依赖外部环境的程序 - 动态链接:适合节省空间、需要共享库更新的场景 - 现代系统通常混合使用:系统库动态链接,自定义库静态链接

Q5:为什么优化后的代码更难调试?

A:优化器会重排指令、删除"无用"代码、内联函数等,这使得汇编代码与源代码的对应关系变得复杂。调试时建议使用 -O0(无优化)选项。


📚 扩展阅读

  1. 《程序员的自我修养》 - 链接、装载与库
  2. 深入讲解链接过程和可执行文件格式

  3. 《编译原理》(龙书) - Aho等

  4. 编译器设计的权威教材

  5. 《深入理解计算机系统》 - Bryant & O'Hallaron

  6. 第7章:链接

  7. 在线资源

  8. Compiler Explorer (godbolt.org)
  9. LLVM文档(了解现代编译器架构)

🎯 下一步

完成本章后,你已经了解了代码从编写到执行的完整流程。接下来进入 02-编译原理基础 阶段,深入学习编译器的各个阶段:

准备好深入编译器的世界了吗?