第 4 章:LLVM IR 详解
第 4 章:LLVM IR 详解
“LLVM IR 是 LLVM 的灵魂。” — 理解 IR,就理解了 LLVM。
4.1 LLVM IR 概述
LLVM IR 是 LLVM 的核心中间表示,设计目标是:
- 轻量级: 不像 AST 那样包含所有源码细节
- 类型安全: 编译期可以捕获类型错误
- 语言无关: 任何语言前端都可以生成 LLVM IR
- SSA 形式: 每个变量只赋值一次,便于优化
- 明确语义: 每条指令的语义都有精确定义
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-gnu | Linux x86-64 |
aarch64-unknown-linux-gnu | Linux ARM64 |
riscv64-unknown-linux-gnu | Linux RISC-V 64 |
x86_64-apple-darwin | macOS x86-64 |
aarch64-apple-darwin | macOS Apple Silicon |
x86_64-pc-windows-msvc | Windows x86-64 (MSVC) |
wasm32-unknown-unknown | WebAssembly 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 类型总结表
| 类型 | 示例 | 说明 |
|---|---|---|
iN | i1, i8, i32, i64 | N 位整数 |
half | half | 16 位浮点 |
float | float | 32 位浮点 |
double | double | 64 位浮点 |
x86_fp80 | x86_fp80 | 80 位扩展精度 |
ptr | ptr | 不透明指针(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) | 函数类型 |
void | void | 无返回值类型 |
label | label | 基本块标签 |
token | token | 不透明令牌 |
metadata | metadata | 元数据 |
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 ; 浮点取余
算术修饰符:
| 修饰符 | 含义 | 用途 |
|---|---|---|
nsw | No Signed Wrap | 有符号溢出时结果为 poison |
nuw | No Unsigned Wrap | 无符号溢出时结果为 poison |
fast | Fast 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 | == | oeq | ueq |
ne | != | one | une |
slt / ult | < | olt | ult |
sgt / ugt | > | ogt | ugt |
sle / ule | <= | ole | ule |
sge / uge | >= | oge | uge |
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 | 内联函数 |
common | common 符号 | 未初始化全局 |
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 | 优化 IR | opt -O2 test.ll -o test.opt.bc |
llc | IR → 汇编/目标文件 | 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 | 内建函数表达高级语义 |
| 元数据 | 调试信息、优化提示 |
扩展阅读
- LLVM Language Reference Manual — IR 语法完整参考
- LLVM Tutorial: Kaleidoscope — 用 LLVM 构建语言前端
- SSA Book — 静态单赋值形式参考
- Godbolt Compiler Explorer — 在线查看 C/C++ 编译后的汇编和 IR
下一章: 第 5 章:Clang 前端 — 学习 Clang 如何将 C/C++/ObjC 源码编译为 LLVM IR。