3. 处理器模式与特权级:ARM的7种模式 vs RISC-V的3种特权级
说到处理器模式,我脑子里第一个蹦出来的画面,就是当年调试ARM Cortex-A8时,被一堆模式切换搞得晕头转向的场景。那时候我就在想,这东西能不能简单点?后来接触RISC-V,发现它还真就简单了。
今天咱们就来掰扯掰扯,ARM那7种模式跟RISC-V的3种特权级,到底差在哪。
3.1 ARM的7种模式:一个复杂的特权世界
ARM的设计哲学,说白了就是「每种异常都有自己的专属模式」。你想想看,一个系统里要处理中断、要处理系统调用、还要处理各种异常,每种情况都给你配一个独立模式,确实方便管理。
这7种模式分别是:
- User模式(USR):普通程序运行的地方,权限最低
- FIQ模式:快中断模式,处理高速中断
- IRQ模式:普通中断模式
- Supervisor模式(SVC):系统调用入口,我经常在这里做任务切换
- Abort模式(ABT):数据/指令预取异常
- Undefined模式(UND):遇到不认识指令时触发
- System模式(SYS):跟User共享寄存器,但有特权
关键点:除了User模式,其他6种都是特权模式。每个特权模式都有自己的栈指针和保存寄存器,这就是所谓的「影子寄存器」。
我在项目中遇到过一个问题:某个中断处理函数里不小心调用了系统调用,结果SVC模式嵌套,栈直接崩了。嗯,这就是ARM模式切换的坑——你得自己管好每个模式的栈。
3.2 RISC-V的3种特权级:极简主义的设计
RISC-V的设计者显然是个「少即是多」的信徒。它只定义了3种特权级:
| 特权级 | 编码 | 名称 | 用途 |
|---|---|---|---|
| U-Mode | 00 | 用户模式 | 普通应用程序 |
| S-Mode | 01 | 监督模式 | 操作系统内核 |
| M-Mode | 11 | 机器模式 | 固件、Bootloader、安全监控 |
你可能会问:为什么编码是00、01、11,跳过了10?其实这是为了未来扩展预留的。RISC-V的规范里明确说了,10编码留给未来的Hypervisor模式(H-Mode)。
我的习惯:在RISC-V的启动代码里,我通常只在M-Mode下初始化硬件,然后直接跳转到S-Mode跑操作系统。中间不需要像ARM那样反复切换模式,清爽多了。
3.3 核心差异:模式切换 vs 异常委托
ARM的模式切换,是硬件自动完成的。比如你触发了一个IRQ,CPU自动就切到IRQ模式,保存当前模式的PC和CPSR。这个过程很快,但有个问题——你没法控制它切到哪个模式。
RISC-V的做法完全不同。它引入了「异常委托机制」(Trap Delegation)。什么意思呢?
- M-Mode可以决定:哪些异常交给S-Mode处理
- S-Mode可以决定:哪些异常交给U-Mode处理
- 通过
medeleg和mideleg寄存器配置
// RISC-V 异常委托示例
// 把系统调用(ecall from U-Mode)委托给S-Mode处理
csrw medeleg, (1 << 8) // 第8位对应ECALL from U-mode
// 把时钟中断委托给S-Mode
csrw mideleg, (1 << 5) // 第5位对应时钟中断
我曾经在移植FreeRTOS到RISC-V时,就被这个委托机制坑过一次。我忘了配置medeleg,结果所有异常都跑到了M-Mode,而我的M-Mode处理函数里啥也没写,系统直接挂在那。后来查了两天才发现是委托没配好。
3.4 寄存器视角的对比
ARM每个特权模式都有自己的影子寄存器组,特别是SP和LR。这意味着:
- 模式切换时,硬件自动保存返回地址到LR_<mode>
- 每个模式有独立的栈指针SP_<mode>
- 总共需要维护7个栈!
RISC-V就简单多了:
- 所有模式共享同一套通用寄存器(x0-x31)
- 异常发生时,硬件自动保存PC到
mepc或sepc - 特权级切换通过
mstatus的MPP/SPP位记录 - 你只需要维护2-3个栈(M栈、S栈、U栈)
注意:RISC-V虽然寄存器少,但每个模式都有独立的CSR(控制状态寄存器)。比如M-Mode有mstatus、mepc、mcause,S-Mode有sstatus、sepc、scause。千万别搞混了!
3.5 实际启动流程中的体现
在ARM的启动流程里,你经常能看到这样的代码:
@ ARM 启动时设置各模式栈
mov r0, #0xD2 @ IRQ模式
msr CPSR_c, r0
ldr sp, =irq_stack_top
mov r0, #0xD3 @ SVC模式
msr CPSR_c, r0
ldr sp, =svc_stack_top
mov r0, #0xD7 @ ABT模式
msr CPSR_c, r0
ldr sp, =abt_stack_top
而在RISC-V的启动里,你只需要:
# RISC-V 启动时设置栈
# 先切到M-Mode(默认就是M-Mode)
la sp, _stack_top
# 如果需要S-Mode,再设置S-Mode栈
csrw sscratch, sp # 保存M栈指针
li t0, (1 << 11) # MPP = 01 (S-Mode)
csrw mstatus, t0
la t0, s_mode_entry
csrw mepc, t0
mret # 切换到S-Mode
你看,ARM要切好几次模式,设好几个栈。RISC-V呢?一次mret就搞定了。我个人更喜欢RISC-V这种方式,少了很多心智负担。
3.6 我的选择建议
如果你在做实时性要求极高的系统,比如工业控制、车载ECU,ARM的多种模式其实有优势——每种异常都有独立栈,不会互相干扰。但代价是代码复杂度高,调试起来也费劲。
如果你在做通用嵌入式系统,或者想快速原型验证,RISC-V的3级特权级绝对够用。我最近一个IoT项目就用的RISC-V,启动代码不到100行,比ARM省了一半。
最后说一句:不管用哪种架构,理解特权级的本质才是关键。它不是为了炫技,而是为了隔离——隔离用户程序和内核,隔离不同安全等级的任务。这个思想,比具体有几个模式重要得多。