3. ARM寄存器详解:通用寄存器、堆栈指针SP、链接寄存器LR、程序计数器PC、程序状态寄存器CPSR
寄存器,说白了就是CPU内部的高速缓存单元。我经常跟团队里的年轻人讲,理解寄存器就是理解ARM处理器的第一步。你想想看,CPU所有运算都要经过寄存器,它就像工程师手里的工具台——工具放得顺手,干活就快。
ARM处理器有37个寄存器,但咱们今天只讲最核心的那几个。嗯,这里要注意,不同工作模式下看到的寄存器集合是不一样的,这个后面会细说。
3.1 通用寄存器:R0-R12
通用寄存器R0到R12,一共13个。它们就像你桌上的便签纸,随手拿来用。ARM规定这些寄存器用途完全自由,没有硬件层面的限制。
但我个人习惯,在写汇编时会约定一些潜规则:
- R0-R3:通常用来传参数。函数调用时,前4个参数就放这里
- R4-R11:保存局部变量。我在项目中遇到过,有人把大循环的计数器放在R4里,性能提升很明显
- R12:也叫IP寄存器,编译器经常拿它做临时中转
重要提醒:虽然硬件不限制用途,但如果你要跟C语言混编,就得遵守ATPCS(ARM-Thumb过程调用标准)。否则,函数调用时寄存器内容被覆盖,bug会非常难查。
举个例子,一个简单的加法函数:
; int add(int a, int b) — a在R0,b在R1
ADD R0, R0, R1 ; R0 = R0 + R1
BX LR ; 返回,结果在R0
你看,ARM的指令集设计得很优雅。操作数直接来自寄存器,结果也写回寄存器,没有多余的访存开销。
3.2 堆栈指针SP:R13
SP,全称Stack Pointer。它指向当前堆栈的栈顶。堆栈这东西,说白了就是一块后进先出的内存区域。函数调用、局部变量、中断现场保护,全得靠它。
ARM支持四种堆栈模式,但我工作中99%的情况只用一种:
| 模式 | 说明 | 常用场景 |
|---|---|---|
| 满递减 | SP指向最后压入的数据,地址向下增长 | 最常用,C编译器默认 |
| 空递减 | SP指向下一个空闲位置,地址向下增长 | 少见 |
| 满递增 | SP指向最后压入的数据,地址向上增长 | 某些RTOS会用 |
| 空递增 | SP指向下一个空闲位置,地址向上增长 | 几乎不用 |
我的经验:刚学ARM时,我总搞不清压栈出栈的方向。后来我记住一句话——「满递减,先减后存」。也就是先SP减4,再把数据存到SP指向的位置。出栈时反过来,先取数据,再SP加4。
堆栈操作指令也很直观:
PUSH {R0, R1} ; 压栈,SP自动减8
POP {R0, R1} ; 出栈,SP自动加8
我曾经在一个中断服务程序里忘记平衡堆栈,结果程序跑飞了整整两天才找到原因。嗯,从那以后我写中断代码,第一件事就是检查PUSH和POP是否成对出现。
3.3 链接寄存器LR:R14
LR,Link Register。它保存了函数调用时的返回地址。你想想看,当CPU执行BL指令跳转到子函数时,下一条指令的地址就被自动保存到LR里了。
为什么需要这个寄存器?因为ARM没有硬件堆栈,函数嵌套全靠LR和SP配合。举个例子:
main:
BL func1 ; 跳转到func1,LR = 下一条指令地址
; 这里继续执行
func1:
PUSH {LR} ; 保存返回地址到堆栈
BL func2 ; 嵌套调用func2
POP {PC} ; 恢复返回地址,等价于 BX LR
; 注意:POP {PC} 会直接跳转
这里有个细节:如果func1不嵌套调用其他函数,其实可以不用PUSH/POP,直接用BX LR返回就行。但一旦有嵌套,就必须保存LR。我在项目中见过有人偷懒没保存LR,结果嵌套两层后程序就乱跳了。
避坑指南:中断处理函数里,LR会被硬件自动修改。如果你在中断里调用了其他函数,一定要先保存LR。我曾经...嗯,有一次在UART中断里调了个打印函数,忘了保存LR,结果打印完就回不去了,系统直接死机。
3.4 程序计数器PC:R15
PC,Program Counter。它指向当前正在执行的指令地址。ARM有个特点:PC的值是当前指令地址+8(在ARM状态下)。为什么是+8?因为ARM是三级流水线——取指、译码、执行。PC指向的是取指阶段,而当前执行的是两条指令之前的地址。
这个特性在调试时特别容易让人困惑。我刚开始做底层开发时,看PC值总觉得不对,后来才明白是流水线在作怪。
PC可以直接读写吗?可以,但要注意:
- 读PC:得到当前指令地址+8
- 写PC:相当于跳转,会刷新流水线
MOV PC, LR ; 等价于 BX LR,函数返回
ADD PC, PC, #4 ; 跳过下一条指令(危险操作,不推荐)
核心要点:PC是ARM体系里最特殊的寄存器。它既是通用寄存器(R15),又有着固定的硬件行为。写PC就是跳转,这个特性让ARM的指令执行流非常灵活,但也容易出错。
3.5 程序状态寄存器CPSR
CPSR,Current Program Status Register。它记录了处理器的当前状态。说白了,就是CPU的「仪表盘」——告诉你现在跑在什么模式、运算结果有没有进位、中断是否开启等等。
CPSR的位定义如下:
| 位域 | 名称 | 说明 |
|---|---|---|
| [31:28] | N, Z, C, V | 条件标志位:负、零、进位、溢出 |
| [27:25] | Q, IT, J | 饱和标志、Thumb状态、Jazelle状态 |
| [24:8] | 保留 | 系统保留,不要动 |
| [7:6] | I, F | IRQ和FIQ中断屏蔽位 |
| [5] | T | Thumb状态位:0=ARM,1=Thumb |
| [4:0] | M[4:0] | 处理器模式:10011=Supervisor,10010=IRQ等 |
条件标志位是ARM指令集的一大特色。几乎所有指令都可以条件执行:
CMP R0, #5 ; 比较R0和5,设置CPSR
ADDGT R1, R1, #1 ; 如果R0 > 5,才执行这条加法
这个特性在编译条件分支时非常有用。我做过一个性能对比测试,用条件执行代替分支跳转,代码执行时间减少了30%左右。为什么?因为跳转会清空流水线,而条件执行不会。
实用技巧:调试时想查看CPSR?在GDB里输入 info registers cpsr 或者 p $cpsr。我习惯用 p/t $cpsr 以二进制形式查看,这样每个标志位一目了然。
最后说一句,CPSR在异常处理时会自动保存到SPSR(Saved Program Status Register)中。每个异常模式都有自己的SPSR,用来保存被中断前的CPSR值。这个机制保证了异常返回后,程序能无缝恢复执行。
这张图把今天讲的五个寄存器组串起来了。你看,通用寄存器是运算的主力,SP和LR负责函数调用和返回,PC控制程序流向,CPSR则时刻监控着处理器的状态。它们配合起来,构成了ARM处理器的执行核心。
嗯,寄存器这块内容比较多,但都是基础中的基础。我建议你动手写几个小例子,比如用汇编实现一个函数调用,观察SP和LR的变化。实践出真知,光看文章是记不住的。