3. RISC-V 特权架构:机器模式、监督模式、用户模式

特权架构,说白了就是给芯片装上一套「权限分级系统」。我刚开始接触 RISC-V 时,觉得这东西不就是几个模式切换嘛,后来在调试一个多任务系统时被坑惨了——用户态程序直接写了个 CSR,整个系统直接挂掉。嗯,从那以后我才真正重视起这套机制。

3.1 三种特权模式:M、S、U

RISC-V 定义了三个特权等级,从高到低分别是:

  • 机器模式(M 模式):最高权限,所有 CSR 都能读写,异常处理的第一站。说白了就是芯片的「上帝模式」。
  • 监督模式(S 模式):操作系统内核运行的地方,可以管理页表、处理系统调用。Linux 内核就跑在这个模式下。
  • 用户模式(U 模式):应用程序的乐园,权限最低,啥都不能碰。想访问硬件?老老实实走系统调用。

我个人习惯把这三个模式想象成「公司层级」:M 模式是 CEO,S 模式是部门经理,U 模式是普通员工。CEO 啥都能干,部门经理管自己的一亩三分地,普通员工只能通过申请来办事。

关键点:RISC-V 允许实现只支持 M 模式(嵌入式场景),或者 M+U 模式(简单安全隔离),或者 M+S+U 全模式(运行 Linux)。你想想看,这种灵活性让 RISC-V 能覆盖从单片机到服务器的全场景。

3.2 CSR 寄存器详解

CSR(Control and Status Register)是特权架构的核心。每个 CSR 都有一个 12 位的地址编码,通过 csrrw、csrrs、csrrc 等指令访问。

我在项目中遇到过一个问题:某同事在中断服务程序里直接修改 mstatus 的 MIE 位,结果导致嵌套中断乱套。后来我们规定:所有 CSR 操作必须封装成宏,统一审核。

3.2.1 机器模式核心 CSR

CSR 名称 地址 功能描述
mstatus 0x300 机器状态寄存器,控制全局中断使能、特权模式切换等
mepc 0x341 异常返回地址,指向发生异常时的 PC
mcause 0x342 异常原因编码,告诉你是哪种异常
mtvec 0x305 异常向量基地址,中断/异常入口
mie 0x304 机器中断使能寄存器,单独控制每个中断源
mip 0x344 机器中断挂起寄存器,显示哪些中断正在等待

避坑指南:我曾经在调试时发现 mstatus 的 MPP 字段没保存好,导致从异常返回后跑到了错误的特权模式。记住:mret 指令会从 mstatus 的 MPP 字段恢复特权级,所以异常入口处必须正确设置 MPP。

3.2.2 监督模式核心 CSR

CSR 名称 地址 功能描述
sstatus 0x100 监督状态寄存器,是 mstatus 的子集
sepc 0x141 监督模式异常返回地址
scause 0x142 监督模式异常原因
stvec 0x105 监督模式异常向量基地址
satp 0x180 地址转换和保护寄存器,控制页表基地址

你想想看,satp 这个寄存器有多重要?它决定了整个虚拟内存系统的根。我调试过一个性能问题,发现页表切换时 satp 写入后没有执行 sfence.vma 指令,导致 TLB 里的旧映射没刷新,数据全乱套了。

3.3 异常与中断处理机制

异常和中断是处理器响应「意外事件」的两种方式。异常是同步的(比如非法指令),中断是异步的(比如定时器到期)。

3.3.1 异常处理流程

当异常发生时,硬件自动完成以下操作:

  1. 将当前 PC 保存到 mepc(或 sepc)
  2. 将异常原因编码写入 mcause(或 scause)
  3. 关闭全局中断(mstatus.MIE 清零)
  4. 跳转到 mtvec(或 stvec)指向的入口地址

异常返回时,执行 mret(或 sret)指令,硬件会:

  1. 从 mepc 恢复 PC
  2. 从 mstatus 的 MPP 字段恢复特权级
  3. 恢复全局中断使能状态

重要提醒:我曾经见过一个案例,工程师在异常处理函数里忘记保存 mepc,结果嵌套异常把返回地址覆盖了。记住:异常处理函数的第一件事就是保存 mepc 和 mcause 到栈上。

3.3.2 中断处理

RISC-V 的中断分为三类:

  • 软件中断:由软件写入 mip 的 MSIP 位触发,常用于多核通信
  • 定时器中断:由 mtime 比较器触发,实现时间片调度
  • 外部中断:由 PLIC(平台级中断控制器)路由,处理外设中断

中断使能需要两级控制:全局使能(mstatus.MIE)和局部使能(mie 寄存器对应位)。两者都使能了,中断才能被响应。

3.3.3 异常与中断的优先级

当多个异常/中断同时发生时,RISC-V 规定了固定优先级:

优先级 事件类型 说明
最高 硬件错误 比如总线错误、ECC 错误
高 同步异常 非法指令、断点、页错误等
中 外部中断 PLIC 路由的外设中断
低 定时器中断 mtime 比较器触发
最低 软件中断 核间通信

嗯,这里要注意:同步异常的优先级高于所有中断。为什么?因为同步异常是当前指令执行出的问题,必须立即处理,否则程序状态就不可信了。

3.4 特权模式切换与陷阱处理

从低特权模式切换到高特权模式,只能通过异常或中断(统称为「陷阱」)。反过来,从高到低则通过 mret/sret 指令。

举个例子,用户态程序执行 ecall 指令:

# 用户态代码
li a7, 64    # 系统调用号:write
li a0, 1     # 文件描述符:stdout
la a1, msg   # 缓冲区地址
li a2, 13    # 长度
ecall        # 触发环境调用异常

# 内核态处理(S 模式)
trap_handler:
  csrr t0, scause    # 读取异常原因
  li t1, 8           # 环境调用 from U-mode
  bne t0, t1, other  # 不是 ecall 就跳走
  # 处理系统调用...
  mret               # 返回用户态

我个人习惯在 ecall 之前先保存所有 caller-saved 寄存器,因为异常处理程序可能会修改它们。这个习惯帮我避免过好几次莫名其妙的 bug。

3.5 知识体系总览

下面这张图把本章的核心知识点串起来了:

RISC-V 特权架构知识体系 RISC-V 特权架构知识体系 机器模式 (M) 最高权限,硬件初始化 监督模式 (S) 操作系统内核 用户模式 (U) 应用程序 CSR 寄存器组 mstatus/mcause/mepc sstatus/scause/sepc ustatus/uepc(可选) satp(页表基址) 异常与中断处理 同步异常(ecall/非法指令) 异步中断(定时器/外部) 陷阱处理(mret/sret) 图:RISC-V 特权架构三层模型、CSR 寄存器组与异常中断处理的关系

实战建议:如果你在移植 Linux 内核到 RISC-V 平台,重点关注 satp 寄存器的配置和 sfence.vma 的使用。我调试过一个页表问题,花了整整两天才发现是 satp 写入后没做 TLB 刷新。记住:sfence.vma 是必须的,不是可选的。

特权架构是 RISC-V 生态的基石。理解了 M/S/U 模式的分工、CSR 的操作方法、异常中断的处理流程,你就能看懂大部分系统级代码了。下次遇到 mret 或者 ecall 指令,你就知道背后发生了什么。