强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

LLVM 开发指南 / 第 4 章:LLVM IR 详解

第 4 章:LLVM IR 详解

“LLVM IR 是 LLVM 的灵魂。” — 理解 IR,就理解了 LLVM。


4.1 LLVM IR 概述

LLVM IR 是 LLVM 的核心中间表示,设计目标是:

  1. 轻量级: 不像 AST 那样包含所有源码细节
  2. 类型安全: 编译期可以捕获类型错误
  3. 语言无关: 任何语言前端都可以生成 LLVM IR
  4. SSA 形式: 每个变量只赋值一次,便于优化
  5. 明确语义: 每条指令的语义都有精确定义

4.1.1 IR 三等价形式

# C 源码 → LLVM IR
clang -S -emit-llvm source.c -o source.ll     # 文本格式
clang -c -emit-llvm source.c -o source.bc     # 二进制格式

# 互相转换
llvm-as source.ll -o source.bc                # 文本 → 二进制
llvm-dis source.bc -o source.ll               # 二进制 → 文本

# 查看 IR
cat source.ll

4.2 模块结构

每个 LLVM IR 文件对应一个模块(Module),模块是 IR 的顶层容器。

; test.ll — 完整的 LLVM IR 模块
; ModuleID = 'test.c'
source_filename = "test.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

; 全局变量
@.str = private unnamed_addr constant [13 x i8] c"Hello World\0A\00", align 1

; 函数声明
declare i32 @printf(ptr, ...)

; 函数定义
define i32 @main() {
entry:
  %call = call i32 (ptr, ...) @printf(ptr @.str)
  ret i32 0
}

4.2.1 Target 信息

; target datalayout 描述了目标平台的数据布局
; e     — 小端序 (little-endian)
; m:e   — ELF 符号修饰
; p270:32:32-p271:32:32-p272:64:64 — 不同地址空间的指针大小
; i64:64 — i64 对齐到 64 位
; f80:128 — x86_fp80 对齐到 128 位
; n8:16:32:64 — 原生整数宽度
; S128 — 栈自然对齐到 128 位
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"

; target triple 描述目标三元组
; 架构-厂商-操作系统[-环境]
target triple = "x86_64-unknown-linux-gnu"

常见 Target Triple:

Triple说明
x86_64-unknown-linux-gnuLinux x86-64
aarch64-unknown-linux-gnuLinux ARM64
riscv64-unknown-linux-gnuLinux RISC-V 64
x86_64-apple-darwinmacOS x86-64
aarch64-apple-darwinmacOS Apple Silicon
x86_64-pc-windows-msvcWindows x86-64 (MSVC)
wasm32-unknown-unknownWebAssembly 32-bit

4.3 类型系统

LLVM IR 有严格的类型系统,所有值(Value)都有类型。

4.3.1 整数类型

; 标准整数类型
; iN — N 位整数 (N 可以是任意正整数)
%a = alloca i1          ; 1 位 (布尔)
%b = alloca i8          ; 8 位 (char)
%c = alloca i16         ; 16 位 (short)
%d = alloca i32         ; 32 位 (int)
%e = alloca i64         ; 64 位 (long long)
%f = alloca i128        ; 128 位
%g = alloca i23         ; 23 位 (任意宽度)

; 宽整数运算
%x = add i256 %a, %b    ; 256 位加法(用于密码学等)

4.3.2 浮点类型

; 浮点类型
%h = alloca half          ; 16 位半精度 (IEEE 754)
%f = alloca float         ; 32 位单精度
%d = alloca double        ; 64 位双精度
%x = alloca x86_fp80      ; 80 位扩展精度 (x86)
%q = alloca fp128          ; 128 位四精度
%p = alloca ppc_fp128      ; 128 位 (PowerPC 双双精度)

; 向量类型
%v4 = alloca <4 x float>       ; 4 个 float 的 SIMD 向量
%v8 = alloca <8 x i32>         ; 8 个 i32 的向量
%vs = alloca <vscale x 4 x float>  ; 可伸缩向量 (SVE/RVV)

4.3.3 指针类型

; LLVM 15+ 推荐使用 opaque pointer (ptr)
%p = alloca i32              ; %p 的类型是 ptr(不透明指针)

