☰
RISC-V特权架构核心:CSR寄存器与M/S/U模式详解
2026/10/7 19:57:48 网站建设 项目流程

如果你在搜索引擎里敲下 CSR 三个字母,前几页大概率是两拨互不相干的人:一边是 RISC-V 开发者在讨论 mstatus、mtvec 怎么配,另一边是数据结构工程师在争论邻接表和 CSR 压缩稀疏行哪个省内存。标题里的 CSR 指的是前者——Control and Status Register,控制与状态寄存器。它之所以和“M/S/U”绑在一起,是因为 RISC-V 的特权架构几乎全靠这套寄存器运转。这篇文章是 risc-v 专栏的第三篇,前两篇聊过指令格式与基础汇编,这次我们直接进入特权世界的核心:把 M(Machine)、S(Supervisor)、U(User)三种模式怎么切换、每个关键 CSR 拿来干嘛、访问 CSR 有哪些隐藏规则,一次讲透。不管你是写裸机程序、调 RTOS,还是准备用 RISC-V 板子启动 Linux,这份速查和踩坑记录都值得留一份。

1. M/S/U 三种特权到底在防谁:没有特权架构的 CPU 只是一堆裸金属

1.1 特权级就是一套门禁系统

先做个生活化类比。一栋楼如果完全没有门禁,任何一个住客都能进配电房、改消防管道、动电梯机房,出了事根本分不清是谁干的。CPU 也是如此:如果所有程序都能直接读写内存映射寄存器、改写中断控制器、关掉时钟,那整个系统就是一台“裸金属”——所有软件共享一切,任何一个 bug 都能把整个设备搞挂。

RISC-V 用 M/S/U 三把钥匙解决了这个问题。M 模式是 Machine Mode,权限最高,相当于物业总控:能做所有事,包括配置内存保护、处理所有异常、管理平台初始化。S 模式是 Supervisor Mode,给操作系统内核用,可以管理页表、开关中断、调度进程,但动不了硬件平台最底层的东西。U 模式是 User Mode,给普通应用程序用,只能访问自己那一亩三分地,一越界立刻触发异常。复位以后 CPU 先进入 M 模式,因为芯片上电后必须有一段最高权限代码来初始化内存控制器、时钟、外设,然后把控制权交给 OS。

很多人会问:两个特权级不够吗?为什么要三档?如果只有 M 和 U,那么“固件层”和“操作系统内核”就得挤在同一层。Linux 的虚拟内存、进程切换都要靠内核代码直接改页表寄存器,一旦内核 bug 写坏了硬件配置,系统没有兜底的固件可以救回来。有了 M 模式做隔离,内核可以毫无顾忌地跑 S 模式,M 模式下放一个类似 OpenSBI 的极薄固件,负责平台初始化、错误兜底、中断路由,这才形成完整的安全链。

1.2 换模式只有一个通道:trap 与 xret

特权级之间不能像普通函数一样随便 jump,模式切换只能走 trap 通道。trap 分成两类:同步异常(比如非法指令、地址越界、页错误)和中断(比如定时器、外部设备、软件中断)。无论哪一类,CPU 都会暂停当前程序,把现场“交给”更高特权级的处理器。

软件主动发起 trap 的指令是ecall。U 模式执行ecall,相当于向 S 模式或 M 模式发系统调用请求;S 模式执行ecall,通常是调 M 模式的固件服务。还有ebreak,是为了调试器准备的断点指令。返回的指令是mret和sret:M 模式处理完 trap 后用mret返回,S 模式用sret返回。这里有个初学者经常忽略的点:mret不只是跳转,它会同时恢复“特权级”和“中断使能状态”。返回时 CPU 会读mepc拿到被中断的地址,读mstatus.MPP知道该回到 M/S/U 哪个模式,读mstatus.MPIE恢复之前的中断开关状态。sret同理,只是对应寄存器变成sepc和sstatus.SPP。

