如何在 no_std 环境实现 panic_handler?
解读
国内嵌入式、操作系统内核、区块链底层模块等面试常问“no_std 下谁来兜底 panic”。
面试官想确认三点:
- 是否理解 no_std = 无 std::panic::catch_unwind、无默认 std panic hook;
- 是否掌握 #[panic_handler] 属性及签名约束;
- 是否能在 裸机/内核/linker script 场景给出可落地、不链接 std 的代码,并说明编译期验证手段(cargo check --target xxx.json)。
回答时先给出最小可编译片段,再解释与异常模型、调试、复位的衔接,即可体现“工程化思维”。
知识点
- #[panic_handler]:no_std 唯一入口,签名固定
fn(&PanicInfo) -> !。 - PanicInfo:提供 payload、location、can_unwind 标志,no_std 下无回溯栈功能。
- core::intrinsics::abort() vs 裸机复位:abort 直接非法指令,内核场景常用 sbi::system_reset 或写看门狗寄存器。
- linkage:必须确保仅一个 panic_handler,rlib 与 bin 同时提供会冲突;用
#[cfg(not(test))]隔离测试。 - 调试辅助:通过 rtt、semihosting、uart 打印 输出 PanicInfo,release 模式需手动关内联
#[inline(never)]才能看到符号。 - CI 验证:在
.cargo/config.toml指定runner = "qemu-system-riscv64 -nographic",cargo test --no-default-features 保证无 std 泄漏。
答案
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use core::fmt::{self, Write};
struct UartWriter(*mut u8); // 假设 MMIO 地址 0x1000_0000
impl Write for UartWriter {
fn write_str(&mut self, s: &str) -> fmt::Result {
for &b in s.as_bytes() {
unsafe { self.0.write_volatile(b) }
}
Ok(())
}
}
#[panic_handler]
#[inline(never)] // 方便 gdb 打断点
fn panic(info: &PanicInfo) -> ! {
let mut uart = UartWriter(0x1000_0000 as _);
let _ = writeln!(uart, "PANIC: {}", info);
// 裸机常见做法:触发看门狗或死循环
unsafe {
core::arch::asm!("ebreak", options(noreturn));
}
}
关键点:
- 函数必须返回
!,否则编译错误 E0308; - 禁止调用任何 std 路径,包括 format! 隐式分配;
- 若用 Rust 1.81+,可启用
-Z panic-abort-tests让单元测试也走同一 handler,CI 统一行为。
拓展思考
- 多架构适配:RISC-V 用
ebreak,ARM Cortex-M 用udf #0xDE,通过 cfg(target_arch) 统一封装。 - panic 日志持久化:在 Flash 末尾预留 4 KiB“panic 分区”,panic 时关中断、写日志、CRC 校验,下次上电通过 bootloader 上传云端。
- 与 RTOS 集成:如果底层是 Zephyr/Rust 混合工程,可把
rust_panic映射到k_oops(),让 C 侧 trace 也能抓到 Rust 崩溃上下文。 - 安全关键领域:ISO 26262 要求不可恢复故障必须进入安全状态,此时 panic_handler 里不再打印,直接激活安全关断路径,并通过 双核锁步 确认另一核也已停转。