你的程序突然段错误?先别急着怪自己
先问一句:最近你升级 Rust 之后,有没有遇到过程序在 -O 优化构建下莫名其妙段错误(segfault),换成 debug 构建又一切正常?
如果有,这大概率不是你的代码问题,而是编译器自己出错了。
2026 年 7 月 16 日,Rust 官方紧急发布了 1.97.1,专门修复一个潜伏已久的 LLVM 误编译(miscompilation):编译器在优化时生成了错误的机器码,让本来正确的 Rust 程序在运行到特定路径时直接崩溃。官方明确说,这个底层问题从 Rust 1.87 开始就存在。
老规矩,先升级:
rustup update stable

事件还原:从发布到紧急修复只用了 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 侧的改动
从有人踩坑到官方发布修复版,正好一周。

根因:一个"优化"如何变成定时炸弹
这个 bug 有两层,缺一不可。
第一层:1.97.0 改了 Option::None 的"存储编码"
Rust 的 Option<T> 在内存里并不是直接存"有没有值",而是有一个 tag(标签) 字段来区分 Some 和 None。为了更省内存,编译器会用一些"偷懒"技巧:比如 Option<bool> 可以直接用一个字节的 0/1 表示。
1.97.0 合入的 PR #155850
调整了这类判别式的存储值:None 的 tag 从 2 改成了 -1(Option<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 时,这条指令让后续的地址计算完全错乱,程序读到错误的内存地址,段错误随之而来。

影响范围: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 -O | Segmentation fault(exit 139) |
rustc 1.97.1 -O | None / 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.92.0 发布:never 类型 lint 升级、Result 更智能、新 API 稳定化 - 版本更新的正确打开方式
- Rust 1.90 升级翻车实录:我的代码库没能活下来 - 升级有风险,测试要跟上
- Rust 1.94 更新:滑动窗口模式匹配让代码优雅起来 - 新版本特性详解
- 你的 Rust 项目只有 8000 行代码,rust-analyzer 却啃了 250 万行依赖 - 编辑器卡顿根源与提速实战
觉得这篇文章有用? 点个赞让更多人看到,收藏起来下次升级的时候翻出来看看。如果你身边也有写 Rust 的朋友,转发给他们——特别是还在 1.97.0 上待着的人,说不定能帮他们省下一个通宵。有什么问题或者想聊的,评论区等你。
