LLVM IR 的三种形态:内存 IR、Bitcode 与文本 IR
说实话,刚接触 LLVM 的时候,我被这三种 IR 形式搞得有点晕。后来在项目里踩过坑才明白——它们其实是同一份代码在不同阶段的「快照」。
内存 IR(In-Memory IR)
这是编译器在运行时操作的形式。说白了,就是一堆 C++ 对象在内存里互相指来指去。我习惯把它想象成「活着的 IR」——你可以随时增删指令、修改基本块、调整控制流。
举个例子,你在写一个 Pass 时,看到的 Function、BasicBlock、Instruction 这些类,就是内存 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 的控制流单元。它有一个入口标签,一个终结指令(比如 br、ret),中间是一串顺序执行的指令。
基本块之间不能有跳转指令乱入。这是 LLVM 的硬性规定——每个基本块只能从顶部进入,从底部离开。
Instruction(指令)
最底层的单元。每条指令对应一个操作,比如加法、加载、存储、函数调用等。指令的操作数可以是常量、寄存器(虚拟的 % 变量)、或者全局符号。
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 就行。虽然实际中很少这么干。
i32* 表示指向 32 位整数的指针。但 LLVM 不检查指针的「类型安全」——你可以把 i32* 强转成 i8*,编译器不会拦你。这是为了兼容 C 语言的指针运算。
常量与全局变量
常量(Constant)
常量在 LLVM IR 里是「编译时可知」的值。它们可以是整数、浮点数、字符串、数组、结构体等。常量没有地址,不能取地址操作。
举个例子:42 是 i32 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链接,除非你显式指定private或internal
知识体系总览
下面这张图把本章的核心内容串起来了。我建议你把它存下来,写代码时对照着看。
这张图从顶层到底层,把 LLVM IR 的三种形式、四种基本结构、七种核心类型、以及常量与全局变量的关系都串起来了。你写 Pass 或者手写 IR 时,可以拿它当「地图」用。
opt -print 看看你的代码被优化成了什么样子。慢慢来,比什么都重要。
公众号:蓝海资料掘金营,微信deep3321