文章目录
- 前言
- 一、CPSR 条件标志位(NZCV)彻底搞懂
- 1. 四个标志位的定义
- 2. 重点辨析:C位 和 V位 完全独立
- 3. 难点突破:N与V的组合逻辑
- 4. 常用条件码对应表
- 二、条件指令与循环结构实现
- 1. while 循环(先判断后执行)
- 2. do-while 循环(先执行后判断)
- 3. 两者转换要点
- 三、跳转指令三兄弟:b、bl、bx 区别
- 关键:LR的返回地址从哪来?
- 四、ARM栈机制与函数现场保护
- 1. ARM默认栈类型:满递减栈(FD)
- 2. 压栈与弹栈的执行过程
- 3. 什么时候必须压栈?
- 4. 踩坑记录:子函数寄存器覆盖bug
- 五、12位立即数的合法性判断
- 1. 什么是立即数
- 2. 合法性判断规则
- 3. 示例
- 六、汇编与C语言互调基础
- 1. 参数与返回值约定
- 2. 汇编调用C函数
- 3. C调用汇编函数
- 总结
前言
继第一天学习了ARM汇编基础指令、段伪操作和基本数据处理后,第二天的学习核心围绕 CPSR条件标志位、条件指令、循环实现、栈操作、函数调用规范 展开,同时踩了寄存器覆盖的经典坑,彻底搞懂了满递减栈的工作逻辑。本文梳理当天所有核心知识点,结合代码实例和踩坑记录,把NZCV标志位、b/bl/bx区别、栈类型、函数压栈弹栈这些重点一次性讲透。
一、CPSR 条件标志位(NZCV)彻底搞懂
CPSR(程序状态寄存器)中的4个条件标志位,是所有条件分支、条件指令的底层依据,由带S后缀的运算指令、cmp比较指令自动更新,普通指令不会修改。
1. 四个标志位的定义
| N | Negative(负标志) | 运算结果的最高位(bit31)为1 | 结果的符号位,有符号视角下1代表负 |
| Z | Zero(零标志) | 运算结果所有位全为0 | 两数相等、计数归零的判断依据 |
| C | Carry(进位标志) | 无符号加法产生第33位进位;无符号减法够减(无借位) | 服务于无符号数,判断是否超出32位容器 |
| V | Overflow(溢出标志) | 有符号正数加正数变负、负数加负数变正 | 服务于有符号数,判断符号逻辑是否正确 |
2. 重点辨析:C位 和 V位 完全独立
这是最容易混淆的点,两者服务于不同的数值解读视角,没有必然联系:
- C位 = 无符号视角的溢出:只看有没有进位到第33位,不关心符号
- V位 = 有符号视角的溢出:只看符号逻辑对不对,不关心第33位
经典对比案例:执行 0x7FFFFFFF + 1
- 无符号视角:结果为0x80000000,仍在32位内,无进位 → C=0
- 有符号视角:最大正数加1变成了负数,符号逻辑错误 → V=1
3. 难点突破:N与V的组合逻辑
为什么有符号“大于”的条件是 Z==0 且 N==V? 核心原因:当有符号运算溢出时,结果的符号位N会反转,必须结合V位才能还原真实正负性。
分两种情况验证规律:
- 无溢出(V=0):N就是真实符号。结果为正则N=0,满足N==V;结果为负则N=1,满足N≠V
- 有溢出(V=1):N与真实符号相反。真实结果为正则N=1,仍满足N==V;真实结果为负则N=0,仍满足N≠V
最终得出万能结论: ✅ N == V → 真实结果为正 → 前者大于后者 ❌ N != V → 真实结果为负 → 前者小于后者
4. 常用条件码对应表
| EQ | 等于 | Z == 1 |
| NE | 不等于 | Z == 0 |
| GT | 大于 | Z==0 且 N==V |
| LE | 小于等于 | Z==1 或 N≠V |
| GE | 大于等于 | N == V |
| LT | 小于 | N != V |
二、条件指令与循环结构实现
借助条件标志位和条件跳转指令,可以直接在汇编中实现C语言的循环结构,核心是把握循环三要素:循环体、终止条件、趋向终结的语句。
1. while 循环(先判断后执行)
核心逻辑:循环开头先判断条件,不满足则直接跳出,零次或多次执行。
; C语言逻辑:while(i <= 100) { sum += i; i++; }
mov r0, #0 ; i = 0
mov r1, #0 ; sum = 0
loop
cmp r0, #100 ; 先判断:i 和 100 比较
bgt finish ; 大于则跳出循环
add r1, r1, r0 ; 循环体:sum += i
add r0, r0, #1 ; i++
b loop ; 跳回开头下次判断
finish
b finish
2. do-while 循环(先执行后判断)
核心逻辑:先无条件执行一次循环体,结尾再判断条件,至少执行一次。
; C语言逻辑:do { sum += i; i++; } while(i <= 100);
mov r0, #0
mov r1, #0
loop
add r1, r1, r0 ; 先执行循环体
add r0, r0, #1 ; i++
cmp r0, #100 ; 后判断条件
ble loop ; 满足则跳回循环开头
finish
b finish
3. 两者转换要点
把while改成do-while,只需要两步:
三、跳转指令三兄弟:b、bl、bx 区别
三者都能修改PC实现程序跳转,但硬件行为和使用场景完全不同。
| b | Branch | 直接修改PC跳转到标签 | ❌ 不保存 | ❌ 不切换 | 循环、分支跳转、死循环 |
| bl | Branch with Link | 先把下一条指令地址存入LR,再跳转 | ✅ 自动存入LR | ❌ 不切换 | 函数调用 |
| bx | Branch and Exchange | 跳转到寄存器存的地址,根据最低位切换ARM/Thumb状态 | ❌ 不保存 | ✅ 可切换 | 函数返回(bx lr) |
关键:LR的返回地址从哪来?
完全是bl指令的硬件自动行为,无需软件赋值:
四、ARM栈机制与函数现场保护
1. ARM默认栈类型:满递减栈(FD)
ARM架构遵循ATPCS规范,默认采用 满递减栈(Full Descending):
- 满栈:SP永远指向栈中最后一个已存入的有效数据
- 递减:栈向低地址方向增长,压栈时SP数值减小
可以简单记忆:栈底在高地址,栈顶向低地址“向下长”,SP永远指着栈顶的有效数据。
2. 压栈与弹栈的执行过程
以 STMFD sp!, {r0, r1, lr} 压栈为例:
以 LDMFD sp!, {r0, r1, lr} 弹栈为例:
3. 什么时候必须压栈?
核心原则:只要函数内部会修改LR(调用了其他函数),就必须压栈保存LR。
- 叶子函数(内部不调用其他函数):不会修改LR,无需压栈,直接bx lr返回
- 嵌套调用函数:内部有bl指令,会覆盖LR的值,必须入口压栈、出口弹栈
4. 踩坑记录:子函数寄存器覆盖bug
问题现象
三数求最大值程序,最终结果为0xC8(200),而非预期的300。
根本原因
两数求最大值函数内部,用r2当临时变量存结果,直接覆盖了主函数存在r2里的第三个数300,导致第二次比较时用200和200比。
; 错误写法:用r2当临时变量,破坏了外部参数
asm_twoNumMax
cmp r0, r1
movgt r2, r0
movle r2, r1
mov r0, r2
bx lr
修正方案(保留MOVGT+MOVLE)
改用空闲的r3当临时变量,全程不碰参数寄存器r2:
; 正确写法:用r3做临时变量,不破坏外部参数
asm_twoNumMax
cmp r0, r1
movgt r3, r0 ; r0>r1则存r0
movle r3, r1 ; r0≤r1则存r1
mov r0, r3 ; 结果放入r0返回
bx lr
完整三数求最大值代码
asm_threeNumMax
stmfd sp!, {lr} ; 入口压栈保存返回地址
bl asm_twoNumMax ; 求前两个数最大值,r2保持300不变
mov r1, r2 ; 第三个数放入r1
bl asm_twoNumMax ; 求最终最大值
ldmfd sp!, {lr} ; 出口弹栈恢复返回地址
bx lr
五、12位立即数的合法性判断
1. 什么是立即数
直接编码在指令中的常量,执行时直接从指令码读取,无需访问寄存器/内存,汇编中以#开头。优点是速度快,缺点是受指令长度限制,不能表示任意32位数。
2. 合法性判断规则
ARM数据处理指令的立即数为12位编码,规则:由一个8位基数,循环右移偶数位(0/2/4…30位)得到。 换句话说:把数值展开成二进制,一定存在一种偶数位的循环右移,使得高24位全为0,低8位是有效基数。
3. 示例
- ✅ 合法:#0xFF、#0xFF000000、#0x0000FF00
- ❌ 非法:#0xFFFFFFFF、#0x12345678、#0x101
超过合法范围的大数,需要用ldr 寄存器, =数值伪指令加载,汇编器会自动存入常量池,通过访存读取。
六、汇编与C语言互调基础
1. 参数与返回值约定
- 前4个参数:依次通过r0、r1、r2、r3传递
- 超过4个的参数:通过栈传递
- 返回值:统一存放在r0中
2. 汇编调用C函数
3. C调用汇编函数
总结
第二天的内容是从“单条指令”走向“结构化程序”的关键:
- NZCV标志位是所有条件判断的底层硬件基础,理解N和V的组合逻辑,就能通吃所有有符号条件指令
- 满递减栈是函数调用的根基,压栈弹栈的本质是保存现场、防止寄存器被覆盖
- bl+lr+栈 三者配合,实现了函数的嵌套调用与正确返回
掌握这些内容后,就具备了阅读ARM启动代码的核心基础,下一篇将学习异常向量表、工作模式切换等启动代码的核心模块。
网硕互联帮助中心



评论前必须登录!
注册