你的程序突然段错误?先别急着怪自己

先问一句:最近你升级 Rust 之后,有没有遇到过程序在 -O 优化构建下莫名其妙段错误(segfault),换成 debug 构建又一切正常?

如果有,这大概率不是你的代码问题,而是编译器自己出错了。

2026 年 7 月 16 日,Rust 官方紧急发布了 1.97.1,专门修复一个潜伏已久的 LLVM 误编译(miscompilation):编译器在优化时生成了错误的机器码,让本来正确的 Rust 程序在运行到特定路径时直接崩溃。官方明确说,这个底层问题从 Rust 1.87 开始就存在

老规矩,先升级:

rustup update stable

quote-card

事件还原:从发布到紧急修复只用了 7 天

整个事件的时间线非常紧凑:

  • 7 月 9 日:Rust 1.97.0 正式发布
  • 7 月 9 日当天:有人提交了 issue #159035 ,标题就叫 “segfault on Rust 1.97.0”。代码很简单——一个嵌套枚举的 match,传入 None,理论上应该打印 None 然后正常退出,结果直接段错误
  • 7 月 9 日-10 日:编译器团队连夜排查,锁定根因:1.97.0 里一个关于 Option 判别式(discriminant)存储表示的改动,撞上了一个 LLVM x86 后端的优化 bug
  • 7 月 10 日:rustc 团队回滚了触发改动的 PR,并向 LLVM 提交了上游修复
  • 7 月 16 日Rust 1.97.1 紧急发布,双管齐下:backport LLVM 的修复 + 回滚 rustc 侧的改动

从有人踩坑到官方发布修复版,正好一周。

误编译时间线:1.87 潜伏、1.97.0 引爆、1.97.1 修复

根因:一个"优化"如何变成定时炸弹

这个 bug 有两层,缺一不可。

第一层:1.97.0 改了 Option::None 的"存储编码"

Rust 的 Option<T> 在内存里并不是直接存"有没有值",而是有一个 tag(标签) 字段来区分 SomeNone。为了更省内存,编译器会用一些"偷懒"技巧:比如 Option<bool> 可以直接用一个字节的 0/1 表示。

1.97.0 合入的 PR #155850 调整了这类判别式的存储值:None 的 tag 从 2 改成了 -1Option<bool> 这类"bool-like"枚举的判别式从 0/1 变成 0/-1)。

单独看这个改动没问题——编译器会在 IR 里把存储的 tag 再映射回逻辑判别式(0 或 1)。就像换了一套内部编码,对外行为不变。

第二层:LLVM x86 后端的 trunc nuw 误优化

问题出在 LLVM 的优化 pass 上。生成的 IR 里有这样一条指令:

%1 = trunc nuw i64 %0 to i1

trunc nuw 的意思是"截断且保证无溢出(no unsigned wrap)"。如果 %0 的截断位里有非零位,nuw 保证下结果应该是 poison(毒值),后续使用 poison 的代码行为未定义。

但这里 %0 存的是 tag:-1(也就是 0xFFFFFFFFFFFFFFFF)。截断到 i1 时,高位全被丢掉,nuw 的约定被打破——LLVM 在 x86 后端把这个场景优化错了

在 LLVM 19 时代,这段代码会被编译成 and ecx, 1(安全,结果正确);LLVM 20 之后变成了 mov ecx, ecx(一条看似多余、实则致命的指令)。当 tag 是 -1 时,这条指令让后续的地址计算完全错乱,程序读到错误的内存地址,段错误随之而来。

LLVM 误编译根因:trunc nuw 在 tag=-1 时产生 poison

影响范围:x86/x64 专属

几个关键事实:

  • 只有 x86/x64 受影响。维护者在 aarch64 和 riscv64gc 上实测均正常,这是 LLVM x86 后端特有的问题
  • 底层 bug 从 Rust 1.87(LLVM 20)就存在,只是 1.97.0 的 IR 改动大大提高了触发概率
  • 触发条件比较刁钻:带多个 payload 字段的枚举 + match + 优化构建(-O。不是所有程序都会踩中,但踩中就是硬崩溃

实测复现:1.97.0 崩,1.97.1 好

光看讨论不够,我直接在本机(Windows x64 / MSVC)用官方 issue 里的最小复现跑了一遍:

use std::hint::black_box;

#[repr(u16)]
enum Checksum {
    X(bool, u64),
    Y(u64, u64),
}

#[inline(never)]
fn run(c: Option<Checksum>) -> Option<u64> {
    match c {
        Some(x) => Some(match x {
            Checksum::X(false, s) => s,
            Checksum::X(true, s) => s,
            Checksum::Y(_, s) => s,
        }),
        None => None,
    }
}

fn main() {
    println!("{:?}", run(black_box(None)));
    println!("{:?}", run(black_box(Some(Checksum::Y(0, 42)))));
    println!("exit-ok");
}

结果:

工具链结果
rustc 1.97.0 -OSegmentation fault(exit 139)
rustc 1.97.1 -ONone / Some(42) / exit-ok,一切正常
rustc 1.96.1 -O正常(Windows 上未复现;issue 报告者在 Linux 上 1.96.1 也崩过)

注意 1.96.1 这一行:官方说底层问题从 1.87 就潜伏,但 1.97.0 才是主要引爆点。不同平台、不同代码形状下,触发表现不完全一致——这正是误编译最可怕的地方:它不保证稳定复现

怎么确认自己有没有中招?

三个检查步骤:

1. 先看版本

rustc --version

如果你还在 1.97.0,别犹豫,直接升级。

2. 升级

rustup update stable

3. 跑一遍测试

如果你的项目在升级前出现过"优化构建才崩、debug 没事"的诡异问题,升级后重点回归:

cargo test --release

特别是代码里有"多字段枚举 + match + 嵌套在 Option/Result 里"这种形状的,建议多跑几遍。

结语

这次事件给我们的启示其实很反直觉:Rust 的内存安全保证,挡不住编译器自己的 bug。Rust 能保证你的代码不会越界、不会悬垂引用——前提是编译器生成的机器码是正确的。而当 LLVM 优化 pass 出错时,内存安全就变成了"在错误内存上的安全操作"。

好在 Rust 团队的反应速度值得点赞:48 小时内定位根因、回滚触发改动,一周内发布修复版本。这也是为什么版本发布类文章我会建议你"等两天再升"——大版本发布后先观察一周,等第一个 point release 落地再升级,是生产环境更稳妥的节奏

好了,今天聊到这。如果你的程序曾经"优化就崩、debug 就好",评论区说说你的经历——没准就是同一个 bug。

相关阅读

如果你对 Rust 感兴趣,这几篇文章你可能也会喜欢:


觉得这篇文章有用? 点个赞让更多人看到,收藏起来下次升级的时候翻出来看看。如果你身边也有写 Rust 的朋友,转发给他们——特别是还在 1.97.0 上待着的人,说不定能帮他们省下一个通宵。有什么问题或者想聊的,评论区等你。