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