第一节 异常处理
按照 RiscV 规范,异常和中断被触发时,当前的执行流程被打断,跳转到异常向量表中相应的例程进行处理。其中,stvec 寄存器指向的是向量表的基地址,scause 寄存器记录的异常编号则作为例程入口的偏移,二者相加得到异常处理例程的最终地址。
本节中,我们将准备一个符合规范的异常向量表,在内核启动阶段设置到 stvec 寄存器,此后,就可以逐步扩展向量表中注册的例程,去支持各种异常。看一下异常向量表,它采用汇编方式定义:
// axhal/src/riscv64/trap.S
.macro SAVE_REGS
addi sp, sp, -{trapframe_size}
PUSH_GENERAL_REGS
csrr t0, sepc
csrr t1, sstatus
csrrw t2, sscratch, zero // save sscratch (sp) and zero it
sd t0, 31*8(sp) // tf.sepc
sd t1, 32*8(sp) // tf.sstatus
sd t2, 1*8(sp) // tf.regs.sp
.endm
.macro RESTORE_REGS
ld t0, 31*8(sp)
ld t1, 32*8(sp)
csrw sepc, t0
csrw sstatus, t1
POP_GENERAL_REGS
ld sp, 1*8(sp) // load sp from tf.regs.sp
.endm
.section .text
.balign 4
.global trap_vector_base
trap_vector_base:
SAVE_REGS
mv a0, sp
call riscv_trap_handler
RESTORE_REGS
sret
异常向量表实际是在 riscv_trap_handler
中处理,但在真正处理异常的前后,分别需要保存和恢复原始的上下文,我们称之为异常上下文,以与上一章提到的任务上下文进行区别。
在内核中,存在多种相互独立的运行环境,每一种环境都抽象为一种上下文,内核需要在必要时进行切换。
在前面一章 - 多任务支持中,重点说明了任务上下文,用于任务间切换;本节的上下文对应异常处理的场景,称之为异常上下文。
关于两种上下文的示意图:
![]()
注意:异常上下文实际是基于当前任务的栈进行保存和恢复的。
然后看一下异常处理的具体逻辑:
#[no_mangle]
fn riscv_trap_handler(tf: &mut TrapFrame) {
let scause = scause::read();
match scause.cause() {
Trap::Exception(E::Breakpoint) => handle_breakpoint(&mut tf.sepc),
_ => {
panic!(
"Unhandled trap {:?} @ {:#x}:\n{:#x?}",
scause.cause(),
tf.sepc,
tf
);
}
}
}
fn handle_breakpoint(sepc: &mut usize) {
log::debug!("Exception(Breakpoint) @ {:#x} ", sepc);
*sepc += 2
}
先处理 BreakPoint
异常作为一个示例,简单的输出触发指令的地址,然后地址加 2,跳到下一条指令。以后可以仿照该异常,扩展异常处理的范围。
最后,设置异常上下文向量表:
// axhal/src/riscv64.rs
unsafe extern "C" fn rust_entry(hartid: usize, dtb: usize) {
extern "C" {
fn trap_vector_base();
fn rust_main(hartid: usize, dtb: usize);
}
trap::set_trap_vector_base(trap_vector_base as usize);
rust_main(hartid, dtb);
}
测试异常机制,在 axorigin 中通过 ebreak
指令触发,make run LOG=debug
查看日志。验证成功。