LLVM 是当今最重要的编译器基础设施——Clang、Rust、Swift 等语言都使用 LLVM 作为后端。理解 LLVM 的架构,就理解了现代编译器的设计哲学:模块化、可扩展、库化。
现在来看LLVM——IR 有哪些层次?Pass 框架如何工作?优化流水线如何组织?如何编写自定义 Pass?
一、LLVM 的设计哲学
1.1 LLVM 的核心思想
LLVM 的名字不是缩写(虽然有人说是 Low Level Virtual Machine),它是一个编译器基础设施——一组可复用的编译器组件库:
1.2 LLVM vs GCC 架构对比
| 特性 | LLVM | GCC |
|---|---|---|
| 设计 | 库化(可嵌入其他程序) | 单体(命令行工具) |
| IR | LLVM IR(SSA,统一) | GIMPLE + RTL(两层) |
| 优化框架 | Pass 管理器 | GCC Pass |
| 可扩展性 | 高(自定义 Pass/后端) | 低(需修改 GCC 源码) |
| 许可证 | Apache 2.0 | GPL |
| IDE 集成 | 好(libclang) | 差 |
| 编译速度 | 中等 | 中等 |
二、LLVM IR 的层次
2.1 三种 IR 表示
LLVM IR 有三种等价的表示形式:
| 表示 | 格式 | 用途 |
|---|---|---|
| 人类可读 | .ll 文本 | 调试、学习 |
| 位码 | .bc 二进制 | 快速序列化 |
| 内存中 | C++ 对象 | 编译器内部 |
# 三种表示的转换clang -emit-llvm -S hello.c -o hello.ll # 源码 → .llclang -emit-llvm -c hello.c -o hello.bc # 源码 → .bcllvm-dis hello.bc -o hello.ll # .bc → .llllvm-as hello.ll -o hello.bc # .ll → .bc2.2 LLVM IR 的核心概念
; 函数定义define i32 @add(i32 %a, i32 %b) #0 {entry: %result = add i32 %a, %b ret i32 %result}
; 全局变量@global_var = global i32 42
; 常量@const_str = private unnamed_addr constant [13 x i8] c"hello world\0A\00"
; 类型%struct.Point = type { i32, i32 }
; 属性attributes #0 = { nounwind uwtable }2.3 LLVM IR 类型系统
| 类型 | 语法 | 说明 |
|---|---|---|
| 整数 | i1, i8, i16, i32, i64, i128 | 任意位宽 |
| 浮点 | half, float, double, fp128 | IEEE 754 |
| 指针 | ptr (opaque) | 统一指针类型 |
| 数组 | [4 x i32] | 固定长度 |
| 向量 | <4 x i32> | SIMD |
| 结构体 | { i32, float } | 匿名 |
| 命名结构体 | %struct.Point = type { i32, i32 } | 有名 |
| 函数 | i32 (i32, i32)* | 函数指针 |
2.4 IR 验证
LLVM 有一个内置的 IR 验证器,检查 IR 的合法性:
# 验证 IRopt -passes=verify hello.ll -o /dev/null
# 常见的验证错误# - 非 SSA 形式(虚拟寄存器多次定义)# - 类型不匹配# - phi 节点参数数量不匹配# - 基本块未以终止指令结尾# - 使用未定义的虚拟寄存器三、Pass 框架
3.1 Pass 的概念
Pass 是 LLVM 的基本优化单元——每个 Pass 执行一个特定的变换或分析:
3.2 Pass 的分类
| 类型 | 说明 | 示例 |
|---|---|---|
| 变换 Pass | 修改 IR | instcombine, LICM, DCE |
| 分析 Pass | 计算信息,不修改 IR | DominatorTree, LoopInfo, AA |
| 不可变 Pass | 提供不变信息 | TargetTransformInfo |
| 工具 Pass | 辅助功能 | Verifier, Printer |
3.3 编写自定义 Pass
// LLVM 自定义 Pass 示例(C++)#include "llvm/IR/Function.h"#include "llvm/IR/PassManager.h"#include "llvm/Passes/PassBuilder.h"#include "llvm/Passes/PassPlugin.h"#include "llvm/Support/raw_ostream.h"
using namespace llvm;
// 定义一个函数级变换 Passstruct HelloPass : public PassInfoMixin<HelloPass> { PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { errs() << "Hello from: " << F.getName() << "\n";
// 遍历基本块和指令 for (BasicBlock &BB : F) { for (Instruction &I : BB) { // 统计指令类型 if (auto *BO = dyn_cast<BinaryOperator>(&I)) { errs() << " Found binary op: " << BO->getOpcodeName() << "\n"; } } }
// 返回哪些分析结果仍然有效 return PreservedAnalyses::all(); }
// 注册为默认启用 static bool isRequired() { return true; }};
// 插件注册extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfollvmGetPassPluginInfo() { return { LLVM_PLUGIN_API_VERSION, "HelloPass", "0.1", [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "hello-pass") { FPM.addPass(HelloPass()); return true; } return false; } ); } };}3.4 Pass 管理器
LLVM 使用Pass 管理器来组织和调度 Pass:
# 使用 opt 运行 Passopt -passes='mem2reg,instcombine,gvn,licm' hello.ll -S -o hello_opt.ll
# 查看可用的 Passopt --print-passes
# 调试 Pass 执行opt -debug-pass-manager -passes='default<O2>' hello.ll -o /dev/null3.5 优化流水线
LLVM 的 -O2 优化流水线包含数十个 Pass,按阶段组织:
3.5 新 Pass 管理器(New Pass Manager)
LLVM 从版本 13 开始全面使用新 Pass 管理器——它取代了旧的管理器,提供了更好的分析结果缓存和更细粒度的控制:
新旧 Pass 管理器的关键区别:
| 维度 | 旧 Pass 管理器 | 新 Pass 管理器 |
|---|---|---|
| 分析缓存 | 手动管理 | 自动管理(AnalysisManager) |
| Pass 间通信 | 通过全局数据 | 通过 AnalysisManager 查询 |
| 增量编译 | 困难 | 自然支持 |
| Pass 依赖 | 隐式 | 显式(isRequired()) |
| 线程安全 | 否 | 是(每个函数独立 AnalysisManager) |
| 失败处理 | abort | 回退到更低优化级别 |
3.6 分析 Pass 与变换 Pass 的协作
分析 Pass 为变换 Pass 提供决策依据——变换 Pass 查询分析结果,然后修改 IR:
// 变换 Pass 查询分析结果struct MyOptPass : public PassInfoMixin<MyOptPass> { PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { // 查询支配树分析 auto &DT = AM.getResult<DominatorTreeAnalysis>(F);
// 查询循环信息分析 auto &LI = AM.getResult<LoopAnalysis>(F);
// 基于分析结果做变换 for (Loop *L : LI) { if (isSafeToHoist(L, DT)) { hoistLoopInvariants(L); } }
// 声明哪些分析结果仍然有效 return PreservedAnalyses::none(); }};常用的分析 Pass 及其用途:
| 分析 Pass | 提供信息 | 被谁使用 |
|---|---|---|
| DominatorTreeAnalysis | 支配关系 | LICM, GVN, 循环优化 |
| LoopAnalysis | 循环结构 | 循环展开, 向量化 |
| ScalarEvolutionAnalysis | 循环变量演化 | 归纳变量优化 |
| AAManager | 别名分析 | 指针优化, 内存优化 |
| TargetTransformInfo | 目标代价模型 | 内联, 向量化决策 |
| BranchProbabilityInfo | 分支概率 | 基本块布局 |
| MemorySSAAnalysis | 内存访问层次 | 内存优化 |
3.7 LLVM IR 实例详解
; 一个完整的 LLVM IR 函数示例; 源码: int sum(int *arr, int n) {; int s = 0;; for (int i = 0; i < n; i++) s += arr[i];; return s;; }
define i32 @sum(ptr %arr, i32 %n) #0 {entry: %cmp0 = icmp sge i32 %n, 1 br i1 %cmp0, label %loop.header, label %exit
loop.header: %i = phi i32 [ 0, %entry ], [ %i.next, %loop.latch ] %s = phi i32 [ 0, %entry ], [ %s.next, %loop.latch ] %idx = zext i32 %i to i64 %ptr = getelementptr inbounds i32, ptr %arr, i64 %idx %val = load i32, ptr %ptr, align 4 %s.next = add i32 %s, %val %i.next = add i32 %i, 1 %cmp = icmp slt i32 %i.next, %n br i1 %cmp, label %loop.header, label %exit
exit: %s.result = phi i32 [ 0, %entry ], [ %s.next, %loop.header ] ret i32 %s.result}
attributes #0 = { nounwind uwtable }注意 phi 节点的用法——它是 SSA 的核心机制。phi i32 [ 0, %entry ], [ %s.next, %loop.header ] 表示:如果从 entry 进入,值为 0;如果从 loop.header 进入,值为 %s.next。phi 节点解决了”同一变量在不同路径上有不同值”的问题。
3.8 opt 与 llc 工具详解
opt 和 llc 是 LLVM 最常用的两个命令行工具:
# opt:IR 级优化器opt -passes=mem2reg input.ll -S -o output.llopt -passes=default<O2> input.ll -S -o output.llopt --print-passes
# llc:IR → 目标代码llc -mtriple=x86_64-linux-gnu input.ll -o output.sllc -mtriple=aarch64-linux-gnu input.ll -o output.sllc -filetype=obj input.ll -o output.o
# 调试后端llc -debug-only=isel input.ll -o output.sllc -debug-only=regalloc input.ll -o output.s| opt 常用 Pass 组合 | 效果 |
|---|---|
-passes=mem2reg | 提升 alloca 到 SSA 寄存器 |
-passes=instcombine | 指令合并简化 |
-passes=gvn | 全局值编号消除冗余 |
-passes=licm | 循环不变量外提 |
-passes=simplifycfg | 简化控制流图 |
-passes=dce | 死代码消除 |
-passes=loop-unroll | 循环展开 |
-passes=loop-vectorize | 循环向量化 |
四、LLVM 后端 Pipeline
4.1 后端的阶段
4.2 后端的关键数据结构
| 数据结构 | 说明 | 阶段 |
|---|---|---|
| SelectionDAG | 指令选择用的 DAG | 指令选择 |
| MachineInstr | 机器指令表示 | 调度、RA |
| MachineBasicBlock | 机器基本块 | 全部 |
| MachineFunction | 机器函数 | 全部 |
| MCInst | 最终指令编码 | 代码发射 |
五、调试信息
5.1 DWARF 调试格式
LLVM 生成 DWARF 格式的调试信息,支持源码级调试:
# 生成带调试信息的可执行文件clang -g hello.c -o hello_debug
# 查看调试信息readelf --debug-dump hello_debug | head -50
# 使用 GDB 调试gdb hello_debug(gdb) break main(gdb) run(gdb) step(gdb) print x5.2 调试信息与优化
| 优化级别 | 调试体验 | 说明 |
|---|---|---|
-O0 -g | 完美 | 变量、行号完全准确 |
-O1 -g | 好 | 部分变量可能被优化掉 |
-O2 -g | 中等 | 内联后行号可能不准确 |
-O3 -g | 差 | 大量优化,调试困难 |
LLVM 支持 -g -O2 的”优化调试”——它尽量在优化的同时保留调试信息。但某些优化(如内联、循环变换)会使调试体验变差。如果需要精确调试,建议使用 -O0。
六、LLVM 工具链
6.1 核心 LLVM 工具
| 工具 | 功能 | 常用命令 |
|---|---|---|
clang | C/C++ 编译器 | clang -O2 hello.c |
opt | IR 优化器 | opt -passes=instcombine hello.ll -S |
llc | IR → 汇编 | llc hello.ll -o hello.s |
lli | IR 解释器/JIT | lli hello.bc |
llvm-as | .ll → .bc | llvm-as hello.ll -o hello.bc |
llvm-dis | .bc → .ll | llvm-dis hello.bc -o hello.ll |
llvm-link | IR 链接器 | llvm-link a.ll b.ll -o combined.ll |
llvm-ar | IR 归档器 | llvm-ar rcs lib.a a.o b.o |
lld | 链接器 | lld hello.o -o hello |
6.2 诊断工具
| 工具 | 功能 |
|---|---|
llvm-objdump | 反汇编目标文件 |
llvm-readelf | 查看 ELF 信息 |
llvm-nm | 查看符号表 |
llvm-size | 查看段大小 |
llvm-dwarfdump | 查看 DWARF 调试信息 |
llvm-profdata | 分析 Profile 数据 |
llvm-cov | 代码覆盖率 |
七、动手实践
7.1 实验一:完整的 LLVM 编译流程
cat > hello.c << 'EOF'int add(int a, int b) { return a + b; }int main() { return add(3, 4); }EOF
# 1. 生成 LLVM IRclang -emit-llvm -S -O0 hello.c -o hello.llcat hello.ll
# 2. 优化 IRopt -passes='mem2reg,instcombine,simplifycfg' hello.ll -S -o hello_opt.llcat hello_opt.ll
# 3. 生成汇编llc hello_opt.ll -o hello.scat hello.s
# 4. 汇编和链接clang hello.s -o hello./hello; echo $?7.2 实验二:编写自定义 Pass
# 创建 Pass 插件cat > HelloPass.cpp << 'EOF'// 使用上面的 HelloPass 代码EOF
# 编译 Pass 插件clang -shared -fPIC HelloPass.cpp -o HelloPass.so \ $(llvm-config --cxxflags --ldflags)
# 使用 Passopt -load-pass-plugin=./HelloPass.so -passes=hello-pass hello.ll -S7.3 实验三:对比优化级别
for opt in O0 O1 O2 O3; do clang -$opt -S hello.c -o hello_${opt}.s echo "$opt: $(wc -l < hello_${opt}.s) lines"done八、本章小结
在上一章中,走完了从机器指令到可执行文件的最后旅程——ELF 格式、重定位、符号解析、动态链接。至此,编译流水线的每个阶段都已拆解完毕。现在换一个视角:不问”每个阶段做什么”,而问”一个工业级编译器基础设施如何组织这些阶段”。LLVM 就是最好的研究对象。
| 概念 | 要点 |
|---|---|
| LLVM 哲学 | 库化、模块化、可扩展、类型化 IR |
| IR 层次 | .ll(文本)、.bc(位码)、内存中(C++ 对象) |
| Pass 框架 | 变换 Pass + 分析 Pass,新 Pass 管理器 |
| 优化流水线 | 早期优化 → 循环优化 → 全局优化 → 晚期优化 |
| 后端 Pipeline | 指令选择 → 调度 → RA → 调度 → PEI → 发射 |
| 调试信息 | DWARF 格式,-g 生成 |
| 工具链 | clang, opt, llc, lli, llvm-as, llvm-dis, lld |
LLVM 架构的要点基本都在这里了。下一章进入 JIT 编译,看看 LLVM 的 ORC API 如何支持运行时代码生成。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






