17.3 未定义行为与 Sanitizer 风格工具
语言语义必须区分几种不同自由度。已定义行为拥有规定结果;实现定义行为允许实现选择并记录一种结果,例如普通整数宽度;未规定行为允许多个结果之一,且不要求记录选择;已定义的 trap 会明确终止执行;未定义行为(UB)则表示一旦该操作发生,规范不再提出任何要求。
UB 不是运行时随机挑一个值。它意味着编译器只需对避免 UB 的执行进行推理。如果有符号溢出属于 UB,优化器就可以对所有已定义执行推断 x + 1 > x,进而简化分支。即使 debug build 中观察到 wraparound,也没有因此获得语言保证。常见 UB 包括非法指针解引用、越界 shift、使用未初始化值、违反 alias 规则,以及在对象生命周期结束后继续访问。
这种契约支持强力优化,却让局部推理变得危险。把检查写在会溢出的操作之后可能已经太晚。安全语言通常用 checked trap、wrapping operation 或受限制的 unsafe region 代替 UB。Compiler IR 也要精确定义自己的语义:poison 与 undefined placeholder 不能随意当成普通源码值,变换还必须保留 effect 何时变得可观察。
考虑 if (x + 1 < x) report_overflow();。在 wrapping arithmetic 中,当 x 是最大整数时条件为真;在 checked arithmetic 中,加法会先 trap,比较根本不会发生;在 C-like 的有符号溢出 UB 语义下,优化器可以认定它需要保持的每个执行中条件都为假。同样的表面语法因此对应三种不同合法变换。Frontend 必须准确编码所选契约;仅仅为了获得更好代码而添加 no-overflow IR flag,本身就是编译器 bug。
Sanitizer 让选定违规行为变得可观察
Sanitizer 风格工具会在编译阶段插桩,并链接一个报告错误的 runtime。AddressSanitizer(ASan)在 allocation 周围放置 red zone,用紧凑 shadow memory 标记地址是否可访问。它能发现很多越界与 use-after-free;quarantine 延迟内存重用,使悬空指针更容易暴露。但它无法发现所有时间错误,自定义 allocator 或未插桩代码也会降低可见性。
UndefinedBehaviorSanitizer(UBSan)在非法 shift、未对齐访问和部分溢出等操作之前插入检查。检查可以 recover 并继续、只报告一次或者直接 trap;模式会同时影响证据和运行风险。MemorySanitizer(MSan)追踪 bit 是否初始化,并让状态随计算传播,因此几乎所有参与代码都要插桩。ThreadSanitizer(TSan)通过 happens-before 模型观察内存访问和同步,从而识别 data race。它的成本和 runtime 设计通常要求独立 build。
设计测试活动,而不只是选择 flags
把所有 sanitizer 塞进一个 binary 通常并不理想。应建立矩阵:每次变更运行快速的 ASan 加部分 UBSan;在兼容平台运行较慢的 MSan 与 TSan;定期进行更广的 fuzz campaign。尽量给依赖插桩,保留 frame pointer 或等价 unwind 信息,同时保存 debug symbol 和精确 build ID。缺少 symbolization、command line、输入和 revision 的报告很难处理。
Sanitizer 开销会改变调度和内存布局。Race 可能在 ASan 下消失,allocation bug 可能只在优化后出现。因此要测试多个 optimization level,并在最小而合适的配置中复现发现。Suppression 必须有负责人、理由和到期日;宽泛 pattern 会悄悄摧毁 coverage。
Sanitizer 只能证明违规在已执行路径上出现过。一次干净运行不能证明程序没有 UB、内存错误或 race。还要结合静态分析、类型与 ownership 规则、IR verifier、fuzzing、review 和 sandbox。工具本身也要安全:处理不可信源码的编译器需要时间和内存限制,因为即使违规能被正确报告,输入仍可能造成拒绝服务。诚实结论必须说明:运行了哪种 checker、覆盖什么 corpus 和配置,以及它实际观察到什么。