Skip to content
Published at:

20. 特权级别与 CSR

前 19 章我们一直在用户态(User mode)写汇编——调用 ecall 请求操作系统代劳 I/O、退出等操作。但你有没有想过:ecall 之后发生了什么?操作系统内核如何获得 CPU 控制权?定时器中断又是怎样让 OS 抢占当前进程的?这些问题的答案藏在 RISC-V 特权架构中。本章讲解三种特权模式以及 CSR(Control and Status Register)体系——它们是理解操作系统、异常处理和虚拟内存的基础。

三种特权模式

模式层级

RISC-V 定义了三种特权级别(privilege level),从高到低分别为:

级别缩写典型职责示例代码
MachineM固件、bootloader、安全监控OpenSBI、U-Boot SPL
SupervisorS操作系统内核Linux 内核、xv6
UserU用户应用程序你的汇编程序、浏览器

层级关系是严格的:高特权级可以访问低特权级的所有资源,反之不行。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 模式存在。

模式切换机制

从低特权向高特权切换只有一条路:

asm
ecall    # U → S(操作系统调用)或 U → M(bare-metal 系统调用)

ecall 指令触发环境调用异常(Environment Call),硬件自动将当前 PC 保存到 mepc/sepc,将原因码写入 mcause/scause,然后跳转到陷阱处理程序。从操作系统视角看,用户程序的 ecall 就是"敲门"——"请 OS 代我执行特权操作"。

从高特权向低特权返回:

asm
mret    # M → 返回原特权级(可能是 S 或 U)
sret    # S → 返回原特权级(通常是 U)

mret/sret 完成三件事:

  1. mepc/sepc 恢复 PC
  2. mstatus.MPP/sstatus.SPP 恢复特权级别
  3. 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 的"读-置位"和"读-清除"语义对中断使能管理特别有用——你可以原子地开启或关闭某个中断位,而不会干扰其他位。

伪指令简化

汇编器提供了几条常用伪指令(在基础指令章节也提过):

asm
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, mhartidcsrrs t0, mhartid, x0 清晰太多。

常用 CSR 分类详解

信息类——识别处理器

CSR地址描述
mvendorid0xF11制造商 ID(JEDEC 编码,0 表示未实现)
marchid0xF12微架构 ID
mimpid0xF13实现版本号
mhartid0xF14硬件线程 ID(多核中区分 hart)

这些是只读 CSR,提供处理器身份信息。多核 boot 流程中,每个 hart 通过读取 mhartid 判断自己是不是主核:

asm
csrr    t0, mhartid           # 读出当前 hart ID
bnez    t0, secondary_hart    # hart 0 是主核,其他 hart 进入等待
# 主核初始化代码...

计数器类——性能分析的核心

RISC-V 规范定义了一组只读的 64 位计数器:

CSR地址描述
cycle0xC00自启动以来的 CPU 时钟周期计数
cycleh0xC80cycle 的高 32 位(RV32)
time0xC01实时计数器(通常由 RTC 驱动)
timeh0xC81time 的高 32 位(RV32)
instret0xC02已退役的指令计数
instreth0xC82instret 的高 32 位(RV32)

这三个计数器对性能分析至关重要:

asm
# 测量一段代码的执行周期
    csrr    t0, cycle          # 起始周期
    # ... 被测代码段 ...
    csrr    t1, cycle          # 结束周期
    sub     t2, t1, t0         # t2 = 执行这段代码的周期数

在 RV64 下,csrr 直接读出 64 位值。RV32 下需要分别读取低 32 位和高 32 位(读低 32 位时会冻结高 32 位快照,避免进位导致的不一致)。

陷阱设置类——控制异常/中断的去向

CSR地址描述
mtvec0x305陷阱向量基址寄存器(Trap Vector Base)
medeleg0x302异常委托寄存器(将异常委托给 S 模式处理)
mideleg0x303中断委托寄存器(将中断委托给 S 模式处理)

mtvec 是陷阱处理的入口——当异常或中断发生时,PC 跳转到 mtvec 指定的地址。它有 Direct 和 Vectored 两种模式(bit[0]=0 为 Direct,bit[0]=1 为 Vectored)。

medeleg/mideleg 允许 M 模式将某些异常/中断"下放"给 S 模式处理。例如,操作系统通常将 ecall 从 U 模式的异常委托给 S 模式,这样系统调用直接进入内核而不经过固件。

陷阱处理类——记录中断/异常的信息

CSR地址描述
mcause0x342陷阱原因(最高位:1=中断,0=异常;低位=原因编号)
mepc0x341陷阱发生时的 PC(mret 从此恢复)
mtval0x343陷阱附加信息(非法指令值、页错误地址等)
mscratch0x340陷阱处理程序暂存寄存器(不参与硬件自动操作)

当陷阱发生时,硬件自动:

  1. mepc ← PC(保存触发指令的地址)
  2. mcause ← 原因码(标识陷阱类型)
  3. mtval ← 附加信息(对调试有价值的数据)
  4. mstatus.MPIE ← mstatus.MIE; mstatus.MIE ← 0(保存旧中断使能并关中断)
  5. mstatus.MPP ← 当前特权级别(记录从哪个级别来)
  6. PC ← mtvec(跳转到陷阱处理程序)

看一个具体的 mcause 示例:

asm
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地址描述
mstatus0x300机器状态寄存器(全局状态控制)
mie0x304中断使能寄存器(位掩码,每位对应一种中断)
mip0x344中断等待寄存器(位掩码,哪些中断已触发但未响应)

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 使用示例

识别当前处理器

asm
    .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

性能测量框架

asm
# 测量函数执行时间和指令数的宏框架
.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

控制全局中断使能

asm
# 禁用 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 伪指令(原子操作,推荐):

asm
    csrc    mstatus, (1 << 3)    # 原子清除 MIE 位(关中断)
    # ... 临界区代码 ...
    csrs    mstatus, (1 << 3)    # 原子设置 MIE 位(开中断)

原子操作的好处是:读-修改-写是一个不可中断的序列,不会出现"读出旧值后、写回新值前被中断"的竞态。

设置陷阱向量

asm
    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 模块化哲学在特权架构中的体现