资讯动态

ARM程序状态寄存器CPSR详解:标志位、模式切换与异常处理

发布时间:2026/10/24 3:01:16 来源:尧图企业网站定制
1. 程序状态寄存器藏在CPU里的隐形指挥台如果你已经看过我写的那篇关于ARM通用寄存器的上篇应该对r0-r15这条主干道有了概念。但ARM处理器里还有一个容易被忽略、却每时每刻都在影响程序行为的角色——程序状态寄存器。在ARM架构模型中它通常被称为CPSRCurrent Program Status Register当前程序状态寄存器是整个CPU状态的实时快照。为什么说它是隐形指挥台因为大部分时候你不会直接去读写它但它一直在那默默决定三件大事当前CPU处于什么模式、哪些中断被屏蔽了、上一条指令执行后结果是正还是负、是零还是溢出。换句话说CPSR就是CPU的心情面板——你要是看不懂这个面板调试时就会一头雾水。这篇文章专注讲ARM架构模型里的程序状态寄存器重点覆盖CPSR的位布局、条件标志位的行为细节、工作模式切换机制、SPSR在异常处理中的角色最后还会分享一些我在裸机开发和嵌入式调试中实际踩过的坑。适合正在学ARM汇编、做裸机驱动、或者准备系统学习嵌入式底层的朋友。先给个精简结论ARM不像x86那样用独立的EFLAGS寄存器来存标志位而是把所有状态信息统一塞进一个CPSR再为每种异常模式配套一个SPSR用来做现场保存。这个设计直接决定了ARM异常处理的优雅程度也影响了你在写中断服务程序时的每一步操作。2. CPSR的位布局从bit0到bit31逐个拆解2.1 CPSR整体结构四个标志位区段动手之前先把CPSR的32个位分成四个区段来理解这样记忆负担小很多。条件标志位bit31-bit28N、Z、C、V这四个是汇编程序员最常用的条件分支靠它们。扩展标志位bit27-bit24Q、J等DSP指令和Jazelle状态会用到平时很少碰。中断与状态控制位bit7、bit6、bit5I、F、T分别控制IRQ中断、FIQ中断和Thumb指令集状态。模式位bit4-bit0M[4:0]5个bit可以编码32种模式ARM实际用了一部分。示意图可以这样脑补31 30 29 28 27 26 25 24 23 ... 7 6 5 4 0 N Z C V Q - - J ... I F T M[4:0]高四位是条件标志低五位是模式编码这种布局意味着你可以用一条MRS指令把CPSR读出来直接按bitmask去判断CPU状态。这在写RTOS内核、异常处理、任务切换时特别顺手。2.2 N、Z、C、V四个条件标志的精确语义很多人学ARM时把N/Z/C/V记成符号、零、进位、溢出这个说法有误导性。它们在ARM里的定义其实更精确NNegative如果指令结果被视为有符号数时是负数则N1。严格说N位存储的就是结果的bit31也就是最高位。一个无符号加法只要结果bit31为1N照样置位所以负数标志这个叫法只对有符号解释有意义。ZZeroalu运算结果的所有位都是0时Z1。注意是结果的所有位不是等于零的运算数。CCarry这块最容易搞混。对加法运算C表示最高位产生的进位对减法运算C表示无借位对移位操作C保存最后移出的那一位。VOverflow只有有符号运算才会溢出也就是说两个正数相加得到负数或者两个负数相加得到正数。无符号运算溢出不会置V它只反映在C上。这里必须展开讲讲C标志在减法中的反直觉行为。看这条指令CMP r0, r1CMP的本质是执行r0 - r1但不把结果写回r0只是更新标志位。如果r0 r1也就是没发生借位C会被置1如果r0 r1发生了借位C被清0。所以你会看到很多代码里BCCBranch if Carry Clear被用来判断小于这在无符号比较里非常常见。新手最容易在这里翻车后面我会专门写一节排坑实录。V标志的判定规则可以总结成一句话当两个操作数的符号相同而结果的符号与它们相反时V1。这也是硬件ALU的标准实现方式它不关心操作数是来自加减法还是比较指令只要是带V更新的指令就会按这个逻辑走。2.3 Q标志和J标志两个平时用不到但关键时刻致命的位Q标志是ARMv5TE引入的专门服务饱和算术指令。所谓饱和运算就是当结果超出数据类型范围时直接钳到最大值而不是回绕。比如QADD、QDSUB这些指令一旦发生饱和就会把Q位置1。Q位的特点是一旦置位它不会自动清零只有你手动写回CPSR才能清除。这意味着你可以通过定时检查Q位来确认一段时间内是否发生过饱和这个特性在做音频算法和信号处理时特别有用。J标志指示CPU是否处于Jazelle状态这是ARM为了跑Java字节码引入的硬件加速模式。对绝大多数嵌入式场景J永远是0。你要是哪天在反汇编工具里看到J位异常置1先怀疑是不是调试器读取状态搞错了再怀疑是不是有指令主动切换了状态。2.4 中断控制位I/F与Thumb状态位TI位控制IRQ中断的使能F位控制FIQ中断的使能。这两位都是置1屏蔽、清0使能。写裸机程序时进入临界区前你通常会做这样一件事MRS r0, CPSR ORR r0, r0, #0x80 置位I屏蔽IRQ MSR CPSR_c, r0把I位置1后外部普通中断就无法打断当前代码。FIQ的优先级更高屏蔽F位通常配合FIQ中断服务程序使用普通裸机开发很少动F位。T位指示当前指令集状态T1时执行Thumb指令T0时执行ARM指令。这里有个很多人忽略的点你不能直接用MSR去改T位来切换指令集因为在某些ARM版本里这样做会导致指令流解码错乱。正确做法是用BX/BLX指令跳转处理器会自动根据目标地址的bit0来切换T位arm-none-eabi-gcc在编译时的-mthumb和-marm选项也都是通过生成不同入口指令来实现切换的。3. 模式位与异常处理CPU如何切换工作状态3.1 七种基本工作模式及模式位编码ARM架构从ARMv4开始稳定支持七种工作模式早期的ARMv3没有System模式M[4:0]编码如下模式M[4:0]可访问寄存器说明User10000通用寄存器PSP应用程序正常执行模式FIQ10001通用寄存器r8_fiq-r12_fiqPSP快速中断有独立r8-r12IRQ10010通用寄存器PSP普通中断Supervisor10011通用寄存器PSP复位和SVC指令进入Abort10111通用寄存器PSP数据/指令预取中止Undefined11011通用寄存器PSP未定义指令System11111通用寄存器PSP与User相同但可自由切换观察这个表你会发现FIQ模式额外拥有r8_fiq到r12_fiq五个专用寄存器。这意味着FIQ中断一进来硬件自动帮你把r8-r12这五个寄存器换成了另一套物理副本你不用在中断入口处保存这五个寄存器。这就是FIQ比IRQ快的原因——省掉了五个寄存器压栈的时间。IRQ模式没有额外的通用寄存器所有寄存器都需要软件保存。3.2 模式切换的触发与硬件自动行为模式切换的发生方式有两种寄存器操作和异常产生。寄存器操作指的是你主动往CPSR的M[4:0]写入目标模式编码。但注意一个限制User模式和System模式是特权绑定的在User模式下写CPSR模式位会被忽略。这点和x86的CPL级别有一点相似User模式相当于最低特权级不能自己做主切换模式。异常产生是更常见的路径。当IRQ、FIQ、未定义指令、预取中止、数据中止、复位、SVC指令发生时硬件自动完成一系列动作将当前CPSR复制到当前异常模式的SPSR中实现状态快照将CPSR的模式位切换到对应的异常模式禁用同类中断防止嵌套打断比如IRQ进入后自动置I位将返回地址保存到对应异常模式的LR链接寄存器跳转到异常向量表对应的入口地址这里最关键的就是第一件事CPSR自动拷贝到SPSR。异常返回时你只需要一条MOVS指令把SPSR的内容搬回CPSR就能恢复中断前的完整状态——包括模式位、中断屏蔽位、条件标志位一步到位。3.3 CPSR_c后缀与MSR操作为什么写CPSR要带_c很多汇编初学者写MSR指令时会困惑为什么别人写的是MSR CPSR_c而不是MSR CPSR。这里有个历史背景早期的ARM通过后缀来指定要写入CPSR的哪个字段_c代表控制域Control也就是bit0-bit7_f代表标志域Flags也就是bit28-bit31_x代表扩展域_s代表状态域。实际使用中最常见的两个操作是 修改模式位和中断位通常用_c因为模式位和控制位都在低8位 MRS r0, CPSR BIC r0, r0, #0x1F 清掉模式位 ORR r0, r0, #0x13 设为Supervisor模式 MSR CPSR_c, r0 修改条件标志位用_f MRS r0, CPSR ORR r0, r0, #0xF0000000 强制N/Z/C/V全置1 MSR CPSR_f, r0为什么开发中推荐用MRS读、修改、再MSR写回这种模式因为直接MSR写死一个值会覆盖掉你不打算改的标志位。比如你在中断服务程序里只想切换模式却把N/Z/C/V也写掉了调试时会出现明明没改变量怎么就跳走了的灵异事件。所以规范做法永远是读-改-写三步。4. SPSR的角色定位每个异常模式的状态保险箱4.1 SPSR为什么每种模式各有一份SPSR的全称是Saved Program Status Register保存程序状态寄存器。它不是只有一个物理实例而是每个特权模式各有一份自己的SPSR寄存器。FIQ有SPSR_fiqIRQ有SPSR_irqSupervisor有SPSR_svc以此类推。User和System模式没有SPSR因为它们不是异常模式不需要保存异常前的状态。这种设计的妙处在于当一个中断嵌套进来时老的CPSR已经在入口处被保存到那个模式的SPSR里。如果你在IRQ处理中允许了更高优先级的FIQ打断FIQ硬件会自动把当前的CPSR此时已经是IRQ模式保存到SPSR_fiq不会覆盖SPSR_irq。不同异常模式的SPSR是并行的互不干扰。但要注意SPSR同一时刻只能保存一份状态。如果你在同一个异常模式里处理完一个中断还没返回又来了同级中断SPSR会被覆盖。所以嵌套中断处理必须靠软件把SPSR压栈保存不能依赖硬件。4.2 异常返回指令MOVS、LDMFD与CPSR恢复异常处理程序结束时要做的核心动作是恢复现场包括恢复通用寄存器和恢复CPSR。恢复通用寄存器用LDM指令恢复CPSR则有两种常见写法 方式一MOVS指令把SPSR搬回CPSR MOVS pc, lr 方式二LDM指令同时回填PC和CPSR LDMFD sp!, {r0-r12, pc}^MOVS pc, lr这条指令是异常返回的标准套路S后缀在这里有特殊含义当目标寄存器是pc时它会自动将当前模式的SPSR拷贝到CPSR。这样一条指令同时完成了回到异常发生前的模式和跳转到返回地址两件事。LDMFD sp!, {r0-r12, pc}^的^后缀表示将SPSR拷贝到CPSR多用于从中断栈上批量恢复寄存器的场景。这两种方式在功能上等价但在嵌套中断设计中更推荐用MOVS pc, lr这种方式因为语义更清晰出问题也好排查。4.3 FIQ模式下的SPSR特殊用法FIQ模式有独立的r8-r12副本这意味着FIQ中断可以完全不压栈就处理小型快速任务。但SPSR_fiq同样只有一个。如果FIQ服务程序里要用BL调用子函数LR会被覆盖因此第一件事通常是保存LR到FIQ模式自己的r8-r12中的一个。还有一种高级玩法在FIQ中临时切换到别的模式操作数据然后用MOVS回到FIQ模式。这时候SPSR_fiq保存的就是FIQ入栈前的CPSR而不是你中间切换模式的CPSR所以才能正确返回来。有一次我调试一个双核通信驱动FIQ服务函数里用BL调了MMU操作函数结果返回地址LR被覆盖程序跳到随机地址。当时的教训就是FIQ模式下能不用BL就不用BL实在要调用先把LR压到堆栈或者存进r8_fiq。5. 实操演练状态寄存器的读写与调试技巧5.1 用MRS/MSR实现指令集状态读取嵌入式调试时最常干的一件事就是确认CPU当前处于ARM还是Thumb状态。你当然可以用JTAG或调试器看但在代码里实时获取更直接MRS r0, CPSR TST r0, #0x20 bit5是T位 BNE thumb_mode T1说明在Thumb下同样的模式位判断也极其常见。启动代码里需要判断当前是从复位启动还是从唤醒中断启动就要读CPSR的模式位MRS r0, CPSR AND r0, r0, #0x1F CMP r0, #0x13 SVC模式 BEQ reset_handler5.2 一个完整的临界区保护范例写裸机代码时保护临界区的核心就是屏蔽中断并保存原状态。新手最容易犯的错误是进入临界区后直接对CPSR写死exit时恢复成固定值这样会丢失中断前的CPSR标志位。正确写法是; 函数入口 MRS r0, CPSR MOV r1, r0 ORR r0, r0, #0xC0 屏蔽IRQ和FIQ MSR CPSR_c, r0 ; ... 临界区代码 ... ; 函数出口 MSR CPSR_c, r1 恢复中断前的CPSR控制位 BX lr这里用r1保存了进入前的原始CPSR退出时原样写回这样就不会误改其他字段。有人会问直接用CPSR_c_f或者CPSR_all写不行吗ARM允许你这样写但通常不推荐因为你可能顺手把条件标志位也改了。只写_c后缀是最可控的。5.3 调试器里如何观察CPSR变化如果你用gdb调试ARM目标每一句单步后查看寄存器时CPSR会以类似0x600000D3的形式显示。这个值的解码过程是0x600000D3对应二进制0011 0000 0000 0000 0000 0000 1101 0011。其中bit29Z和bit28C为1表示上一条指令结果为零且无借位0xD3的低五位是10011对应SVC模式bit7的I1表示IRQ屏蔽bit6的F0表示FIQ使能。调试时如果发现某条条件分支指令没有按预期跳转第一反应就应该是查看CPSR的对应标志位是否是预期的值。我曾经遇到一个bug一段乘法的后处理函数偶发算错单步跟踪时发现溢出标志V在一条分支指令前被上一个函数的标志更新指令破坏了。根源是编译器在生成代码时把CMP和条件跳转分开优化了算法逻辑没问题但标志位被隔空污染了。从那以后我养成了习惯跨函数调用后不依赖函数内部保存的任何条件标志状态一切判断用显式比较指令重新建立。6. 常见错误与排查实录速查表6.1 五个高频翻车场景整理我这些年看到的和亲自踩过的问题浓缩成一张表现象根因解决方案条件分支跳转方向反了C标志理解错误把借位当作进位记住CMP减法中C1表示大于等于C0表示小于中断返回后CPU模式混乱返回时用普通MOV pc, lr而非MOVS pc, lr确认S后缀是否带上严格用异常返回专用指令临界区失效中断照常打断修改CPSR时没有实际屏蔽I位检查CPSR_c的写入值确认bit7置1反汇编地址模式错乱T位和PC低bit不一致用BX切换指令集不要手动MSR改T位嵌套中断后SPSR被覆盖进入嵌套时没有保存第一层SPSR在中断入口手工将SPSR压栈保存6.2 一个完整的调试故事一次我为某ARM内核从零写调度器时任务切换函数里有一段代码负责在上下文切换时保存和恢复CPSR。当时我图省事在恢复现场时直接写了一个固定CPSR值LDR r0, 0x60000013 固定Z1、C1、SVC模式 MSR CPSR_c, r0结果调度器跑一阵子就随机崩溃。排查了很久才定位到被切换出去的任务可能因为一条比较指令留下了N/V标志位状态而恢复现场时被我用固定值覆盖掉那个任务恢复后的第一条条件分支指令比如BMI、BVS就会误判走到完全错误的代码路径。从那以后我郑重记住了CPSR的恢复必须完整地恢复而不是挑几个位恢复。条件标志位看似无关紧要丢了它程序照样能跑但所有依赖隐式标志的优化路径都会变得不可信。6.3 标志位跨函数污染的防护建议ARM的C编译器在函数调用边界上是允许标志位被随意改动的因为AAPCS调用约定不要求Callee保存CPSR。这带来一个隐蔽问题老代码往往依赖某个函数内部执行加法后留下的C标志跨了函数调用再使用标志位行为是未定义的。安全的做法是条件标志位视为临时的、指令级的状态不要试图跨函数传递。需要保留标志位状态的场景要么把标志位保存在spsr或通用寄存器中显式传递要么在调用返回后重新执行算术操作产生标志位。写内联汇编时如果代码改变了条件标志位又希望保留记得用cc约束让编译器知道你在破坏cc否则优化器会假设标志位没有变化。业内有一种说法是ARM的条件标志位是最廉价也最容易被浪费的寄存器资源实际调试中你会深刻体会到这句话。善用TST、CMP、CMN这些不写回目的寄存器的指令可以让你在保持标志位的同时完成检查而不用额外寄存器存中间结果。6.4 从CPSR看处理器启动流程最后分享一个实际应用场景看门狗复位后的启动路径判断。很多ARM SoC在复位时会自动进入SVC模式CPSR的M[4:0]是10011同时I和F默认置1屏蔽中断。你可以通过读取复位后的CPSR来确认启动模式unsigned int cpsr_val; __asm volatile (MRS %0, CPSR : r (cpsr_val)); printf(Mode%d IRQ%d FIQ%d\n, cpsr_val 0x1F, (cpsr_val 7) 1, (cpsr_val 6) 1);如果你的系统是直接从ROM里跑启动代码这个打印结果应该稳定输出Mode19、IRQ1、FIQ1。如果哪一次读出来IRQ位不是1说明之前有代码提前放开中断了而你的启动临界区还没执行完——这就是典型的中断过早打开bug的排查起点。我在实际调试中体会到程序状态寄存器是理解ARM处理器架构最值得投入精力的模块之一。上篇那些通用寄存器再复杂也只是数据搬运工真正决定CPU行为的大脑指挥逻辑全都集中在CPSR和SPSR上。把条件标志位的精确语义、模式切换的硬件行为、SPSR的保存恢复机制搞透你写启动代码、中断系统和操作系统上下文切换时就能游刃有余。最后再分享一个小技巧每次单步调试遇到诡异行为时先把CPSR的原始值记录下来再跟手册逐位对照很多问题都能在对照过程中自动浮现出答案。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