3、LLVM IR基础语法:模块、函数、基本块、指令、操作数、类型系统与常量

LLVM IR(Intermediate Representation)是LLVM编译框架的核心枢纽,它将高级语言(如C/C++、Rust、Swift)与目标机器指令(如x86、ARM、RISC-V)解耦。理解LLVM IR的语法与结构,是编写高效、正确Pass的前提。本章将系统拆解IR的六大核心要素:模块(Module)、函数(Function)、基本块(BasicBlock)、指令(Instruction)、操作数(Operand)以及类型系统与常量(Type System & Constant)。我们将从静态结构出发,结合动态数据流视角,帮助你建立对IR的立体认知。

LLVM IR 核心结构关系图 Module (模块) 包含全局变量、函数声明/定义、元数据、目标特性 Function (函数) 签名: ret_type @name(params) BasicBlock (基本块) entry: %a = add i32 %x, 1 br label %loop BasicBlock (基本块) loop: %b = phi i32 [0, %entry], [%c, %loop] %c = add i32 %b, %a BasicBlock (基本块) exit: ret i32 %c 类型系统与常量 类型系统 (Type System) • 第一类类型: i1, i8, i32, float, ptr • 聚合类型: array, struct, vector • 函数类型: i32 (i32, i32) • 标签类型: label (用于分支) • 元数据: !llvm.module.flags 常量 (Constant) • 整数常量: 42, -7 • 浮点常量: 3.14, 0x3FF0000000000000 • 全局变量: @g_var = global i32 10 • 常量表达式: getelementptr, ptrtoint • 零初始化: zeroinitializer • 未定义值: undef, poison 引用 使用 指令: add, load, br, phi 操作数: %x, %a, 常量, 基本块 SSA形式: 每个变量只被赋值一次,phi节点解决控制流合并

上图展示了LLVM IR的层次化结构。最外层是Module,它像一个容器,装载了所有全局实体。Module内部包含多个Function,每个Function又由若干BasicBlock组成,BasicBlock内是线性排列的Instruction。右侧展示了类型系统与常量的分类,它们是IR的"静态骨架"。指令与操作数之间通过SSA(Static Single Assignment)形式建立数据依赖关系。下面我们逐一深入剖析每个要素。

3.1 模块(Module):IR的顶层容器

核心原理:Module是LLVM IR的编译单元,对应一个源文件或一个链接单元。它负责管理全局命名空间,包括全局变量、函数声明与定义、目标特性(如数据布局、目标三元组)以及元数据。在Pass编写中,我们通常以Module为入口遍历所有函数。

关键组成:

  • 全局变量:以@开头,如@count = global i32 0,可指定constant修饰符。
  • 函数声明与定义:声明只有签名(如declare i32 @printf(i8*, ...)),定义包含函数体。
  • 目标特性:target datalayout和target triple,影响数据对齐与ABI。
  • 元数据:以!开头,如!llvm.module.flags,用于传递编译选项或调试信息。

实战场景:在编写一个"函数内联"Pass时,你需要遍历Module中所有Function,检查其属性(如alwaysinline),并操作CallInst指令。Module提供了getFunctionList()接口,这是所有函数级Pass的起点。

常见误区:初学者容易混淆全局变量与局部变量。全局变量使用@前缀,存储在静态内存区;局部变量使用%前缀,是SSA虚拟寄存器。在IR中,全局变量是GlobalVariable类,局部变量是AllocaInst或SSA值。

3.2 函数(Function)与调用约定

核心原理:Function在IR中是一等公民,它包含函数签名(返回类型、参数列表、调用约定)和函数体(由BasicBlock组成)。函数可以声明或定义,声明用于外部符号链接。

函数结构拆解:

组成部分示例说明
返回类型i32可以是void、整数、指针、结构体等
函数名@add全局唯一,以@开头
参数列表(i32 %a, i32 %b)每个参数有类型和名称
调用约定ccc, fastcc影响参数传递方式(寄存器/栈)
函数属性#0如nounwind, readonly
基本块列表entry:, loop:第一个基本块是入口块

操作流程:在Pass中获取函数信息,通常使用Function类的API:arg_begin()遍历参数,getEntryBlock()获取入口块,getCallingConv()获取调用约定。

