如何在 no_std 环境实现 panic_handler?

解读

国内嵌入式、操作系统内核、区块链底层模块等面试常问“no_std 下谁来兜底 panic”。
面试官想确认三点:

  1. 是否理解 no_std = 无 std::panic::catch_unwind、无默认 std panic hook
  2. 是否掌握 #[panic_handler] 属性及签名约束;
  3. 是否能在 裸机/内核/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));
    }
}

关键点

  1. 函数必须返回 !否则编译错误 E0308
  2. 禁止调用任何 std 路径,包括 format! 隐式分配
  3. 若用 Rust 1.81+,可启用 -Z panic-abort-tests 让单元测试也走同一 handler,CI 统一行为

拓展思考

  1. 多架构适配:RISC-V 用 ebreak,ARM Cortex-M 用 udf #0xDE通过 cfg(target_arch) 统一封装
  2. panic 日志持久化:在 Flash 末尾预留 4 KiB“panic 分区”,panic 时关中断、写日志、CRC 校验,下次上电通过 bootloader 上传云端
  3. 与 RTOS 集成:如果底层是 Zephyr/Rust 混合工程,可把 rust_panic 映射到 k_oops()让 C 侧 trace 也能抓到 Rust 崩溃上下文
  4. 安全关键领域:ISO 26262 要求不可恢复故障必须进入安全状态,此时 panic_handler 里不再打印,直接激活安全关断路径,并通过 双核锁步 确认另一核也已停转。