我们之前都是假设指令可以成功执行
如果指令执行失败, 应该怎么办?
本次课内容:
计算机系统应该如何应对?
例如整数除0
-mcheck-zero-division
,
默认打开
编译器发现语法错误, 悄悄生成错误的代码
CPU作为数字电路, 要输出报错信息/自动修bug比较困难
回顾: 程序/指令集/CPU都是状态机
需要把当前程序P的状态保存起来, 并跳转到异常处理程序进行诊断
保存\(M\)的需求好像很奇怪: \(M\)这么大, 要保存到哪里?
一个观察: 异常处理程序和P是两个不同的程序, 它们使用不同的\(M\)
\(R\)只有一份, 异常处理程序也要用, 肯定要保存
要把P的\(R\)保存到哪里呢?
\(S = \{<R, M>\}\), 也只能保存到\(S\)里面了 😂
谁来保存?
\(R\) | \(M\) | |
---|---|---|
硬件保存 | 硬件保存到\({R_{save}}^1\) | 硬件保存到\(M\) |
软件保存 | 软件保存到\(R_{save}\) | 软件保存到\(M\) |
流程: P发生异常 -> 硬件保存 -> 跳转到异常处理程序 -> 软件保存
注1: 异常处理程序诊断时需要读取\(R_{save}\), 故CPU还要添加相应指令
用于控制和反映处理器状态的特殊寄存器(例如刚才的mepc)
31 20 19 15 14 12 11 7 6 0
+--------------------+-------+------+-------+------+
| csr | rs1 |funct3| rd |opcode|
+--------------------+-------+------+-------+------+
+------+------+---------------------------------+
|Number|Name |Description |
+------+------+---------------------------------+
|0x341 |mepc |Machine exception program counter|
+------+------+---------------------------------+
|0x342 |mcause|Machine trap cause |
+------+------+---------------------------------+
|... |... |... |
mtvec - 异常处理程序的入口地址
一个最简单的异常处理程序(用了AM提供的运行时环境)
mcause - 异常原因
uintptr_t mepc, mcause;
asm volatile ("csrr %0, mepc" : "=r"(mepc));
asm volatile ("csrr %0, mcause" : "=r"(mcause));
printf("exception mcause = %p at mepc = %p\n", mcause, mepc);
# 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保存的状态(恢复\(R\)即可)
然后返回到P
ret
指令返回
jalr
指令需要先把返回地址写入一个寄存器,
但这样会改变P的状态
mret
,
跳转到mepc中存放的地址实现方案很直接, 没有特别困难的地方
ecall
指令等
mret
指令
raise_intr 异常号
, 并更新状态如下: \(f_{ex}\)函数是处处有定义的吗?
能! RTFM!
addr % 访存位宽 != 0
inst == 0x00100073
(RISC-V的ebreak
指令)inst.opcode
未在手册中定义
一个重要的结论: 不考虑IOE的输入指令, 异常处理的行为是确定的!
不同指令集的不同: 异常号及其含义/触发异常的条件/保存的状态
但上层软件真正关心的是: 发生了什么事件, 该如何处理
老规矩, 加个抽象层: CTE (ConText Extension)
abstract-machine/am/include/am.h
中的事件定义Context* (*h)(Event ev, Context *ctx)
abstract-machine/am/include/arch/$ARCH.h
中的Context结构体现代计算机系统都支持多用户多任务
让用户程序直接访问系统中的资源并不是一个好主意
特权级 | 说明 |
---|---|
M | 机器模式 |
S | 监管模式 |
U | 用户模式 |
唯一合法方式: 自陷类异常 - 执行一条无条件触发异常的指令
ecall
指令mcause
得知该异常是合法的请求
为了让用户程序指定请求何种服务, 系统调用也需要传递参数
Context
结构中读出系统调用的参数man syscall
SBI调用涉及的功能(M模式才有权限进行的操作):
BBL(Berkeley BootLoader)的SBI实现
//riscv-pk/machine/mtrap.c
void mcall_trap(uintptr_t* regs, uintptr_t mcause, uintptr_t mepc) {
write_csr(mepc, mepc + 4);
uintptr_t n = regs[17], arg0 = regs[10], arg1 = regs[11], retval, ipi_type;
switch (n) {
case SBI_CONSOLE_PUTCHAR: retval = mcall_console_putchar(arg0); break;
case SBI_CONSOLE_GETCHAR: retval = mcall_console_getchar(); break;
case SBI_SEND_IPI: ipi_type = IPI_SOFT; goto send_ipi;
case SBI_REMOTE_SFENCE_VMA:
case SBI_REMOTE_SFENCE_VMA_ASID: ipi_type = IPI_SFENCE_VMA; goto send_ipi;
case SBI_REMOTE_FENCE_I: ipi_type = IPI_FENCE_I;
send_ipi:
send_ipi_many((uintptr_t*)arg0, ipi_type); retval = 0; break;
case SBI_CLEAR_IPI: retval = mcall_clear_ipi(); break;
case SBI_SHUTDOWN: retval = mcall_shutdown(); break;
case SBI_SET_TIMER: retval = mcall_set_timer(arg0); break;
default: retval = -ENOSYS; break;
}
regs[10] = retval;
}
RISC-V是模块化的
让异常处理函数代替CPU执行
这个过程和NEMU几乎一样
BBL可模拟浮点指令
//riscv-pk/machine/emulation.c
void illegal_insn_trap(uintptr_t* regs,
uintptr_t mcause, uintptr_t mepc) {
// ...
insn = get_insn(mepc, &mstatus);
if ((insn & 3) != 3)
return emulate_rvc(regs, mcause, mepc, mstatus, insn);
extern uint32_t illegal_insn_trap_table[];
int32_t* pf = (void*)illegal_insn_trap_table + (insn & 0x7c);
emulation_func f = (void*)illegal_insn_trap_table + *pf;
f(regs, mcause, mepc, mstatus, insn);
write_csr(mepc, mepc + 4);
}
//riscv-pk/machine/fp_emulation.c
void emulate_fmul(uintptr_t* regs, uintptr_t mcause,
uintptr_t mepc, uintptr_t mstatus, insn_t insn) {
if (GET_PRECISION(insn) == PRECISION_S) {
uint32_t rs1 = GET_F32_RS1(insn, regs);
uint32_t rs2 = GET_F32_RS2(insn, regs);
SET_F32_RD(insn, regs, f32_mul(f32(rs1), f32(rs2)).v);
} else if (GET_PRECISION(insn) == PRECISION_D) {
uint64_t rs1 = GET_F64_RS1(insn, regs);
uint64_t rs2 = GET_F64_RS2(insn, regs);
SET_F64_RD(insn, regs, f64_mul(f64(rs1), f64(rs2)).v);
} else {
return truly_illegal_insn(regs, mcause, mepc, mstatus, insn);
}
}
在RISC-V早期, mtime是一个CSR
这是一个不兼容旧版的改动, 那些读mtime CSR的程序无法正确执行了
解决方案: 让异常处理函数重定向
如果访存指令的地址不对齐, 将抛出不对齐访存异常
Even when misaligned loads and stores complete successfully, these accesses might run
extremely slowly depending on the implementation (e.g., when implemented via an
invisible trap). Furthermore, whereas naturally aligned loads and stores are
guaranteed to execute atomically, misaligned loads and stores might not, and hence
require additional synchronization to ensure atomicity.
// riscv-pk/machine/misaligned_ldst.c
void misaligned_load_trap(uintptr_t* regs, uintptr_t mcause, uintptr_t mepc) {
union byte_array val;
uintptr_t mstatus;
insn_t insn = get_insn(mepc, &mstatus);
uintptr_t npc = mepc + insn_len(insn);
uintptr_t addr = read_csr(mtval);
int shift = 0, len;
if ((insn & MASK_LW) == MATCH_LW) len = 4, shift = 8*(sizeof(uintptr_t) - len);
else if ((insn & MASK_LD) == MATCH_LD) len = 8, shift = 8*(sizeof(uintptr_t) - len);
else if ((insn & MASK_LWU) == MATCH_LWU) len = 4;
else if ((insn & MASK_LH) == MATCH_LH) len = 2, shift = 8*(sizeof(uintptr_t) - len);
else if ((insn & MASK_LHU) == MATCH_LHU) len = 2;
else {
mcause = CAUSE_LOAD_ACCESS;
write_csr(mcause, mcause);
return truly_illegal_insn(regs, mcause, mepc, mstatus, insn);
}
val.int64 = 0;
for (intptr_t i = 0; i < len; i++)
val.bytes[i] = load_uint8_t((void *)(addr + i), mepc);
SET_RD(insn, regs, (intptr_t)val.intx << shift >> shift);
write_csr(mepc, npc);
}
C++在语言特性上支持异常处理
借助mtvec可实现类似功能(当然只能用于M模式)
// riscv-pk/machine/minit.c
void setup_pmp(void) {
// Set up a PMP to permit access to all of memory.
// Ignore the illegal-instruction trap if PMPs aren't supported.
uintptr_t pmpc = PMP_NAPOT | PMP_R | PMP_W | PMP_X;
asm volatile ("la t0, 1f\n\t"
"csrrw t0, mtvec, t0\n\t" // 临时将mtvec更换成代码末尾
"csrw pmpaddr0, %1\n\t" // 尝试访问PMP CSR, 若失败, 则抛出非法指令异常
"csrw pmpcfg0, %0\n\t"
".align 2\n\t"
"1: csrw mtvec, t0" // 在代码末尾恢复mtvec
: : "r" (pmpc), "r" (-1UL) : "t0");
}
对回调函数进行巧妙的修改:
这样就实现了从A切换到B的效果 (操作系统的原型)
示例: am-kernels/kernels/yield-os/yield-os.c