llvm ir中add指令是理解其核心契约的起点,它强制要求显式类型(如i32)、ssa命名(如%a)和溢出语义标记(如nsw),并体现强类型、静态单赋值与底层操作显式化三大原则。

直接看懂一个加法函数,是进入 LLVM IR 最稳妥的起点——它足够小,没有控制流干扰,又能暴露 IR 的核心契约:强类型、SSA、显式操作。
为什么 add 是第一个必须盯住的指令
add 看似简单,但它的签名里藏着 LLVM IR 的底层约定。C 里写 a + b,IR 里必须明确:操作数位宽、有无符号性、是否允许溢出。漏掉任一细节,后续看递归或内存访问时就会卡住。
-
add i32 %a, %b中的i32不是可选修饰,而是强制类型声明;C 的隐式提升在这里不存在 - 变量名带
%前缀(如%a)不是语法糖,而是 SSA 要求每个值有唯一标识——%a和%a1是两个完全不同的值 - 默认
add不检查溢出,但你可以加nsw(no signed wrap)或nuw(no unsigned wrap)标记,这直接影响优化器能否把x + 1 > x当作永真来删减
从 C 到 define i32 @add(i32 %a, i32 %b) 的三步拆解
用 clang -emit-llvm -S 生成 IR 后,第一眼看到的函数头不是装饰,而是关键接口定义。
-
define i32:返回类型固定,不能省略;LLVM 不推导,也不接受auto -
@add:函数名带@,区分于局部变量%;C++ 名字重整(name mangling)会让它变成类似@_Z3addii,先从纯 C 入手避坑 -
(i32 %a, i32 %b):参数类型和名称都显式写出;没有const、restrict等 C 语义,只有类型和 SSA 名
load/store 为什么在简单加法里可能突然出现
你以为 int add(int a, int b) { return a + b; } 对应的 IR 就是两行?实际 clang 默认启用栈帧,尤其在调试模式(-O0)下会插入 alloca + store + load:
define i32 @add(i32 %a, i32 %b) {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
store i32 %a, i32* %1, align 4
store i32 %b, i32* %2, align 4
%3 = load i32, i32* %1, align 4
%4 = load i32, i32* %2, align 4
%5 = add i32 %3, %4
ret i32 %5
}
这不是冗余,而是编译器为后续优化(比如把局部变量升为寄存器)预留的统一内存模型。想跳过它?加 -O1,你会看到 IR 直接变成干净的 %1 = add i32 %a, %b ——但此时你得接受优化器已重排甚至内联了部分逻辑。
容易被忽略的“小”字符:空格、换行、inbounds
IR 文件里看似无关紧要的字符,其实绑定着语义:
-
getelementptr后面的inbounds不是注释,它告诉优化器“此处索引绝不会越界”,否则 GEP 行为未定义;少写它,某些基于边界的优化(如数组边界折叠)就失效 - 指令间换行和缩进无语法意义,但
br指令后的 label 名(如label %if.then)必须与目标基本块名严格一致,大小写和下划线都不能错 -
icmp sgt i32 %a, %b中的sgt(signed greater than)不能写成ugt,否则比较逻辑翻转——类型标记和比较谓词必须匹配
真正卡住人的,往往不是大结构,而是这些紧贴指令的标记字符。它们不报错,但会让行为偏离预期,尤其在跨平台生成或手动改写 IR 时。











