异常处理过程:
当我们遇到异常时,我们首先需要把当前程序P的状态保存起来,而后跳到异常处理程序进行诊断。
- 这里我们从指令集状态机S = {<R,M>}的视角来讨论咯 R为寄存器,M为内存。
异常处理程序和P事两个不同的程序,它们使用不同的M,所以:只要异常处理程序不随意修改P的M,则不必进行实质性的保存操作。
但是R只有一份,即P和异常处理程序共用寄存器,所以我们需要把P的寄存器状态保存起来。
但是R保存到哪里呢??
有几种方法:
1.保存到R:也就是增加新的一组寄存器,把P的寄存器状态复制到新寄存器中。
2.保存到M:也就是存到内存中,但是需要找到一处空闲的内存呢
3.保存到栈:栈有空间的话,这个可以有。
谁来保存?
- 硬件保存:在CPU状态机的控制下保存
- 软件保存: 通过指令控制CPU进行保存
所以保存R的设计,共有四种方法
R | M | |
---|---|---|
硬件保存 | 硬件保存到R_save | 硬件保存到M |
软件保存 | 软件保存到R_save | 软件保存到M |
当发生异常时的流程如下:P发生异常->硬件保存->跳转到异常处理程序->软件保存
这里要注意的是,当异常处理程序诊断时需要读取R_save。所以CPU还要添加相应的读取等指令。
RISCV硬件将PC保存到mepc这个特殊的寄存器中,也叫控制状态寄存器(CSR,control and status register)
CSR
用于控制和反映处理器状态的特殊寄存器(eg:mepc)
- 硬件发生某些事会自动更新CSR,或者从CSR中读出值直接用
- 软件也可以通过CSR指令来访问CSR
- 所以每个CSR都有一个软件可见的编号(CSR地址空间)
下面的图片中最上就是一条CSR指令,其中0x341代表mepc寄存器的地址空间(也是编号:-)
最简单异常处理还需要的CSR——mtvec
mtvec:异常处理程序的入口地址,即当发生异常时CPU自动跳转到这个地址
一个简单的异常处理程序
#include <klib.h>
void handler() {
uintptr_t mepc; // 定义一个变量来存储异常发生时的程序计数器 (Program Counter)
// 使用内联汇编读取 mepc 寄存器的值
asm volatile ("csrr %0, mepc" : "=r"(mepc));
printf("exception at mepc = %p\n", mepc); // 打印异常发生的地址
while (1); // 无限循环,防止返回
}
int main() {
// 设置 mtvec 寄存器,指向异常处理程序 handler
asm volatile ("csrw mtvec, %0" : :"r"(handler));
// 触发非法指令异常
asm volatile (".word 0"); // 这行代码试图执行一个无效的指令
printf("I am alive!\n");
while (1);
}
类似的还有mcause寄存器,即发生异常时,CPU将异常号写入这个CSR
具体的异常号如下
# RTFM了解异常号的含义
0 - Instruction address misaligned
1 - Instruction access fault
2 - Illegal Instruction
3 - Breakpoint
4 - Load address misaligned
5 - Load access fault
6 - Store/AMO address misaligned
7 - Store/AMO access fault
8 - Environment call from U-mode
9 - Environment call from S-mode
11 - Environment call from M-mode
12 - Instruction page fault
13 - Load page fault
15 - Store/AMO page fault
从异常状态返回
若诊断问题不大,P可以继续执行,则需要从异常处理程序返回P。那么返回需要先回复之前为P保存的状态(恢复寄存器就行)
- RISC架构通过load指令将M中保存的内容恢复到R,然后返回P
- 但是异常处理程序和P事两个不同的程序,不可以通过ret/jal返回
- jalr指令需要先把返回地址写入一个寄存器,但是那样会改变P的状态,如果返回后P需要用这个寄存器,就会报错。
所以综上,我们需要添加一条特殊的返回指令mret,即跳转到mepc中存放的地址
uintptr_t mepc, mcause;
// 读取 mepc 寄存器的值(异常发生时的程序计数器值)
asm volatile ("csrr %0, mepc" : "=r"(mepc));
// 读取 mcause 寄存器的值(异常原因)
asm volatile ("csrr %0, mcause" : "=r"(mcause));
// 打印异常原因和异常发生时的程序计数器值
printf("exception mcause = %p at mepc = %p\n", mcause, mepc);
// 仅限于演示,没有恢复其他寄存器
// 检查异常原因是否为2(通常表示非法指令异常)
if (mcause == 2) {
// 更新 mepc 寄存器的值为 mepc + 4(跳过导致异常的指令)
asm volatile ("csrw mepc, %0; mret" : : "r"(mepc + 4));
}
while (1);
硬件实现异常处理
在单周期实现异常
实现异常处理我们得先
-
实现CSR
-
添加CSR的读写指令,而后在指令异常时注意通用寄存器GPR和CSR之间的数据交换。
-
实现异常的触发,在译码时要检查非法指令,识别ecall指令等,识别到异常事件后,我们还需要通过电路更新mmepc,,mcause等CSR,而后跳转到mevec中存放的地址
-
实现mret指令,即跳转到mepc中存放的地址
异常处理的状态机模型
在异常处理状态下,状态机模型又需要改变了,状态机模型需要加一个扩展(CSR),也就是:
R = {PC, GPR,CSR},M无需扩展,注意在这里指令的执行并不是always成功了,定义一个函数
f_ex : S->{0,1},给定任意状态S:
- f_ex(S) = 0,则按照当前指令的语义进行状态转移
- f_ex\(S)= 1,则执行一条特殊指令raise_intr异常号,并更新状态如下
CSR[mepc] <- PC
CSR[mcause] <- 异常号
PC <- CSR[mtvec]
说白了,模型状态机一旦异常(f_ex(S)=1),就是把各种异常号,异常的pc值存到CSR寄存器中