这一套设计给软件留下的想象空间很大:你完全可以在启动代码里故意配好mepc和MPP,然后用一条mret从 M 模式“下降”到 S 模式甚至 U 模式,这就是后面实操章节要做的事。

2. 从 12 位地址到六条指令:CSR 定位与读写机制一网打尽

2.1 用地址分段代替死记硬背

RISC-V 的 CSR 地址一共 12 位,范围 0x000 到 0xFFF,总共 4096 个编号,但没有任何芯片会全部实现。这 12 位不是瞎排的,高位的“区段”直接告诉你这个寄存器属于哪个特权域。核心规律是:以 0xx、4xx、8xx、Cxx 开头的,通常用户态可访问;1xx、5xx、9xx、Dxx 是监管级;2xx、6xx、Axx、Exx 一般留给 H 扩展(虚拟化);3xx、7xx、Bxx、Fxx 是机器级。记不住后面那些,至少要把最常用的一批背下来。

下面这张表覆盖了日常开发 90% 的场景:

地址CSR说明
0x001/0x002/0x003fflags / frm / fcsr浮点异常标志、舍入模式、控制状态
0x100sstatusS 模式状态,mstatus 的子集视图
0x104 / 0x144sie / sipS 模式中断使能 / 中断挂起
0x105stvecS 模式 trap 入口地址
0x140 / 0x141sscratch / sepcS 模式暂存 / 异常返回地址
0x142 / 0x143scause / stvalS 模式异常原因 / 附加信息
0x180satp地址翻译与保护,开分页的关键
0xC00 / 0xC01 / 0xC02cycle / time / instret周期数、时钟时间、退休指令数
0x300mstatusM 模式状态
0x301misa指令集扩展位图
0x302 / 0x303medeleg / mideleg异常 / 中断委托位图
0x304 / 0x344mie / mipM 模式中断使能 / 挂起
0x305mtvecM 模式 trap 入口地址
0x340 / 0x341mscratch / mepcM 模式暂存 / 异常返回地址
0x342 / 0x343mcause / mtvalM 模式异常原因 / 附加信息
0xF11 / 0xF12mvendorid / marchid厂商 ID / 架构 ID
0xF13 / 0xF14mimpid / mhartid实现 ID / 硬件线程 ID

2.2 六条 CSR 指令与它们的“原子读写”玩法

访问 CSR 不能用普通的 load/store,而是专用指令:

  • csrrw rd, csr, rs1:读旧值到rd,把rs1的值整个写入 CSR
  • csrrs rd, csr, rs1:读旧值到rd,然后把rs1中为 1 的位在 CSR 里置 1
  • csrrc rd, csr, rs1:读旧值到rd,然后把rs1中为 1 的位在 CSR 里清 0
  • csrrwi / csrrsi / csrrci rd, csr, zimm:对应指令的立即数版,zimm只有 5 位

这里最有价值的设计是位操作版本。修改 CSR 的某个位通常需要“读-改-写”,如果两条指令之间被中断打断,或者两个核同时改同一个 CSR,就会互相覆盖。csrrs/csrrc在硬件层面保证“读改写”是一个原子操作,这对多核同步极有价值。汇编器还定义了几个伪指令,实际开发里更常用:csrr rd, csr等价于csrrs rd, csr, x0,专门用来读;csrw csr, rs等价于csrrw x0, csr, rs,专门用来写;csrs/csrc则对应置位和清位。

2.3 三句不写在文档里的禁忌

第一,在低特权级访问高特权级 CSR,一定会触发非法指令异常,mcause会变成 2。第二,写只读 CSR,同样触发非法指令异常。mvendorid、marchid、mimpid、mhartid这些身份寄存器统统只读,有人试图用csrw改它们,立刻就能收获一个异常。第三,读一个不存在但编号“看着眼熟”的 CSR,也会触发非法指令异常。不是返回 0,而是直接异常。所以写通用函数时,不要试图通过“读一下某个 CSR 返回 0”来判断硬件是否支持,正确做法是先读misa的扩展位或者查设备树。

