一、ARM汇编学习目的
学习 ARM 汇编的主要目的是为了理解和编写 ARM 启动代码。
处理器复位以后,需要通过启动代码完成最基本的系统初始化,然后引导程序进入 C 语言环境运行。
启动代码的主要任务包括:
-
初始化异常向量表;
-
初始化各工作模式的栈指针寄存器;
-
开启 ARM 内核中断允许;
-
将工作模式设置为 User 模式;
-
完成初始化后,引导程序进入
main()函数执行。
因此,ARM 汇编指令的学习主要围绕启动代码和处理器基本运行过程展开。
二、ARM汇编程序基本格式
ARM 汇编程序中既包含处理器真正执行的指令 ,也包含用于指导汇编器工作的伪操作。
1. 伪操作
伪操作不是 ARM 处理器实际执行的指令,而是用于告诉汇编器应该如何组织和处理程序。
例如:
AREA reset, CODE, READONLY
CODE32
ENTRY
END
2. AREA
AREA reset, CODE, READONLY
AREA 用于定义一个程序段。
其中:
-
reset:段名称; -
CODE:表示代码段; -
READONLY:表示只读属性。
程序、数据等内容需要按照不同功能组织在不同的段中。
3. CODE32
CODE32
表示后面的程序使用 32位 ARM 指令集。
4. THUMB
THUMB
表示后面的程序使用 Thumb 指令集。
因此:
CODE32 → ARM指令集
THUMB → Thumb指令集
5. ENTRY
ENTRY
表示指定程序的入口位置。
6. END
END
表示汇编源程序结束。
三、MOV数据传送指令
MOV 主要用于数据传送。
基本形式:
MOV Rd, #const
MOV Rd, Rm
例如:
MOV r0, #0x8
MOV r1, r0
MOV r3, #31
执行后:
r0 = 8
r1 = r0
r3 = 31
可以将它理解为 C 语言中的赋值操作。
1. MOV与移位
MOV 指令可以配合移位操作:
MOV r6, r0, LSL #31
MOV r7, r0, LSL r3
常见移位方式:
| 指令 | 含义 |
|---|---|
| LSL | 逻辑左移 |
| LSR | 逻辑右移 |
| ASR | 算术右移 |
| ROR | 循环右移 |
| RRX | 扩展右移 |
移位量通常为:
0~31
2. RRX
RRX 是扩展右移,不需要单独指定移位量。
它会结合 CPSR 中的 C 标志位进行操作。
四、ADD加法指令
ADD 用于完成加法运算。
基本形式:
ADD Rd, Rn, #const
ADD Rd, Rn, Rm
ADD Rd, Rn, Rm, shift
例如:
MOV r0, #0x0F
MOV r1, #0xF0
MOV r2, #1
ADD r6, r0, #0xF0
ADD r7, r0, r1
ADD r7, r0, r1, LSL #1
ADD r8, r0, r1, LSL r2
例如:
ADD r7, r0, r1, LSL #1
可以理解为:
r7 = r0 + (r1 << 1)
ARM 指令可以直接对操作数进行移位后再参与运算。
五、SUB减法指令
SUB 用于完成减法运算。
基本形式:
SUB Rd, Rn, #const
SUB Rd, Rn, Rm
SUB Rd, Rn, Rm, shift
例如:
MOV r0, #0xFF
MOV r1, #0xF0
MOV r2, #1
SUB r6, r0, #0xF0
SUB r7, r0, r1
SUB r7, r0, r1, LSL #1
SUB r8, r0, r1, LSL r2
例如:
SUB r7, r0, r1
相当于:
r7 = r0 - r1
六、ARM中的立即数
ARM 指令中的立即数并不是任意一个 32 位数都可以直接使用。
这里主要涉及 12位立即数 imm12。
判断一个数是否能够作为合法的 12 位立即数,可以采用以下方法:
-
将该数转换成二进制;
-
判断是否存在一种偶数位循环右移;
-
循环右移后,高24位全部为0;
-
低8位能够表示有效的
imm8。
可以理解为:
12位立即数
↓
4位 rotate + 8位 imm8
因此,下面这种形式:
MOV r0, #某个数
并不是任意 32 位数都能够直接编码。
七、LDR加载指令
LDR 是加载指令,主要用于将数据从内存加载到寄存器。
例如:
LDR sp, =0x40001000
表示将:
0x40001000
加载到 sp 中。
由于某些立即数不能通过 MOV 直接表示:
MOV sp, #0x40001000
可能无法使用,因此可以采用:
LDR sp, =0x40001000
八、STR存储指令
STR 是存储指令,用于将寄存器中的数据写入内存。
可以简单记忆:
LDR
内存 → 寄存器
STR
寄存器 → 内存
九、MVN按位取反指令
MVN 用于对操作数进行按位取反。
基本形式:
MVN Rd, Rm
例如:
MVN r0, r1
表示:
r0 = ~r1
每一位都进行取反:
0 → 1
1 → 0
十、BIC位清零指令
BIC:
Bit Clear,位清除。
作用:
将指定位置清零。
基本形式:
BIC Rd, Rn, Rm
例如:
MOV r0, #0xFFFFFFFF
MOV r1, #1
BIC r2, r0, r1, LSL #31
这里:
r1 << 31
可以得到:
10000000 00000000 00000000 00000000
然后使用 BIC 将 r0 对应位置清零。
因此:
BIC → 指定位清0
十一、ORR位置1指令
ORR 是按位或指令。
主要作用:
将指定位置置1。
例如:
MOV r0, #0x00
MOV r1, #1
ORR r8, r0, r1, LSL #31
可以将第31位置为1。
因此:
BIC → 指定位清0
ORR → 指定位清1
这两个指令在修改寄存器中的某一位时非常常用。
十二、CPSR条件标志位
CPSR 中包含用于条件判断的标志位:
N Z C V
分别表示:
N → Negative
Z → Zero
C → Carry
V → Overflow
1. N标志位
N 是符号标志位。
如果运算结果最高位:
bit31 = 1
则:
N = 1
当结果按照有符号数解释时,通常表示结果为负数。
2. Z标志位
Z 是零标志位。
如果运算结果为:
0
则:
Z = 1
也就是结果的 bit0~bit31 全部为0。
3. C标志位
C 是进位标志位。
对于无符号运算:
加法
如果最高位向更高位产生进位:
C = 1
减法
如果减法过程中没有借位:
C = 1
4. V标志位
V 是溢出标志位,用于有符号数运算。
例如:
两个正数相加
结果变成负数
或者:
两个负数相加
结果变成正数
说明发生了有符号溢出:
V = 1
十三、ARM条件码
ARM 指令可以根据 CPSR 中的条件标志位决定是否执行。
常见条件码:
EQ
GE
GT
LE
LT
AL
其中:
EQ → Equal,等于
例如:
CMP r0, r1
MOVEQ r2, #1
表示比较 r0 和 r1,如果两个数相等,则执行:
MOV r2, #1
AL 表示无条件执行。
十四、CMP比较指令
CMP:
Compare,比较。
基本形式:
CMP Rn, #const
CMP Rn, Rm
CMP Rn, Rm, shift
例如:
CMP r0, r1
可以理解为进行:
r0 - r1
但是计算结果一般不会保存到普通寄存器中,而是根据比较结果修改 CPSR 中的条件标志位。
因此:
CMP
↓
比较两个数
↓
修改NZCV
↓
配合条件码进行判断
十五、利用CMP比较两个数的大小
例如:
CMP r0, r1
MOVGE r2, r0
MOVLT r2, r1
执行过程:
比较 r0 和 r1
↓
┌────┴────┐
↓ ↓
r0 >= r1 r0 < r1
↓ ↓
r2 = r0 r2 = r1
最终:
r2 = max(r0, r1)
因此可以利用:
CMP + 条件码
实现类似 C 语言中的条件判断。
十六、B、BL、BX跳转指令
ARM 中常见的跳转指令有:
B
BL
BX
1. B
B:
Branch,跳转。
例如:
B fun
表示直接跳转到 fun。
主要用于改变程序执行流程。
2. BL
BL:
Branch with Link,带链接的跳转。
例如:
BL fun
主要用于函数调用。
执行 BL 时,会将返回位置保存到:
LR
即:
R14 = LR
因此:
BL
↓
调用函数
↓
保存返回地址
3. BX
BX:
Branch and Exchange。
函数返回时经常使用:
BX LR
可以理解为:
PC = LR
即根据 LR 中保存的返回地址返回到原来的执行位置。
十七、循环结构
C语言中的循环通常包含三个基本要素:
-
循环结束条件;
-
推动循环趋向结束的语句;
-
循环体。
例如:
int i = 0;
int sum = 0;
while(i <= 100)
{
sum += i;
i++;
}
可以使用 ARM 汇编实现:
MOV r0, #0
MOV r1, #0
loop
CMP r0, #100
BGT finish
ADD r1, r1, r0
ADD r0, r0, #1
B loop
finish
B finish
其中:
r0 → i
r1 → sum
执行过程:
初始化
↓
比较条件
↓
是否满足?
↓
是
↓
执行循环体
↓
修改循环变量
↓
重新判断
因此,C语言中的 while、for 等循环,本质上都可以通过:
CMP + 条件跳转 + B
实现。
十八、ARM函数调用
ARM 汇编中,函数调用主要通过:
BL
+
LR
+
BX LR
完成。
例如:
func
MOV r0, #1
MOV r1, #2
ADD r3, r0, r1
BX LR
main
MOV r0, #100
MOV r1, #200
BL func
MOV r3, #300
执行过程:
main
↓
BL func
↓
进入func
↓
执行函数
↓
BX LR
↓
返回main
十九、为什么需要保护现场
在函数调用过程中,如果被调用函数修改了原来寄存器中的数据,就可能影响调用者后面的程序执行。
另外,当出现函数嵌套调用时:
main
↓
func0
↓
func1
返回地址可能被覆盖。
因此需要对重要寄存器进行:
保护现场
↓
压栈
↓
调用函数
↓
恢复现场
↓
弹栈
二十、ARM中的栈
栈主要用于:
-
保存寄存器;
-
保存函数调用现场;
-
保存返回地址;
-
保存函数运行过程中需要的数据;
-
支持函数嵌套调用。
常见的栈类型有:
空增栈
空减栈
满增栈
满减栈
分别对应:
Empty / Full
Ascending / Descending
二十一、SP栈指针
ARM 中:
R13 = SP
SP:
Stack Pointer,栈指针。
它用于指向当前栈的位置。
程序运行前,需要先对 SP 进行初始化。
例如:
LDR sp, =0x40001000
表示:
SP = 0x40001000
二十二、STMFD保护现场
保护现场可以使用:
STMFD sp!, {r0-r12, lr}
STMFD:
Store Multiple Full Descending
表示按照满递减栈的方式,将多个寄存器保存到栈中。
其中:
sp
↓
栈顶指针
!
↓
操作完成后自动更新SP
{r0-r12, lr}
↓
需要保存的寄存器
二十三、LDMFD恢复现场
恢复现场可以使用:
LDMFD sp!, {r0-r12, pc}
LDMFD:
Load Multiple Full Descending
用于从栈中恢复多个寄存器。
典型过程:
STMFD sp!, {r0-r12, lr}
BL func1
LDMFD sp!, {r0-r12, pc}
整体过程:
保存寄存器
↓
调用函数
↓
执行函数
↓
恢复寄存器
↓
恢复PC
↓
返回
二十四、ARM工作模式修改
ARM 处理器具有不同的工作模式。
修改工作模式时,需要修改:
CPSR
主要使用:
MRS
MSR
1. MRS
MRS 用于读取状态寄存器。
例如:
MRS r0, CPSR
表示:
CPSR → r0
2. MSR
MSR 用于写入状态寄存器。
例如:
MSR CPSR, r0
表示:
r0 → CPSR
3. 修改CPSR模式位
例如:
MRS r0, CPSR
BIC r0, r0, #(0x1FF << 0)
ORR r0, r0, #(0x10 << 0)
MSR CPSR, r0
基本过程:
读取CPSR
↓
清除原来的模式相关位
↓
设置新的模式位
↓
写回CPSR
二十五、异常向量表
异常向量表是 ARM 启动代码中的重要组成部分。
当处理器发生异常时,需要根据异常类型找到对应的异常处理入口。
例如软件可以通过:
SWI #7
产生软件中断异常。
基本过程:
发生异常
↓
找到对应异常向量
↓
进入异常处理程序
↓
执行异常处理
因此,在处理器启动过程中,需要正确建立异常向量表。
二十六、ARM启动代码整体流程
ARM 启动代码的基本流程:
处理器复位
↓
初始化异常向量表
↓
初始化各工作模式SP
↓
开启中断允许
↓
设置User工作模式
↓
建立基本运行环境
↓
进入main()
↓
执行C语言程序
处理器复位之后,程序是如何从最底层的启动代码逐步进入正常程序运行状态的。
二十七、重点指令总结
| 指令 | 作用 |
|---|---|
| MOV | 数据传送 |
| ADD | 加法 |
| SUB | 减法 |
| LDR | 从内存加载数据 |
| STR | 向内存存储数据 |
| MVN | 按位取反 |
| BIC | 指定位清0 |
| ORR | 指定位清1 |
| CMP | 比较 |
| B | 直接跳转 |
| BL | 函数调用 |
| BX | 寄存器跳转,常用于函数返回 |
| MRS | 读取状态寄存器 |
| MSR | 写入状态寄存器 |
| STMFD | 多寄存器入栈 |
| LDMFD | 多寄存器出栈 |