本文基于你的
riscv32 + NEMU + AM代码,逐层拆解异常响应机制。环境路径:
NEMU:~/ysyx-workbench/nemu AM:~/ysyx-workbench/abstract-machine
一、异常响应机制在讲什么
PA3 开始,我们要让用户程序和操作系统之间能切换执行流。
讲义的核心是:
用户程序不能随便 jal 到操作系统的任意函数
硬件必须提供一种"限制入口"的执行流切换方式
RISC-V 的实现是:
异常 / 中断发生
-> 保存返回地址到 mepc
-> 保存异常原因到 mcause
-> 跳转到 mtvec 指定的入口
-> 软件保存上下文
-> 软件处理异常
-> mret 返回
三个核心 CSR:
| CSR | 作用 |
|---|---|
mtvec |
异常入口基址 |
mepc |
异常返回地址 |
mcause |
异常原因 |
mstatus |
机器模式状态 |
二、NEMU CPU 状态中的 CSR
文件:
nemu/src/isa/riscv32/include/isa-def.h
typedef struct {
word_t gpr[32];
vaddr_t pc;
word_t mstatus;
word_t mtvec;
word_t mepc;
word_t mcause;
} riscv32_CPU_state;
你的模拟 CPU 已经具备这些 CSR。
三、CSR 读写函数
文件:
nemu/src/isa/riscv32/init.c
#define MTVEC_ADDR 0x305
#define MEPC_ADDR 0x341
#define MCAUSE_ADDR 0x342
#define MSTATUS_ADDR 0x300
word_t csr_read(word_t addr) {
switch (addr) {
case MTVEC_ADDR: return cpu.mtvec;
case MEPC_ADDR: return cpu.mepc;
case MCAUSE_ADDR: return cpu.mcause;
case MSTATUS_ADDR: return cpu.mstatus;
default: panic("未实现的CSR寄存器: 0x%x", addr);
}
}
void csr_write(word_t addr, word_t value) {
switch (addr) {
case MTVEC_ADDR: cpu.mtvec = value; break;
case MEPC_ADDR: cpu.mepc = value; break;
case MCAUSE_ADDR: cpu.mcause = value; break;
case MSTATUS_ADDR: cpu.mstatus = value; break;
default: panic("未实现的CSR寄存器: 0x%x", addr);
}
}
四、NEMU 异常入口:isa_raise_intr()
文件:
nemu/src/isa/riscv32/system/intr.c
word_t isa_raise_intr(word_t NO, vaddr_t epc) {
csr_write(MEPC_ADDR, epc);
csr_write(MCAUSE_ADDR, NO);
if (NO == 3) {
nemu_state.state = NEMU_END;
nemu_state.halt_pc = epc;
nemu_state.halt_ret = 0;
return epc;
}
word_t mtvec = csr_read(MTVEC_ADDR);
return mtvec & ~0x3;
}
逐行解释
csr_write(MEPC_ADDR, epc);
保存异常返回地址。
csr_write(MCAUSE_ADDR, NO);
保存异常原因。
常见异常号:
3 ebreak
11 ecall
0x80000007 机器定时器中断
if (NO == 3)
你的代码把 ebreak 当作 AM 的 nemu_trap,直接结束 NEMU。
return mtvec & ~0x3;
跳转到异常入口,并保证 4 字节对齐。
五、AM 设置异常入口:cte_init()
文件:
abstract-machine/am/src/riscv/nemu/cte.c
bool cte_init(Context*(*handler)(Event, Context*)) {
asm volatile("csrw mtvec, %0" : : "r"(__am_asm_trap));
user_handler = handler;
return true;
}
解释
csrw mtvec, __am_asm_trap
把异常入口设置为 __am_asm_trap。
这就是"限制入口"的具体实现:以后异常只能进这个函数。
六、汇编异常入口:trap.S
文件:
abstract-machine/am/src/riscv/nemu/trap.S
__am_asm_trap:
addi sp, sp, -CONTEXT_SIZE
MAP(REGS, PUSH)
csrr t0, mcause
csrr t1, mstatus
csrr t2, mepc
STORE t2, OFFSET_EPC(sp)
STORE t0, OFFSET_CAUSE(sp)
STORE t1, OFFSET_STATUS(sp)
mv a0, sp
call __am_irq_handle
mv sp, a0
LOAD t1, OFFSET_STATUS(sp)
LOAD t2, OFFSET_EPC(sp)
csrw mstatus, t1
csrw mepc, t2
MAP(REGS, POP)
addi sp, sp, CONTEXT_SIZE
mret
逐段解释
1. 保存寄存器
addi sp, sp, -CONTEXT_SIZE
MAP(REGS, PUSH)
在栈上保存所有通用寄存器。
2. 读取 CSR
csrr t0, mcause
csrr t1, mstatus
csrr t2, mepc
读取异常原因、状态、返回地址。
3. 写入 Context
STORE t2, OFFSET_EPC(sp)
STORE t0, OFFSET_CAUSE(sp)
STORE t1, OFFSET_STATUS(sp)
保存到 C 结构体 Context。
4. 调用 C 处理函数
mv a0, sp
call __am_irq_handle
把 Context * 传给 C 函数。
5. 恢复并返回
csrw mstatus, t1
csrw mepc, t2
MAP(REGS, POP)
mret
恢复 CSR 和寄存器,最后 mret。
七、C 语言异常分发:__am_irq_handle()
文件:
abstract-machine/am/src/riscv/nemu/cte.c
Context* __am_irq_handle(Context *c) {
if (user_handler) {
Event ev = {0};
switch (c->mcause) {
case 11:
if (c->gpr[17] == -1) {
ev.event = EVENT_YIELD;
} else {
ev.event = EVENT_SYSCALL;
}
c->mepc += 4;
break;
case 0x80000007:
ev.event = EVENT_IRQ_TIMER;
break;
default:
ev.event = EVENT_ERROR;
break;
}
c = user_handler(ev, c);
assert(c != NULL);
}
return c;
}
逐行解释
case 11:
ecall 异常。
if (c->gpr[17] == -1)
如果 a7 = -1,说明是 yield()。
否则是系统调用。
c->mepc += 4;
跳过 ecall,返回后执行下一条指令。
case 0x80000007:
机器定时器中断。
ev.event = EVENT_IRQ_TIMER;
交给用户 handler。
八、用户事件处理函数
文件:
abstract-machine/am/src/platform/nemu/trm.c
static Context *event_handler(Event ev, Context *ctx) {
switch (ev.event) {
case EVENT_YIELD:
break;
case EVENT_SYSCALL:
if (ctx->GPRx == 0) {
halt(ctx->GPR1);
}
break;
case EVENT_IRQ_TIMER:
break;
default:
panic("Unhandled event");
break;
}
return ctx;
}
void _trm_init() {
cte_init(event_handler);
int ret = main(mainargs);
halt(ret);
}
event_handler 就是未来操作系统的入口雏形。
九、yield() 如何触发异常
文件:
abstract-machine/am/src/riscv/nemu/cte.c
void yield() {
asm volatile("li a7, -1; ecall");
}
流程:
yield()
-> a7 = -1
-> ecall
-> mcause = 11
-> pc = mtvec
十、定时器中断如何触发
文件:
nemu/src/device/intr.c
void dev_raise_intr() {
cpu.pc = isa_raise_intr(0x80000007, cpu.pc);
}
链路:
Linux 虚拟定时器
-> alarm_sig_handler
-> timer_intr
-> dev_raise_intr
-> isa_raise_intr(0x80000007, pc)
-> pc = mtvec
十一、完整异常响应流程图
1. yield() 流程
用户程序
-> yield
-> ecall
-> isa_raise_intr(11, pc)
-> mepc = pc
-> mcause = 11
-> pc = mtvec
-> __am_asm_trap
-> 保存 Context
-> __am_irq_handle
-> EVENT_YIELD
-> event_handler
-> mret
-> pc = mepc
2. 定时器中断流程
定时器触发
-> dev_raise_intr
-> isa_raise_intr(0x80000007, pc)
-> mepc = pc
-> mcause = 0x80000007
-> pc = mtvec
-> __am_asm_trap
-> __am_irq_handle
-> EVENT_IRQ_TIMER
-> event_handler
-> mret
十二、测试验证
1. 编译 NEMU
cd ~/ysyx-workbench/nemu
make -j$(nproc)
2. 运行中断测试
export NEMU_HOME=~/ysyx-workbench/nemu
export AM_HOME=~/ysyx-workbench/abstract-machine
cd ~/ysyx-workbench/am-kernels/tests/am-tests
make ARCH=riscv32-nemu run mainargs=i
3. 成功标志
hello_intr
-> __am_asm_trap
-> __am_irq_handle
-> simple_trap
-> putch
最终会看到:
ttttttttttt
其中:
t = 定时器中断
d = 设备中断
y = yield
因为 hello_intr() 是死循环,测试会一直运行,timeout 是正常的。
十三、讲义概念与代码对照
| 异常响应机制 | 对应代码 |
|---|---|
| 保存返回地址 | csr_write(MEPC_ADDR, epc) |
| 保存异常原因 | csr_write(MCAUSE_ADDR, NO) |
| 跳转异常入口 | return mtvec & ~0x3 |
| 设置异常入口 | cte_init -> csrw mtvec |
| 保存上下文 | trap.S |
| 异常分发 | __am_irq_handle |
| 用户处理 | event_handler |
| 异常返回 | mret |
十四、总结
异常响应机制就是一套固定流程:
1. 硬件保存 mepc / mcause
2. 硬件跳转到 mtvec
3. 软件保存上下文
4. 软件根据 mcause 处理
5. 软件 mret 返回
你现在已经具备完整链路。
下一步 PA3 要做的,就是在这个机制上实现:
1. 操作系统入口
2. 用户程序加载器
3. 批处理调度
