comprehensive-rust 裸机实战:基于 aarch64-rt 编写 AArch64 异常处理器(Exception Handlers)
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
本篇技术指南来自 Google Android 团队的 Rust 课程项目 comprehensive-rust 的裸机(bare-metal)实验部分,聚焦于aarch64-rtcrate 提供的异常处理抽象:它用一个ExceptionHandlerstrait 和一个exception_handlers!宏,替代手工编写异常向量表的汇编代码,让开发者可以纯 Rust 地定义同步异常(Synchronous)、IRQ、FIQ 与 SError 的处理逻辑。读完本文,你将掌握 AArch64 异常向量表的 16 项布局、aarch64-rt的 trait + 宏使用范式、RegisterStateRef的底层含义,以及如何在 QEMU 上运行完整可验证的示例。
背景:AArch64 的 16 项异常向量表
在动手使用aarch64-rt之前,需要先理解它背后封装的硬件模型。AArch64 架构定义了一张包含16 个条目的异常向量表,由4 种异常类型×4 种来源状态组合而成:
- 异常类型:同步异常(synchronous)、IRQ、FIQ、SError;
- 来源状态:
- 当前异常级别(current EL)使用 SP0;
- 当前异常级别使用 SPx(当前栈指针);
- 低异常级别(lower EL)使用 AArch64;
- 低异常级别使用 AArch32。
这一模型在课程文档 src/bare-metal/aps/exceptions.md 中作了明确说明。课程示例全部运行在EL1(Exception Level 1),为了保持简单,示例并未区分当前 EL 下的 SP0 与 SPx,也没有区分低 EL 下的 AArch32 与 AArch64——这意味着 16 个条目最终收敛到 8 个不同的处理函数(见下文手工实现)。
异常发生时,CPU 会跳转到向量表中对应条目,硬件上下文切换需要保存通用寄存器与elr_el1、spsr_el1等系统寄存器,随后才能安全地进入 Rust 代码。
手工实现回顾:汇编 +no_mangle函数
在引入aarch64-rt之前,课程示例用手写汇编构建了完整的 EL1 异常向量表。汇编文件 src/bare-metal/aps/examples/src/exceptions.S 展示了其核心结构:
- 向量表位于
.text.vector_table_el1段,符号vector_table_el1通过.balign 0x800保证 2 KiB 对齐; - 每个条目通过
.balign 0x80保持 128 字节(32 条指令)间隔; save_volatile_to_stack宏用stp指令将 x0–x18、x29、x30 压栈,并用mrs x0, elr_el1/mrs x1, spsr_el1保存返回地址与程序状态,以便支持嵌套异常时的栈回溯;current_exception宏将「保存易失寄存器 → 跳转 Rust 处理函数 → 恢复寄存器 →eret」串成一条流水线,整套保存与恢复各占 14 条指令,加上跳转/返回共 30 条指令,恰好控制在 32 条指令上限之内。
对应的 Rust 端(src/bare-metal/aps/examples/src/exceptions.rs)则用#[unsafe(no_mangle)] extern "C"导出 8 个全局处理函数:
use log::error; use smccc::Hvc; use smccc::psci::system_off; // SAFETY: There is no other global function of this name. #[unsafe(no_mangle)] extern "C" fn sync_current(_elr: u64, _spsr: u64) { error!("sync_current"); system_off::<Hvc>().unwrap(); } // irq_current / fiq_current / serror_current / sync_lower / irq_lower // fiq_lower / serror_lower 结构相同,仅日志文本不同每个函数接收elr_el1与spsr_el1作为前两个参数(由汇编中的mrs指令准备好),示例中只是记录日志并调用 PSCIsystem_off关机——因为课程预期这些异常在实际运行中不会发生。
aarch64-rt抽象:trait + 宏
手工方案可行但繁琐:每增加一个处理器都要维护两处代码(汇编表 + Rust 函数)。aarch64-rt将这个模式抽象为两个构件,正如核心文档 src/bare-metal/aps/aarch64-rt/exceptions.md 所述:
aarch64-rtprovides a trait to define exception handlers, and a macro to generate the assembly code for the exception vector to call them.
ExceptionHandlerstrait:用于定义异常处理器;exception_handlers!宏:生成调用这些处理器的异常向量汇编代码。
trait 的每个方法都有默认实现——默认行为是直接panic,因此对于预期不会发生的异常,可以放心省略对应的方法,代码量大幅精简。
完整代码示例
核心文档内嵌的示例来自 src/bare-metal/aps/examples/src/exceptions_rt.rs,这里给出完整版本:
use aarch64_rt::{ExceptionHandlers, RegisterStateRef, exception_handlers}; use log::error; use smccc::Hvc; use smccc::psci::system_off; struct Handlers; impl ExceptionHandlers for Handlers { extern "C" fn sync_current(_state: RegisterStateRef) { error!("sync_current"); system_off::<Hvc>().unwrap(); } extern "C" fn irq_current(_state: RegisterStateRef) { error!("irq_current"); system_off::<Hvc>().unwrap(); } extern "C" fn fiq_current(_state: RegisterStateRef) { error!("fiq_current"); system_off::<Hvc>().unwrap(); } extern "C" fn serror_current(_state: RegisterStateRef) { error!("serror_current"); system_off::<Hvc>().unwrap(); } } exception_handlers!(Handlers);使用要点:
- 定义一个空结构体(如
Handlers),为它实现ExceptionHandlers; - 每个方法必须是
extern "C"调用约定,参数为RegisterStateRef; - 只覆盖你关心的异常类型,其余继承默认的 panic 行为;
- 最后调用
exception_handlers!(Handlers)生成向量表。
相比手工版本,这里不再需要#[unsafe(no_mangle)],也不用手动约定elr/spsr参数位置,宏会负责把汇编层的寄存器状态包装好后交给 Rust 方法。
底层原理:宏如何生成异常向量
exception_handlers!宏并非魔法,它在背后做了和手工exceptions.S相同的工作。文档明确指出:
- 宏会生成一个
global_asm!块,内含完整的异常向量,用于跳转到 Rust 代码——这与之前手写的exceptions.S是同一套思路; RegisterStateRef是对栈帧的引用封装——汇编代码在异常发生时把寄存器值保存到栈上,RegisterStateRef就指向这块保存区。
RegisterStateRef的两种典型用途:
- 提取参数:当异常来自低 EL(lower EL)时,例如收到一个 SMC(安全监控调用)或 HVC(虚拟机管理调用),可以从保存的寄存器栈帧中读出调用方传入的参数;
- 改写恢复值:在处理函数返回、寄存器从栈上恢复之前,修改栈帧中保存的寄存器值,从而改变
eret返回后 CPU 的状态——这是实现系统调用参数回写、返回值注入等功能的基石。
也就是说,aarch64-rt把「汇编负责保存/恢复、Rust 负责策略」的边界固定下来,RegisterStateRef就是跨越这条边界的窗口。
两种实现方式对比
| 维度 | 手工实现 | aarch64-rt实现 |
|---|---|---|
| 向量表 | 手写exceptions.S汇编 | exception_handlers!宏生成global_asm! |
| Rust 端 | #[unsafe(no_mangle)] extern "C"函数 | impl ExceptionHandlerstrait 方法 |
| 参数传递 | 显式_elr: u64, _spsr: u64 | RegisterStateRef(寄存器栈帧引用) |
| 未处理异常 | 必须为全部 16 项提供函数 | trait 默认实现直接 panic,可省略 |
| 代码量 | 汇编 + 8 个函数 | 1 个 impl 块 + 1 个宏调用 |
在完整示例中集成:entry!与initial_pagetable!
异常处理器不是孤立存在的,课程示例 src/bare-metal/aps/examples/src/main_rt.rs 展示了它与aarch64-rt其他能力的组合使用。该文件顶部通过mod exceptions_rt;引入上文实现的异常模块,然后:
- 用
initial_pagetable!宏在 Rust 中定义初始静态页表(取代此前手工idmap.S的做法),把 1 GiB 设备内存、1 GiB 普通内存以及 256 GiB 处的另一块设备内存放入恒等映射; - 用
entry!(main)宏标记入口函数,main(x0, x1, x2, x3)接收启动时由汇编传入的 4 个参数; - 直接使用
arm-pl011-uartcrate 的Uart驱动输出日志,而不是自己实现 PL011; panic_handler同样调用system_off::<Hvc>()关机。
异常处理器、页表、入口点与 UART 驱动全部由aarch64-rt生态接管,这正是该 crate 的设计目标——如 src/bare-metal/aps/aarch64-rt.md 所说,它提供了此前手工实现的汇编入口点和异常向量。
依赖版本可在 src/bare-metal/aps/examples/Cargo.toml 中确认:aarch64-rt = "0.4.3"、aarch64-paging = "0.12.1"、arm-pl011-uart = "0.5.0"、smccc = "0.2.3"、log = "0.4.30"。
在 QEMU 中运行验证
在src/bare-metal/aps/examples目录下,Makefile(src/bare-metal/aps/examples/Makefile)提供了现成的运行目标:
make qemu_rt该目标依次执行:cargo build→cargo objcopy --bin rt -- -O binary rt.bin→ 启动 QEMU:
qemu-system-aarch64 -machine virt -cpu max -serial mon:stdio -display none -kernel rt.bin -s模拟器使用virt机器与maxCPU,通过串口输出日志。在正常运行路径下,程序打印main(...)参数后调用 PSCI 关机;若异常被触发(例如访问非法地址),sync_current等处理器会输出sync_current之类的错误日志后再关机——这为验证异常处理逻辑提供了直接的观测手段。
异常处理器与并发模型:Send / Sync 视角
文档还给出了一个重要的思维模型:异常处理器与主执行上下文之间的关系,可以类比为不同的线程。
与线程间通信一样,两者之间能共享什么数据,由Send与Sync决定。举例来说:
- 如果某个值要在异常处理器与程序其他部分之间共享,且它是
Send但不是Sync(例如RefCell这类内部可变但非线程安全的类型),就需要把它包装进Mutex之类的同步原语,并放入static中; - 如果异常处理器与主流程并发访问共享状态,必须保证访问的原子性,否则可能产生数据竞争——这在裸机环境同样成立,因为没有操作系统替你兜底。
记住这一点,可以避免写出在异常上下文中触发未定义行为的共享逻辑。
实践要点小结
- 只实现需要的异常:
ExceptionHandlers的默认方法会 panic,未预期异常可以整体省略; - 保持
extern "C"约定:trait 方法必须标注extern "C",参数类型为RegisterStateRef; - 用好
RegisterStateRef:它是读写寄存器栈帧的入口,是实现 SMC/HVC 参数提取与返回值注入的关键; - 向量表对齐与指令预算:若你仍需要手写汇编,注意 2 KiB 表对齐、128 字节条目间隔以及 32 条指令上限(详见 src/bare-metal/aps/examples/src/exceptions.S 中的注释);
- 共享状态考虑 Send/Sync:异常上下文近似线程,共享数据需要合适的同步原语。
aarch64-rt将课程中手工实现的汇编异常向量、入口点、初始页表一并收编为安全、可组合的 Rust 抽象,让开发者把精力聚焦在异常处理策略本身——这也是在 bare-metal 场景下用 Rust 写出可维护系统软件的一条清晰路径。进一步可阅读 src/bare-metal/aps/exceptions.md 了解手工方案的完整对比,以及 src/bare-metal/aps/aarch64-rt.md 了解该 crate 的入口点与页表能力。
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考