; 旧式 typed pointer(LLVM 14 及更早)
; %p = alloca i32            ; %p 的类型是 i32*

; 加载和存储
store i32 42, ptr %p
%val = load i32, ptr %p

; 指针运算
%next = getelementptr i32, ptr %p, i64 1

4.3.4 聚合类型

; 结构体
%Point = type { i32, i32 }             ; struct { int x; int y; }
%Rect = type { %Point, %Point }        ; struct { Point tl; Point br; }

; 数组
%arr = alloca [10 x i32]               ; int arr[10]
%mat = alloca [3 x [3 x double]]       ; double mat[3][3]

; 匿名结构体
%anon = type { i32, float, ptr }

; 结构体访问
%p = alloca %Point
%x_ptr = getelementptr %Point, ptr %p, i32 0, i32 0  ; p.x
%y_ptr = getelementptr %Point, ptr %p, i32 0, i32 1  ; p.y
store i32 10, ptr %x_ptr
store i32 20, ptr %y_ptr

4.3.5 函数类型

; 函数类型: 返回值 (参数列表)
; i32 (i32, i32) — 接受两个 i32,返回 i32
; void (ptr) — 接受一个 ptr,无返回值
; i32 (ptr, ...) — 变参函数

declare i32 @printf(ptr, ...)
declare void @llvm.memset.p0.i64(ptr, i8, i64, i1)

4.3.6 类型总结表

类型示例说明
iNi1, i8, i32, i64N 位整数
halfhalf16 位浮点
floatfloat32 位浮点
doubledouble64 位浮点
x86_fp80x86_fp8080 位扩展精度
ptrptr不透明指针(LLVM 15+)
<N x T><4 x float>固定长度向量
<vscale x N x T><vscale x 4 x i32>可伸缩向量
[N x T][10 x i32]数组
{T1, T2, ...}{i32, float}结构体
T (T1, T2, ...)i32 (i32, i32)函数类型
voidvoid无返回值类型
labellabel基本块标签
tokentoken不透明令牌
metadatametadata元数据

4.4 指令集

LLVM IR 提供一组精简但完备的指令集。

4.4.1 算术指令

; 加法
%r = add i32 %a, %b           ; 整数加法
%r = add nsw i32 %a, %b       ; 带符号溢出是 poison
%r = add nuw i32 %a, %b       ; 无符号溢出是 poison

; 浮点加法
%r = fadd float %a, %b
%r = fadd fast float %a, %b   ; 允许重排序(快速数学)

; 减法
%r = sub i32 %a, %b
%r = fsub double %a, %b

; 乘法
%r = mul i32 %a, %b
%r = fmul float %a, %b

; 除法
%r = sdiv i32 %a, %b          ; 有符号除法
%r = udiv i32 %a, %b          ; 无符号除法
%r = fdiv double %a, %b       ; 浮点除法

; 取余
%s = srem i32 %a, %b          ; 有符号取余
%u = urem i32 %a, %b          ; 无符号取余
%f = frem double %a, %b       ; 浮点取余

算术修饰符:

修饰符含义用途
nswNo Signed Wrap有符号溢出时结果为 poison
nuwNo Unsigned Wrap无符号溢出时结果为 poison
fastFast Math允许浮点重排序、忽略 NaN 等

4.4.2 位运算指令

; 位与
%r = and i32 %a, %b

; 位或
%r = or i32 %a, %b

; 位异或
%r = xor i32 %a, %b

; 左移
%r = shl i32 %a, 3             ; 立即数移位
%r = shl i32 %a, %b            ; 变量移位

; 逻辑右移(补零)
%r = lshr i32 %a, 4

; 算术右移(符号扩展)
%r = ashr i32 %a, 4

4.4.3 比较指令

; 整数比较 → i1 (布尔)
%c = icmp eq  i32 %a, %b      ; 等于
%c = icmp ne  i32 %a, %b      ; 不等于
%c = icmp slt i32 %a, %b      ; 有符号小于
%c = icmp sgt i32 %a, %b      ; 有符号大于
%c = icmp sle i32 %a, %b      ; 有符号小于等于
%c = icmp sge i32 %a, %b      ; 有符号大于等于
%c = icmp ult i32 %a, %b      ; 无符号小于
%c = icmp ugt i32 %a, %b      ; 无符号大于

