如何编写 reset handler?
解读
在国内嵌入式/系统级 Rust 岗位面试中,“reset handler” 并不是让你背诵标准库 API,而是考察候选人能否在 “无标准库(no_std)+ 裸机(bare-metal)” 环境下,用 Rust 完成 “上电第一条指令” 到 “Rust 运行时初始化” 的完整链条。
面试官常通过此题验证三点:
- 是否理解 Cortex-M/RISC-V 等国产 MCU/SoC 的启动流程(取中断向量表第 0 项作为 SP,第 1 项作为 PC);
- 能否用 Rust 内联汇编/全局汇编 精确放置符号到
.vector_table段,并保证 链接脚本与 rustc codegen 对齐; - 是否掌握 零成本抽象 地初始化
.data、.bss、时钟、FPU、MPU,最终安全跳转到main,同时 不给后续代码留下任何未定义行为(UB)。
知识点
- #[naked] 函数:禁止编译器生成序言/尾声,用于编写 纯汇编入口;
- #[link_section = ".vector_table.reset_vector"]:把符号硬塞进 中断向量表第二条条目,确保 上电后 CPU 第一条执行的就是你的符号;
- extern "Rust" fn main() -> !:Rust 2018 版本后约定 的 应用程序单入口,必须由 reset handler 最终调用;
- r0 零开销初始化库:用 纯 Rust 实现 memcpy-fill 算法,在 const fn 里完成
.data搬运与.bss清零,不依赖 libc; - cortex-m-rt 与 riscv-rt 官方 crate:PAC(Peripheral Access Crate) 之上最薄的一层运行时,面试时需能徒手写出其简化版;
- 链接脚本关键语句:
KEEP(*(.vector_table))防止 LTO 把向量表优化掉;. = ALIGN(4); PROVIDE(_stext = .);保证 代码段 4 字节对齐,符合 国内车规芯片 的 ECC 要求;
- 编译器屏障:asm!("", options(nostack, nomem, preserves_flags)) 防止 LLVM 重排 初始化顺序,在国产航电 MCU 上必须通过适航审查;
- 安全抽象边界:一旦进入
main(),必须保证 &mut 引用满足 Rust 别名规则,否则 后续借用检查会误判。
答案
以下代码基于 国产 CH32V307(RISC-V,青稞内核) 裸机环境,无需任何 C 文件,可直接烧录。
重点展示 “汇编入口 → Rust 初始化 → 进入安全 main” 全过程,符合国内芯片厂面试白板编程难度。
// 1. 汇编入口,硬塞到向量表
#[naked]
#[link_section = ".vector_table.reset_vector"]
#[export_name = "_start"]
unsafe extern "C" fn reset_handler() -> ! {
core::arch::asm!(
// 1.1 重新取栈顶,防止 BootROM 篡改
"la sp, _estack",
// 1.2 保存全局指针,青稞内核要求 gp 在 .data 初始化前有效
"la gp, __global_pointer$",
// 1.3 调用 Rust 写的 sys_init
"call {sys_init}",
// 1.4 绝不返回
"j _panic_loop",
sys_init = sym sys_init,
options(noreturn)
)
}
// 2. Rust 运行时初始化,完全零开销
#[inline(never)]
unsafe fn sys_init() -> ! {
// 2.1 初始化 .data 段:从 FLASH 到 RAM
let mut src = &_sidata as *const u32;
let mut dst = &mut _sdata as *mut u32;
while dst < &_edata as *const u32 as *mut u32 {
dst.write(src.read());
dst = dst.add(1);
src = src.add(1);
}
// 2.2 清零 .bss
let mut bss = &mut _sbss as *mut u32;
while bss < &_ebss as *const u32 as *mut u32 {
bss.write(0);
bss = bss.add(1);
}
// 2.3 使能 FPU(青稞单精度)
const MSTATUS_FS: usize = 0x00006000;
let mstatus: usize;
core::arch::asm!("csrr {}, mstatus", out(reg) mstatus);
core::arch::asm!("csrw mstatus, {}", in(reg) mstatus | MSTATUS_FS);
// 2.4 数据/指令屏障,防止重排
core::arch::asm!("", options(nostack, nomem, preserves_flags));
// 2.5 进入安全 Rust 世界
extern "Rust" {
fn main() -> !;
}
main()
}
// 3. 链接脚本片段(memory.x)
/*
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}
SECTIONS
{
.vector_table ORIGIN(FLASH) :
{
KEEP(*(.vector_table))
} >FLASH
.text ALIGN(4) :
{
*(.text .text.*)
} >FLASH
.rodata ALIGN(4) :
{
*(.rodata .rodata.*)
} >FLASH
_sidata = LOADADDR(.data);
.data ALIGN(4) :
{
_sdata = .;
*(.data .data.*)
_edata = .;
} >RAM AT>FLASH
.bss ALIGN(4) :
{
_sbss = .;
*(.bss .bss.*)
_ebss = .;
} >RAM
}
*/
// 4. 用户 main,完全 safe
#[inline(never)]
fn main() -> ! {
let _x = 42;
loop {
// 点亮国产开发板 LED
unsafe {
const GPIOA_ODR: *mut u32 = 0x4001080C as *mut u32;
GPIOA_ODR.write_volatile(GPIOA_ODR.read_volatile() ^ 1 << 0);
for _ in 0..100_000 { core::arch::asm!("nop"); }
}
}
}
// 5. 恐慌处理,符合国产车规“永不复位”要求
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { _panic_loop() }
}
#[naked]
#[inline(never)]
unsafe extern "C" fn _panic_loop() -> ! {
core::arch::asm!("wfi", options(noreturn))
}
关键技巧总结:
- #[naked] + link_section 保证 第一条指令地址绝对可控,面试时可直接在白板写出;
- 手写 r0 逻辑 而非依赖外部 crate,展示对“零成本抽象”的信仰;
- main() 完全 safe,证明 初始化完成后 Rust 所有权系统生效,不给面试官留 UB 把柄。
拓展思考
- 双核异构 SoC 的 reset handler:国产 XR872/ESP32-C3 等芯片 先跑 ROM 第二级 bootloader,再跳转到用户 reset vector。此时需用 Rust 编写二级 bootloader 的验证签名逻辑,在跳用户固件前关闭 RISC-V 调试接口,防止 JTAG 注入攻击。
- 安全启动(Secure Boot)与 Rust 常量求值:利用 const fn + inline-asm 在 编译期计算 SHA-256 摘要,把 公钥哈希硬编码进 .vector_table 末尾,reset handler 第一步就比对,失败直接擦除全部 Flash,满足国密 SM4/SM3 要求;
- M 核与 A 核的 AMP 场景:Rust reset handler 只负责 M 核,A 核 Linux 由 separate binary 启动。需用 Rust 实现 RPMsg 协议栈,在 reset handler 里初始化共享内存环形队列,保证 0-copy 通信;
- 功能安全 ISO 26262 ASIL-D:reset handler 必须记录上电原因(BOR/POR/Watchdog) 到 备份寄存器,**Rust 代码需用 “单入口单出口” 原则,禁止任何动态内存分配,全部用 const-generic 数组 替代 Vec,以满足静态分析工具 Cantata 的 MC/DC 覆盖率要求。