返回「计算机、信息技术与工程」
C++ 编译流程
本文目录 19 个章节
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:
// 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) 预处理
clang -E hello.cpp -o hello.i
产物: hello.i(文本),所有的 #include、宏都已展开,可能看起来像:
# 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→目标代码)
clang -O2 -c hello.cpp -o hello.o
hello.o 已含机器指令,可用 objdump -d hello.o 反汇编查看。
- 词法分析:把源文本切成一连串 token(标识符、字面量、操作符……)
- 语法分析:把 token 按 C++ 语法规则组装成 AST(抽象语法树)
- 语义检查:类型检查、名称解析、模板实例化
- 生成“目标代码”:Clang 会在内存中直接把 AST 降低为机器指令的中间数据结构(GCC 用 GIMPLE/RTL,Clang 用 LLVM IR 或内部指令序列),但它不会再产出一个文件,除非你指定输出。
如果你让它直接产出一个“目标”文件:
clang -O0 -c hello.i -o hello.o
- 结果:
hello.o是一个 ELF relocatable object(或在 Emscripten 下是 Wasm relocatable object),里面已经包含了机器指令、符号表、重定位信息。 - Clang 并不会产出可读的中间文本,这里的“目标代码”实际就是硬编码在二进制的机器指令。
对该步骤进行进一步拆分如下:
词法/语法分析 → AST
- AST(抽象语法树):Clang 内部表示,不直接生成到磁盘。
clang -cc1 -ast-dump hello.cpp -fsyntax-only
- 上面命令会在终端输出类似:
TranslationUnitDecl ...
├─FunctionDecl square
│ ├─ParmVarDecl x
│ └─CompoundStmt
│ └─ReturnStmt
│ └─BinaryOperator '*'
└─FunctionDecl main
├─CompoundStmt
│ └─CXXOperatorCallExpr '<<'
│ ├─...
生成 LLVM IR 文本(.ll)
clang -O0 -emit-llvm -S hello.cpp -o hello.ll
查看 hello.ll
; 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)
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:
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 对象
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:
## 先生成汇编代码
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)
ar rcs libhello.a hello.o
- ELF 静态库:
libhello.a是一个普通的 UNIX.ararchive,成员是 ELF.o。ar只是把若干.o打包成一个.aarchive,不做地址分配,只保存重定位记录与符号,留待用户的链接阶段使用。
链接成可执行程序
## 可执行
clang hello.o -o hello
- 可执行:链接器(ld)会把所有需要的
.o和系统库(libc、libm等)合并,重定位所有外部符号,把机器码铺到最终内存地址,然后产出 ELF 可执行文件hello。
编译和汇编步骤的关系
当执行
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)生成目标文件。