注意:csrrw x0, csr, rs1这种“只写不读”形态是合法的,硬件可以跳过读操作。但千万别用它来探测只读 CSR——规范允许“跳过读”,不代表实现不会做权限检查,行为可能因核而异。

3. 机器级 CSR 盘点:启动汇编、中断入口、异常现场全靠它们

3.1 身份四件套和指令集识别

mvendorid(0xF11)、marchid(0xF12)、mimpid(0xF13)、mhartid(0xF14)是 CPU 的“四张身份证”。mvendorid能告诉你芯片来自哪个厂商,marchid能帮你确认用的哪种微架构,mimpid是实现细节版本,mhartid则是每个 hart(硬件线程)的唯一编号。多核处理器启动时,每一个核都会从自己的mhartid判断身份,主核做全局初始化,从核等着被主核唤醒。很多移植脚本和 Bring-up 代码里第一件事就是打印这四个值。

更有用的其实是misa。它是一个位图,每一位对应一个指令集扩展:A 位是原子指令,C 位是压缩指令,D/F 是双精度/单精度浮点,I 是基础整数指令,M 是乘除,S 是 S 模式,U 是 U 模式。比如你想确认芯片支不支持压缩指令,就看misa的 bit 2:

csrr t0, misa li t1, 1 slli t1, t1, 2 # 检查 C 扩展,bit 2 and t2, t0, t1 beqz t2, no_c_ext # 支持 C 扩展

在 QEMU 里这些 CSR 大多可以配置,硬件上则是出厂定死的。用户态程序一般读不到misa,会收到非法指令异常,所以 Linux 用户态想识别 CPU 特性得靠设备树或AT_PLATFORM,这点后面讲实测时还会遇到。

3.2 mstatus:整个特权架构的总电闸

mstatus是机器模式里最重要、也最容易改错的一个 CSR。关键字段我按使用频率排一下:

字段位作用
MIEbit 3M 模式全局中断使能
MPIEbit 7进 trap 前的 MIE 备份
MPPbits 12:11进 trap 前的特权级:11=M,01=S,00=U
SIEbit 1S 模式全局中断使能
SPIEbit 5进 trap 前的 SIE 备份
SPPbit 8进 trap 前的 S 级模式:1=S,0=U
FSbits 14:13浮点单元状态:00=Off,01=Initial,10=Clean,11=Dirty
MPRVbit 17下一条 load/store 按 MPP 权限执行
SUMbit 18允许 S 模式访问 U 模式页面
MXRbit 19允许 S 模式读取可执行页

第一次写裸机浮点程序时最容易踩的坑就是FS。如果你的代码执行到浮点指令但mstatus.FS还是 00,不少实现会直接抛非法指令异常。所以启动代码里要么先把FS置成Dirty,要么明确自己不用浮点。MPP更是mret的“目的地”:如果 M 模式代码希望mret后回到 S 模式,就要在mret前把MPP设为 01。很多初学者直接在mepc里填一个地址然后mret,结果发现回不到想要的特权级,多半是MPP没配。

关于MPRV,多说一句:它不影响当前特权级,只让接下来的 load/store 临时按MPP指定的特权级来做权限检查。调试器和虚拟机监视器经常用它“模拟”低特权访问,很有用。

3.3 trap 五件套:mtvec、mepc、mcause、mtval、mscratch

这五个 CSR 是异常处理的五脏六腑。mtvec是 trap 入口地址,低 2 位表示模式:00 是直接模式,所有 trap 都跳到同一个地址;01 是向量模式,中断按 cause 编号跳base + 4 * cause。写mtvec时基础地址要 4 字节对齐,否则行为未定义。mepc保存被 trap 打断的 PC:异常场景下指向出问题的那条指令,中断场景下指向被打断的下一条指令。mret会把mepc写回 PC,所以如果你在 trap handler 里提前改过mepc,就能实现任务切换——这是很多轻量级调度器的理论基础。

