undef 表示未定义值,是合法常量,可被优化器替换为同类型任意值;poison 表示毒值,触发延迟未定义行为且具传播性,需 freeze 阻断。

undef 表示“未定义值”,不是运行时错误,但行为不可预测
undef 是 LLVM IR 中一个合法的常量值,代表“该值存在,但编译器不承诺它是什么”。它不触发 immediate UB,也不会让程序崩溃;但它在优化中可能被任意替换为同类型任意值(比如 i32 类型的 undef 可能被替换成 0、-1 或 42),只要这对当前模块的语义没影响。
常见使用场景包括:
- 初始化未显式赋值的栈变量:
%x = alloca i32后直接load i32, i32* %x,加载结果是undef - 结构体或数组的部分初始化:
%s = {i32 1, i32 undef} - 某些 intrinsic(如
@llvm.ctlz对零输入)返回undef
注意:undef 不传播——它只影响直接依赖它的指令;后续用它算出的值仍可能是正常值(除非触发了其他 UB)。
poison 表示“毒值”,一旦产生就注定导致 deferred UB
poison 不是常量,而是一种运行时标记:它表示某个计算结果“逻辑上无效”,比如除零后的商、有符号溢出的加法结果(带 nsw)、越界指针算术。它本身不立即崩溃,但任何试图“观察”它的行为(如 icmp 比较、store 到内存、作为分支条件)都会触发 undefined behavior。
关键特性是传播性:
-
%a = add nsw i32 %x, %y若发生溢出 →%a是poison -
%b = mul i32 %a, 2→%b也是poison(即使乘法本身不溢出) -
%c = freeze i32 %b→%c变成一个确定的undef或具体值,阻断传播
也就是说,poison 像污染源,不加干预会一路传染到所有派生值;而 freeze 是唯一标准手段来“消毒”。
freeze 指令是控制 poison 传播的开关,不是“取值”操作
freeze 的作用不是把 poison 转成某个固定数,而是告诉优化器:“从此刻起,这个值可以被当作任意但稳定的同类型值看待”。它不指定选哪个值,也不保证跨次执行一致——只是让后续使用不再构成 UB。
典型误用:
- 以为
%r = freeze i32 %p后%r就等于某个安全整数 → 错,它仍可能是任意i32,只是不会再引发 UB - 对非
poison值滥用freeze→ 没害处但冗余,LLVM 可能直接删掉 - 漏掉关键路径上的
freeze→poison进入br或store,后端生成的机器码可能静默出错或崩溃
真实例子:循环中带 nsw 的计数器若可能溢出,又没 freeze 就用于数组索引,生成的代码在某些优化级别下可能跳过边界检查。
实际调试时怎么区分 undef 和 poison?
LLVM 的文本 IR 不显式写出 poison,它只在语义层面存在;undef 则明确写作 undef。所以你看到 IR 里有 undef,那是人为写的或由前端插入的;而 poison 往往藏在带 nsw/nuw/exact 的指令结果里,得结合上下文和优化行为反推。
最容易踩的坑是:用 opt -O2 看 IR 时,poison 可能已被优化器提前“折叠”或“替换”,导致你根本看不到它——但它引发的 UB 仍在。这时候必须配合 llc -march=host -debug-pass=Structure 或用 llvm-objdump --debug-dump=info 查看实际生成的机器码行为。
复杂点在于,poison 的传播规则在不同 LLVM 版本间有微调(比如 15.x 后对 select 的处理更严格),且部分后端(如 AArch64)对 poison 的响应方式与 X86 不同。别指望靠“看懂 IR”就一劳永逸,得结合 sanitizer 运行时检测和 llvmsymbolizer 定位源头。

















