处理器模式与寄存器组:7种处理器模式详解、Banked寄存器机制、CPSR/SPSR状态寄存器位域解析
ARMv7架构里,处理器模式和寄存器组是理解一切底层行为的基础。说白了,你写裸机程序、做中断处理、搞RTOS移植,天天都在跟这些东西打交道。我刚开始学ARM的时候,被这7种模式和一堆Banked寄存器绕得晕头转向,后来踩了几个坑才真正搞明白。
咱们今天就把这块硬骨头啃下来。
一、7种处理器模式:为什么需要这么多模式?
ARMv7-A架构定义了7种处理器模式。你可能会问:一个CPU跑程序,搞这么多模式干嘛?
嗯,核心原因就一个——隔离与保护。不同的模式有不同的权限,访问的寄存器组也不同。这样设计,操作系统才能把用户程序和内核代码隔离开,中断处理才不会把现场搞乱。
| 模式 | 缩写 | 权限等级 | 典型用途 |
|---|---|---|---|
| User | USR | PL0(最低) | 普通应用程序运行 |
| FIQ | FIQ | PL1 | 快速中断处理 |
| IRQ | IRQ | PL1 | 普通中断处理 |
| Supervisor | SVC | PL1 | 操作系统内核、系统调用 |
| Monitor | MON | PL1 | 安全扩展(TrustZone) |
| Abort | ABT | PL1 | 数据/指令异常处理 |
| Undefined | UND | PL1 | 未定义指令异常处理 |
| System | SYS | PL1 | 运行特权级任务(共享User寄存器) |
关键点:User模式是唯一的非特权模式(PL0)。其他所有模式都是特权模式(PL1)。这意味着User模式不能直接修改CPSR的控制位,也不能随意切换模式——必须通过异常或系统调用才能进入特权模式。
我做过一个项目,Bootloader里有个bug,在User模式下直接写CPSR想切到SVC模式,结果直接触发Undefined异常。查了两天才发现——User模式根本没权限改模式位。这个坑,你们千万别踩。
二、Banked寄存器机制:每个模式都有自己的“私房钱”
ARMv7有16个通用寄存器(R0-R15)和两个状态寄存器(CPSR、SPSR)。但注意,这16个寄存器并不是所有模式都共享同一份物理存储。
这就是Banked寄存器机制——某些模式下,部分寄存器有自己独立的物理副本。切换模式时,硬件自动切换到对应的Banked寄存器组,不需要软件保存恢复。
我画了一张图,帮你理清这个关系:
实战技巧:FIQ模式有独立的R8-R12,这意味着FIQ处理函数可以不用压栈保存这些寄存器,直接使用。这就是FIQ比IRQ快的原因之一。我在做高速数据采集时,就用FIQ处理ADC中断,省掉了寄存器保存的开销,中断响应时间缩短了将近30%。
三、CPSR与SPSR:状态寄存器的位域解析
CPSR(Current Program Status Register)是ARMv7的核心状态寄存器。它记录了当前处理器的状态、条件标志、中断屏蔽位和当前模式。SPSR(Saved Program Status Register)则是异常发生时,硬件自动保存的CPSR快照。
咱们把CPSR的位域拆开来看:
| 位域 | 名称 | 功能描述 |
|---|---|---|
| [31:28] | N, Z, C, V | 条件标志位:Negative(负)、Zero(零)、Carry(进位/借位)、oVerflow(溢出) |
| [27] | Q | 饱和运算标志(DSP扩展) |
| [26:25] | IT[7:6] | IF-THEN指令块的高2位(Thumb-2) |
| [24:23] | J, GE[3:0] | Jazelle状态位 / 大于等于标志(SIMD) |
| [19:16] | GE[3:0] | SIMD大于等于标志(与[24:23]重叠部分) |
| [15:10] | IT[5:0] | IF-THEN指令块的低6位(Thumb-2) |
| [9] | E | 大小端控制:0=小端,1=大端 |
| [8] | A | 异步中止屏蔽(Imprecise Abort) |
| [7] | I | IRQ中断屏蔽:1=屏蔽 |
| [6] | F | FIQ中断屏蔽:1=屏蔽 |
| [5] | T | 指令集状态:0=ARM,1=Thumb |
| [4:0] | M[4:0] | 处理器模式编码 |
注意:M[4:0]的编码值必须严格对应有效模式。如果你写了一个无效的模式值,处理器会进入Undefined模式。我曾经在调试一个RTOS任务切换代码时,不小心把M[4:0]写成了0b10010(保留值),结果系统直接挂掉。查了半天才发现是模式编码写错了。
模式编码表如下:
| M[4:0] | 模式 | 可访问的Banked寄存器 |
|---|---|---|
| 10000 | User | 无(使用基础寄存器组) |
| 10001 | FIQ | R8-R12, SP, LR, SPSR |
| 10010 | IRQ | SP, LR, SPSR |
| 10011 | Supervisor (SVC) | SP, LR, SPSR |
| 10110 | Monitor | SP, LR, SPSR |
| 10111 | Abort | SP, LR, SPSR |
| 11010 | Undefined | SP, LR, SPSR |
| 11111 | System | 无(使用User寄存器组) |
四、异常发生时,硬件自动做了什么?
理解了这个,你才能真正看懂中断向量表和异常处理入口代码。当一个异常发生时,ARMv7硬件自动完成以下操作:
- 保存返回地址:将当前PC值存入对应模式的R14(LR)中。注意,不同异常保存的PC偏移量不同(比如IRQ保存的是PC-4,SVC保存的是PC)。
- 保存CPSR:将当前CPSR复制到对应模式的SPSR中。
- 切换模式:修改CPSR的M[4:0]位,切换到对应的异常模式。
- 屏蔽中断:根据需要设置CPSR的I位和F位(比如进入IRQ会自动置位I位,防止中断嵌套)。
- 跳转向量表:从异常向量表取出对应地址,更新PC。
核心理解:异常返回时,你需要手动从SPSR恢复CPSR,从LR恢复PC。ARM提供了两条专用指令:MOVS PC, LR(用于没有SPSR的模式)和SUBS PC, LR, #4(用于需要调整返回地址的异常)。实际上,这两条指令会同时完成PC恢复和CPSR恢复(从SPSR)。
五、实战:如何读取和修改CPSR
在汇编层面,操作CPSR有专门的指令:
@ 读取CPSR到R0
MRS R0, CPSR
@ 修改CPSR(比如关闭IRQ中断)
MRS R0, CPSR
ORR R0, R0, #0x80 @ 置位I位(bit 7)
MSR CPSR_c, R0 @ 只写控制域(低8位)
@ 切换模式到SVC(从特权模式)
MRS R0, CPSR
BIC R0, R0, #0x1F @ 清除模式位
ORR R0, R0, #0x13 @ 设置为SVC模式(0b10011)
MSR CPSR_c, R0
@ 异常返回(以IRQ为例)
SUBS PC, R14_irq, #4 @ 恢复PC,同时从SPSR_irq恢复CPSR
经验之谈:MSR CPSR_c, R0中的_c表示只写CPSR的控制域(低8位)。ARMv7支持按域写入:c=控制域(低8位)、x=扩展域([15:8])、s=状态域([23:16])、f=标志域([31:24])。我建议你写CPSR时尽量用域限定符,避免意外修改其他位。
六、一个常见的坑:中断嵌套与SPSR覆盖
假设你在IRQ处理函数中,又触发了一个FIQ。硬件会怎么做?
它会自动切换到FIQ模式,把当前的CPSR保存到SPSR_fiq。但注意——SPSR_irq不会被覆盖,因为每个异常模式都有自己的SPSR。这就是Banked寄存器的价值所在。
但是,如果你在IRQ处理函数中手动切换到了SVC模式(比如为了调用内核服务),然后又触发了一个IRQ——这时候问题就来了。因为IRQ模式下的SPSR_irq已经被覆盖了,你再也拿不到最初进入IRQ时的CPSR了。
避坑指南:我曾经在一个中断嵌套的场景里,IRQ处理函数先切到SVC模式执行,结果同一个IRQ再次触发,SPSR_irq被覆盖,返回时CPSR恢复错误,系统直接崩溃。解决方案是:在IRQ入口处立即把SPSR_irq保存到栈上,或者使用FIQ处理高优先级中断,避免嵌套冲突。
七、小结:一张表记住核心要点
| 知识点 | 一句话总结 |
|---|---|
| 7种模式 | User(非特权)+ 6种异常模式(特权)+ System(特权但用User寄存器) |
| Banked寄存器 | 每个异常模式有自己的SP、LR、SPSR;FIQ额外有R8-R12 |
| CPSR | 条件标志 + 中断屏蔽 + 指令集状态 + 当前模式 |
| SPSR | 异常发生时CPSR的快照,返回时恢复用 |
| 异常流程 | 硬件自动保存LR和SPSR、切换模式、跳转向量表;软件负责返回 |