ARM 汇编指令:

本篇博客的目的:介绍一些指令的使用方法,以及一些基础概念,为后面编写处理器的启动代码服务。

一、ARM 汇编学习的目标

我们学习 ARM 汇编,最终目的是编写处理器的启动代码。当处理器复位后,硬件环境是裸机状态,无法直接运行 C 语言代码,启动代码的核心作用就是完成硬件基础初始化,搭建 C 语言运行的最小环境,最终引导程序跳转到 C 语言主函数执行。

启动代码的核心任务只有 5 项,本文所有知识点都将围绕这 5 项任务展开:

  1. 初始化异常向量表;
  2. 初始化各工作模式的栈指针寄存器;
  3. 开启 ARM 内核中断允许;
  4. 将工作模式设置为 User 用户模式;
  5. 完成初始化后,引导程序跳转到 C 语言 main 函数执行。

二、ARM 汇编基础:立即数解析

2.1 什么是立即数

立即数(Immediate Value)是直接包含在机器指令中的常数操作数,指令执行时可以直接使用,无需从寄存器或内存中读取,执行效率极高。

在 ARM32 位架构中,数据处理指令的立即数是12 位编码,但这 12 位并不是直接存储数值,而是采用了特殊的压缩编码方案。

2.2 12 位立即数的编码规则

ARM 将 12 位的立即数字段拆分为两部分:

  • 高 4 位:循环右移值rotate(取值范围 0~15)
  • 低 8 位:无符号常数imm8(取值范围 0~255)

最终的有效数值计算公式为:有效数值 = imm8 循环右移 (2 * rotate) 位

这里的核心限制是:移位步长固定为 2 位,因此实际有效移位只能是 0、2、4...30 位的偶数位,这也是立即数合法性判断的核心依据。

图 1:《ARM 12 位立即数编码结构图》示意图说明:32 位 ARM 指令中,[11:8] 位为 4 位 rotate 循环移位值,[7:0] 位为 8 位 imm8 常数,标注计算公式与有效数值的生成逻辑,直观展示 12 位编码如何表示 32 位数值。

2.3 12 位立即数合法性判断三大规则

判断一个数是否为合法的 12 位立即数,只需遵循以下 3 条规则,满足所有规则即为合法立即数:

  1. 若数值范围在 0~0xFF(0~255)之间,一定是合法立即数;
  2. 数值转换为二进制后,最高位 1 到最低位 1 之间的有效位数,不超过 8 位;
  3. 上述 8 位有效序列的右侧,必须有偶数个连续的 0

我们结合正反案例,搞懂判断逻辑:

表格

数值 二进制展开 合法性判断 原因
0x234 0000 0000 0000 0000 0000 0010 0011 0100 合法 有效位 10001101 共 8 位,末尾有 2 个 0(偶数)
0x3f4 0000 0000 0000 0000 0000 0011 1111 0100 合法 有效位 11111101 共 8 位,末尾有 2 个 0(偶数)
0x132 0000 0000 0000 0000 0000 0001 0011 0010 非法 有效位 10011001 共 8 位,但末尾只有 1 个 0(奇数)
0x7f8 0000 0000 0000 0000 0000 0111 1111 1000 非法 有效位 11111111 共 8 位,但末尾有 3 个 0(奇数)
0xfab4 0000 0000 0000 0000 1111 1010 1011 0100 非法 最高位 1 到最低位 1 的有效位数超过 8 位

图 2:《立即数合法性判断流程图》示意图说明:从输入数值开始,第一步判断是否≤0xFF,是则直接合法;否则转第二步判断有效位数是否≤8 位,不满足则非法;满足则转第三步判断末尾 0 的个数是否为偶数,是则合法,否则非法。

2.4 非立即数的处理方案

如果我们需要给寄存器加载一个非法立即数,需使用ldr伪指令。

ldr伪指令可以直接加载任意 32 位数值到寄存器,语法如下:

arm

复制代码
ldr r0, =0x12345678  @ 将32位非立即数0x12345678加载到r0寄存器