mcause告诉你为什么进来。它的最高位是中断标志:最高位为 1 表示中断,否则是同步异常。低若干位是原因编号。我整理了一份最常用的对照:

编号含义
中断标志+1S 模式软件中断
中断标志+3M 模式软件中断
中断标志+5S 模式定时器中断
中断标志+7M 模式定时器中断
中断标志+9S 模式外部中断
中断标志+11M 模式外部中断
2非法指令
8U 模式 ecall
9S 模式 ecall
11M 模式 ecall
12 / 13 / 15指令页错误 / 装载页错误 / 存储页错误

mtval存附加信息:地址类异常放出错虚拟地址,非法指令异常放指令编码。排错时mcause告诉你“发生了什么”,mtval告诉你“具体是哪个地址/哪条指令”。mscratch则是软件自用的暂存寄存器,硬件不主动读写它,典型用法是在 trap 入口处需要一个临时寄存器来保存 t0。一个极简的 trap 入口长这样:

trap_entry: csrw mscratch, t0 # 先把 t0 存走 csrr t0, mcause # 再读异常原因 # 根据 mcause 分支处理 csrr t0, mscratch # 处理完恢复 t0 mret

没有mscratch这一手,你在 trap 入口连个能用的寄存器都没有,现场保护会非常痛苦。

3.4 medeleg / mideleg:把中断“外包”给 S 模式

默认情况下,无论 U、S 还是 M 模式发生 trap,最终都会跳去 M 模式的mtvec。这在最简单裸机里没问题,但跑 Linux 就太蠢了:每次系统调用、每个定时器中断都要先经过 M 模式固件,再转交给 S 模式内核,性能被白白浪费。RISC-V 的解法是委托。medeleg是异常委托位图,mideleg是中断委托位图,每一位对应一个异常/中断原因号。只要 M 模式把对应位置 1,那个 trap 就会直接交给 S 模式的stvec处理,M 模式全程不掺和。

典型配置是:把 S 能处理的 ecall、定时器中断、外部中断都委托给 S。这样 Linux 内核在 S 模式就能自己处理系统调用和中断,只有真正需要固件服务的场合才ecall进 M。OpenSBI 启动时就会做这套委托,这也是为什么你在 RISC-V Linux 上看到的裸机 firmware 层那么薄。

4. 监管级与用户级 CSR:从裸机跑到 Linux 的必修课

4.1 S 模式七件套速查

S 模式的 CSR 几乎就是 M 模式的“阉割版”,地址一致、功能对应,但有些字段被硬件屏蔽。sstatus是mstatus在 S 模式下的视图,你可以改SIE、SPIE、SPP,也能看到FS、SUM、MXR,但看不见MPP、MPRV这类 M 专属字段。sie/sip对应 M 模式的mie/mip,只是中断位映射有区别:S 模式软件中断看 bit 1,S 模式定时器中断看 bit 5,S 模式外部中断看 bit 9。

对照表我放在这里:

M 模式S 模式说明
mstatussstatus状态寄存器,S 视图字段更少
miesie中断使能
mipsip中断挂起
mtvecstvectrap 入口地址
mepcsepc异常返回地址
mcausescause异常原因
mtvalstval异常附加信息
mscratchsscratchtrap 入口暂存寄存器

写 OS 内核时,sepc和scause是系统调用处理的核心。U 模式ecall触发系统调用后,若已委托给 S,硬件会填scause=8、sepc=ecall地址,内核看到scause==8就知道是系统调用,而不是某种错误异常。这也是“ecall 作为系统调用指令”在 RISC-V Linux 里的实际依据。

4.2 satp:从裸机到虚拟内存的分水岭