; 浮点比较 → i1
%c = fcmp oeq float %a, %b    ; 有序等于(排除 NaN)
%c = fcmp one float %a, %b    ; 有序不等于
%c = fcmp olt float %a, %b    ; 有序小于
%c = fcmp ugt float %a, %b    ; 无序大于(NaN 时返回 true)
%c = fcmp ueq float %a, %b    ; 无序等于
%c = fcmp ord float %a, %b    ; 有序(两个都不是 NaN)
%c = fcmp uno float %a, %b    ; 无序(至少一个是 NaN)

icmp/fcmp 谓词总结:

整数谓词含义浮点谓词 (ordered)浮点谓词 (unordered)
eq==oequeq
ne!=oneune
slt / ult<oltult
sgt / ugt>ogtugt
sle / ule<=oleule
sge / uge>=ogeuge

4.4.4 类型转换指令

; 截断(大→小)
%r = trunc i32 %a to i8          ; 截断高 24 位

; 零扩展(小→大,无符号)
%r = zext i8 %a to i32           ; 高位补零

; 符号扩展(小→大,有符号)
%r = sext i8 %a to i32           ; 高位符号扩展

; 浮点转整数(截断)
%r = fptoui float %a to i32      ; 浮点→无符号整数
%r = fptosi float %a to i32      ; 浮点→有符号整数

; 整数转浮点
%r = uitofp i32 %a to float      ; 无符号整数→浮点
%r = sitofp i32 %a to float      ; 有符号整数→浮点

; 浮点扩展/截断
%r = fpext float %a to double    ; 精度扩展
%r = fptrunc double %a to float  ; 精度截断

; 指针转换
%r = ptrtoint ptr %p to i64      ; 指针→整数
%r = inttoptr i64 %a to ptr      ; 整数→指针

; 位转换(类型变化,位模式不变)
%r = bitcast i32 %a to float     ; i32 位模式→float
%r = bitcast <4 x i8> %v to i32  ; 4字节向量→i32

4.4.5 内存指令

; 在栈上分配内存
%a = alloca i32                       ; 分配一个 i32
%b = alloca i32, i64 10               ; 分配 10 个 i32 (数组)
%c = alloca i32, i64 10, align 16     ; 带对齐

; 存储
store i32 42, ptr %a                  ; *a = 42
store volatile i32 42, ptr %a         ; volatile 存储

; 加载
%v = load i32, ptr %a                 ; v = *a
%v = load volatile i32, ptr %a        ; volatile 加载
%v = load i32, ptr %a, align 4        ; 带对齐

; 获取元素指针 (GEP)
; getelementptr <类型>, <基址>, <索引列表>
%p = alloca {i32, float}
%x = getelementptr {i32, float}, ptr %p, i32 0, i32 0  ; &p->x
%y = getelementptr {i32, float}, ptr %p, i32 0, i32 1  ; &p->y

; 数组 GEP
%arr = alloca [10 x i32]
%elem = getelementptr [10 x i32], ptr %arr, i64 0, i64 5  ; &arr[5]

4.4.6 控制流指令

; 无条件跳转
br label %target

; 条件跳转
br i1 %cond, label %true_bb, label %false_bb

; switch 语句
switch i32 %val, label %default [
  i32 0, label %case0
  i32 1, label %case1
  i32 2, label %case2
]

; 间接跳转(计算 goto)
indirectbr ptr %addr, [label %bb1, label %bb2, label %bb3]

; 返回
ret i32 42                 ; 返回整数
ret void                   ; 无返回值
ret {i32, i32} {i32 1, i32 2}  ; 返回结构体

4.4.7 函数调用指令

; 普通调用
%r = call i32 @add(i32 %a, i32 %b)

; 尾调用优化
%r = tail call i32 @add(i32 %a, i32 %b)

; musttail — 必须尾调用(保证栈不增长)
%r = musttail call i32 @add(i32 %a, i32 %b)

; 间接调用
%r = call i32 %func_ptr(i32 %a, i32 %b)

