---
title: "C++ 编译流程"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/cpp-compilation-pipeline/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/02-编程语言与运行时/C++ 编译流程.md"
content_hash: 49dc13556a8f81b352f17d2e0a1af165c163e67e796186dcad5cd0ce83ff550a
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# C++ 编译流程


## 传统 C++ 编译五阶段

#### 1. 预处理（Preprocessing）

* **工作**：宏展开、条件编译、头文件插入。
* **命令示例**：`clang -E hello.cpp -o hello.i`
* **产物**：`hello.i` —— 纯文本 C/C++ 源码，所有 `#include` 与宏均已解析。

#### 2. 编译（Front-end Compilation）

* **工作**：词法分析 → 语法分析 → 语义检查 → 生成内部 IR（GCC：GIMPLE/RTL；Clang：LLVM IR）。
* **命令示例**：`clang -c hello.i -o hello.o`
* **产物**：若未显式输出汇编，则直接得到 **目标文件** `hello.o`（ELF relocatable object，包含机器指令、符号表、重定位记录）。

- Clang 前端把 AST 降低成 LLVM IR（隐式在内存中，不写磁盘）。
- LLVM 后端再把 IR 直接**翻译成机器指令**（对应您如果输出汇编会看到的那些助记符）。


#### 3. 目标级优化（Machine-code Optimization，可选）

* **工作**：寄存器分配、指令调度、内联、常量合并等。
* **触发方式**：`-O1/-O2/-O3` 等优化级别。
* **产物**：优化后的 `.o`，与步骤 2 同一格式但指令序列已改写。

#### 4. 汇编（Assembly，可选显式步）

* **目的**：生成人类可读汇编文本或从汇编文本生成 `.o`。
* **查看汇编**：`clang -S hello.cpp -o hello.s`
* **再汇编**：`as hello.s -o hello_asm.o`
* **差异**：`.s` 为助记符文本；`.o` 为二进制机器码。

- LLVM 后端会“就地”组装（assemble）这些指令成**机器码字节**，连同符号表／重定位信息一起打包进 `.o`。

#### 5. 链接 / 归档

* **可执行**：`clang hello.o -o hello` → 链接器分配地址、解析外部符号并合并库。
* **静态库**：`ar rcs libhello.a hello.o` → 仅把多个 `.o` 归档，无地址分配。


## 示例：从源文件到库 / 可执行

源文件 `hello.cpp`：
```cpp
// hello.cpp
#include <cstdio>
#define SQUARE(x) ((x)*(x))

static const char* MSG = "3² = %d\n";

int square(int x) {
    return x * x;
}

int main() {
    int v = square(3);
    std::printf(MSG, v);
    return 0;
}
```


### （1） 预处理

```bash
clang -E hello.cpp -o hello.i
```

**产物：** `hello.i`（文本），所有的 `#include`、宏都已展开，可能看起来像：

```c
# C++ 编译流程
## 1 "<built-in>"
## 1 "<command-line>"
## 1 "hello.cpp"
static const char* MSG = "3² = %d\n";

int square(int x) {
    return x * x;
}

int main() {
    int v = square(3);
    std::printf("3² = %d\n", v);
    return 0;
}
```

* **宏** `SQUARE(3)` 已被直接替换为 `((3)*(3))`（本例没用到）；
* **头文件** `<cstdio>` 展开为底层实现和声明（大量代码，已被剥掉示例）。


### （2） 编译（词法→语法→AST→目标代码）

```bash
clang -O2 -c hello.cpp -o hello.o
```

`hello.o` 已含机器指令，可用 `objdump -d hello.o` 反汇编查看。

1. **词法分析**：把源文本切成一连串 token（标识符、字面量、操作符……）
2. **语法分析**：把 token 按 C++ 语法规则组装成 AST（抽象语法树）
3. **语义检查**：类型检查、名称解析、模板实例化
4. **生成“目标代码”**：Clang 会在内存中直接把 AST 降低为机器指令的中间数据结构（GCC 用 GIMPLE/RTL，Clang 用 LLVM IR 或内部指令序列），但它不会再产出一个文件，除非你指定输出。

如果你让它直接产出一个“目标”文件：

```bash
clang -O0 -c hello.i -o hello.o
```

* **结果**：`hello.o` 是一个 **ELF relocatable object**（或在 Emscripten 下是 Wasm relocatable object），里面已经包含了机器指令、符号表、重定位信息。
* Clang 并不会产出可读的中间文本，这里的“目标代码”实际就是**硬编码在二进制**的机器指令。


对该步骤进行进一步拆分如下:
#### 词法/语法分析 → AST
- **AST（抽象语法树）**：Clang 内部表示，不直接生成到磁盘。

```bash
clang -cc1 -ast-dump hello.cpp -fsyntax-only
```
- 上面命令会在终端输出类似：

```text
TranslationUnitDecl ...
 ├─FunctionDecl square
 │   ├─ParmVarDecl x
 │   └─CompoundStmt
 │       └─ReturnStmt
 │           └─BinaryOperator '*'
 └─FunctionDecl main
     ├─CompoundStmt
     │   └─CXXOperatorCallExpr '<<'
     │       ├─...

```

#### 生成 LLVM IR 文本（`.ll`）

```bash
clang -O0 -emit-llvm -S hello.cpp -o hello.ll
```

**查看 `hello.ll`**