除此之外,ldr原生指令的核心作用是从内存中加载数据到寄存器,常用的三种寻址方式如下:

arm

复制代码
@ 1. 基址偏移寻址:将内存地址R1+4的字数据读入R0, 类似r0 = *(r1 + 4)
ldr r0, [r1, #4]

@ 2. 事后更新寻址:将R1地址的字数据读入R0,之后R1自增1,类似r0 = *r1++
ldr r0, [r1], #1

@ 3. 前索引带更新寻址:R1更新为R1+1, 将R1+8地址的字数据读入R0,类似r0 = *++r1
ldr r0, [r1, #1]!

只是举个例子说明实际基本不会一个字节的自增,一般为4的整数倍

三、ARM 核心数据处理指令

3.1 基础数据操作指令

表格

指令 核心作用 语法示例 示例说明
mov 数据搬移,将立即数 / 寄存器值加载到目标寄存器 mov r0, #2 mov r1, r0 立即数 2 加载到 r0 r0 的值复制到 r1
add 加法运算 add r0, r1, #5 add r0, r1, r2 r0 = r1 + 5 r0 = r1 + r2
sub 减法运算 sub r0, r1, #3 sub r0, r1, r2 r0 = r1 - 3 r0 = r1 - r2
bic 位清零指令,将指定位清零 bic r0, r0, #0x1F 将 r0 的低 5 位全部清零
orr 位置位指令,将指定位置 1 orr r0, r0, #0x10 将 r0 的第 4 位(bit4)置 1

3.2 指令的 S 后缀与状态标志位

几乎所有 ARM 数据处理指令,都可以在指令后添加s后缀,其核心作用是:指令执行后,自动更新 CPSR 当前程序状态寄存器的 N、Z、C、V 四个标志位

四个标志位的核心含义:

  • N 位(负号位):有符号数运算结果为负数时,N=1;非负数时 N=0
  • Z 位(零位):运算结果为 0 时,Z=1;结果非零时 Z=0
  • C 位(进位位):无符号数运算发生进位时 C=1;减法发生借位时 C=0
  • V 位(溢出位):会在下面两种情形变为1,两个最高有效位均为0的数相加,得到的结果最高有效位为1;两个最高有效位均为1的数相加,得到的结果最高有效位为0;除了这两种情况以外V位为0

示例:

arm

复制代码
mov r0, #0xFFFFFFFF
adds r1, r0, #1  @ 加s后缀,运算后更新标志位
@ 运算结果为0,Z=1;无符号数发生进位,C=1

3.3 ARM 汇编:指令的条件执行

ARM 架构的核心优势之一,就是几乎所有指令都支持条件执行 ------ 指令只有在满足预设的标志位条件时,才会真正执行。无需频繁跳转指令,即可实现分支逻辑,大幅提升流水线执行效率。

所有条件码与对应规则如下表:

表 :《ARM 指令条件码速查表》表格示意图,包含条件码、助记符、核心含义、触发标志位状态四列,核心内容如下:

表格

条件码 助记符 核心含义 标志位触发条件
0000 EQ 相等 Z == 1
0001 NE 不相等 Z == 0
0010 CS/HS 进位 / 无符号数大于等于 C == 1
0011 CC/LO 无进位 / 无符号数小于 C == 0
0100 MI 负数 / 减号 N == 1
0101 PL 正数 / 零 N == 0
0110 VS 有符号溢出 V == 1
0111 VC 无有符号溢出 V == 0
1000 HI 无符号数大于 C == 1 且 Z == 0
1001 LS 无符号数小于等于 C == 0 或 Z == 1
1010 GE 有符号数大于等于 N == V
1011 LT 有符号数小于 N != V
1100 GT 有符号数大于 Z == 0 且 N == V
1101 LE 有符号数小于等于 Z == 1 或 N != V
1110 AL 无条件执行(默认) 无限制

cmp比较指令经常搭配条件执行,它的本质是对两个数做减法,不保存运算结果,只更新 CPSR 的标志位,语法如下:

arm

复制代码
mov r0, #100
cmp r0, #100  @ 比较r0和100,结果为0,Z标志位置1
moveq r1, #1  @ 只有Z=1(相等)时,才将1加载到r1

四、ARM 跳转与函数调用指令

跳转指令是实现程序分支、循环、函数调用的核心,我们重点区分 3 个核心跳转指令的区别与使用场景。

4.1 三大跳转指令核心区别

表格

指令 核心作用 执行逻辑 核心使用场景
b 无条件跳转 直接修改 PC 程序计数器的值,跳转到目标地址 简单分支、循环、无需返回的跳转
bl 带链接的跳转 先将下一条指令的地址保存到 LR 链接寄存器,再修改 PC 跳转到目标地址 函数调用,可通过 LR 寄存器返回调用处
bx 带状态切换的跳转 将寄存器中的地址加载到 PC,同时根据地址最低位切换 ARM/Thumb 指令集 函数返回,常用bx lr实现函数返回

4.2 条件跳转与循环实现

结合条件码,我们可以用b+条件码实现循环逻辑,最经典的示例:实现 0 到 100 的累加求和

arm

复制代码
mov r0, #0  @ 循环变量i,初始值0
mov r1, #0  @ 累加和sum,初始值0

loop:
add r1, r1, r0  @ sum = sum + i
add r0, r0, #1  @ i = i + 1
cmp r0, #100    @ 比较i和100
ble loop         @ 若i <= 100,跳转到loop继续循环
@ 循环结束后,r1中存储0+1+2+...+100的结果

4.3 函数调用的核心:现场保护与恢复

bl指令实现函数调用时,会遇到两个问题:

  1. 函数内部修改了通用寄存器的值,会破坏调用处的寄存器数据;
  2. 发生函数嵌套调用时,LR 寄存器的值会被覆盖,导致无法返回最开始的调用处。

解决这两个问题的方案,就是:函数调用前,将需要保护的寄存器入栈保存;函数执行完毕返回后,再出栈恢复寄存器数据,这就是嵌入式开发中常说的 "保护现场" 与 "恢复现场"。


五、ARM 栈机制解析

栈是 ARM 汇编与 C 语言运行的核心基础,现场保护、函数传参、局部变量存储都离不开栈。

5.1 栈的核心概念

栈是一段连续的内存空间,遵循 先进后出 的访问规则,SP(r13)栈指针寄存器永远指向栈顶,ARM 架构中通过stmfd/ldmfd指令实现入栈和出栈操作。

5.2 栈的四大分类与特性

根据栈指针的移动方向、数据写入与指针移动的顺序,栈分为四大类:

  1. 空增栈:先写入数据,再让栈指针向高地址自增
  2. 空减栈:先写入数据,再让栈指针向低地址自减
  3. 满增栈:先让栈指针向高地址自增,再写入数据
  4. 满减栈:先让栈指针向低地址自减,再写入数据

核心概念区分:

  • 增栈:入栈时栈指针向内存高地址移动;减栈:入栈时栈指针向内存低地址移动
  • 空栈:栈指针指向空闲的可写入位置,先写数据再移动指针;满栈:栈指针指向最后一个已写入的有效数据,先移动指针再写数据

5.3 ARM 标准栈:满减栈

ARM 体系架构默认采用满减栈 ,对应的入栈指令为stmfd,出栈指令为ldmfd,语法如下:

arm

复制代码
@ 入栈:将r0-r12、lr寄存器的值入栈保存,!表示SP自动更新
stmfd sp!, {r0-r12, lr}

@ 出栈:从栈中恢复数据到r0-r12、pc寄存器,^表示同时恢复SPSR到CPSR
ldmfd sp!, {r0-r12, pc}^

满减栈的操作流程:

  1. 栈底设置为内存高地址,例如0x40001000
  2. 入栈时,SP 先向低地址自减 4(32 位数据),再将寄存器数据写入 SP 指向的地址;
  3. 出栈时,先读取 SP 指向地址的数据到寄存器,再将 SP 向高地址自增 4。

六、汇编与 C 语言互调规则

启动代码的最终目标是跳转到 C 语言主函数,因此汇编与 C 语言的互调是必须掌握的核心规则。

6.1 规范

  1. 函数传参规则:参数个数≤4 个时,通过 R0-R3 寄存器依次传递;参数个数>4 个时,第 5 个及以后的参数通过栈传递。
  2. 函数返回值规则:C 函数的返回值,通过 R0 寄存器传递给汇编代码。
  3. 现场保护规则:调用者负责保护 R0-R3、R12、LR 寄存器;被调用者如果使用 R4-R11 寄存器,必须自行入栈保护,函数返回前出栈恢复。
  4. 内存对齐规则 :汇编代码开头必须添加preserve8伪指令,保证 8 字节内存对齐,兼容 C 语言的编译要求。

6.2 汇编调用 C 语言函数

汇编调用 C 函数的完整步骤:

  1. import关键字声明 C 函数名,告知汇编器该函数在外部 C 文件中定义;
  2. 调用前通过stmfd入栈保护需要保留的寄存器,完成现场保护;
  3. 按照参数顺序,给 R0-R3 寄存器赋值,超过 4 个的参数入栈;
  4. 通过bl指令调用 C 函数;
  5. 调用完成后,通过ldmfd出栈恢复寄存器,完成现场恢复;
  6. 从 R0 寄存器中读取 C 函数的返回值。

示例:汇编调用 C 语言的加法函数C 函数定义:

c

运行

复制代码
// add.c
int add_c(int a, int b, int c, int d)
{
    return a + b + c + d;
}

汇编调用代码:

arm

复制代码
preserve8
import add_c  @ 声明C函数

@ 调用前保护现场
stmfd sp!, {r0-r12, lr}

@ 给4个参数赋值,依次放入R0-R3
mov r0, #1
mov r1, #2
mov r2, #3
mov r3, #4

@ 调用C函数
bl add_c

@ 调用后恢复现场,函数返回值已存入R0
ldmfd sp!, {r0-r12, lr}

6.3 C 语言调用汇编函数

C 调用汇编函数的完整步骤:

  1. 汇编代码中,用export关键字导出汇编函数名,让 C 文件可以链接调用;
  2. C 代码中,用extern关键字声明汇编函数的原型,和普通 C 函数声明一致;
  3. C 代码调用函数时,编译器自动通过 R0-R3 / 栈传递参数;
  4. 汇编函数中完成业务逻辑,将返回值放入 R0 寄存器,通过bx lr返回。

示例:C 调用汇编实现的加法函数汇编代码:

arm

复制代码
preserve8
export asm_add  @ 导出汇编函数

asm_add:
@ 函数参数a在R0,参数b在R1
add r0, r0, r1  @ 计算a+b,结果放入R0作为返回值
bx lr           @ 函数返回

C 代码调用:

c

运行

复制代码
// main.c
#include <stdio.h>

// 声明汇编函数
extern int asm_add(int a, int b);

int main(void)
{
    int res = asm_add(10, 20);
    printf("res = %d\n", res);
    return 0;
}
相关推荐
小马学嵌入式~3 小时前
linux开发深度学习-时钟
linux·arm开发·嵌入式硬件·学习
XMAIPC_Robot3 小时前
基于RK3588 ARM+FPGA的电火花数控硬件平台总体设计(二)
运维·arm开发·人工智能·fpga开发·边缘计算
路溪非溪5 小时前
Linux下wifi子系统的数据流
linux·arm开发·驱动开发
somi77 小时前
ARM-05-中断
arm开发
2401_858936888 小时前
ARM 汇编核心知识点精讲:从基础指令到实战应用
汇编·arm开发
always_TT8 小时前
理解编译过程:预处理→编译→汇编→链接
汇编·microsoft
昵称只能一个月修改一次。。。8 小时前
汇编相关知识
汇编
蜕变的小白8 小时前
嵌入式硬件的学习----ARM
arm开发·嵌入式硬件·学习·arm
’长谷深风‘8 小时前
嵌入式ARM开发入门解析2
汇编·arm开发·arm指令集·立即数