---
title: LLVM IR 实战
url: https://doc.liz6.com/compilers/04-intermediate-representation/02-llvm-ir-in-practice
locale: zh
area: compilers
tags:
- compilers
- 中间表示
date: 2026-06-30
modified: 2026-07-16
description: LLVM IR 不只是 SSA 的教科书例子——它是 Clang、Rustc、Swiftc 等数十个编译前端的共同"目标语言",全 SSA、强类型、三地址码。看懂 IR 的语法和内存模型,才能自己实现从 AST 到 LLVM 的 codegen。
---

# LLVM IR 实战

> LLVM IR 不只是 SSA 的教科书例子——它是 Clang、Rustc、Swiftc 等数十个编译前端的共同"目标语言",全 SSA、强类型、三地址码。看懂 IR 的语法和内存模型,才能自己实现从 AST 到 LLVM 的 codegen。

## 概述

[SSA 形式](/compilers/04-intermediate-representation/01-ssa-form.md)讲了 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`)恰被赋值一次:

```llvm
%x = add i32 1, 2          ; %x 被定义
%y = mul i32 %x, 3          ; %x 被使用, %y 被定义
; 不能再次给 %x 赋值 — 违反 SSA，需用 φ 或不同版本
```

### 类型系统

LLVM IR 的类型远比 C 丰富且明确,每个值都有类型:

```llvm
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 的控制流基于基本块和终结指令:

```llvm
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 指令:控制流汇合处的值选择

```llvm
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 值,在寄存器上)和**内存**(在栈/堆上,通过指针访问):

```llvm
%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 中最容易让人困惑的指令,它的语义是**纯地址计算,不访问内存**:

```llvm
%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)。

## 函数与模块结构

```llvm
; 外部声明
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*
