异常响应机制
【一生一芯 / PA】异常响应机制:从 ecall 到 mtvec 再到 mret 的完整代码解析
代码环境:
NEMU:~/ysyx-workbench/nemu
AM:~/ysyx-workbench/abstract-machine
ISA:riscv32
一、异常响应机制到底在解决什么问题
在 PA3 中,我们要让程序之间能切换执行流。
但讲义特别强调:
不能用 call / jal 随便跳转
必须提供一种“限制入口”的执行流切换方式
这种机制就是:
异常响应机制
它的核心流程是:
异常 / 中断发生
-> 硬件保存返回地址
-> 硬件保存异常原因
-> 硬件跳转到固定入口 mtvec
-> 软件保存上下文
-> 软件处理异常
-> 软件通过 mret 返回
在 RISC-V 中,这三个关键 CSR 是:
mtvec -> 异常入口
mepc -> 异常返回地址
mcause -> 异常原因
二、NEMU 硬件侧:isa_raise_intr()
文件:
nemu/src/isa/riscv32/system/intr.c
word_t isa_raise_intr(word_t NO, vaddr_t epc) {
// 1. 保存返回地址
csr_write(MEPC_ADDR, epc);
// 2. 保存异常原因
csr_write(MCAUSE_ADDR, NO);
// 3. 特殊处理 ebreak
if (NO == 3) {
nemu_state.state = NEMU_END;
nemu_state.halt_pc = epc;
nemu_state.halt_ret = 0;
return epc;
}
// 4. 读取异常入口
word_t mtvec = csr_read(MTVEC_ADDR);
// 5. 返回 mtvec,并保证 4 字节对齐
return mtvec & ~0x3;
}
逐行解释
csr_write(MEPC_ADDR, epc);
保存异常发生时的 PC,将来 mret 要返回这里。
csr_write(MCAUSE_ADDR, NO);
保存异常原因。
例如:
11 -> ecall
3 -> ebreak
0x80000007 -> 机器定时器中断
if (NO == 3)
你的代码把 ebreak 特殊处理为 NEMU 正常结束。
return mtvec & ~0x3;
把 PC 修改为 mtvec,这就是异常入口跳转。
三、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。
这样,无论用户程序如何触发异常,CPU 都只能跳转到:
mtvec = __am_asm_trap
这就是“限制入口”的具体实现。
四、汇编异常入口:保存和恢复上下文
文件:
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 语言异常分发
文件:
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:
mcause == 11 表示 ecall。
if (c->gpr[17] == -1)
如果 a7 = -1,说明是 yield(),否则是系统调用。
c->mepc += 4;
跳过 ecall 指令,返回后从下一条指令继续。
case 0x80000007:
mcause == 0x80000007 表示机器定时器中断。
ev.event = EVENT_IRQ_TIMER;
交给用户注册的 handler 处理。
六、用户侧事件处理函数
修改后的 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;
}
这个 event_handler 就是未来操作系统的入口雏形。
七、定时器中断如何触发
文件:
nemu/src/device/intr.c
void dev_raise_intr() {
cpu.pc = isa_raise_intr(0x80000007, cpu.pc);
}
解释
isa_raise_intr(0x80000007, cpu.pc)
表示:
异常原因 = 机器定时器中断
返回地址 = 当前 PC
然后:
pc = mtvec
进入 __am_asm_trap。
八、完整异常响应链路
1. yield() 链路
用户程序执行 ecall
↓
NEMU isa_raise_intr(11, pc)
↓
mcause = 11
mepc = pc
pc = mtvec
↓
__am_asm_trap
↓
保存 Context
↓
__am_irq_handle
↓
mcause == 11
a7 == -1 -> EVENT_YIELD
↓
event_handler
↓
返回 Context
↓
mret
↓
pc = mepc
2. 定时器中断链路
Linux 虚拟定时器
↓
alarm_sig_handler
↓
timer_intr
↓
dev_raise_intr
↓
isa_raise_intr(0x80000007, pc)
↓
mcause = 0x80000007
mepc = pc
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 |
十一、总结
异常响应机制就是一套标准流程:
硬件:保存 mepc / mcause,跳转 mtvec
软件:保存上下文
软件:根据 mcause 分发事件
软件:处理事件
软件:mret 返回
你的代码现在已经具备这条完整链路。
接下来 PA3 要做的,就是在这个机制之上实现:
1. 操作系统入口 event_handler
2. 用户程序加载器
3. 批处理调度
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)