satp是 S 模式最特殊的 CSR,它负责地址翻译。RV64 上它的结构是:MODE 在最高 4 位,9 表示启用 Sv48 分页,8 表示 Sv39 分页,0 表示裸机模式(不翻译);中间 16 位是 ASID,用于区分不同进程的 TLB 条目;低 44 位是根页表的物理页号,页表基址就是PPN << 12。写satp等于打开分页开关,从那以后 S 模式看到的所有地址都要经过页表翻译。

Sv39 的地址翻译可以这样理解:39 位虚拟地址分成 12 位页内偏移和 27 位虚拟页号,27 位再切成三份,每份 9 位,对应三级页表的每一级索引。每次访问内存,CPU 拿第一段索引查根页表,读到第二级页表地址;再查第二级,读到第三级;最后一级页表项里放着物理页号,加上页内偏移就是物理地址。这个过程也叫“多级页表步行”,是 RISC-V 缺页异常和 TLB 机制的基础。

页表项的低 12 位是权限和状态位:V(有效)、R/W/X(读/写/执行)、U(用户页)、G(全局映射)、A(访问过)、D(脏页)。S 模式默认不能访问 U 模式页,除非sstatus.SUM置 1;可执行页默认不能读,除非sstatus.MXR置 1。这两条规则在写内核的copy_to_user、内核模块加载器时都会遇到。

改完satp或修改页表项之后,必须在合适地方执行sfence.vma,否则旧 TLB 可能还缓存着转换结果,你改了页表却看不到效果。这个系列如果要继续往下写内存管理,我会单独展开一次。

4.3 U 模式 CSR 与时间计数器

U 模式能直接碰的 CSR 非常少,主要就是浮点控制寄存器和几个性能计数器。fflags、frm、fcsr与浮点异常标志和舍入模式相关;cycle、time、instret这三个计数器最常用:cycle数 CPU 周期,time是墙钟时间,instret数退休指令数,RV32 上还有对应的cycleh/timeh/instreth保存高 32 位。

M 模式可以用mcounteren控制这些计数器能不能被 S/U 模式访问,S 模式再用scounteren进一步控制用户态。做性能分析时这套机制很有用:内核可以给用户态开cycle和instret,让 benchmark 程序自己测量区间指令数,又不至于把整个计数器权限全放开。

一个小坑:RV32 上读取 64 位计数器时,高低位可能被更新撕裂。稳妥做法是读两次:先读高 32 位,再读低 32 位,再读一次高 32 位,如果两次高位不一致就重试。RISC-V 规范里专门给了这套伪代码,值得背下来。

5. 动手验证:用一段汇编把 M 模式降级到 S/U 并观察 trap

5.1 最小启动:设置 trap 入口后用 mret 降级

理论说再多,不如亲手跑一遍。下面是一段极简的启动汇编,在 QEMU 的-machine virt或任何 RISC-V 裸机环境里都能跑。目标是从 M 模式初始化好后,用mret降到 S 模式:

.section .text.init .globl _start _start: la t0, trap_entry csrw mtvec, t0 # 设置 M 模式 trap 入口 # 把 mstatus 的 MPP 配成 S 模式 csrr t0, mstatus li t1, ~(3 << 11) # 清掉 MPP[12:11] and t0, t0, t1 li t1, (1 << 11) # MPP = 01,表示 S 模式 or t0, t0, t1 csrw mstatus, t0 la t0, kernel_entry csrw mepc, t0 # mret 跳到这里 mret # 进入 S 模式,PC = kernel_entry kernel_entry: # 这里已经是 S 模式 # 故意执行一条非法指令,观察 trap .word 0xffffffff 1: j 1b # 防止跑飞 trap_entry: csrw mscratch, t0 csrr t0, mcause # 预期读到 2:非法指令 csrr t0, mepc # 预期指向 .word 0xffffffff csrr t0, mtval # 预期是 0xffffffff csrr t0, mscratch mret

