云计算百科
云计算领域专业知识百科平台

【ARM汇编学习笔记02】条件标志位、栈机制与函数调用全解析

文章目录

  • 前言
  • 一、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. 四个标志位的定义

标志位全称置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指令的硬件自动行为,无需软件赋值:

  • 执行bl 函数名时,CPU先把bl下一条指令的地址自动写入LR寄存器
  • 再跳转到目标函数执行
  • 函数结尾执行bx lr,把LR的值写回PC,程序就回到了调用处继续执行

  • 四、ARM栈机制与函数现场保护

    1. ARM默认栈类型:满递减栈(FD)

    ARM架构遵循ATPCS规范,默认采用 满递减栈(Full Descending):

    • 满栈:SP永远指向栈中最后一个已存入的有效数据
    • 递减:栈向低地址方向增长,压栈时SP数值减小

    可以简单记忆:栈底在高地址,栈顶向低地址“向下长”,SP永远指着栈顶的有效数据。

    2. 压栈与弹栈的执行过程

    以 STMFD sp!, {r0, r1, lr} 压栈为例:

  • SP一次性减去总字节数(3个寄存器共12字节)
  • 按寄存器编号从大到小,依次存入从高地址到低地址的内存
  • SP最终指向最低地址的有效数据(栈顶)
  • 以 LDMFD sp!, {r0, r1, lr} 弹栈为例:

  • 从SP指向的低地址(栈顶)开始,依次向高地址读取数据
  • 按编号从小到大对应赋值给寄存器列表
  • 读完后SP一次性加上总字节数,回到压栈前的位置
  • 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函数

  • 汇编中用import 函数名声明外部C函数
  • 按约定在r0~r3放好参数
  • 执行bl 函数名调用
  • 注意保持栈8字节对齐,可添加preserve8伪指令解决对齐报错
  • 3. C调用汇编函数

  • 汇编中用export 函数名导出函数
  • C语言中用extern声明函数原型
  • 直接像普通C函数一样调用,参数和返回值自动按约定匹配

  • 总结

    第二天的内容是从“单条指令”走向“结构化程序”的关键:

    • NZCV标志位是所有条件判断的底层硬件基础,理解N和V的组合逻辑,就能通吃所有有符号条件指令
    • 满递减栈是函数调用的根基,压栈弹栈的本质是保存现场、防止寄存器被覆盖
    • bl+lr+栈 三者配合,实现了函数的嵌套调用与正确返回

    掌握这些内容后,就具备了阅读ARM启动代码的核心基础,下一篇将学习异常向量表、工作模式切换等启动代码的核心模块。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【ARM汇编学习笔记02】条件标志位、栈机制与函数调用全解析
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!