```cpp
; ModuleID = 'hello.cpp'
source_filename = "hello.cpp"
target datalayout = "..."
target triple = "x86_64-unknown-linux-gnu"

@_ZSt4cout = external dso_local global %class.std::ostream, align 8

; 定义 square
define dso_local i32 @_Z6squarei(i32 %x) #0 {
entry:
  %mul = mul nsw i32 %x, %x
  ret i32 %mul
}

; 定义 main
define dso_local i32 @_Z4main() #0 {
entry:
  ; 调用 square(3)
  %call = call i32 @_Z6squarei(i32 3)
  ; 打印
  %0 = call %class.std::ostream* @_ZStlsISt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKc(
            %class.std::ostream* @_ZSt4cout, i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str, i32 0, i32 0))
  ; ...
  ret i32 0
}
```
- `.ll` 是**人可读**的 LLVM IR。
- 包含函数签名（mangled name）、基本块、指令（`mul`, `call`, `ret`）等。
- LLVM IR 是一种**SSA 形式**的中间表示，既包含类型信息，又支持跨平台优化。

#### 生成 LLVM Bitcode（`.bc`）

```text
clang -O0 -emit-llvm -c hello.cpp -o hello.bc
```
- `hello.bc` 是**二进制**形式的 LLVM IR（bitcode），比 `.ll` 更紧凑。
- 可以通过 `llvm-dis hello.bc -o hello_dis.ll` “反编译”回文本形式；`file hello.bc` 会显示 “LLVM IR bitcode”。

#### 优化（目标代码级）

在上一步，你可以加 `-O2` 或者使用链接时的 LTO：

```bash
clang -O2 -c hello.i -o hello_opt.o
## 或者
clang -flto -O2 -c hello.i -o hello_lto.o
```

* **作用**：编译器会对刚生成的指令作 **寄存器分配、指令调度、常量合并、内联** 等优化，甚至在 `-flto` 模式下跨不同 `.o` 文件做优化。
* **产物**：仍然是一个 `.o`，但机器指令序列已经被重写以提高性能或减小体积。


### （3） 汇编（生成机器码/目标机器文件 `.o`）

从源到 ELF 对象
```bash
clang -O2 -c hello.cpp -o hello.o
```
- `hello.o` 是一个 ELF relocatable object，包含已编译的机器码（x86_64 指令）、符号表、本地/全局符号、重定位信息。

- 可用 `readelf -h hello.o` / `readelf -S hello.o` 查看节区 (`.text`, `.data`, `.rela.text`, etc.)；

- 用 `nm hello.o` 查看符号列表（包括 `_Z6squarei`, `_Z4main` 等）。

#### 可手动生成可读的汇编指令
如果你想看到可读的汇编指令，可以让编译器先输出汇编再汇编成 `.o`：

```bash
## 先生成汇编代码
clang -O2 -S hello.i -o hello.s

## hello.s （示例 x86-64 汇编）
    .text
    .globl square
square:
    movl    %edi, %eax
    imull   %edi, %eax
    ret

    .globl main
main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    movl    $3, -4(%rbp)            # int v = 3;
    movl    -4(%rbp), %edi
    call    square
    movl    %eax, -8(%rbp)         # v = square(3)
    movl    -8(%rbp), %esi
    leaq    .LC0(%rip), %rdi       # address of format string
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    leave
    ret

    .section    .rodata
.LC0:
    .string "3² = %d\n"

## 再汇编成机器码
as hello.s -o hello.o
```

* `hello.s` 是**人可读的汇编语言**，每条指令对应一条固定模式的机器指令。
* `as`（汇编器）把汇编指令翻译成**二进制机器码**、打包进 `.o`，并生成重定位表与符号表。


### （4） 链接（将 `.o` 组装成可执行/库）


#### 打包静态库 (`.a`)

```bash
ar rcs libhello.a hello.o
```

* **ELF 静态库**：`libhello.a` 是一个普通的 UNIX `.ar` archive，成员是 ELF `.o`。`ar` 只是把若干 `.o` 打包成一个 `.a` archive，不做地址分配，只保存重定位记录与符号，留待用户的链接阶段使用。


#### 链接成可执行程序

```bash
## 可执行
clang hello.o -o hello
```
* **可执行**：链接器（ld）会把所有需要的 `.o` 和系统库（`libc`、`libm` 等）合并，**重定位**所有外部符号，把机器码铺到最终内存地址，然后产出 ELF 可执行文件 `hello`。


## 编译和汇编步骤的关系
当执行

```bash
clang -O2 -c hello.cpp -o hello.o
```

Clang 做了下面两件事：
- **Codegen**：把 C++ 源码（经过 AST 和 IR）“编译”成汇编层面上的指令序列（相当于您看到的 `hello.s` 内容）。
- **Assemble & Pack**：再把那些“汇编指令”**翻译成机器码**，并生成一个包含了这些码流的目标文件 `hello.o`。

**逻辑上是两步，实践中常被合并**
- **编译 (front-end + back-end)**
    - 前端：词法 / 语法 / 语义分析，生成 LLVM IR（Clang）或等价内部表示。
    - 后端：将 IR 翻译成目标体系结构的指令序列，并**立即调用内部汇编器**把指令装配成目标文件。
    - 触发方式：`clang -c foo.cpp -o foo.o`


如果进行显示汇编:
- 先用 `-S` 将指令序列写出为人可读汇编文本 `.s`；
- 再手动调用 `as`（或 `clang -c foo.s -o foo.o`）生成目标文件。