如果你把这个跑起来,会发现非法指令 trap 确实进来了,mcause是 2,mtval是 0xffffffff。这验证了前面说的“非法指令异常会携带指令编码”,也验证了M 模式默认接管所有 trap的行为。

5.2 手动触发 U 模式 ecall,看异常原因怎么变

继续往下做:在 S 模式里再降一级到 U 模式,然后执行ecall。要点是 S 模式的sstatus.SPP要置 0,sepc指向用户代码,然后sret。U 模式ecall之后,回到 S 模式的stvec,scause会变成 8,sepc指向那条ecall指令本身——这个值在系统调用实现里特别重要,因为内核要根据它算用户程序的返回地址。

如果你没有配置medeleg,同样的 U 模式ecall会落到 M 模式,mcause也是 8,但处理者是 M 模式固件。这就是“委托之前谁兜底、委托之后谁处理”的直观体现:原因编号不变,变化的是入口地址和由谁接管。

5.3 我踩过的坑和排查建议

访问 CSR 最常见的报错就是非法指令异常,但很多新手会真的以为自己执行了非法指令。后来我总结出一个排查套路:第一,看mcause是不是 2;第二,看mtval里是不是一条看起来“正常的指令”或某个寄存器地址;第三,如果mtval像个 CSR 地址,那你十有八九是访问了不存在的、只读的、或者权限不够的 CSR。在 QEMU 里调试这种问题特别舒服,每条指令都能单步,还能看 trap 向量。真实硬件上就麻烦一些,所以我一般先在 QEMU 上把 CSR 访问流程调通,再上板子。

还有一个我常犯的错:在中断处理函数里改了mepc但忘记同步备份和恢复。中断嵌套一旦发生,前一个现场直接被覆盖,返回地址就乱了。RISC-V 的mepc只有一个,硬件不会帮你叠层,所以要么中断里禁止嵌套,要么在进入 handler 第一时间把mepc保存到栈上。

6. 番外:当 CSR 撞上稀疏矩阵——一次概念撞名现场

6.1 两个世界里的 CSR

如果你搜“CSR 和邻接表 内存空间消耗”,那是在问数据结构里的 Compressed Sparse Row,压缩稀疏行,跟 RISC-V 一点关系都没有。做算法的人用三个数组——values(非零值)、col_index(列号)、row_ptr(每行起始位置)——表示稀疏矩阵,省下大量零元素的存储。做处理器的人看到 CSR 则立刻想到mstatus、mtvec这些控制与状态寄存器。同一个缩写,在架构师和算法工程师嘴里完全是两个物种。

6.2 邻接表和 CSR 在内存上是不是同一量级

回到那个热搜问题:邻接表的空间复杂度是 O(V + E),V 是顶点数,E 是边数;CSR 的存储是 O(nnz + n + 1),nnz 是非零元素个数,近似于稀疏图的边数。从渐进复杂度的角度说,两者确实属于同一量级,都是线性于“顶点数加边数”。但量级相同不等于常数相同:CSR 用三个连续数组,对 cache 极其友好,适合矩阵运算和批量遍历;邻接表用链表或动态数组,灵活度更高,动态增删边容易得多。真实场景选型不能只看量级,还要看你是静态建图还是频繁改图。

6.3 撞名提醒

这篇专栏既然要讲“CSR 速查”,我特意把这个撞名放在最后提醒一句:写文档、提问、或者跟人讨论时,先确认对方说的 CSR 是 Control and Status Register 还是 Compressed Sparse Row。我在社区看到过两次因为缩写歧义吵起来的帖子,一方以为对方在问机器学习稀疏矩阵优化,另一方以为在问处理器特权寄存器配置。搞清楚上下文,比急着背寄存器列表更重要。如果这篇文章后你还想深入某一块——比如satp分页细节、mstatus每个位的实操含义、或者怎么用 OpenSBI 配置中断委托——都可以继续沿用这个专栏的方式,一块一块拆开来聊。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询