; 变参调用
%r = call i32 (ptr, ...) @printf(ptr @.str, i32 42)

; 调用约定
%r = call fastcc i32 @func(i32 %a)    ; fast calling convention
%r = call x86_stdcallcc void @func()  ; stdcall

; 常用属性
declare i32 @func(i32) nounwind       ; 不抛异常
declare i32 @func(i32) readnone       ; 不读写内存
declare i32 @func(i32) readonly       ; 只读内存
declare i32 @func(i32) noinline       ; 不允许内联
declare i32 @func(i32) alwaysinline   ; 总是内联
declare i32 @func(i32) optsize        ; 优化大小

4.4.8 内联汇编

; 内联汇编
%result = call i32 asm "add $1, $0", "=r,r"(i32 %a)

; 带约束
; "=r" — 输出约束(可写寄存器)
; "r"  — 输入约束(寄存器)
; "~{memory}" — 副作用(修改内存)

; x86 示例:读取 RDTSC
%tsc = call i64 asm "rdtsc", "={eax},={edx}"()

; x86 示例:CPUID
%res = call {i32, i32, i32, i32} asm sideeffect
    "cpuid",
    "={eax},={ebx},={ecx},={edx},{eax}"(i32 1)

4.5 全局变量

; 全局变量定义
@global = global i32 42                    ; 有初始值,可修改
@constant = constant i32 42               ; 常量,不可修改
@zero_init = global i32 0                 ; 零初始化
@uninit = global i32                      ; 未初始化(BSS 段)

; 链接类型
@external = global i32 0                  ; 外部可见 (external)
@internal = internal global i32 0         ; 模块内部 (static)
@private = private global i32 0           ; 仅文件内部
@weak = weak global i32 0                 ; 弱符号
@common = common global i32 0             ; common 符号

; 字符串常量
@.str = private unnamed_addr constant [6 x i8] c"hello\00"
@.str.1 = private unnamed_addr constant [14 x i8] c"Hello World!\0A\00"

; 全局数组
@arr = global [3 x i32] [i32 1, i32 2, i32 3]

; 全局结构体
@point = global {i32, i32} {i32 10, i32 20}

链接类型汇总:

链接类型含义C/C++ 对应
external外部可见全局函数/变量
internal模块内部static
private文件内部匿名命名空间
weak弱符号__attribute__((weak))
weak_odr弱符号,一个定义规则模板实例化
linkonce链接时合并未使用(C++ 不常见)
linkonce_odr链接时合并 + ODR内联函数
commoncommon 符号未初始化全局
appending仅用于数组
extern_weak外部弱引用extern __attribute__((weak))

4.6 函数定义

; 完整的函数定义
define dso_local i32 @factorial(i32 %n) #0 {
entry:
  ; 比较 n <= 1
  %cmp = icmp sle i32 %n, 1
  br i1 %cmp, label %base_case, label %recursive_case

base_case:
  ret i32 1

recursive_case:
  ; n - 1
  %sub = sub i32 %n, 1
  ; factorial(n - 1)
  %call = call i32 @factorial(i32 %sub)
  ; n * factorial(n - 1)
  %mul = mul i32 %n, %call
  ret i32 %mul
}

; 函数属性
attributes #0 = { noinline nounwind optnone uwtable "frame-pointer"="all" }

4.6.1 函数参数属性

; 参数属性
define i32 @func(i32 %a, i32* %b, i32 zeroext %c, i32 signext %d) {
  ; zeroext — 零扩展传递
  ; signext — 符号扩展传递
  ; inreg   — 通过寄存器传递
  ; byval   — 按值传递(实际是复制指针内容)
  ; sret    — 返回值通过指针返回
  ; noalias — 指针不别名
  ; nocapture — 指针不逃逸
  ; readonly — 只读
  ; readnone — 不读不写
  ret i32 0
}

; sret 示例:返回大结构体
define void @make_point(ptr sret(%Point) %result) {
  store %Point {i32 10, i32 20}, ptr %result
  ret void
}

4.7 PHI 节点

PHI 节点是 SSA 形式的关键,用于在控制流合并时选择正确的值。