经验提醒:当处理递归函数或间接调用时,注意FunctionType与PointerType的区别。函数指针类型是ptr指向函数类型,例如ptr @func_ptr。

3.3 基本块(BasicBlock)与控制流

核心原理:BasicBlock是单入口、单出口的指令序列。它只有一个终止指令(Terminator),如br、ret、switch、unreachable。基本块之间通过显式跳转形成控制流图(CFG)。

基本块特性:

  • 入口唯一:只能从块首开始执行,不能从中间进入。
  • 出口唯一:最后一条指令必须是终止指令。
  • PHI节点:位于块首,用于合并来自不同前驱的值。
  • 前驱与后继:通过getPredecessors()和getSuccessors()遍历。

案例拆解:考虑以下C代码片段:

int max(int a, int b) {
  if (a > b) return a;
  else return b;
}

对应的IR基本块结构如下:

define i32 @max(i32 %a, i32 %b) {
entry:
  %cmp = icmp sgt i32 %a, %b
  br i1 %cmp, label %then, label %else

then:
  ret i32 %a

else:
  ret i32 %b
}

这里entry块通过br指令分支到then或else,两个分支块都直接返回。注意icmp指令产生i1类型的比较结果,作为分支条件。

常见误区:基本块不能以普通指令结尾,必须以终止指令结尾。如果Pass修改了控制流,必须确保每个基本块仍然满足这一约束,否则IR验证会失败(verifyModule报错)。

3.4 指令(Instruction)与操作数(Operand)

核心原理:指令是IR的执行单元,分为几大类:终结指令(ret, br, switch)、二元运算(add, sub, mul)、内存访问(load, store, alloca)、类型转换(trunc, zext, bitcast)、比较指令(icmp, fcmp)等。操作数是指令的输入,可以是常量、基本块标签、函数参数或其他指令的结果。

SSA形式:每个指令定义一个新值(除非是store等无结果指令),且只能被赋值一次。这简化了数据流分析,但引入了PHI节点来处理控制流合并。

操作数遍历方法:

// 遍历指令的操作数
for (Use &U : I.operands()) {
  Value *V = U.get();
  // V 可以是 Constant、Argument、Instruction 等
}

实战场景:在实现"常量传播"Pass时,你需要检查每条指令的操作数是否为ConstantInt。如果是,则尝试在编译期计算结果并替换指令。例如:

%x = add i32 2, 3   ; 操作数都是常量,可折叠为 5

案例闭环:假设我们有一个简单的算术表达式a = b + 1; c = a * 2;。在IR中,这会产生两条指令:%1 = add i32 %b, 1和%2 = mul i32 %1, 2。如果通过分析得知%b恒为10,那么%1可折叠为11,%2可折叠为22。这个过程中,操作数的类型(i32)和常量值(1和2)是折叠的关键输入。实际操作时,我们调用ConstantExpr::getAdd()创建新常量,然后调用replaceAllUsesWith()替换旧值。最终,两条指令被删除,%b的使用点直接得到常量22。这个优化减少了运行时计算,但代价是需要额外的分析时间。在-O2级别,LLVM会自动执行此优化。

3.5 类型系统(Type System)详解

核心原理:LLVM IR是强类型语言,每个值都有明确的类型。类型系统分为第一类类型(First-class types)和非第一类类型。第一类类型可以作为指令的操作数,包括整数、浮点、指针、向量、聚合类型等。

类型分类表:

类型类别语法示例说明
整数类型i1, i8, i32, i64位宽任意,i1用于布尔
浮点类型half, float, double对应IEEE 754
指针类型ptrLLVM 15+统一为ptr,不再区分指向类型
向量类型<4 x i32>SIMD操作,元素类型和数量固定
数组类型[10 x i8]固定长度,元素类型相同
结构体类型{i32, i32}可包含不同类型,可命名
函数类型i32 (i32, i32)返回类型 + 参数类型列表
标签类型label用于分支目标,不是值
元数据类型!{...}附加信息,不影响语义

类型转换规则:不同类型之间不能隐式转换,必须使用显式指令:trunc(截断)、zext(零扩展)、sext(符号扩展)、fptrunc(浮点降精度)、bitcast(位重解释)。

