我们之前都是假设指令可以成功执行
如果指令执行失败, 应该怎么办?
本次课内容:
计算机系统应该如何应对?
例如整数除0
-mcheck-zero-division
,
可以自动在除法前生成指令检查除数是否为0
编译器发现语法错误, 悄悄生成错误的代码
CPU作为数字电路, 要输出报错信息/自动修bug比较困难
回顾: 程序/指令集/CPU都是状态机
需要把当前程序P的状态保存起来, 并跳转到异常处理程序进行后续诊断
S = {<R, M>}
保存M
的需求好像很奇怪: M
这么大,
要保存到哪里?
一个观察: 异常处理程序和P是两个不同的程序,
它们使用不同的M
M
,
则不必进行实质性的保存操作
R
只有一份, 异常处理程序也要用, 肯定要保存
R
要把P的R
保存到哪里呢?
S = {<R, M>}
, 也只能保存到S
里面了 😂
R
: 增加一组寄存器R_save
,
专门用来保存R
M
: 需要找一处空闲的内存区域
谁来保存?
R
的设计P发生异常 -> 硬件保存 -> 跳转到异常处理程序 -> 软件保存
R |
M |
|
---|---|---|
硬件 | 硬件保存到R_save |
硬件保存到M |
软件 | 软件保存到R_save |
软件保存到M |
考虑到异常处理程序也需要读取R_save
,
CPU还要添加相应指令
R
保存到M
R
保存到M
用于控制和反映处理器状态的特殊寄存器
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);
while (1);
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
R = {PC, GPR, CSR}
,
M
无需扩展fex: S -> {0, 1}
,
给定任意状态S
, fex(S)
表示当前指令执行是否失败
fex(S) = 0
, 则按照当前指令的语义进行状态转移fex(S) = 1
,
则执行一条特殊指令raise_intr 异常号
, 并更新状态如下: fex(S)
函数是处处有定义的吗?
答案是肯定的: RTFM!
一个重要的结论: 异常处理过程是确定的!
不同指令集的不同: 异常号及其含义/触发异常的条件/保存的状态
老规矩, 加个抽象层: CTE (ConText Extension)
abstract-machine/am/include/am.h
中的事件定义Context* (*h)(Event ev, Context *ctx)
abstract-machine/am/include/arch/$ARCH.h
中的Context结构体上层程序关心的是: 发生了什么事件, 该如何处理
riscv64-nemu CTE代码导读
现代计算机系统都支持多用户多任务
让用户程序直接访问系统中的资源并不是一个好主意
特权级 | 说明 |
---|---|
M | 机器模式 |
S | 监管模式 |
U | 用户模式 |
唯一合法方式: 自陷类异常 - 执行一条无条件触发异常的指令
为了让用户程序指定请求何种服务, 系统调用也需要传递参数
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"
"csrw pmpaddr0, %1\n\t"
"csrw pmpcfg0, %0\n\t"
".align 2\n\t"
"1: csrw mtvec, t0"
: : "r" (pmpc), "r" (-1UL) : "t0");
}
这样就实现了从A切换到B的效果 (操作系统的原型)
#include <am.h>
union task { Context *ctx; uint8_t stack[8192]; } tasks[2], *current = NULL;
static void delay() { for (int volatile i = 0; i < 10000000; i++) ; }
static void f1(void *arg) { while (1) { putch('a'); yield(); delay(); } }
static void f2(void *arg) { while (1) { putch('b'); yield(); delay(); } }
static void create_task(union task *t, void *f) {
Area stack = (Area) { &t->ctx + 1, t + 1 };
t->ctx = kcontext(stack, f, NULL);
}
static Context *handler(Event ev, Context *ctx) {
if (!current) current = &tasks[0];
else current->ctx = ctx;
current = (current == &tasks[0] ? &tasks[1] : &tasks[0]);
return current->ctx;
}
int main() {
cte_init(handler);
create_task(&tasks[0], f1);
create_task(&tasks[1], f2);
yield();
while (1);
}
真实系统通过时钟中断强制触发上下文切换
while (1)
无法卡死整个系统了