; if-else 示例
define i32 @abs(i32 %x) {
entry:
  %cmp = icmp sgt i32 %x, 0
  br i1 %cmp, label %then, label %else

then:
  br label %merge

else:
  %neg = sub i32 0, %x
  br label %merge

merge:
  ; PHI 节点:如果来自 then,用 %x;如果来自 else,用 %neg
  %result = phi i32 [ %x, %then ], [ %neg, %else ]
  ret i32 %result
}

; while 循环示例
define i32 @sum(i32 %n) {
entry:
  br label %loop

loop:
  %i = phi i32 [ 0, %entry ], [ %i.next, %loop ]
  %sum = phi i32 [ 0, %entry ], [ %sum.next, %loop ]
  %cmp = icmp slt i32 %i, %n
  br i1 %cmp, label %body, label %exit

body:
  %i.next = add i32 %i, 1
  %sum.next = add i32 %sum, %i
  br label %loop

exit:
  ret i32 %sum
}

4.8 常量表达式

; 常量表达式在编译时求值
@g = global i32 42

; 常量 GEP
@ptr = global ptr getelementptr (i32, ptr @g, i64 1)

; 常量算术
@c = global i32 add (i32 10, i32 20)    ; 等于 30

; 常量转换
@cast = global i64 ptrtoint (ptr @g to i64)

; 常量比较
@cmp = global i1 icmp eq (i32 10, i32 20)   ; false

; undef 和 poison
@undef = global i32 undef          ; 任意位模式
@poison = global i32 poison        ; poison value(触碰即崩)

4.9 元数据

; 调试信息
!llvm.dbg.cu = !{!0}
!0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, ...)
!1 = !DIFile(filename: "test.c", directory: "/home/user")

; !prof 元数据(分支概率)
br i1 %cond, label %likely, label %unlikely, !prof !2
!2 = !{!"branch_weights", i32 1000, i32 1}

; !tbaa 元数据(类型别名分析)
store i32 42, ptr %p, !tbaa !3
!3 = !{!"omnipotent char", !4, i64 0}
!4 = !{!"Simple C/C++ TBAA"}

; !range 元数据(值范围)
%v = load i32, ptr %p, !range !5
!5 = !{i32 0, i32 100}    ; 值在 [0, 100)

; !noalias / !alias.scope
load i32, ptr %p, !noalias !6, !alias.scope !7

; 循环元数据
br i1 %cond, label %loop, label %exit, !llvm.loop !8
!8 = distinct !{!8, !9}
!9 = !{!"llvm.loop.vectorize.enable", i1 true}

4.10 Intrinsics(内建函数)

LLVM 提供一系列内建函数(intrinsics),用于表达高级语义。

; 内存操作
call void @llvm.memset.p0.i64(ptr %dst, i8 0, i64 100, i1 false)
call void @llvm.memcpy.p0.p0.i64(ptr %dst, ptr %src, i64 100, i1 false)
call void @llvm.memmove.p0.p0.i64(ptr %dst, ptr %src, i64 100, i1 false)

; 数学函数
%r = call float @llvm.sqrt.f32(float %x)
%r = call float @llvm.sin.f32(float %x)
%r = call float @llvm.cos.f32(float %x)
%r = call float @llvm.log.f32(float %x)
%r = call float @llvm.exp.f32(float %x)
%r = call float @llvm.pow.f32(float %x, float %y)
%r = call float @llvm.fma.f32(float %a, float %b, float %c)

; 溢出检查
{ i32, i1 } @llvm.sadd.with.overflow.i32(i32, i32)
{ i32, i1 } @llvm.smul.with.overflow.i32(i32, i32)

; 位操作
%i = call i32 @llvm.ctpop.i32(i32 %x)    ; popcount
%i = call i32 @llvm.ctlz.i32(i32 %x, i1 false)  ; count leading zeros
%i = call i32 @llvm.cttz.i32(i32 %x, i1 false)  ; count trailing zeros
%i = call i32 @llvm.bswap.i32(i32 %x)    ; 字节序翻转

; 向量操作
%v = call <4 x float> @llvm.fabs.v4f32(<4 x float> %x)
%v = call <4 x float> @llvm.sqrt.v4f32(<4 x float> %x)

; 栈保护
call void @llvm.stackprotector(ptr %guard, ptr %slot)

