20. 特权级别与 CSR
前 19 章我们一直在用户态(User mode)写汇编——调用 ecall 请求操作系统代劳 I/O、退出等操作。但你有没有想过:ecall 之后发生了什么?操作系统内核如何获得 CPU 控制权?定时器中断又是怎样让 OS 抢占当前进程的?这些问题的答案藏在 RISC-V 特权架构中。本章讲解三种特权模式以及 CSR(Control and Status Register)体系——它们是理解操作系统、异常处理和虚拟内存的基础。
三种特权模式
模式层级
RISC-V 定义了三种特权级别(privilege level),从高到低分别为:
| 级别 | 缩写 | 典型职责 | 示例代码 |
|---|---|---|---|
| Machine | M | 固件、bootloader、安全监控 | OpenSBI、U-Boot SPL |
| Supervisor | S | 操作系统内核 | Linux 内核、xv6 |
| User | U | 用户应用程序 | 你的汇编程序、浏览器 |
层级关系是严格的:高特权级可以访问低特权级的所有资源,反之不行。M 模式拥有对硬件的完全控制权——这是 RISC-V 设计中的一个关键决策:即使在最复杂的系统中,总有一个"最高权威"可以处理任何硬件事件。
嵌入式简化:仅 M 模式
RISC-V 的模块化哲学同样适用于特权架构。对于没有操作系统的嵌入式系统(bare-metal),处理器可以只实现 M 模式——所有代码都在最高特权下运行,无需模式切换:
# 嵌入式 bare-metal 程序的典型启动
.section .text.init
.globl _start
_start:
la sp, _stack_top # 设置栈指针
j main # 直接跳转到 C main()这种场景下,CSR 访问和异常处理仍然可用——M 模式并不要求 S/U 模式存在。
模式切换机制
从低特权向高特权切换只有一条路:
ecall # U → S(操作系统调用)或 U → M(bare-metal 系统调用)ecall 指令触发环境调用异常(Environment Call),硬件自动将当前 PC 保存到 mepc/sepc,将原因码写入 mcause/scause,然后跳转到陷阱处理程序。从操作系统视角看,用户程序的 ecall 就是"敲门"——"请 OS 代我执行特权操作"。
从高特权向低特权返回:
mret # M → 返回原特权级(可能是 S 或 U)
sret # S → 返回原特权级(通常是 U)mret/sret 完成三件事:
- 从
mepc/sepc恢复 PC - 从
mstatus.MPP/sstatus.SPP恢复特权级别 - 从
mstatus.MPIE/sstatus.SPIE恢复中断使能
这意味着进入陷阱时硬件自动保存上下文到 CSR,返回时从 CSR 自动恢复——设计得非常规整。
权限检查
当处理器在 U 模式下尝试访问 M 模式 CSR 时,会触发非法指令异常。这是硬件级别的隔离——用户态程序无法绕过 OS 直接操作硬件。
CSR:控制与状态寄存器
CSR 是什么
RISC-V 的通用寄存器(x0-x31)是 32 个——对大多数计算任务够用,但对系统控制远远不够。因此 RISC-V 设计了一个独立的 12 位地址空间(0x000-0xFFF,共 4096 个槽位)专门存放控制和状态寄存器——这就是 CSR(Control and Status Register)。
关键特性:
- 独立于通用寄存器:不是 x0-x31 的一部分,有自己的地址空间
- 专用指令访问:不能
add csr, x0, x0——必须用csr*系列指令 - 按特权级分配:低 12 位决定了 CSR 所属的特权级(bit[9:8]:00=U, 01=S, 10=HS, 11=M)
CSR 指令一览
RISC-V 定义了六条 CSR 操作指令,分为寄存器源和立即数源两组:
| 指令 | 语义 | 伪代码 |
|---|---|---|
csrrw rd, csr, rs1 | 原子读-写 | t = CSR; CSR = rs1; rd = t |
csrrs rd, csr, rs1 | 原子读-置位 | t = CSR; CSR = t | rs1; rd = t |
csrrc rd, csr, rs1 | 原子读-清除 | t = CSR; CSR = t & ~rs1; rd = t |
csrrwi rd, csr, imm | 立即数读-写 | t = CSR; CSR = imm; rd = t |
csrrsi rd, csr, imm | 立即数读-置位 | t = CSR; CSR = t | imm; rd = t |
csrrci rd, csr, imm | 立即数读-清除 | t = CSR; CSR = t & ~imm; rd = t |
核心约定:
- 总是返回旧值:
rd始终获得 CSR 的旧值——这让你在修改后仍能知道之前的状态 - rs1=x0 时:对于
csrrw,若rs1=x0,则不写入 CSR(读而不写);对于csrrs/csrrc,若rs1=x0,则只读不改 - 立即数范围:
imm是 5 位无符号数(0-31),由rs1字段编码
csrrs/csrrc 的"读-置位"和"读-清除"语义对中断使能管理特别有用——你可以原子地开启或关闭某个中断位,而不会干扰其他位。
伪指令简化
汇编器提供了几条常用伪指令(在基础指令章节也提过):
csrr rd, csr # → csrrs rd, csr, x0 纯读 CSR
csrw csr, rs1 # → csrrw x0, csr, rs1 纯写 CSR(丢弃旧值)
csrs csr, rs1 # → csrrs x0, csr, rs1 置位(不读旧值)
csrc csr, rs1 # → csrrc x0, csr, rs1 清除(不读旧值)
csrwi csr, imm # → csrrwi x0, csr, imm 立即数纯写
csrsi csr, imm # → csrrsi x0, csr, imm 立即数量位
csrci csr, imm # → csrrci x0, csr, imm 立即数清除日常编码中几乎只用伪指令——csrr t0, mhartid 比 csrrs t0, mhartid, x0 清晰太多。
常用 CSR 分类详解
信息类——识别处理器
| CSR | 地址 | 描述 |
|---|---|---|
mvendorid | 0xF11 | 制造商 ID(JEDEC 编码,0 表示未实现) |
marchid | 0xF12 | 微架构 ID |
mimpid | 0xF13 | 实现版本号 |
mhartid | 0xF14 | 硬件线程 ID(多核中区分 hart) |
这些是只读 CSR,提供处理器身份信息。多核 boot 流程中,每个 hart 通过读取 mhartid 判断自己是不是主核:
csrr t0, mhartid # 读出当前 hart ID
bnez t0, secondary_hart # hart 0 是主核,其他 hart 进入等待
# 主核初始化代码...计数器类——性能分析的核心
RISC-V 规范定义了一组只读的 64 位计数器:
| CSR | 地址 | 描述 |
|---|---|---|
cycle | 0xC00 | 自启动以来的 CPU 时钟周期计数 |
cycleh | 0xC80 | cycle 的高 32 位(RV32) |
time | 0xC01 | 实时计数器(通常由 RTC 驱动) |
timeh | 0xC81 | time 的高 32 位(RV32) |
instret | 0xC02 | 已退役的指令计数 |
instreth | 0xC82 | instret 的高 32 位(RV32) |
这三个计数器对性能分析至关重要:
# 测量一段代码的执行周期
csrr t0, cycle # 起始周期
# ... 被测代码段 ...
csrr t1, cycle # 结束周期
sub t2, t1, t0 # t2 = 执行这段代码的周期数在 RV64 下,csrr 直接读出 64 位值。RV32 下需要分别读取低 32 位和高 32 位(读低 32 位时会冻结高 32 位快照,避免进位导致的不一致)。
陷阱设置类——控制异常/中断的去向
| CSR | 地址 | 描述 |
|---|---|---|
mtvec | 0x305 | 陷阱向量基址寄存器(Trap Vector Base) |
medeleg | 0x302 | 异常委托寄存器(将异常委托给 S 模式处理) |
mideleg | 0x303 | 中断委托寄存器(将中断委托给 S 模式处理) |
mtvec 是陷阱处理的入口——当异常或中断发生时,PC 跳转到 mtvec 指定的地址。它有 Direct 和 Vectored 两种模式(bit[0]=0 为 Direct,bit[0]=1 为 Vectored)。
medeleg/mideleg 允许 M 模式将某些异常/中断"下放"给 S 模式处理。例如,操作系统通常将 ecall 从 U 模式的异常委托给 S 模式,这样系统调用直接进入内核而不经过固件。
陷阱处理类——记录中断/异常的信息
| CSR | 地址 | 描述 |
|---|---|---|
mcause | 0x342 | 陷阱原因(最高位:1=中断,0=异常;低位=原因编号) |
mepc | 0x341 | 陷阱发生时的 PC(mret 从此恢复) |
mtval | 0x343 | 陷阱附加信息(非法指令值、页错误地址等) |
mscratch | 0x340 | 陷阱处理程序暂存寄存器(不参与硬件自动操作) |
当陷阱发生时,硬件自动:
mepc ← PC(保存触发指令的地址)mcause ← 原因码(标识陷阱类型)mtval ← 附加信息(对调试有价值的数据)mstatus.MPIE ← mstatus.MIE; mstatus.MIE ← 0(保存旧中断使能并关中断)mstatus.MPP ← 当前特权级别(记录从哪个级别来)PC ← mtvec(跳转到陷阱处理程序)
看一个具体的 mcause 示例:
csrr t0, mcause # 读陷阱原因
srli t1, t0, 63 # 提取最高位:1=中断,0=异常
andi t2, t0, 0xFFF # 提取低 12 位:具体原因编号常见原因编号(异常):0=指令地址不对齐, 2=非法指令, 3=断点, 5=Load 访问错误, 7=Store 访问错误, 8=U 模式 ecall, 9=S 模式 ecall, 11=M 模式 ecall, 12/13/15=页错误。
常见原因编号(中断):1=Supervisor 软件中断, 3=M 模式软件中断, 5=Supervisor 定时器中断, 7=M 模式定时器中断, 9=Supervisor 外部中断, 11=M 模式外部中断。
状态与控制类
| CSR | 地址 | 描述 |
|---|---|---|
mstatus | 0x300 | 机器状态寄存器(全局状态控制) |
mie | 0x304 | 中断使能寄存器(位掩码,每位对应一种中断) |
mip | 0x344 | 中断等待寄存器(位掩码,哪些中断已触发但未响应) |
mstatus 是 RISC-V 最重要的 CSR 之一——它的字段控制着几乎所有特权行为。关键字段:
| 字段 | 位 | 描述 |
|---|---|---|
| MIE | [3] | M 模式全局中断使能(1=开中断) |
| SIE | [1] | S 模式全局中断使能 |
| UIE | [0] | U 模式全局中断使能 |
| MPIE | [7] | 进入陷阱前的 MIE 值(mret 时恢复) |
| SPIE | [5] | 进入陷阱前的 SIE 值 |
| MPP | [12:11] | 进入陷阱前的特权级(00=U, 01=S, 11=M) |
| FS | [14:13] | 浮点单元状态(00=Off, 01=Initial, 10=Clean, 11=Dirty) |
| XS | [16:15] | 用户扩展状态 |
MPIE/MPP 这三个字段的核心作用是自动保存和恢复陷阱发生时的 CPU 状态——硬件在进入陷阱时自动保存,mret 时自动恢复。这比 x86 的手动压栈/弹栈简洁得多。
CSR 使用示例
识别当前处理器
.section .text
.globl _start
_start:
csrr a0, mhartid # a0 = hart ID
csrr a1, mvendorid # a1 = 制造商 JEDEC 编码
csrr a2, marchid # a2 = 微架构 ID
# 将 hart ID 作为参数传给 C 函数
call kernel_init性能测量框架
# 测量函数执行时间和指令数的宏框架
.macro MEASURE_BEGIN
csrr s0, cycle # s0 = 起始周期
csrr s1, instret # s1 = 起始指令数
.endm
.macro MEASURE_END
csrr s2, cycle # s2 = 结束周期
csrr s3, instret # s3 = 结束指令数
sub s2, s2, s0 # s2 = 总周期数
sub s3, s3, s1 # s3 = 总指令数
# s2/s3 = CPI (cycles per instruction)
.endm
# 使用示例
some_function:
MEASURE_BEGIN
# ... 实际代码 ...
MEASURE_END
ret控制全局中断使能
# 禁用 M 模式中断(临界区开始)
csrr t0, mstatus
li t1, ~(1 << 3) # 清除 MIE 位 (bit 3)
and t0, t0, t1
csrw mstatus, t0
# ... 临界区代码 ...
# 重新启用 M 模式中断(临界区结束)
csrr t0, mstatus
ori t0, t0, (1 << 3) # 设置 MIE 位
csrw mstatus, t0更简洁的方式是使用 csrc/csrs 伪指令(原子操作,推荐):
csrc mstatus, (1 << 3) # 原子清除 MIE 位(关中断)
# ... 临界区代码 ...
csrs mstatus, (1 << 3) # 原子设置 MIE 位(开中断)原子操作的好处是:读-修改-写是一个不可中断的序列,不会出现"读出旧值后、写回新值前被中断"的竞态。
设置陷阱向量
la t0, trap_handler # 陷阱处理函数的地址
csrw mtvec, t0 # 设置陷阱向量基址(Direct 模式)
# mtvec bit[1:0] = 00, 所以直接写地址即可(4 字节对齐)本章要点
- M/S/U 三级特权,高可访问低;
ecall从低向高切换(U→S→M),mret/sret从高向低返回——硬件自动在 CSR 中保存/恢复上下文 - CSR 是独立的 12 位地址空间(4096 个槽位),有六条专用指令(
csrrw/csrrs/csrrc+ 立即数变体i)及对应的便捷伪指令(csrr/csrw/csrs/csrc) - 计数器 CSR(
cycle/time/instret)为性能分析提供硬件级精度,是汇编程序员最常用的 CSR 类别 mstatus是整个特权架构的控制中枢——MIE/MPIE/MPP 三个字段协同实现了陷阱发生时的自动状态保存和mret时的自动恢复- 嵌入式系统可仅实现 M 模式:bare-metal 程序跳过整个 S/U 体系,直接在最高特权级运行——这是 RISC-V 模块化哲学在特权架构中的体现