【一生一芯 / PA】异常响应机制:RISC-V 中 ecall → mtvec → mret 的完整代

本文基于你的 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. 批处理调度
相关推荐
北京盛世宏博1 小时前
工业环境监测选型:Modbus TCP以太网温湿度传感器部署、调试、避坑全指南
开发语言·网络·php
沫璃染墨1 小时前
《Linux工程实践篇(二):线程池实战——从线程安全单例到完整源码实现》
linux·开发语言·c++·单例模式·设计模式·系统架构
泡海椒2 小时前
JQuick-Excel 多字段 TRANSFORM:让当前行字段、JContext 与展示列各归其位
开发语言·python·excel
m0_646429972 小时前
MySQL 初始化 SQL 中文乱码问题总结
数据库·sql·mysql
JPower_mr.g2 小时前
SmartCall 音色管理技术解析:基于 SPI 的可扩展音色注册架构
java·开发语言·人工智能·ai·架构·开源
实战派K8S&DB2 小时前
TDSQL 核心模块与进程体系
运维·数据库·分布式·sql·mysql
FfHUCisI2 小时前
Go 性能分析工具 pprof:CPU、Heap 与 Goroutine 实战
数据库·golang
淡海水2 小时前
02-04-原理篇-GC Roots与对象图遍历
算法·unity·c#·图·gc·遍历·roots
仍然.2 小时前
Redis---分布式锁
数据库·redis·分布式