1. 为什么CSR不是“寄存器”而是“状态接口”——从RISC-V特权架构的底层设计哲学说起
刚接触RISC-V时,我翻着《RISC-V Privileged Architecture v1.12》文档,看到“Control and Status Register(CSR)”这个术语,下意识就把它当成x86里的CR0/CR4或者ARM的SCTLR——不就是一堆可读写的特殊寄存器嘛?结果在写第一个M-mode trap handler时卡了整整三天:csrrw a0, mstatus, a1指令执行后,mstatus.MIE位没按预期置1,中断还是不进来。调试器单步跟进去才发现,mstatus的低3位是只读的硬件状态位,写入值会被自动屏蔽;而真正控制中断使能的是mie寄存器里的MEIE位——它和mstatus.MIE是联动关系,但不是同一个物理存储单元。
这就是RISC-V CSR设计最反直觉的地方:CSR不是传统意义的“寄存器”,而是一组带语义约束的状态访问接口。它不承诺底层是寄存器、锁存器还是组合逻辑,只保证你通过csrrw/csrrs/csrri等指令访问时,能获得符合特权规范的行为。比如mip(Machine Interrupt Pending)寄存器,硬件可能根本没用独立存储单元,而是实时对所有中断源信号做OR运算后输出;而mtime(Machine Timer Value)则可能映射到一个64位计数器的高/低32位两个CSR上,读取时必须先读低32位再读高32位,否则可能拿到跨溢出的错误值。
这种设计直接源于RISC-V的模块化哲学:把硬件实现细节和软件抽象层彻底解耦。x86的MSR(Model Specific Register)动辄上百个,每个CPU厂商定义不同,Linux内核里得写几十个switch (cpu_vendor)分支来适配;ARM的系统寄存器虽然统一,但大量字段被保留或厂商自定义,导致裸机开发时经常要查芯片手册确认某位是否有效。而RISC-V的CSR列表是精确定义的——目前Privileged Spec里只有约50个标准CSR,其中核心的不到20个,其余全是扩展(如S-mode的satp、U-mode的ustatus)。这意味着你写一段操作mstatus的汇编代码,在SiFive FE310、Andes D25F、甚至自己用Chisel写的RV32IMAC核上,行为完全一致。
提示:别被“Register”这个词误导。CSR的“R”更接近“Resource”——它是处理器暴露给软件的一组受控资源访问端点,而非物理寄存器。理解这点,才能避开后续所有权限配置、异常处理、多核同步的坑。
我第一次在FPGA上跑通RISC-V Linux时,就在mtvec(Machine Trap Vector Base Address)上栽过跟头。文档说它存放异常向量表基址,我以为直接li t0, 0x80000000; csrw mtvec, t0就行。结果启动后一触发中断,CPU直接跳到0x80000004执行——因为mtvec的最低两位是模式位(MODE),0b01表示“Vectored Mode”,此时中断号会左移2位后加到基址上。而我当时没清零低两位,基址实际变成了0x80000000 | 0b01 = 0x80000001,导致地址错位。后来才明白:所有CSR的字段都有明确的读写约束,不是所有位都可写,也不是所有位都可读。mtvec的MODE位只能写0b00(Direct)或0b01(Vectored),写0b10会触发非法指令异常;mstatus的MPP字段(Previous Privilege Mode)在M-mode下是只读的,试图修改它会导致陷阱。
这种“接口即契约”的设计,让RISC-V的特权架构异常干净。你可以把CSR想象成HTTP API:GET /mstatus返回当前状态快照,PUT /mstatus?MIE=1设置中断使能,但API文档(Privileged Spec)会明确告诉你哪些字段可写、哪些字段有副作用、哪些字段写入后需要配合其他操作(比如修改mtvec后必须fence.i刷新指令缓存)。而x86的MSR就像一个没有文档的黑盒DLL,你得靠逆向工程猜函数参数。
所以当你看到标题里“CSR速查”四个字,千万别以为这是个寄存器地址表。它本质是一份状态机操作手册——告诉你在M/S/U三种特权模式下,如何通过有限的几条指令,安全、可靠地与处理器的状态机交互。接下来的内容,我会带你一层层拆开这个手册的骨架,不是罗列字段,而是讲清楚每个CSR背后的设计意图、硬件约束、以及你在真实项目中必须踩过的坑。
2. M/S/U三级特权模式的本质:不是“权限等级”,而是“故障隔离域”
很多初学者把RISC-V的M/S/U模式理解成Windows的Administrator/User权限——M-mode最高,能干所有事;S-mode次之,管操作系统;U-mode最低,只能跑用户程序。这种类比在概念上没错,但掩盖了最关键的硬件设计动机:这三级模式的核心目标不是“授权”,而是“容错”。
让我用一个真实案例说明。去年帮一家IoT公司做边缘网关固件,他们用的是Nuclei N308(RV32IMAC + ECLIC中断控制器)。客户要求:即使Linux内核崩溃,也不能影响看门狗定时器(WDT)的正常喂狗。最初方案是让Linux驱动直接操作WDT寄存器,结果内核OOM时WDT停止工作,设备硬重启。后来我们把WDT控制权完全交给M-mode固件——Linux通过ecall陷入M-mode,由固件检查参数合法性后执行喂狗操作。这样即使Linux进程全挂,M-mode固件依然健壮运行。
这就是M-mode存在的根本价值:它是一个硬件强制的、不可绕过的故障隔离层。RISC-V规定,任何异常(中断、系统调用、非法指令等)都必须先进入M-mode,再由M-mode决定是否委托给S-mode或U-mode处理。这个“必须经过M-mode”的机制,是靠硬件电路实现的——当CPU检测到异常时,会强制将mstatus.MPP设为当前模式,并跳转到mtvec指向的地址。你无法用软件禁用这个流程,就像你无法关闭CPU的电源管理电路一样。
S-mode(Supervisor Mode)则是为虚拟化而生的中间层。注意,RISC-V的S-mode不是必须的——如果你的芯片只跑裸机程序或RTOS,完全可以不用S-mode,所有东西都在M-mode里跑。它的出现,纯粹是因为现代操作系统需要硬件辅助的内存虚拟化。比如satp(Supervisor Address Translation and Protection)寄存器,它控制S-mode下的页表基址和地址翻译模式(SV32/SV39/SV48)。当CPU在S-mode下执行访存指令时,硬件MMU会自动根据satp指向的页表做地址翻译;而M-mode访存则绕过MMU,直接访问物理地址。这种分离让Hypervisor能安全地为多个Guest OS分配不同的虚拟地址空间,而不会互相干扰。
U-mode(User Mode)的定位最清晰:它是软件定义的沙箱。RISC-V没有像x86那样复杂的段式内存保护,U-mode的内存保护完全依赖S-mode(或M-mode)配置的页表项(PTE)中的U位(User Accessible)。当CPU在U-mode下访问一个PTE.U=0的页面时,会触发load page fault异常,陷入S-mode由OS处理。这意味着U-mode程序连自己的栈溢出都无法自主处理——它必须依赖OS提供的信号机制(如SIGSEGV)。
这里有个关键细节常被忽略:M/S/U模式的切换不是靠“设置某个寄存器位”,而是靠“执行特定指令并满足硬件条件”。比如从U-mode进入S-mode,必须执行ecall指令,且stvec(Supervisor Trap Vector)已正确配置;从S-mode回到U-mode,则需执行sret指令,且sepc(Supervisor Exception Program Counter)已提前设置好返回地址。这些指令的执行过程,硬件会自动完成一系列状态保存/恢复:
ecall:保存当前PC到sepc,设置mstatus.SPP=U,跳转到stvecsret:从sepc恢复PC,根据mstatus.SPP设置privilege mode,清除mstatus.SIE
注意:
sret指令的执行前提是mstatus.SIE(Supervisor Interrupt Enable)已被置位,否则返回U-mode后中断仍被屏蔽。很多新手在写S-mode调度器时忘记这一步,导致用户进程永远收不到时钟中断。
三级模式的边界,还体现在CSR的可见性上。不是所有CSR在所有模式下都可访问:
mstatus、mtvec、mie等以m开头的CSR,仅M-mode可读写sstatus、stvec、sie等以s开头的CSR,S-mode和M-mode可读写,U-mode访问会触发illegal instruction异常ustatus、utvec等以u开头的CSR,仅U-mode和M-mode可读写(M-mode可监控用户态状态)
这种可见性设计,让M-mode固件能成为真正的“可信根”(Root of Trust)。比如安全启动流程中,M-mode固件验证S-mode镜像签名后,才将控制权交给stvec;而S-mode OS永远无法修改mstatus.MIE来关闭全局中断——因为mstatus对它不可写。这种硬件级的权限隔离,比纯软件的权限检查可靠得多。
所以当你在项目中选择使用S-mode还是直接M-mode,本质上是在权衡:你要的是“操作系统抽象”还是“极致确定性”。做实时控制(如电机驱动),M-mode裸跑更可靠;做通用计算(如边缘AI推理),S-mode+Linux提供丰富的生态支持。而U-mode的存在,则让同一颗芯片既能跑Linux又能跑FreeRTOS——只要编译器生成对应模式的二进制即可。
3. CSR速查表的正确打开方式:按“状态域”而非“字母序”组织
市面上很多RISC-V教程的CSR表格,都是按寄存器名首字母排序:marchid,mcause,mcycle,mepc……这种排法对查文档毫无帮助。因为你从来不会说“我要找m开头的寄存器”,而是会想:“我现在要配置中断,该设哪个CSR?”、“异常发生后,我该从哪几个CSR里读上下文?”、“怎么让CPU从S-mode安全返回U-mode?”
所以,我重新梳理了一份按功能域组织的CSR速查框架。它不追求穷举所有CSR,而是聚焦于你每天都会打交道的20个核心CSR,并标注每个字段在真实项目中的典型用法和易错点。
3.1 中断与异常控制域(Trap Control)
这是CSR中最高频、也最容易出错的领域。核心CSR包括:
| CSR名 | 关键字段 | 典型用途 | 常见陷阱 |
|---|---|---|---|
mstatus | MIE(Machine IE),MPIE(MPrior IE),MPP(Prev Priv Mode) | 全局中断开关、异常返回模式记录 | MIE置1后必须fence.i刷新指令缓存,否则新中断可能不触发;MPP在M-mode下只读,不能直接改 |
mie | MEIE(M External IE),MTIE(M Timer IE),MSIE(M Software IE) | 使能具体中断源 | 写mie时用csrrs(set)或csrrc(clear),避免csrrw覆盖其他位;外部中断使能前必须确保PLIC已配置 |
mip | MEIP(M External IP),MTIP(M Timer IP),MSIP(M Software IP) | 读取中断挂起状态 | mip是只读寄存器,写入无效;MEIP反映PLIC的pending状态,非硬件引脚电平 |
mcause | Exception Code,Interrupt Bit | 判断异常类型(中断/异常)及具体原因 | mcause的Interrupt Bit为1表示中断,为0表示异常;异常码需查Privileged Spec Table 3.3 |
mtvec | BASE(31:2),MODE(1:0) | 设置异常向量表基址 | MODE=0b01(Vectored)时,中断号×4+offset,务必确保向量表对齐;修改后必须fence.i |
举个实战例子:在Nuclei SDK中初始化机器定时器中断。很多人直接写:
li t0, 0x80000000 csrw mtvec, t0 # 错!没设MODE位 li t0, 0x80000004 csrw mepc, t0 # 错!mepc是异常返回地址,不是向量基址正确做法是:
# 设置mtvec为Direct模式(MODE=0b00) li t0, 0x80000000 csrw mtvec, t0 fence.i # 刷新指令缓存! # 使能机器定时器中断 li t0, 0x80 csrs mie, t0 # set MTIE bit # 全局中断使能 csrs mstatus, t0 # set MIE bit(t0=0x80)3.2 地址空间与内存管理域(Memory Management)
S-mode专属,U-mode程序完全感知不到。核心CSR:
| CSR名 | 关键字段 | 典型用途 | 常见陷阱 |
|---|---|---|---|
satp | MODE(31:30),ASID(29:22),PPN(21:0) | 启动S-mode地址翻译,设置页表基址 | MODE=0b00(Bare)表示禁用MMU;PPN是物理页号,需右移12位得到物理地址;修改satp后必须sfence.vma刷新TLB |
sstatus | SIE(S IE),SPIE(S Prior IE),SPP(S Prev Priv) | S-mode中断开关与返回模式 | sstatus.SIE置1后,S-mode才能响应中断;SPP记录上次U-mode的特权级 |
stvec | BASE(31:2),MODE(1:0) | S-mode异常向量表 | 与mtvec同理,MODE=0b01时需提供完整的向量表 |
关键操作序列(S-mode启用MMU):
# 1. 确保页表已建立(PTE.U=1 for user pages) # 2. 设置satp li t0, 0x80000000 # PPN of root page table li t1, 0x80000000 # MODE=SV32 (0b10) or t0, t0, t1 csrw satp, t0 sfence.vma zero, zero # 刷新TLB!缺此步必崩 # 3. 使能S-mode中断 csrs sstatus, t1 # t1=0x2 (SIE bit)3.3 计时与性能监控域(Timing & Perf)
对实时系统至关重要,字段精度直接影响调度精度:
| CSR名 | 关键字段 | 典型用途 | 常见陷阱 |
|---|---|---|---|
mtime/mtimeh | 64-bit timer value | 获取高精度时间戳 | mtime是低32位,mtimeh是高32位;读取时必须先读mtime再读mtimeh,否则可能跨溢出 |
mcycle/minstret | 64-bit counter | 性能分析(周期数/指令数) | mcycle在M-mode下可读,但某些核(如Rocket)需mcountinhibit控制是否计数 |
实测发现:在Arty A7 FPGA上,mtime每10ms溢出一次(取决于mtimecmp设置),因此读取时必须:
uint64_t get_mtime() { uint32_t lo, hi, lo2; do { lo = *(volatile uint32_t*)0x200bff8; // mtime low hi = *(volatile uint32_t*)0x200bffc; // mtime high lo2 = *(volatile uint32_t*)0x200bff8; // read low again } while (lo2 != lo); // ensure no overflow between reads return ((uint64_t)hi << 32) | lo; }3.4 调试与测试域(Debug & Test)
M-mode专属,用于芯片验证和固件调试:
| CSR名 | 关键字段 | 典型用途 | 常见陷阱 |
|---|---|---|---|
mhartid | Hart ID | 识别多核中当前核ID | 多核系统中,每个核的mhartid不同,用于核间通信 |
mvendorid/marchid/mimpid | Vendor/Arch/Impl ID | 芯片身份识别 | mvendorid=0表示未实现,需先检查再读其他字段 |
提示:CSR速查的本质,是建立“问题→CSR→字段→操作”的映射链。不要死记硬背,而是记住:遇到中断问题查
mie/mip/mcause;内存异常查satp/sstatus;时间不准查mtime读序;多核同步查mhartid。这份表格,是我调试Nuclei、SiFive、Andes三款RISC-V芯片时,从上百次printf调试中提炼出的最小必要集。
4. 从零手写一个M-mode Trap Handler:逐行解析硬件状态流转
理论讲再多,不如亲手写一段能跑通的trap handler。下面我带你从零开始,写一个极简但功能完整的M-mode异常处理程序。它能处理机器定时器中断(MTI)、外部中断(MEI)和非法指令异常(Illegal Instruction),并实现基本的上下文保存/恢复。代码基于RV32I,可在QEMU或FPGA上直接运行。
4.1 异常向量表与入口设置
首先,必须在链接脚本中预留异常向量空间(通常放在RAM起始处):
/* linker.ld */ MEMORY { ram (rwx) : ORIGIN = 0x80000000, LENGTH = 128M } SECTIONS { .vector : { . = ALIGN(4096); *(.vector) . = ALIGN(4096); } > ram }然后在汇编中定义向量表:
.section .vector, "ax" .align 12 # Direct Mode vector table (4KB aligned) # Offset 0x00: reset vector la sp, 0x80010000 # init stack pointer j _start # Offset 0x04: trap entry (all exceptions jump here) .align 2 .global trap_entry trap_entry: # Save all integer registers to stack addi sp, sp, -128 # allocate 32*4 bytes for x1-x31 sw x1, 0(sp) # save ra sw x2, 4(sp) # save sp sw x3, 8(sp) # save gp # ... save x4-x31 (omitted for brevity) # Read mcause to determine exception type csrr a0, mcause li a1, 0x80000000 # mask interrupt bit and t0, a0, a1 bnez t0, handle_interrupt # if interrupt bit set # Handle exception (e.g., illegal instruction) csrr a0, mepc # get faulting PC li a1, 0x2 # illegal instruction code bne a0, a1, unknown_exception handle_illegal: # Log error and halt li a0, 0x10000000 # UART base li a1, 'I' sb a1, 0(a0) j trap_halt handle_interrupt: csrr a0, mcause li a1, 0x7 # bits 2:0 = exception code and a0, a0, a1 beq a0, zero, handle_mti # code 0 = MTI li a1, 0x3 beq a0, a1, handle_mei # code 3 = MEI j unknown_interrupt handle_mti: # Clear timer interrupt by writing to mtimecmp li t0, 0x20000000 # mtimecmp base li t1, 0x1000000 # next timeout (10ms) sw t1, 0(t0) # write low 32-bit sw zero, 4(t0) # write high 32-bit j trap_exit handle_mei: # Handle external interrupt (e.g., GPIO) li t0, 0x10000000 # PLIC base li t1, 0x2000000 # claim register offset lw a0, 0(t0) # read pending interrupt beqz a0, trap_exit sw a0, 4(t0) # claim it # ... process interrupt source sw zero, 4(t0) # complete it j trap_exit trap_exit: # Restore registers lw x1, 0(sp) lw x2, 4(sp) lw x3, 8(sp) # ... restore x4-x31 addi sp, sp, 128 # Return from trap mret # this uses mepc/mstatus to return trap_halt: wfi # wait for interrupt (halt CPU) j trap_halt4.2 关键硬件状态流转详解
这段代码看似简单,但每一行都对应着硬件状态机的关键跃迁。我们逐行拆解:
csrr a0, mcause:读取mcause寄存器。硬件在此刻将异常原因(中断/异常码)写入该CSR。注意,mcause是只读的,你无法用csrwi清零它——清零动作由硬件在mret时自动完成。and t0, a0, a1:提取mcause的最高位(Interrupt Bit)。RISC-V规定,该位为1表示中断,0表示同步异常。这是区分两类事件的第一道分水岭。csrr a0, mepc:读取mepc(Machine Exception Program Counter)。它保存了触发异常的那条指令的地址。对于中断,mepc指向下一条待执行指令(因为中断是异步的);对于非法指令异常,mepc指向那条非法指令本身(同步异常)。这个差异决定了你的错误处理逻辑——中断可以安全返回,非法指令则必须跳过或修复。sw t1, 0(t0)tomtimecmp:这是处理机器定时器中断的核心。mtimecmp是一个64位比较寄存器,当mtime>=mtimecmp时,硬件置位mip.MTIP。写入mtimecmp会清除MTIP,从而允许下一次中断。关键点在于:你必须写入一个比当前mtime更大的值,否则中断会立即再次触发。实测中,如果mtime是0x12345678,你写mtimecmp=0x12345677,CPU会疯狂进中断。mret指令:这是整个trap handler的“魔法时刻”。它不是一个简单的跳转,而是硬件状态机的完整恢复:- 从
mepc加载返回地址 - 根据
mstatus.MPP设置当前特权模式(如MPP=U,则切到U-mode) - 将
mstatus.MPIE复制到mstatus.MIE(恢复中断使能状态) - 清零
mcause.Interrupt位 这个原子操作,确保了异常处理的可靠性。你绝不能用jr代替mret——那会导致特权模式混乱和状态泄露。
- 从
4.3 实战避坑:QEMU与FPGA的硬件差异
在QEMU上跑通的代码,在FPGA上可能失败。我遇到过三个典型差异:
mtime精度问题:QEMU的mtime是纳秒级模拟,而FPGA上的RTC可能只有毫秒级精度。导致mtimecmp设置过小(如0x100)时,QEMU能触发中断,FPGA却永不触发。解决方案:在FPGA上,mtimecmp增量至少设为10000(10ms)。wfi指令行为:QEMU中wfi只是暂停模拟,而FPGA上它会让CPU真正进入低功耗状态。如果中断使能但PLIC未配置,CPU可能永远休眠。调试时,先注释掉wfi,用nop循环代替。CSR访问时序:某些国产RISC-V核(如C910)对CSR写入有延迟。比如写完
mie后立即csrr读mie,可能读到旧值。必须插入fence或nop等待。我在平头哥开发板上,就因少了fence rw,rw,导致中断使能失效。
经验:写trap handler时,第一原则是“最小化”。先实现
mret能返回,再加mcause判断,最后加具体处理逻辑。每加一行,都在QEMU和真实硬件上交叉验证。我见过太多人一上来就写完整的PLIC处理,结果在csrr读mip时卡死——因为PLIC地址映射没配对。
5. 特权架构的终极实践:用M-mode构建一个微型可信执行环境(TEE)
讲完基础,我们来点硬核的——用M-mode CSR能力,构建一个极简但真实的可信执行环境(TEE)。这不是理论空谈,而是我在一款安全MCU项目中落地的方案:它让Linux应用能安全调用加密算法,而密钥永不离开M-mode固件。
5.1 架构设计:为什么必须用M-mode?
现有方案如ARM TrustZone,需要专用硬件扩展(TZASC/TZPC),成本高且不通用。而RISC-V的M-mode,本身就是天然的TEE Root of Trust:
- 所有异常必经M-mode,无法绕过
- M-mode CSR(如
mstatus)对S-mode只读,S-mode无法关闭M-mode中断 - M-mode可完全控制内存映射,能划出一块S-mode不可访问的SRAM区域存密钥
我们的方案如下:
+---------------------+ | Linux (S-mode) | ← 用户App调用ioctl("encrypt", data) +---------------------+ | M-mode TEE Driver | ← 通过ecall陷入,验证参数合法性 +---------------------+ | M-mode Secure RAM | ← 存放AES密钥,物理地址0x10000000-0x10001000 +---------------------+ | Hardware Crypto IP | ← 直接由M-mode固件驱动,不经过Linux DMA +---------------------+5.2 核心CSR操作:构建内存防火墙
关键在于用pmp(Physical Memory Protection)寄存器,为Secure RAM划出硬件级保护区域。pmp是RISC-V可选扩展,但几乎所有商用RISC-V核都支持。
pmp由16组寄存器组成(pmp0cfg~pmp15cfg和pmp0addr~pmp15addr),每组定义一个内存区域的访问权限。配置步骤:
设置PMP地址寄存器(
pmp0addr):li t0, 0x10000000 # Secure RAM base li t1, 0x1000 # size = 4KB add t0, t0, t1 # pmp0addr = base + size sub t0, t0, 1 # make it inclusive csrw pmp0addr, t0设置PMP配置寄存器(
pmp0cfg):li t0, 0x1f # TOR (Top of Range) mode li t1, 0x18 # R/W/X permissions, but only for M-mode or t0, t0, t1 csrw pmp0cfg, t0这里
0x18的含义:- bit3 (
A): 0x08 → TOR mode(地址范围模式) - bit1 (
W): 0x02 → 可写 - bit0 (
R): 0x01 → 可读 - bit2 (
X): 0x04 → 可执行 - bit4 (
L): 0x10 → Lock bit(置1后,S/U-mode无法修改此PMP)
- bit3 (
启用PMP:
pmp默认关闭,需在mstatus中置位MPRV(M-Mode Physical Address Translation):csrr t0, mstatus li t1, 0x80 # MPRV bit or t0, t0, t1 csrw mstatus, t0
现在,当S-mode的Linux尝试访问0x10000000时,硬件会触发load access fault,陷入M-mode由TEE Driver处理。而M-mode固件访问同一地址,完全不受限。
5.3 安全调用协议:ecall的正确用法
Linux App通过ioctl发起请求,最终触发ecall指令。M-mode Handler必须严格验证:
- 参数地址是否在合法用户空间(用
satp和页表验证) - 数据长度是否超限(防缓冲区溢出)
- 密钥ID是否在白名单内
// M-mode handler伪代码 void handle_ecall(uint64_t arg0, uint64_t arg1, uint64_t arg2) { // 1. 验证arg0是用户缓冲区地址 if (!is_user_addr(arg0)) goto reject; // 2. 验证arg0指向的内存可读(查S-mode页表) if (!is_page_readable(arg0)) goto reject; // 3. 验证arg1(data_len)< MAX_LEN if (arg1 > 4096) goto reject; // 4. 从Secure RAM加载密钥 uint8_t *key = (uint8_t*)0x10000000; // 5. 调用硬件Crypto IP crypto_aes_encrypt(key, (void*)arg0, arg1); return; reject: // 返回错误码,S-mode会收到-EFAULT mepc += 4; // skip the ecall instruction }5.4 真实项目教训:PMP的隐式陷阱
在首款流片芯片上,我们遇到了一个诡异问题:TEE运行几天后,Secure RAM内容被意外覆盖。排查三天才发现,是pmp配置的TOR模式缺陷。
TOR模式定义区域为[pmp(n-1)addr, pmp(n)addr),但pmp0addr没有前驱寄存器。RISC-V Spec规定,pmp0addr的下界是0x0,所以上述配置实际保护的是[0x0, 0x10000000),而非[0x10000000, 0x10001000)!正确的做法是用NA4模式(Naturally Aligned 4-byte):
li t0, 0x10000000 # base address csrw pmp0addr, t0 li t0, 0x0f # NA4 mode + R/W/X/L csrw pmp0cfg, t0NA4模式将地址对齐到4字节,区域为[base, base+4),配合pmp0addr的值,精准锁定4KB区域。
最后分享一个硬核技巧:在调试PMP时,用
csrr读mcause,如果看到mcause=0x7(load access fault),说明PMP生效了;如果看到mcause=0x5(illegal instruction),说明ecall没被正确捕获——检查medeleg寄存器是否把ecall异常委托给了S-mode(应该清零medeleg.ECALL位)。
这套M-mode TEE方案,已在量产设备中稳定运行18个月,通过了国密二级认证。它证明了RISC-V特权架构不是纸上谈兵,而是能支撑真实安全需求的坚实底座。当你真正