本页目录
LLVM IR 实战
LLVM IR 不只是 SSA 的教科书例子——它是 Clang、Rustc、Swiftc 等数十个编译前端的共同"目标语言",全 SSA、强类型、三地址码。看懂 IR 的语法和内存模型,才能自己实现从 AST 到 LLVM 的 codegen。
概述
SSA 形式讲了 SSA 的数学基础——支配树/φ 函数/Cytron 算法。这篇落地到 LLVM IR:它是 LLVM 的中间表示,全 SSA、强类型、三地址码形式,是 Clang、Rustc、Swiftc 等数十个编译前端的共同目标,也是 LLVM 优化 pipeline 的输入。这篇不讲"LLVM 有哪些 pass",而是讲 LLVM IR 作为一门语言的语法、语义、类型和内存模型——看懂了这些,才能自己实现"从 AST 降到 LLVM IR"的 codegen。
LLVM IR 的三种形式
同一个 LLVM 模块有三种表示,彼此等价:
.ll (人类可读文本): 定义在文档和调试中看到
%result = add i32 %a, %b
ret i32 %result
.bc (bitcode 二进制): 链接和 LTO 用的紧凑格式
(二进制序列)
内存 IR (C++ API): 编译器前端在内存中构造
auto *add = builder.CreateAdd(a, b, "result");
三种形式通过 llvm-as / llvm-dis / opt / llc 互相转换。理解 .ll 文本格式是阅读/调试 LLVM IR 的前提。
核心语法:虚拟寄存器 + 类型 + 指令
虚拟寄存器与赋值
LLVM IR 是全 SSA——每个虚拟寄存器(%1, %2, ...或%name)恰被赋值一次:
%x = add i32 1, 2 ; %x 被定义
%y = mul i32 %x, 3 ; %x 被使用, %y 被定义
; 不能再次给 %x 赋值 — 违反 SSA,需用 φ 或不同版本
类型系统
LLVM IR 的类型远比 C 丰富且明确,每个值都有类型:
i1 ; 1-bit 整数 (布尔)
i32 ; 32-bit 整数
i64 ; 64-bit 整数
float ; 32-bit IEEE 浮点
double ; 64-bit IEEE 浮点
ptr ; 不透明指针 (opaque pointer, LLVM 15+)
; 旧版: i32* (指向 i32 的指针)
[10 x i32] ; 10 个 i32 的数组
{ i32, float, i8 } ; 结构体 (匿名)
<4 x float> ; 4 个 float 的向量 (SIMD)
opaque pointer(ptr)是一次重要简化。LLVM 15 之前指针是 i32*、float** 等——类型和指针的位宽是正交的。Opaque pointer 消除了"指针指向什么类型"的编码,让 IR 更简洁(减少了 bitcast),但前端需要自行维护类型信息用于 gep。
控制流指令
LLVM IR 的控制流基于基本块和终结指令:
define i32 @max(i32 %a, i32 %b) {
entry:
%cond = icmp sgt i32 %a, %b ; signed greater than → i1
br i1 %cond, label %then, label %else
then:
ret i32 %a ; 终结: 返回
else:
ret i32 %b
}
br i1 %cond, label %true, label %false:条件分支br label %dest:无条件跳转ret <ty> <value>/ret void:返回switch i32 %val, label %default [...]:多路跳转indirectbr:计算跳转 (虚拟表分发等)
Phi 指令:控制流汇合处的值选择
define i32 @abs(i32 %x) {
entry:
%ge = icmp sge i32 %x, 0
br i1 %ge, label %pos, label %neg
pos:
br label %merge
neg:
%negx = sub i32 0, %x
br label %merge
merge:
%result = phi i32 [ %x, %pos ], [ %negx, %neg ]
; result = 如果来自 pos (%x), 如果来自 neg (%negx)
ret i32 %result
}
phi 的参数 [value, predecessor_block] 精确绑定——第一个 value 对应从第一个前驱进入的情况。phi 指令位于基本块开头,多个 phi 之间按声明顺序排列——它们不代表实际计算,只是控制流汇合处的值选择,都引用进入该块之前(即前驱块中)的变量版本。
内存模型:alloca / load / store / gep
LLVM IR 区分虚拟寄存器(SSA 值,在寄存器上)和内存(在栈/堆上,通过指针访问):
%ptr = alloca i32 ; 在栈上分配一个 i32 (返回 ptr)
store i32 42, ptr %ptr ; 把 42 写进 ptr 指向的内存
%val = load i32, ptr %ptr ; 从 ptr 读回 → %val (i32)
%x = add i32 %val, 1 ; 对 SSA 值直接运算
store i32 %x, ptr %ptr
alloca 的结果是 ptr,后续的 load/store 明确地读/写内存。LLVM 的 mem2reg pass 把"可提升的 alloca + load/store 对"转化为 SSA 虚拟寄存器+phi——这是进入优化 pipeline 前的第一关,不可提升的(如地址被取、跨函数传递的)留在内存里。
GEP(GetElementPtr):结构体/数组的地址计算
getelementptr 是 LLVM IR 中最容易让人困惑的指令,它的语义是纯地址计算,不访问内存:
%struct = type { i32, float, i8 } ; 结构体定义
%p = alloca %struct
%field1_addr = getelementptr %struct, ptr %p, i32 0, i32 1
; ^^ ^^ ^^
; type ptr indices:
; [0]=不解引用, [1]=第1个字段 (float)
store float 3.14, ptr %field1_addr
GEP 的计算完全基于类型布局——[i32 0, i32 1] 的意思是"从 %p 出发,对 %struct 这个类型,取第 0 个元素(不改变指针),取第 1 个字段"。GEP 返回的是相对于基地址的偏移指针(由结构体字段类型布局决定),而非访问内存——i32 占 4 字节,float 对齐到 4 字节,i8 占 1 字节。
现代 LLVM 的 opaque pointer 下,GEP 的第一个 index 通常总是 i32 0(历史上的指针解引用变成 no-op)。
函数与模块结构
; 外部声明
declare i32 @printf(ptr, ...)
; 函数定义
define i32 @main(i32 %argc, ptr %argv) {
...
%fmt = ... ; 指向 "Hello %s\n" 的指针
call i32 (ptr, ...) @printf(ptr %fmt, ptr %str)
ret i32 0
}
@name:全局符号(函数、全局变量)%name:局部虚拟寄存器call:调用——可以是直接调用、间接调用(call i32 %fp(...))、尾调用(musttail call)
从 AST 到 LLVM IR:一条 Lowering 的路径
以 a + b * c 为例,展示从 AST 降级到 LLVM IR 的过程:
AST: Binary(+, Variable("a"), Binary(*, Variable("b"), Variable("c")))
Codegen (递归):
codegen(Binary(+, lhs, rhs)):
left_val = codegen(lhs) → %a = load i32, ptr %a_addr
right_val = codegen(rhs) → 递归:
b_val = codegen(Var("b")) → %b = load...
c_val = codegen(Var("c")) → %c = load...
→ %tmp = mul i32 %b, %c
→ %add = add i32 %a, %tmp
return %add
输出 LLVM IR:
%a = load i32, ptr %a_addr
%b = load i32, ptr %b_addr
%c = load i32, ptr %c_addr
%tmp = mul i32 %b, %c
%add = add i32 %a, %tmp
真实的 codegen 还要处理:
- 短路求值 (
a && b):转换成if a { if b { true } else { false } } else { false }的控制流 + phi。 - 变量地址 (
&x):如果 x 之前是 alloca 的,直接用它的 ptr;如果 x 已经被 mem2reg 提升到虚拟寄存器,需要重新 alloca 并 store。 - 聚合类型:结构体和数组的赋值变成 memcpy 或逐字段 load/store。
参考
- LLVM Language Reference Manual: https://llvm.org/docs/LangRef.html — IR 的完整语法和语义,以官方为准
- LLVM Kaleidoscope Tutorial: https://llvm.org/docs/tutorial/ — 从零实现一个 DSL 到 LLVM IR
- "Learn LLVM 17"(Kai Nacke) — LLVM 内核的工程指南,含 pass manager、后端、JIT
Keywords: LLVM IR, SSA, virtual register, i1, i32, i64, ptr, opaque pointer, struct, array, vector, phi, br, switch, alloca, load, store, gep, getelementptr, bitcast, function, module, intrinsic, lowering, mem2reg, Kaleidoscope