; 不可达标记
call void @llvm.trap()           ; 陷入(trap)
call void @llvm.unreachable()    ; 标记不可达
call void @llvm.assume(i1 %cond) ; 假设条件为真

4.11 完整示例

4.11.1 C 源码

// fibonacci.c
#include <stdio.h>

int fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

int main() {
    for (int i = 0; i < 10; i++) {
        printf("fib(%d) = %d\n", i, fibonacci(i));
    }
    return 0;
}

4.11.2 生成的 LLVM IR(-O0,简化)

; 命令: clang -S -emit-llvm -O0 fibonacci.c -o fibonacci.ll

define i32 @fibonacci(i32 %n) {
entry:
  %n.addr = alloca i32
  store i32 %n, ptr %n.addr
  %n1 = load i32, ptr %n.addr
  %cmp = icmp sle i32 %n1, 1
  br i1 %cmp, label %if.then, label %if.end

if.then:
  %n2 = load i32, ptr %n.addr
  ret i32 %n2

if.end:
  %n3 = load i32, ptr %n.addr
  %sub = sub i32 %n3, 1
  %call = call i32 @fibonacci(i32 %sub)
  %n4 = load i32, ptr %n.addr
  %sub5 = sub i32 %n4, 2
  %call6 = call i32 @fibonacci(i32 %sub5)
  %add = add i32 %call, %call6
  ret i32 %add
}

4.11.3 优化后的 IR(-O2

; 命令: clang -S -emit-llvm -O2 fibonacci.c -o fibonacci_opt.ll

define i32 @fibonacci(i32 %n) {
entry:
  %cmp = icmp slt i32 %n, 2
  br i1 %cmp, label %return, label %if.end

if.end:
  %sub = add nsw i32 %n, -1
  %call = call i32 @fibonacci(i32 %sub)
  %sub1 = add nsw i32 %n, -2
  %call2 = call i32 @fibonacci(i32 %sub1)
  %add = add nsw i32 %call2, %call
  ret i32 %add

return:
  ret i32 %n
}

注意: 优化后的 IR 简洁了很多——alloca 被消除,变量直接使用 SSA 值传递,多余的 load/store 被消除。


4.12 常用 LLVM IR 工具

工具功能示例
llvm-as文本 IR → 二进制llvm-as test.ll -o test.bc
llvm-dis二进制 IR → 文本llvm-dis test.bc -o test.ll
opt优化 IRopt -O2 test.ll -o test.opt.bc
llcIR → 汇编/目标文件llc test.bc -o test.s
llvm-link链接多个 IR 模块llvm-link a.bc b.bc -o merged.bc
llvm-extract提取函数llvm-extract -func=foo test.bc
llvm-diff比较两个 IR 模块llvm-diff a.bc b.bc
llvm-reduce自动缩减 IR 测试用例llvm-reduce --test=check.sh test.bc
verify-uselistorder验证 use-list 顺序verify-uselistorder test.bc
# 查看优化效果对比
clang -S -emit-llvm -O0 test.c -o test_O0.ll
clang -S -emit-llvm -O2 test.c -o test_O2.ll
diff test_O0.ll test_O2.ll

# 查看优化 Pass 流水线
opt -O2 -S -print-pipeline-passes test.ll -o /dev/null 2>&1 | head -20

4.13 本章小结

概念要点
IR 形式.ll (文本) / .bc (二进制) / 内存
SSA每个变量只赋值一次,PHI 节点处理合并
类型系统整数、浮点、指针、聚合、函数类型
指令算术、位运算、比较、转换、内存、控制流
全局变量链接类型控制可见性
Intrinsics内建函数表达高级语义
元数据调试信息、优化提示

扩展阅读

  1. LLVM Language Reference Manual — IR 语法完整参考
  2. LLVM Tutorial: Kaleidoscope — 用 LLVM 构建语言前端
  3. SSA Book — 静态单赋值形式参考
  4. Godbolt Compiler Explorer — 在线查看 C/C++ 编译后的汇编和 IR

下一章: 第 5 章:Clang 前端 — 学习 Clang 如何将 C/C++/ObjC 源码编译为 LLVM IR。