Rust 编译器中的 panic 实现机制:从 `panic!` 宏到栈展开与中止的完整调用链
Rust 编译器中的 panic 实现机制:从 panic! 宏到栈展开与中止的完整调用链
本篇技术指南以 rustc 仓库的 Rust 开发指南(src/doc/rustc-dev-guide/src/panic-implementation.md)为核心脉络,系统梳理 Rust 中一次 panic 从宏调用、lang item 符号解析、std 侧 panic hook 分发,到进入 panic_abort / panic_unwind 运行时、最终执行栈展开或进程中止的完整流程。读完本文,你将掌握 core 与 std 两套 panic! 入口如何通过弱 lang item 在链接期汇合,理解 __rust_start_panic 为何以 usize 传递胖指针,并能对照源码精确定位每一步实现。
一、总体流程概览
一次 Rust panic 的生命周期可以划分为两个阶段:
- Step 1:
panic!宏的调用。从宏展开的入口函数出发,最终调用名为rust_begin_unwind(源码中的panic_impllang item)的符号,将控制权从core移交到std。 - Step 2:panic 运行时(panic runtime)。
std执行全局 panic hook、检测双重 panic 后,调用由 panic 运行时提供的__rust_start_panic,由panic_abort(直接中止进程)或panic_unwind(执行平台相关的栈展开)完成收尾。
下面沿着这两步深入源码。
二、Step 1:panic! 宏的调用
Rust 实际存在两个 panic 宏:一个定义在 core,一个定义在 std。原因在于 core 中的代码本身也可能 panic,而 core 先于 std 被构建;为了让无论源自 core 还是 std 的 panic 在运行时都走同一套机制(panic hook、栈展开等),两者最终必须在链接期汇聚到同一个符号上。
2.1 core 中的 panic! 定义
core 的 panic! 宏最终会调用 library/core/src/panicking.rs 中的 panic_fmt(其 lang item 名为 panic_fmt):
// NOTE This function never crosses the FFI boundary; it's a Rust-to-Rust call
unsafe extern "Rust" {
#[lang = "panic_impl"]
fn panic_impl(pi: &PanicInfo<'_>) -> !;
}
let pi = PanicInfo::new(
&fmt,
Location::caller(),
/* can_unwind */ true,
/* force_no_backtrace */ false,
);
unsafe { panic_impl(&pi) }
这段代码实际解决的是一个多层间接的符号解析问题,链路如下:
- 在
core中,panic_impl被声明为 weak lang item,其关联符号为rust_begin_unwind。当前仓库中该声明位于 compiler/rustc_attr_ir/src/weak_lang_items.rs(对应PanicImpl, rust_begin_unwind;这一条目),并在rustc_hir_analysis的collect.rs中把实际符号名设置为rust_begin_unwind。 - 注意
panic_impl声明在extern "Rust"块中,这意味着core会在链接期尝试解析一个名为rust_begin_unwind的外部符号——这是一个 Rust 到 Rust 的调用,并不跨越 FFI 边界。
2.2 std 中的 panic_handler 与符号汇合
在 library/std/src/panicking.rs 中,存在如下定义:
/// Entry point of panics from the core crate (`panic_impl` lang item).
#[cfg(not(any(test, doctest)))]
#[panic_handler]
pub fn panic_handler(info: &core::panic::PanicInfo<'_>) -> ! {
...
}
这里的特殊属性 #<a href="https://link.gitcode.com/i/0d99226e1db22ee6aa5fdedf0866bde6" target="_blank">panic_handler] 由 [compiler/rustc_passes/src/lang_items.rs 解析:extract_ast 函数会把 panic_handler 属性转换为 panic_impl lang item。
于是 std 中有了与 core 中 extern { fn panic_impl } 声明相匹配的 panic_impl lang item。std 的这个函数经历与 core 相同的过程,最终也得到符号名 rust_begin_unwind。在链接期:
core中对rust_begin_unwind的符号引用被解析到std中panic_handler函数的定义;- 运行时控制流从
core传递到std。
这就是为什么 core 产生的 panic 也能享受 std 提供的全套基础设施(panic hooks、unwinding 等)。
2.3 std 中 panic! 的实现:真正的 panic 逻辑起点
在 library/std/src/panicking.rs 中,panic_handler 根据 panic 消息的形态选择 payload 包装方式:
- 若消息是
'static字符串,直接使用StaticStrPayload(take_box时零拷贝装箱); - 否则使用
FormatStringPayload,在take_box时惰性执行格式化并装箱为String。
随后控制权进入 panic_with_hook。该函数(位于同文件的 panic_with_hook)负责:
- 通过
panic_count::increase(true)增加当前线程的 panic 计数; - 检查是否必须立即中止(
MustAbort::PanicInHook表示 panic hook 内部再次 panic,MustAbort::AlwaysAbort表示已调用过panic::always_abort(),例如在libc::fork之后的子进程中,此时访问线程局部状态是非法的); - 调用全局 panic hook(默认 hook 或用户通过
set_hook/update_hook注册的自定义 hook); - 检查
can_unwind:若为false(例如在extern "C"等 nounwind 函数中),直接打印 "thread caused non-unwinding panic. aborting." 并process::abort(); - 最终调用
rust_panic(payload),后者再调用__rust_start_panic,进入 panic 运行时。
关于 __rust_start_panic 的奇怪类型
对 __rust_start_panic 的调用非常特别:它接收的是一个 *mut &mut dyn PanicPayload 转换而来的 usize。分解这个类型:
PanicPayload是定义在 library/alloc/src/panicking.rs 中的内部 trait(pub trait PanicPayload: Display),由 std 侧的 payload 包装类型实现,其关键方法是fn take_box(&mut self) -> Box<dyn Any + Send>。它把用户提供的 payload(T: Any + Send)装箱,再转成原始指针。使用 trait 对象接口而不是直接传值,是为了惰性分配:采用 abort 运行时(panic_abort)时,take_box根本不会被调用,从而避免不必要的堆分配。- 调用
__rust_start_panic时,我们持有&mut dyn PanicPayload——这是一个胖指针(大小是usize的两倍,包含数据指针和 vtable 指针),无法直接跨 FFI 传递。解决方案是:先取这个可变引用的可变引用(&mut &mut dyn PanicPayload),再转成原始指针(*mut &mut dyn PanicPayload)。外层原始指针是瘦指针,因为它指向的是Sized类型(一个引用),因此可以安全地转为usize,从而适合跨越 FFI 边界。
至此我们正式进入 panic 运行时。
三、Step 2:panic 运行时
Rust 提供两个 panic 运行时:panic_abort 和 panic_unwind。用户在构建期通过 Cargo.toml 选择:
[profile.release]
panic = "unwind" # 默认值,栈展开
# panic = "abort" # 直接中止进程
std 本身带有 #![needs_panic_runtime] 标记(引入自 RFC 1513),表明它要求某个带有 #![panic_runtime] 标记的 crate 存在,并实现 __rust_start_panic 等约定的符号(见 library/std/src/panicking.rs 中对外部符号的声明)。
3.1 panic_abort:极简的中止实现
library/panic_abort/src/lib.rs 的实现正如其名,极其简单:
#[rustc_std_internal_symbol]
pub fn __rust_start_panic(_payload: &mut dyn PanicPayload) -> u32 {
// Android / zkvm 平台可以附加 abort 消息
unsafe extern "Rust" {
#[rustc_std_internal_symbol]
safe fn __rust_abort() -> !; // 定义在 std::rt
}
__rust_abort()
}
它直接调用定义在 std::rt 中的 __rust_abort 终止进程。附带的效果是:PanicPayload::take_box 永远不会被调用,panic 消息的格式化与装箱全部被跳过,因此这种模式下 panic 路径几乎零分配。另外,同文件的 __rust_panic_cleanup 直接 unreachable!(),因为不会有任何展开需要清理。
3.2 panic_unwind:平台相关的栈展开
library/panic_unwind/src/lib.rs 是更有趣的情形。其 __rust_start_panic 只是委托给平台相关模块:
#[rustc_std_internal_symbol]
pub fn __rust_start_panic(payload: &mut dyn PanicPayload) -> u32 {
imp::panic(payload)
}
在 imp::panic 中,我们取回那个 usize,转换回 *mut &mut dyn PanicPayload,解引用后调用 take_box。此时我们得到指向 payload 本身的原始指针(*mut (dyn Send + Any)),即用户调用 panic! 时提供的实际值的指针。
这一步之后,平台无关的代码到此结束,控制权转入平台相关的展开逻辑。imp 模块通过 cfg_select! 按平台选择:
- MSVC 目标(非 32 位 ARM)使用 SEH(结构化异常处理),实现在
seh.rs; - Miri 环境使用 Miri 内建展开支持(
miri.rs),因为 Miri 不支持基于 funclet 的展开,只支持 landingpad; - 大多数 Unix / wasm / SGX 等使用 libunwind/libgcc,实现在
gcc.rs; - 不支持展开的目标(如
os = "none"的裸机、UEFI、ESP-IDF、Hermit、NVPTX、AVR)使用dummy.rs,其实现实际上退化为中止。
这段平台代码负责:展开栈、执行与每个栈帧关联的 "landing pad"(当前语境下即运行析构函数),并将控制权转移到 catch_unwind 所在的帧。
3.3 所有 panic 的最终归宿:中止或被 catch_unwind 捕获
值得强调的结论是:所有 panic 要么中止进程,要么被某次 catch_unwind 调用捕获。特别的,在 std 的 runtime 服务中,用户提供的 main 函数调用被包裹在 catch_unwind 中——见 library/std/src/rt.rs 中 lang_start_internal 的实现:
panic::catch_unwind(move || {
let ret_code = panic::catch_unwind(main).unwrap_or_else(move |payload| {
// 默认 panic hook 已打印过 panic 消息,这里处理返回码
...
});
...
})
std 侧的 catch_unwind 实现位于 library/std/src/panicking.rs:它通过 intrinsics::catch_unwind(do_call, &raw mut data, do_catch) 调用编译器内建能力,使用一个 union Data<F, R> 在闭包正常返回(写入 r 字段)与 panic(do_catch 中经 __rust_panic_cleanup 恢复 payload 后写入 p 字段)两条路径之间安全地传递所有权。同时 panic_count::decrease() 会在清理时递减 panic 计数,确保 thread::panicking() 与双重 panic 检测保持正确。
四、补充:更多 panic 入口与细节
4.1 core 中的其他 panic 入口
library/core/src/panicking.rs 不只是 panic_fmt,它还定义了多个带 lang item 的入口,供编译器在特定场景直接生成跳转(避免每个调用点都展开字符串参数,减少二进制体积):
panic(lang itempanic):panic!("...")的底层实现,使用fmt::Arguments::from_str而非format_args!,可让编译器省略Formatter::pad等代码,节省数 KB;panic_nounwind_fmt/panic_nounwind(lang itempanic_nounwind,带#[rustc_nounwind]):用于不能展开的场景;若 panic handler 无视can_unwind = false仍然展开,会触发 "panic in a function that cannot unwind" 防护;panic_bounds_check:数组/切片越界访问(OOB)的 panic;panic_misaligned_pointer_dereference、panic_null_pointer_dereference、panic_invalid_enum_construction等:由对应 MIR 检查 pass 生成的 panic;panic_const_*系列:溢出检查等编译器生成断言使用的常量消息(如 "attempt to add with overflow");const_panic_fmt:供 const-eval 机器在编译期求值 panic 时替换panic_fmt调用。
4.2 std 中的其他入口
begin_panic(lang itembegin_panic):支持任意 payload(M: Any + Send)的 panic 入口,也是panic_any!的基础;resume_unwind:catch_unwind捕获后再次抛出 payload 的入口,直接转发给 panic 运行时;panic::set_hook/take_hook/update_hook:操作全局 panic hook(静态HOOK: RwLock<Hook>),分别对应替换、取回、链式更新 hook。
五、总结:一条完整的调用链
将上述内容串联起来,一次 panic!("boom") 的完整路径是:
panic! 宏展开
└─> core::panicking::panic_fmt (lang item "panic_fmt")
└─> panic_impl lang item 调用 (weak lang item, 符号 rust_begin_unwind)
└─> std::panicking::panic_handler (#[panic_handler] 属性解析为 panic_impl)
└─> panic_with_hook (全局 hook + 双重 panic 检查)
└─> rust_panic
└─> __rust_start_panic (由 panic 运行时提供)
├─ panic_abort → __rust_abort → 进程中止
└─ panic_unwind → imp::panic → take_box 装箱 payload
└─> 平台展开(SEH / libunwind / Miri / dummy)
└─> 运行析构函数 → 转移至 catch_unwind 帧
└─> 或被 std::rt 中的 main 包装器捕获
理解这条链路的关键在于两个"汇合点":其一是 lang item 符号解析——core 的弱引用 rust_begin_unwind 与 std 的 #[panic_handler] 在链接期绑定;其二是 __rust_start_panic 的胖指针瘦化——以 usize 形式把 &mut dyn PanicPayload 传入运行时。这两处机制共同保证了:无论 panic 起源于 core 还是 std、无论选择展开还是中止策略,Rust 都能以统一、可控的方式处理错误路径。
延伸阅读:与本文直接相关的源码文件均在当前仓库中,可按需深入:library/core/src/panicking.rs、library/std/src/panicking.rs、library/alloc/src/panicking.rs(PanicPayload trait 定义)、library/panic_unwind/src/lib.rs、library/panic_abort/src/lib.rs、library/std/src/rt.rs(main 的 catch_unwind 包装)、compiler/rustc_attr_ir/src/weak_lang_items.rs(panic_impl 弱 lang item 声明)以及 compiler/rustc_passes/src/lang_items.rs(#[panic_handler] 属性解析)。