LLVM IR 的三种形态:内存 IR、Bitcode 与文本 IR

说实话,刚接触 LLVM 的时候,我被这三种 IR 形式搞得有点晕。后来在项目里踩过坑才明白——它们其实是同一份代码在不同阶段的「快照」。

内存 IR(In-Memory IR)

这是编译器在运行时操作的形式。说白了,就是一堆 C++ 对象在内存里互相指来指去。我习惯把它想象成「活着的 IR」——你可以随时增删指令、修改基本块、调整控制流。

举个例子,你在写一个 Pass 时,看到的 FunctionBasicBlockInstruction 这些类,就是内存 IR 的实体。它们有完整的迭代器、访问器,可以随意遍历和修改。

我的习惯:调试阶段我几乎只用内存 IR。因为可以随时用 dump() 打印出来看,改起来也方便。

Bitcode(.bc 文件)

这是序列化后的二进制格式。体积小、加载快,适合做链接时优化(LTO)。我记得有一次做跨模块内联,如果不用 bitcode,光解析文本 IR 就多花了 3 倍时间。

Bitcode 是平台无关的。你可以把 .bc 文件从 x86 机器拷到 ARM 机器上,只要 LLVM 版本一致就能用。嗯,这里要注意——版本不一致会崩,我吃过这个亏。

文本 IR(.ll 文件)

人类可读的 IR 形式。调试、学习、写测试用例时最常用。你打开一个 .ll 文件,能看到 define%@ 这些符号,结构一目了然。

我个人建议:刚开始学 LLVM IR 时,先用 clang -S -emit-llvm 生成 .ll 文件,对着看。比直接啃文档快多了。

三种形式的转换关系:
文本 IR ↔ Bitcode:llvm-as / llvm-dis
内存 IR ↔ 文本 IR:parseIRFile() / print()
内存 IR ↔ Bitcode:WriteBitcodeToFile() / parseBitcodeFile()

基本语法结构:Module、Function、BasicBlock、Instruction

LLVM IR 的层次结构非常清晰。从上到下,像俄罗斯套娃一样层层嵌套。

Module(模块)

最顶层的容器。一个 Module 对应一个编译单元(比如一个 .c 文件)。它里面装着全局变量、函数声明、函数定义、以及一些元数据。

你想想看,Module 就像是一个「翻译单元」的档案袋。所有跨函数、跨模块的信息,都得通过 Module 来访问。

Function(函数)

Function 包含函数签名(返回值类型、参数列表)和函数体。函数体由若干个 BasicBlock 组成。

我遇到过一个问题:写 Pass 时想获取函数的参数个数,直接用 Function::arg_size() 就行。但要注意,如果函数是声明(没有函数体),这个值可能跟你预期的不一样。

BasicBlock(基本块)

基本块是 LLVM IR 的控制流单元。它有一个入口标签,一个终结指令(比如 brret),中间是一串顺序执行的指令。

基本块之间不能有跳转指令乱入。这是 LLVM 的硬性规定——每个基本块只能从顶部进入,从底部离开。

Instruction(指令)

最底层的单元。每条指令对应一个操作,比如加法、加载、存储、函数调用等。指令的操作数可以是常量、寄存器(虚拟的 % 变量)、或者全局符号。

我曾经踩过的坑:在写 Pass 时,我试图直接修改一条指令的操作数,结果忘了更新 Use-Def 链。程序跑起来直接崩了。记住:修改指令一定要通过 setOperand()replaceAllUsesWith()

类型系统:void、int、float、pointer、struct、array、function

LLVM 的类型系统是强类型的。每个值都有明确的类型,编译器靠这个做优化和验证。

类型 表示方式 说明
void void 无返回值,不能声明变量
int i32, i64 整数类型,位数任意(如 i1 表示布尔)
float float, double 浮点类型,IEEE 754 标准
pointer i32*, i8* 指针类型,指向某个类型的地址
struct { i32, i8 } 结构体,可以嵌套
array [4 x i32] 数组,固定长度,元素类型相同
function i32 (i32, i8*) 函数类型,包含返回类型和参数列表

我刚开始学的时候,最不习惯的是 i32 这种写法。后来发现它其实很灵活——你想用 17 位整数?i17 就行。虽然实际中很少这么干。

一个小技巧:指针类型在 LLVM 里是显式的。比如 i32* 表示指向 32 位整数的指针。但 LLVM 不检查指针的「类型安全」——你可以把 i32* 强转成 i8*,编译器不会拦你。这是为了兼容 C 语言的指针运算。

常量与全局变量

常量(Constant)

常量在 LLVM IR 里是「编译时可知」的值。它们可以是整数、浮点数、字符串、数组、结构体等。常量没有地址,不能取地址操作。

举个例子:42i32 42"hello"[6 x i8] c"hello\00"。注意字符串末尾的 \00——LLVM 不会自动帮你加 null 终止符,你得自己处理。

全局变量(GlobalVariable)

全局变量是存储在 Module 级别的变量。它们有地址,可以被多个函数访问。全局变量可以初始化,也可以只是声明(外部链接)。

我遇到过一个问题:两个 .c 文件里定义了同名的全局变量,链接时冲突了。LLVM 的解决方案是给全局变量加 linkage 属性——internal 表示文件内可见,external 表示全局可见。

; 一个简单的全局变量示例
@my_global = global i32 42          ; 可修改的全局变量
@my_const = constant i32 100        ; 常量,不可修改
@my_extern = external global i32    ; 外部声明,定义在其他模块
核心区别:
  • 常量(constant):编译时确定,不可修改,没有地址
  • 全局变量(global):有地址,可读写,可以初始化或声明
  • 全局变量默认是 external 链接,除非你显式指定 privateinternal

知识体系总览

下面这张图把本章的核心内容串起来了。我建议你把它存下来,写代码时对照着看。

LLVM IR 知识体系 内存 IR Bitcode (.bc) 文本 IR (.ll) Module Function BasicBlock Instruction (从上到下嵌套,一个 Module 包含多个 Function,每个 Function 包含多个 BasicBlock,每个 BasicBlock 包含多个 Instruction) void int float pointer struct array function 常量(Constant) 全局变量(GlobalVariable)

这张图从顶层到底层,把 LLVM IR 的三种形式、四种基本结构、七种核心类型、以及常量与全局变量的关系都串起来了。你写 Pass 或者手写 IR 时,可以拿它当「地图」用。

我的建议:刚开始学的时候,别急着写复杂的 Pass。先对着 .ll 文件,把 Module、Function、BasicBlock、Instruction 这四个层次认清楚。然后试着用 opt -print 看看你的代码被优化成了什么样子。慢慢来,比什么都重要。

公众号:蓝海资料掘金营,微信deep3321