经验提醒:在编写Pass时,经常需要判断两个类型是否一致。使用Type::getIntegerBitWidth()获取整数位宽,使用isa<PointerType>()判断指针类型。注意ptr类型在LLVM 15之后不再携带元素类型信息,这改变了getElementType()的用法。

3.6 常量(Constant)与常量表达式

核心原理:常量是编译期已知的值,它们在IR中是不可变节点。常量不仅包括字面量,还包括全局变量、常量表达式(如getelementptr的常量形式)和undef/poison值。

常量分类:

  • ConstantInt:整数常量,如i32 42。
  • ConstantFP:浮点常量,如double 3.14。
  • ConstantArray/ConstantStruct:聚合常量。
  • GlobalValue:全局变量或函数地址,也是常量。
  • ConstantExpr:常量表达式,如ptrtoint、getelementptr。
  • UndefValue/PoisonValue:表示未定义或毒值。

常量折叠(Constant Folding):LLVM在构建IR时自动执行简单的常量折叠。例如add i32 1, 2会直接生成i32 3。Pass可以利用ConstantFoldInstruction接口手动触发折叠。

实战场景:在实现"全局变量替换"优化时,如果发现全局变量@g只被初始化一次且从未修改,可以将其替换为常量。操作步骤:

  1. 检查GlobalVariable是否有constant修饰符或hasInitializer()。
  2. 遍历所有使用点,确认没有store指令写入该变量。
  3. 将@g的所有load指令替换为初始值常量。
  4. 删除全局变量声明。

常见误区:不要混淆undef和poison。undef表示值可以是任意位模式,每次读取可能不同;poison表示值已经"中毒",任何依赖它的操作都会产生poison。在优化时,poison比undef更激进,允许更多优化。

3.7 综合案例:从C代码到IR的完整映射

为了巩固上述概念,我们以一个包含循环和数组访问的C函数为例,展示其IR生成过程。

// C源码
int sum_array(int *arr, int n) {
  int sum = 0;
  for (int i = 0; i < n; i++) {
    sum += arr[i];
  }
  return sum;
}

对应的LLVM IR(简化版,未优化):

define i32 @sum_array(ptr %arr, i32 %n) {
entry:
  %sum = alloca i32, align 4
  store i32 0, ptr %sum, align 4
  %i = alloca i32, align 4
  store i32 0, ptr %i, align 4
  br label %cond

cond:
  %i.load = load i32, ptr %i, align 4
  %cmp = icmp slt i32 %i.load, %n
  br i1 %cmp, label %body, label %exit

body:
  %i.load1 = load i32, ptr %i, align 4
  %idx = sext i32 %i.load1 to i64
  %elem_ptr = getelementptr i32, ptr %arr, i64 %idx
  %val = load i32, ptr %elem_ptr, align 4
  %sum.load = load i32, ptr %sum, align 4
  %new_sum = add i32 %sum.load, %val
  store i32 %new_sum, ptr %sum, align 4
  %i.load2 = load i32, ptr %i, align 4
  %inc = add i32 %i.load2, 1
  store i32 %inc, ptr %i, align 4
  br label %cond

exit:
  %sum.final = load i32, ptr %sum, align 4
  ret i32 %sum.final
}

案例拆解表:

IR要素实例作用
Module整个define块包含函数定义
Function@sum_array函数入口
BasicBlockentry, cond, body, exit控制流节点
Instructionalloca, load, store, icmp, br, add, getelementptr执行操作
Operand%arr, %n, 0, %i.load指令输入
Typeptr, i32, i64, i1数据宽度与语义
Constant0, 1循环初始值和步长

分析过程:注意这里使用了alloca来模拟局部变量,这是未优化IR的典型特征。在-O1以上,LLVM会通过mem2regPass将alloca提升为SSA寄存器,消除冗余的load/store。这个案例展示了IR的"显式"特性:所有内存访问、类型转换、控制流跳转都清晰可见,这正是Pass可以分析和变换的基础。

复盘总结:理解IR的关键在于把握"静态结构"与"动态语义"的映射。模块-函数-基本块-指令构成静态层次,而SSA数据流和控制流边构成动态执行模型。类型系统是IR的"语法约束",常量是优化的"燃料"。在后续章节中,我们将基于这些基础编写第一个Pass,实现函数级别的变换。