Comprehensive Rust 课程实战:AArch64 裸机异常向量表与 Rust 异常处理实现 📅 发布时间:2026/9/10 17:39:01 👁 浏览次数: Comprehensive Rust 课程实战AArch64 裸机异常向量表与 Rust 异常处理实现【免费下载链接】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导读在 Comprehensive Rust 课程Google Android 团队使用的 Rust 教学项目的 Application Processors应用处理器专题中exceptions.md讲解了一个核心裸机知识点如何在 AArch64 架构上为 EL1 异常级别搭建 16 条目的异常向量表并用汇编保存易失寄存器后进入 Rust 代码执行处理逻辑。本文以该文档为骨架结合仓库中src/bare-metal/aps/examples/的完整源码exceptions.rs、exceptions.S、entry.S等系统讲解 AArch64 异常模型、汇编宏的寄存器保存/恢复布局、向量表内存对齐规则、启动时代码设置VBAR_EL1的过程以及aarch64-rtcrate 提供的 trait 抽象方案帮助读者完整掌握在无操作系统环境下用 Rust 汇编处理异常的实战方法。一、背景为什么 AArch64 裸机程序需要自建异常向量表在微控制器如 Arm Cortex-M 系列中异常处理通常由厂商提供的启动文件或中断向量表机制直接承接。而应用处理器Application Processor场景下——课程使用 QEMU 的 aarch64virt开发板采用-machine virt -cpu max参数模拟——我们像编写操作系统一样直接面对硬件异常向量表必须由我们自己放在正确的地址并把处理逻辑接进 Rust。AArch64 架构定义了一张包含16 个条目的异常向量表结构为4 种异常类型 × 4 种来源状态来源状态 \ 异常类型Synchronous同步异常IRQFIQSError当前 EL使用 SP0✓✓✓✓当前 EL使用 SPxSP_ELx✓✓✓✓低 ELAArch64 状态✓✓✓✓低 ELAArch32 状态✓✓✓✓其中Synchronous同步异常包括未定义指令、系统调用SVC/HVC/SMC、数据中止、指令中止、栈对齐错误等IRQ / FIQ中断请求与快速中断请求属于异步异常SError系统错误属于异步异常。向量表条目之间以0x80128 字节为步长对齐这正是exceptions.S中连续出现的.balign 0x80指令的作用。整个向量表本身还必须 2KB0x800对齐对应汇编中的.balign 0x800。课程文档明确指出本次下午练习的所有示例都运行在EL1Exception Level 1即内核/OS 所在特权级。详见 src/bare-metal/aps/examples/src/exceptions.S。二、Rust 侧8 个extern C处理函数异常向量表用汇编编写但真正的处理逻辑放在 Rust 中。仓库中的 src/bare-metal/aps/examples/src/exceptions.rs 定义了与 16 个向量表条目一一对应的 8 个处理函数每条汇编入口跳转到同一个 Rust 函数故 16 条目只需 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(); } #[unsafe(no_mangle)] extern C fn irq_current(_elr: u64, _spsr: u64) { error!(irq_current); system_off::Hvc().unwrap(); } // ...fiq_current、serror_current、sync_lower、irq_lower、fiq_lower、serror_lower 同构关键设计要点#[unsafe(no_mangle)]extern C汇编通过bl带链接分支直接调用这些函数因此必须使用 C ABI 并保留符号原名保证汇编侧符号解析成功。源码注释中每条都标注了// SAFETY: There is no other global function of this name.即安全前提是仓库内不存在同名的其他全局函数no_mangle不会导致符号冲突。两个形参_elr: u64与_spsr: u64汇编侧在调用前会把elr_el1异常返回地址寄存器和spsr_el1保存的程序状态寄存器读入x0、x1恰好作为 C ABI 下函数的第一、第二个参数。这样 Rust 处理函数可以直接拿到异常发生时在哪条指令、处于什么 PSTATE为后续恢复/诊断提供依据。处理策略记录日志并关机每个处理函数都调用log::error!输出异常类型名随后调用system_off::Hvc().unwrap()通过 PSCI 固件接口以 HVC 调用方式关闭系统。课程文档说明这样做的原因示例中不预期任何异常真正发生一旦发生只记录并停机便于在 QEMU 串口上看到是哪一类异常被触发。三、汇编侧保存易失寄存器并转入 Rust汇编实现位于 src/bare-metal/aps/examples/src/exceptions.S核心思路是用两条汇编宏分别完成保存易失寄存器到栈与从栈恢复中间插入一次对 Rust 处理函数的调用。3.1save_volatile_to_stack14 条指令的现场保存.macro save_volatile_to_stack /* Reserve stack space and save registers x0-x18, x29 x30. */ stp x0, x1, [sp, #-(8 * 24)]! stp x2, x3, [sp, #8 * 2] stp x4, x5, [sp, #8 * 4] stp x6, x7, [sp, #8 * 6] stp x8, x9, [sp, #8 * 8] stp x10, x11, [sp, #8 * 10] stp x12, x13, [sp, #8 * 12] stp x14, x15, [sp, #8 * 14] stp x16, x17, [sp, #8 * 16] str x18, [sp, #8 * 18] stp x29, x30, [sp, #8 * 20] /* Save elr_el1 spsr_el1... */ mrs x0, elr_el1 mrs x1, spsr_el1 stp x0, x1, [sp, #8 * 22] .endm要点一次性压栈 24 个 8 字节槽位共 192 字节覆盖x0–x18、x29帧指针、x30链接寄存器以及elr_el1、spsr_el1同时保存elr_el1/spsr_el1的目的见源码注释是支持嵌套异常发生时仍能正确回溯unwind宏末尾把elr_el1、spsr_el1分别装入x0、x1正好满足 Rust 处理函数(elr, spsr)的参数约定。3.2restore_volatile_from_stack对称恢复.macro restore_volatile_from_stack ldp x2, x3, [sp, #8 * 2] /* ... 依次恢复 x2-x18, x29, x30 ... */ ldp x0, x1, [sp, #8 * 22] msr elr_el1, x0 msr spsr_el1, x1 ldp x0, x1, [sp], #8 * 24 .endm恢复时先还原通用寄存器再把栈上保存的elr_el1、spsr_el1写回系统寄存器用x0/x1作临时寄存器最后恢复x0/x1并释放 192 字节栈空间。3.3current_exception组装完整处理流程.macro current_exception handler:req save_volatile_to_stack bl \handler restore_volatile_from_stack eret .endm保存现场 → 调用 Rust 处理函数 → 恢复现场 →eret返回被中断的执行流。值得注意的工程细节源码注释有明确计算保存现场 跳转共 15 条指令恢复 返回共 15 条指令整个 handler 恰好 30 条指令能塞进 128 字节32 条指令的向量表条目配额内——这正是把保存/恢复提取为宏而不是逐条展开的根本原因。3.4 16 条目向量表.section .text.vector_table_el1, ax .global vector_table_el1 .balign 0x800 vector_table_el1: sync_cur_sp0: current_exception sync_current .balign 0x80 irq_cur_sp0: current_exception irq_current /* ... 依序排列到 serr_lower_32 ... */16 个条目的排布严格遵循 AArch64 规范顺序当前 ELSP0四类 → 当前 ELSPx四类 → 低 EL AArch64 四类 → 低 EL AArch32 四类。课程文档同时说明了两处刻意简化不区分 SP0 与 SPx当前 EL 的两组异常共用一个处理函数不区分 AArch32 与 AArch64 低 EL 异常。四、启动流程把向量表地址写进VBAR_EL1有了向量表还不够——CPU 必须知道它在哪里。这一职责由启动汇编 src/bare-metal/aps/examples/src/entry.S 承担在跳转 Rustmain之前完成/* Prepare the stack. */ adr_l x30, boot_stack_end mov sp, x30 /* Set up exception vector. */ adr x30, vector_table_el1 msr vbar_el1, x30 /* Call into Rust code. */ bl mainVBAR_EL1Vector Base Address Register持有异常向量表基地址entry.S注释总结了整个入口的职责链配置 MMU 恒等映射并开启缓存 → 开启浮点 → 清零 bss → 准备栈 → 设置异常向量 → 跳转 Rustmain。同时entry.S明确提到入口会保留x0–x3给 Rust 入口点因为它们可能携带引导参数这正是main_rt.rs中fn main(x0: u64, x1: u64, x2: u64, x3: u64)四个参数的来源。五、把异常处理看作线程Send/Sync的考量课程文档给出一条非常实用的心智模型可以把异常处理函数与主执行上下文近似地视为不同的线程。因此想在两者之间共享的数据其类型必须满足相应的Send/Sync约束与线程间共享数据完全同理例如某值只Send而不Sync可移动所有权但不可多线程同时引用那么在异常处理函数与程序其他部分之间共享它时就需要用Mutex之类的互斥原语包裹后放入static静态变量中。这一节对应课程 Send 和 Sync 专题的内容在裸机环境下同样是编写安全并发/中断代码的基本功。特别要注意Send/Sync是unsafe trait除非数据确实满足共享条件否则不能随意实现。六、进阶抽象用aarch64-rtcrate 免除手写汇编手写 16 条目向量表属于从零做起的教学路线。课程还提供了另一条生产级路线aarch64-rtcrate 的 trait 抽象对应文档 src/bare-metal/aps/aarch64-rt/exceptions.md 与示例 src/bare-metal/aps/examples/src/exceptions_rt.rsuse aarch64_rt::{ExceptionHandlers, RegisterStateRef, exception_handlers}; 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(); } // ... } exception_handlers!(Handlers);核心机制结合 src/bare-metal/aps/aarch64-rt/exceptions.md 的说明ExceptionHandlerstrait每个方法都有默认实现默认直接 panic因此不期望发生的异常可以干脆不重写对应方法代码更精简exception_handlers!宏编译期生成global_asm!块内含与手写版exceptions.S同构的异常向量表自动把汇编侧的栈帧接到 Rust 方法上RegisterStateRef包装异常发生时汇编保存寄存器栈帧的引用可用于从低 EL 的 SMC/HVC 调用中提取参数或在处理函数返回时更新将被恢复的寄存器值——为编写虚拟化/固件场景如 SMC/HVC 转发提供了便捷入口。对应的可运行程序是 src/bare-metal/aps/examples/src/main_rt.rs#![no_main] #![no_std]通过initial_pagetable!建立恒等页表0x900_0000处 PL011 UART 设备内存、普通内存与 256 GiB 处的设备内存entry!(main)注入启动入口main中通过Uart打印x0–x3引导参数后system_off。该二进制对应Cargo.toml中的rtbin 目标。七、构建与运行QEMU 上验证异常处理示例工程的构建与运行命令集中在 src/bare-metal/aps/examples/Makefilert.bin: build cargo objcopy --bin rt -- -O binary $ qemu_rt: rt.bin qemu-system-aarch64 -machine virt -cpu max -serial mon:stdio -display none -kernel $ -s流程为cargo build编译工程为 workspace依赖见 src/bare-metal/aps/examples/Cargo.toml含aarch64-paging、aarch64-rt、smccc、log等→cargo objcopy --bin rt -- -O binary生成裸二进制 → QEMU 以virt机器、maxCPU、串口直连终端的方式加载-kernel。运行后可在串口观察到main(0x0, 0x0, ...)之类的输出若程序运行中触发了异常则会看到对应处理函数打出的sync_current/irq_current等日志并触发关机——这就是验证向量表工作与否的直接手段。八、总结从课程文档 src/bare-metal/aps/exceptions.md 出发本文完整还原了 AArch64 EL1 裸机异常处理的四条主线异常模型16 条目向量表 4 种异常类型 × 4 种来源状态条目 128 字节对齐、整表 2KB 对齐汇编现场保护用save_volatile_to_stack/restore_volatile_from_stack两条宏在 30 条指令内完成保存、调用 Rust、恢复、eret的完整闭环并传递elr_el1、spsr_el1给 Rust 侧启动接线entry.S在进入 Rust 前把向量表地址写入VBAR_EL1两种落地方式手写汇编 extern C函数或aarch64-rt的 trait 宏抽象无论哪种都要遵循Send/Sync视角看待异常处理函数即隐式线程的共享数据纪律。这套实现既是 Comprehensive Rust 课程应用处理器专题的教学核心也是一份可以直接参考的裸机中断/异常处理工程范式。【免费下载链接】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),仅供参考