如何编写 reset handler?

解读

在国内嵌入式/系统级 Rust 岗位面试中,“reset handler” 并不是让你背诵标准库 API,而是考察候选人能否在 “无标准库(no_std)+ 裸机(bare-metal)” 环境下,用 Rust 完成 “上电第一条指令”“Rust 运行时初始化” 的完整链条。
面试官常通过此题验证三点:

  1. 是否理解 Cortex-M/RISC-V 等国产 MCU/SoC 的启动流程(取中断向量表第 0 项作为 SP,第 1 项作为 PC);
  2. 能否用 Rust 内联汇编/全局汇编 精确放置符号到 .vector_table 段,并保证 链接脚本与 rustc codegen 对齐
  3. 是否掌握 零成本抽象 地初始化 .data.bss、时钟、FPU、MPU,最终安全跳转到 main,同时 不给后续代码留下任何未定义行为(UB)

知识点

  1. #[naked] 函数:禁止编译器生成序言/尾声,用于编写 纯汇编入口
  2. #[link_section = ".vector_table.reset_vector"]:把符号硬塞进 中断向量表第二条条目,确保 上电后 CPU 第一条执行的就是你的符号
  3. extern "Rust" fn main() -> !Rust 2018 版本后约定应用程序单入口,必须由 reset handler 最终调用;
  4. r0 零开销初始化库:用 纯 Rust 实现 memcpy-fill 算法,在 const fn 里完成 .data 搬运与 .bss 清零,不依赖 libc
  5. cortex-m-rt 与 riscv-rt 官方 cratePAC(Peripheral Access Crate) 之上最薄的一层运行时,面试时需能徒手写出其简化版
  6. 链接脚本关键语句
    • KEEP(*(.vector_table)) 防止 LTO 把向量表优化掉;
    • . = ALIGN(4); PROVIDE(_stext = .); 保证 代码段 4 字节对齐,符合 国内车规芯片 的 ECC 要求;
  7. 编译器屏障asm!("", options(nostack, nomem, preserves_flags)) 防止 LLVM 重排 初始化顺序,在国产航电 MCU 上必须通过适航审查
  8. 安全抽象边界:一旦进入 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 把柄

拓展思考

  1. 双核异构 SoC 的 reset handler:国产 XR872/ESP32-C3 等芯片 先跑 ROM 第二级 bootloader,再跳转到用户 reset vector。此时需用 Rust 编写二级 bootloader 的验证签名逻辑在跳用户固件前关闭 RISC-V 调试接口,防止 JTAG 注入攻击
  2. 安全启动(Secure Boot)与 Rust 常量求值:利用 const fn + inline-asm编译期计算 SHA-256 摘要,把 公钥哈希硬编码进 .vector_table 末尾,reset handler 第一步就比对失败直接擦除全部 Flash满足国密 SM4/SM3 要求
  3. M 核与 A 核的 AMP 场景Rust reset handler 只负责 M 核A 核 Linux 由 separate binary 启动。需用 Rust 实现 RPMsg 协议栈在 reset handler 里初始化共享内存环形队列保证 0-copy 通信
  4. 功能安全 ISO 26262 ASIL-Dreset handler 必须记录上电原因(BOR/POR/Watchdog)备份寄存器,**Rust 代码需用 “单入口单出口” 原则,禁止任何动态内存分配全部用 const-generic 数组 替代 Vec以满足静态分析工具 Cantata 的 MC/DC 覆盖率要求