目录
- ARM汇编语言基础
- [1. ARM汇编的分类](#1. ARM汇编的分类)
- [2. 关于指令集](#2. 关于指令集)
- [3. 汇编格式](#3. 汇编格式)
- [4. 操作内存的汇编指令](#4. 操作内存的汇编指令)
-
- LDR:从内存加载数据到CPU寄存器
- STR:将数据从寄存器存储到内存
- LDM:从存储器中加载多个数据到寄存器组
-
- [满增、满减, 空增,空减](#满增、满减, 空增,空减)
- [5. 数据处理汇编指令](#5. 数据处理汇编指令)
- 6.跳转指令
ARM汇编语言基础
对于学习汇编,我有话说,哈,我们实际工作中用到的并不多,我们学会常用的,遇见不会的直接大模型问就可以了,通过汇编,我们可以体会到数据底层的流动规律,写代码自然就会通透,更近一步,我们要彻底学会RTOS,底层的知识也是必不可少的。
写在前面
在CPU的世界里,一切外设都被抽象为寄存器或带有地址的内存数据。CPU只关心其内部的16个寄存器以及内存数据,外设寄存器虽然物理上与内存不同,但在CPU眼中并无区别,唯一的差异在于地址空间。
1. ARM汇编的分类
ARM汇编语言大致可以分为以下几类:
- 内存读写:用于访问内存数据。
- 运算类:执行加法、减法、逻辑运算等。
- 跳转/分支:实现程序流程控制。
- 比较 :类似于高级语言中的
if
语句。
这些指令组合成指令集,构成了ARM汇编语言的核心。
2. 关于指令集
ARM公司发布了两类主要的指令集:
- ARM指令集:32位指令,每条指令占用32位。优点是执行效率高,但占用空间较大。
- Thumb指令集:16位指令,每条指令占用16位。优点是节省空间,但执行效率稍低。
在实际应用中:
- 如果需要节省空间,优先使用Thumb指令集。
- 如果需要高效执行,优先使用ARM指令集。
一个CPU既可以运行Thumb指令,也可以运行ARM指令。如何区分当前指令是Thumb还是ARM指令呢?程序状态寄存器(CPSR)中有一位名为"T",当T=1时,表示当前运行的是Thumb指令。
指令集切换
指令集的切换通过设置PC寄存器的最低位(BIT0)来实现:
- 调用函数A(Thumb指令)时,将PC寄存器的最低位设置为1,即
PC = 函数A地址 + (1<<0)
。 - 调用函数B(ARM指令)时,将PC寄存器的最低位设置为0,即
PC = 函数B地址
。
这种切换方式是通过跳转指令(如B
或BL
)实现的,目标地址的最低位决定了目标函数的指令集模式。在实际开发中,通常使用链接器或编译器工具来管理指令集切换,而不是手动设置PC寄存器的最低位。
Thumb2指令集
为了简化指令集切换,ARM公司引入了Thumb2指令集。Thumb2指令集支持16位和32位指令混合使用,允许在同一个指令集中无缝切换指令长度,而无需显式切换指令集。编译器通常会自动选择最优的指令长度,以平衡代码大小和执行效率。
统一汇编语言(UAL)
为了简化开发,ARM公司推出了统一汇编语言(UAL)。开发者无需区分ARM、Thumb或Thumb2指令集,只需在程序中使用CODE32
、CODE16
或THUMB
指令来指定代码段的指令集类型。
常用汇编指令
在日常工作中,常用的ARM汇编指令包括:
- 数据传输:
MOV
、LDR
、STR
、LDM
、STM
- 逻辑运算:
AND
、OR
- 算术运算:
ADD
、SUB
- 跳转:
B
、BL
- 数据定义:
DCD
- 地址操作:
ADR
、LDR
- 比较:
CMP
3. 汇编格式
以"数据处理"指令为例,UAL汇编格式为:
Operation{cond}{S} Rd, Rn, Operand2
- Operation :表示各类汇编指令,如
ADD
、MOV
。 - cond :条件码,指定指令执行的条件。例如,
EQ
(相等)、NE
(不相等)、CS
(无符号大于等于)、CC
(无符号小于)等。条件码可以应用于大多数指令,但某些指令(如LDM
和STM
)不支持条件码。 - S :是否修改程序状态寄存器(CPSR)。如果指令带有
S
后缀,执行结果会影响CPSR中的条件标志。 - Rd:目标寄存器,用于存储运算结果。
- Rn:第一个源操作数。
- Operand2:第二个操作数。
立即数与伪指令
在16位指令中,由于指令长度限制,立即数(直接给出的数值)的范围有限。例如,MOV R0, #VAL
指令中,VAL不可能占用全部16位,毕竟MOV ,R0也需要一些位来表示其含义。为此,ARM提供了伪指令,让编译器自动处理复杂的立即数转换。
例如:
assembly
MOV R0, #0x12345678 ; 伪指令,编译器会自动将其拆分为多条指令实现
在ARM指令集中,32位指令可以支持更大的立即数范围,但仍有限制。伪指令由编译器自动转换为多条指令,以实现复杂的立即数加载。
4. 操作内存的汇编指令
LDR:从内存加载数据到CPU寄存器
LDR
指令用于从内存中加载数据到CPU寄存器。如果不指定数据类型,默认为32位数据。LDR
支持多种寻址方式,包括偏移寻址、基址寻址和后索引寻址等,但是实际我们工作中用的的都是相对比较简单的。
例如:
assembly
LDR R0, [R1] ; 将R1指向的内存地址中的数据加载到R0
LDR R0, [R1, #4] ; 将R1+4地址中的数据加载到R0
LDRB R0, [R1] ; 加载8位数据到R0
LDRH R0, [R1] ; 加载16位数据到R0
STR:将数据从寄存器存储到内存
STR
指令用于将数据从寄存器存储到内存。STR
也支持多种寻址方式。
例如:
assembly
MOV R0, #0x20000
MOV R1, #0x10
MOV R2, #0x12
STR R2, [R0] ; 将R2的值存储到R0指向的地址
STR R2, [R0, #4] ; 将R2的值存储到R0+4地址
STR R2, [R0, #8]! ; 将R2的值存储到R0+8地址,并将R0更新为R0+8
STR R2, [R0, R1] ; 将R2的值存储到R0+R1地址
STR R2, [R0, R1, LSL #4] ; 将R2的值存储到R0+(R1<<4)地址
STR R2, [R0], #0x20 ; 将R2的值存储到R0地址,并将R0更新为R0+0x20
STRB R2, [R0] ; 存储8位数据到R0指向的地址
LDM:从存储器中加载多个数据到寄存器组
LDM
指令用于批量加载多个寄存器,格式为:
LDM{addr_mode}{cond} Rn{!}, reglist{^}
- addr_mode :
IA
:Increment After,每次传输后增加基地址(默认,可省略)。IB
:Increment Before,每次传输前增加基地址(仅ARM指令可用)。DA
:Decrement After,每次传输后减少基地址(仅ARM指令可用)。DB
:Decrement Before,每次传输前减少基地址。
- ! :表示修改后的基地址会写回Rn寄存器。如果没有
!
,则Rn保持原值。 - ^ :表示会影响程序状态寄存器(CPSR),通常在异常处理中使用。
要注意:低位置寄存机对应低地址,比如R0寄存器属于最低的位置寄存器,R15是最高的寄存器。
有个问题,为什么需要这么多的addr_mode?
比如我们往栈中写数据的时候,有两种情况
1.SP寄存器所指的地址有数据(满栈),这个时候我们需要先调整地址再写数据。
2.SP寄存器所指的地址没有数据(空栈),这种情况是先写数据再调整地址。
很明显我们需要不同的行为的需求,自然也就要有了多种addr_mode.
满增、满减, 空增,空减
根据栈指针指向,可分为满(Full)/空(Empty):
满SP指向最后一个入栈的数据,需要先修改SP再入栈
空SP指向下一个空位置,先入栈再修改SP
根据压栈时SP的增长方向,可分为增/减:
增(Ascending):SP变大
减(Descending):SP变小
组合后,就有4种方式: 满增、满减, 空增,空减。
常用的"满减": 入栈时用STMDB,也可以用STMFD,作用一样;注意是先减地址后存内容
出栈时用LDMIA,也可以用LDMFD,作用一样。先读内容后加地址
下面是几个例子,加黑的内容是汇编指令,方框内为内存结果,

5. 数据处理汇编指令
数据处理汇编指令有很多,我们并不需要全部掌握,只需要注意关键的几条即可。
加法指令ADD:
ADD R1, R2, R3 ; R1 = R2 + R3
ADD R1, R2, #0x12 ; R1 = R2 + 0x12
减法指令SUB:
SUB R1, R2, R3 ; R1 = R2 - R3
SUB R1, R2, #0x12 ; R1 = R2 - 0x12
位操作:
; VisUAL里不支持(1<<4)这样的写法,写成:0x10
AND R1, R2, #(1<<4) ; 位与,R1 = R2 & (1<<4)
AND R1, R2, R3 ; 位与,R1 = R2 & R3
BIC R1, R2, #(1<<4) ; 清除某位,R1 = R2 & ~(1<<4)
BIC R1, R2, R3 ; 清除某位,R1 = R2 & ~R3
ORR R1, R2, R3
比较:
关于比较要说明一下,CMP的对比结果会储存在状态寄存器里面,下面的语句会根据状态寄存器里面的值决定是否执行,比如MOVEQ,相等时才执行
CMP R0, R1 ; 比较R0-R1的结果
CMP R0, #0x12 ; 比较R0-0x12的结果
TST R0, R1 ; 测试 R0 & R1的结果
TST R0, #(1<<4) ; 测试 R0 & (1<<4)的结果
6.跳转指令
C程序中,函数A调用函数B的实质是什么?
void A()
{
int a = 10;
B(a);
printf("ok");
}
实质是:跳转去执行函数B的代码,函数B执行完后,还要回到函数A继续执行后面的代码。
对应的汇编指令就是跳转指令,要主要,凡事有正反,跳走了也要知道怎么跳回来,所以要保存返回地址。
