news 2026/9/10 18:10:35

comprehensive-rust 裸机实战:基于 aarch64-rt 编写 AArch64 异常处理器(Exception Handlers)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
comprehensive-rust 裸机实战:基于 aarch64-rt 编写 AArch64 异常处理器(Exception Handlers)

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;
  • 来源状态
    1. 当前异常级别(current EL)使用 SP0;
    2. 当前异常级别使用 SPx(当前栈指针);
    3. 低异常级别(lower EL)使用 AArch64;
    4. 低异常级别使用 AArch32。

这一模型在课程文档 src/bare-metal/aps/exceptions.md 中作了明确说明。课程示例全部运行在EL1(Exception Level 1),为了保持简单,示例并未区分当前 EL 下的 SP0 与 SPx,也没有区分低 EL 下的 AArch32 与 AArch64——这意味着 16 个条目最终收敛到 8 个不同的处理函数(见下文手工实现)。

异常发生时,CPU 会跳转到向量表中对应条目,硬件上下文切换需要保存通用寄存器与elr_el1spsr_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_el1spsr_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);

使用要点:

  1. 定义一个空结构体(如Handlers),为它实现ExceptionHandlers
  2. 每个方法必须是extern "C"调用约定,参数为RegisterStateRef
  3. 只覆盖你关心的异常类型,其余继承默认的 panic 行为;
  4. 最后调用exception_handlers!(Handlers)生成向量表。

相比手工版本,这里不再需要#[unsafe(no_mangle)],也不用手动约定elr/spsr参数位置,宏会负责把汇编层的寄存器状态包装好后交给 Rust 方法。

底层原理:宏如何生成异常向量

exception_handlers!宏并非魔法,它在背后做了和手工exceptions.S相同的工作。文档明确指出:

  • 宏会生成一个global_asm!,内含完整的异常向量,用于跳转到 Rust 代码——这与之前手写的exceptions.S是同一套思路;
  • RegisterStateRef是对栈帧的引用封装——汇编代码在异常发生时把寄存器值保存到栈上,RegisterStateRef就指向这块保存区。

RegisterStateRef的两种典型用途:

  1. 提取参数:当异常来自低 EL(lower EL)时,例如收到一个 SMC(安全监控调用)或 HVC(虚拟机管理调用),可以从保存的寄存器栈帧中读出调用方传入的参数;
  2. 改写恢复值:在处理函数返回、寄存器从栈上恢复之前,修改栈帧中保存的寄存器值,从而改变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: u64RegisterStateRef(寄存器栈帧引用)
未处理异常必须为全部 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 buildcargo 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 视角

文档还给出了一个重要的思维模型:异常处理器与主执行上下文之间的关系,可以类比为不同的线程

与线程间通信一样,两者之间能共享什么数据,由SendSync决定。举例来说:

  • 如果某个值要在异常处理器与程序其他部分之间共享,且它是Send但不是Sync(例如RefCell这类内部可变但非线程安全的类型),就需要把它包装进Mutex之类的同步原语,并放入static中;
  • 如果异常处理器与主流程并发访问共享状态,必须保证访问的原子性,否则可能产生数据竞争——这在裸机环境同样成立,因为没有操作系统替你兜底。

记住这一点,可以避免写出在异常上下文中触发未定义行为的共享逻辑。

实践要点小结

  1. 只实现需要的异常ExceptionHandlers的默认方法会 panic,未预期异常可以整体省略;
  2. 保持extern "C"约定:trait 方法必须标注extern "C",参数类型为RegisterStateRef
  3. 用好RegisterStateRef:它是读写寄存器栈帧的入口,是实现 SMC/HVC 参数提取与返回值注入的关键;
  4. 向量表对齐与指令预算:若你仍需要手写汇编,注意 2 KiB 表对齐、128 字节条目间隔以及 32 条指令上限(详见 src/bare-metal/aps/examples/src/exceptions.S 中的注释);
  5. 共享状态考虑 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 18:10:10

配电网最优潮流二阶锥松弛建模与Matlab代码实现详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 18:09:23

STM32静态库制作与链接原理:从.a文件到固件

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 18:08:23

数据仓库四层架构详解:从ODS到ADS的落地实践与数据科学应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 18:05:48

AI驱动的元数据语义补全:多源协同推理实战方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 18:05:35

数字化转型成熟度模型详解:五级等级与七大能力域评估指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 18:04:38

童年铅暴露与中年大脑健康:JAMA追踪几十年的研究揭示什么

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华