本页目录

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