文章目录
- 1. 汇编文件中的主要符号
- 1.1 汇编指令
- 1.2 伪指令
- 1.3 伪操作
- 2. 汇编指令的分类
- 3. 基本汇编指令语法格式
- 4. 数据操作指令
- 4.1 数据搬移指令
- 4.1.1 指令格式
- 4.1.2 测试代码
- 4.1.3 立即数
- 4.1.4 ldr伪指令
- 4.1.5 验证PC寄存器
- 4.2 移位操作指令
- 4.2.1 指令格式
- 4.2.2 测试代码
- 4.3 位运算指令
- 4.3.1 指令格式
- 4.3.2 测试代码
- 4.4 算数运算指令
- 4.4.1 指令格式
- 4.4.2 测试代码
- 4.5 比较指令
- 4.5.1 指令格式
- 4.5.2 测试指令代码
- 5. 跳转指令
- 5.1 指令格式
- 5.2 测试代码
- 5.3 实现跳转的其他方式
- 5.4 练习
- 5.4.1 求两个数的最大公约数
- 5.4.2 实现for循环,求1-100的和
- 6. 特殊功能寄存器操作指令
- 6.1 指令格式
- 6.2 测试代码
- 7. Load/Store内存读写指令
- 7.1 单寄存器指令
- 7.1.1 指令码
- 7.1.2 指令格式
- 7.1.3 测试代码
- 7.1.4 练习
- 7.2 多寄存器操作指令
- 7.2.1 指令码
- 7.2.2 指令格式
- 7.2.3 测试代码
- 7.3 栈操作指令
- 7.3.1 栈的类型
- 7.3.2 栈空间的操作方式
- 7.3.3 指令格式
- 7.3.4 测试代码
1. 汇编文件中的主要符号
1.1 汇编指令
汇编指令:汇编指令是一条指令,编译器可以将其编生成32位的机器码,汇编指令占用代码段的内存空间,执行汇编指令可以完成某个特定的功能。
mov r0, r1 @将r1寄存器中的内容拷贝到寄存器r0中
1.2 伪指令
伪指令:本身不是一条指令,但是编译器可以将其编译生成多条汇编指令,通过多条汇编指令最终完成一条伪指令的功能。
ldr r0, =0x12345678 @ 汇编指令只有32位,0x12345678就32位了,也不是立即数,所以指令无法存储,所以需要伪指令
1.3 伪操作
伪操作:伪操作不占用代码段的内存空间,给编译器使用,直到编译器对代码进行编译的。
.text .end .data .global .globl .word .short .byte .if 0/1 .else .endif
2. 汇编指令的分类
1. 数据操作指令
数据搬移指令
算数运算指令
移位操作指令
位运算指令
比较指令
2. 跳转指令
3. Load/Store内存读写指令
4. 软中断指令
5. 特殊功能寄存器读写指令
3. 基本汇编指令语法格式
<opcode>{cond}{s} Rd, Rn, oprand_shifter
<opcode> :指令码 比如mov b add sub
{cond} : 条件标志, 实现汇编指令的有条件执行
{s} :状态位, 指令码后边加s,表示指令的执行结果影响CPSR的NZCV的变化;指令码后边不加s, 表示指令的执行结果不影响CPSR的NZCV位
Rd : 目标寄存器,只能是一个普通的寄存器 R0-R15
Rn : 第一个操作寄存器,只能是一个普通的寄存器 R0-R15
oprand_shifter : 第二个操作数
1> 可以是一个普通的寄存器 R0-R15
2> 可以是一个立即数
3> 可以是经过移位操作的寄存器
注意:
<opcode>{cond}{s} : 连到一起写
Rd, Rn, oprand_shifter :三个之间使用英文的逗号隔开
<opcode>{cond}{s}和Rd, Rn, oprand_shifter使用空格隔开
每条汇编指令单独占用一行。
汇编指令不区分大小写:
mov r0, #0xff <==> MOV R0, #0xFF <==> MoV R0, #0xff
4. 数据操作指令
4.1 数据搬移指令
4.1.1 指令格式
{cond}{s} Rd, oprand_shifter
1> 没有第一个操作寄存器
2> oprand_shifter可以是寄存器也可以是立即数
mov : 将oprand_shifter赋值给Rd寄存器
mvn : 将oprand_shifter按位取反之后赋值给Rd寄存器
4.1.2 测试代码
@ 0xFF 是一个立即数,使用立即数时前边需要加#
@ 第二个操作数是一个立即数
mov r0, #0xFF @ r0 = 0xFF
@ 第二个操作数是一个寄存器
mov r1, r0 @ r1 = r0
@ mvn指令
mvn r2, #0xFF @ r2 = ~0xFF
mvn r3, r2 @ r3 = ~r2
4.1.3 立即数
mov r0, #0xF @ OK
mov r1, #0xFF @ OK
@mov r2, #0xFFF @ ERROR
@mov r3, #0xFFFF @ ERROR
@mov r4, #0xFFFFF @ ERROR
mov r5, #0xFFFFFF @ OK
mov r6, #0xFFFFFFF @ OK
mov r7, #0xFFFFFFFF @ OK
mov r8, #0xFF000000
@mov r9, #0xFFF00000
mov r10, #0x1F800000
4.1.4 ldr伪指令
格式:ldr{cond} Rd, =0-4G之间的数 @ Rd = 0-4G之间的数
ldr r0, =0x1234567
4.1.5 验证PC寄存器
@ mov pc, #0x10
@ 给PC寄存器赋值时需要是4的整数倍,原因是一条ARM指令占4字节的空间
@ 如果给PC寄存器赋值不是4的整数倍,编译内部会自动的忽略数据的第[1:0]位
@ 给PC赋值时,如果第1位和第0位为1,都当成0处理。
mov pc, #5 @ pc = 0x5
mov pc, #6 @ pc = 0x6
mov pc, #7 @ pc = 0x7
4.2 移位操作指令
4.2.1 指令格式
<opcode>{cond}{s} Rd, Rn, oprand_shifter
Rd = Rn lsl/lsr/ror/asr oprand_shifter
lsl : 逻辑左移/无符号数左移
lsr :逻辑右移/无符号数右移
asr :算数右移/有符号数的右移
ror :循环右移
4.2.2 测试代码
mov r0, #0xFF
@ 高位移出,低位补0
lsl r1, r0, #0x4 @ r1 = r0 << 4 = 0xFF0
@ 低位移出,高位补0
lsr r2, r0, #0x4 @ r2 = r0 >> 4 = 0x0F
@ 低位移出,高位补符号位,因为数据在计算机中按补码存储
asr r3, r0, #0x4 @ r3 = r0 >> 4 = 0x0F
@ 低位移出,补到高位
ror r4, r0, #0x4 @ r4 = r0 >> 4 = 0xF000000F
mov r0, #-0x7F000000
asr r5, r0, #0x4
@ 移位操作指令中的立即数的范围为0-31
lsl r6, r0, #31
@ 第二个操作为一个移位操作的寄存器
@ 第二个操作数只能是一个寄存器经过移位指令进行移位
mov r0, r1, lsl #4 @ r0 = (r1 << 4)
@ 如果第二个操作数是一个立即数,可以使用<< >> ~ 进行运算
mov r0, #(0xFF << 4)
4.3 位运算指令
4.3.1 指令格式
<opcode>{cond}{s} Rd, Rn, oprand_shifter
@ Rd = Rn 与/或/异或 oprand_shifter
and : 按位与运算 &
orr : 按位或运算 |
eor : 按位异或运算 ^
bic : 按位清除
与运算
左操作数 | 位运算符 | 右操作数 | 运算结果 |
1 | & | 0 | 0 |
0 | & | 0 | 0 |
1 | & | 1 | 1 |
0 | & | 1 | 0 |
或运算
左操作数 | 位运算符 | 右操作数 | 运算结果 |
1 | | | 0 | 0 |
0 | | | 0 | 0 |
1 | | | 1 | 1 |
0 | | | 1 | 0 |
异或运算
左操作数 | 位运算符 | 右操作数 | 运算结果 |
1 | ^ | 0 | 0 |
0 | ^ | 0 | 0 |
1 | ^ | 1 | 1 |
0 | ^ | 1 | 0 |
与0清0,与1不变
或1置1,或0不变
异或1取反,异或0不变
4.3.2 测试代码
@ 假设你不知道R0寄存器中存储的值
ldr r0, =0x12345678
@ 31 0
@ **** **** **** **** **** **** **** ****
@ 1> 将R0寄存器中的值的第[3]位清0,保持其他位不变
and r0, r0, #0xFFFFFFF7
and r0, r0, #~(0x1 << 3)
bic r0, r0, #(0x1 << 3)
@ 2> 将r0寄存器中的值的第[29]位置1,保持其他位不变
orr r0, r0, #(0x1 << 29)
@ 3> 将R0寄存器中的值的第[7:4]位清0,保持其他位不变
and r0, r0, #(~(0xF << 4))
bic r0, r0, #(0xF << 4)
@ 4> 将R0寄存器中的值的第[15:8]位置1,保持其他位不变
orr r0, r0, #(0xFF << 8)
@ 5> 将R0寄存器中的值的第[3:0]位按位取反,保持其他位不变
eor r0, r0, #(0xF << 0)
@ 6> 将R0寄存器中的值的第[11:4]位修改为10101011,保持其他位不变
@ 6.1> 先将对应的位清0
and r0, r0, #(~(0xFF << 4))
@ 6.2> 再将对应的位置1
orr r0, r0, #(0xAB << 4)
@ 一般情况不使用先置1,后清0
@ 6.1 先将对应的位置1
orr r0, r0, #(0xFF << 4)
@ 6.2 再将对应的位清0
and r0, r0, #(~(0x54 << 4))
@ bic位清除指令
@ 第二个操作数据的哪位是1,最终将第一个操作寄存器中的置的
@ 哪一位清0,并将结果写到目标寄存器中
bic r0, r0, #0xFF @ 将R0的[7:0]位清0
@ 如果目标寄存器的编号和第一个操作寄存器的编号相同,可以合并写一个
and r0, #~(0xF << 4) @ 等价于 r0 &= ~(0xF << 4)
4.4 算数运算指令
4.4.1 指令格式
<opcode>{cond}{s} Rd, Rn, oprand_shifter
add : 普通的加法指令,不需要考虑进位标志位(C位)
adc :带进位的加法指令,需要考虑进位标志位(C位)
sub : 普通的减法指令,不需要考虑借位标志位(C位)
sbc : 带借位的减法指令,需要考虑借位标志位(C位)
mul : 乘法指令
div : 除法指令, ARM-V7之前架构之前的架构都不支持除法指令,
ARM-v8架构之后架构支持除法指令
4.4.2 测试代码
@ 案例1:实现两个64位数相加
@ r0和R1存放第1个64位的数
@ r2和r3存放第2个64位的数
@ r4和r5存放运算的结果
mov r0, #0xFFFFFFFE @ 低32位
mov r1, #5 @ 高32位
mov r2, #6 @ 低32位
mov r3, #7 @ 高32位
@ s : 指令的执行结果影响CPSR的NZCV位,修改NZCV值
adds r4, r0, r2 @ r4 = r0 + r2 = 0x4
@ adc : 带进位的加法指令,自动读取C位的值。
adc r5, r1, r3 @ r5 = r1 + r3 + C = 0xD
@ 案例2:实现两个64位数相减
@ r0和R1存放第1个64位的数
@ r2和r3存放第2个64位的数
@ r4和r5存放运算的结果
mov r0, #5 @ 低32位
mov r1, #10 @ 高32位
mov r2, #6 @ 低32位
mov r3, #7 @ 高32位
subs r4, r0, r2 @ r4 = r0 - r2 = 0xFFFFFFFF
sbc r5, r1, r3 @ r5 = r1 - r3 - !C = 0x2
mov r0, #4
mov r1, #5
mul r2, r0, r1 @ r2 = r0 * r1 = 0x14
@ 乘法指令的第二个操作数只能是一个寄存器
@ mul r2, r0, #5 @ error
4.5 比较指令
4.5.1 指令格式
cmp{cond} Rn, oprand_shifter
1. cmp指令没有目标寄存器,只有第一个操作寄存器和第二个操作数
2. 比较指令比较的就是第一个操作寄存器和第二个操作数的大小,本质就是做减法运算
3. 比较指令的运算结果最终影响的是CPSR的NZCV位,并且CMP指令后不需要加S.
4. 使用比较指令之后的结果,必须和条件码配合使用,条件码如下如所示:
就是省略了回写的subs指令,可以直接用减法指令替换,其功能就是将cpsr寄存器的某些位置一清零。
4.5.2 测试指令代码
@ 比较R0和R1寄存器中的值
@ 如果R0>R1,则R0 = R0-r1
@ 如果R0<R1, 则R1 = r1-r0
mov r0, #9
mov r1, #15
cmp r0, r1 @ 左操作数 比较运算符 右操作数
@ 使用cmp指令和注记符可以实现指令的有条件执行
subhi r0, r0, r1
subcc r1, r1, r0
5. 跳转指令
5.1 指令格式
b/bl{cond} Label(标签,汇编函数的名字,表示函数的入口地址)
b ----> 不带返回值的跳转指令,执行跳转指令是不保存返回地址到LR寄存器中
bl ----> 带返回值的跳转指令,执行跳转指令是保存返回地址到LR寄存器中
b : 有去无回就用b,比如while(1)死循环
bl : 有去有回就用bl,比如函数的调用
跳转指令的本质就是修改PC值,执行跳转指令时将Label标签标识的地址赋值给PC.
5.2 测试代码
@ nop 空指令
nop
nop
nop
@ 执行跳转指令时修改PC值执行add_func函数的第一条指令
@ 同时保存跳转指令的下一条指令的地址到LR寄存器
bl add_func
nop
nop
nop
@ 跳转到loop标签下的第一条指令执行,
@ 不保存返回地址到LR中
b loop
add_func:
mov r0, #0x3
mov r1, #0x4
add r2, r0 , r1
mov pc, lr @ mov r15, r14
5.3 实现跳转的其他方式
mov pc, lr @ 一般用于函数的返回,
mov pc, oprand_shifter @ 需要确定跳转的地址, oprand_shifter必须是4的整数倍
ldr pc, =Label @ 等价于b Label
ldr r0, =Label
mov pc, r0 @ 等价于b Label
注意:
mov pc, #Label
一般没人这么干,因为你不能保证你的标签是一个立即数
要么是使用寄存器直接给pc赋值
mov pc, rn
要么是使用ldr伪指令
ldr pc, =Label
5.4 练习
5.4.1 求两个数的最大公约数
思路:
- 先判断两个数是否相等,若相等则这个数就是最大公约数
- 若不相等,则让大数减小数
- 再次执行步骤1,直至找到最大公约数
代码实现:
mov r0, #9
mov r1, #15
mm:
cmp r0, r1
beq loop
subhi r0, r0, r1
subcc r1, r1, r0
b mm
5.4.2 实现for循环,求1-100的和
用C语言实现:
sum = 0;
for(i = 1;i <= 100; i++){
sum += i;
}
for循环语句的执行过程
for (1;2;3)
{
4;
}
[1,2][4,3,2][4,3,2][4,3,2]........
汇编实现:
mov r0, #0 @ 等价于sum
mov r1, #1 @ 等价于i, 表达式i
mm:
cmp r1, #100 @ 表达式2
bhi loop
add r0, r0, r1 @ 表达式4
add r1, r1, #1 @ 表达式3
b mm
6. 特殊功能寄存器操作指令
6.1 指令格式
mrs Rd, cpsr @ Rd = cpsr
msr cpsr, oprand_shifter @ cpsr = oprand_shifter
mrs : 将特殊功能寄存器中的值读到普通寄存器中
msr : 将普通寄存器中的值读到特殊功能寄存器中
对于特殊功能寄存器的读写访问,只能使用mrs或者msr完成。
6.2 测试代码
@ 系统上电就是一个复位的信号,默认处理器工作在SVC模式
@ 从SVC模式切换到用户模式,修改CPSR的模式位,保证其他位不变
@ 在SVC模式下CPSR寄存器中的值
@ I[7] = 1 F[6] = 1 T[5] = 0 M[4:0] = 10011
@ 修改模式位,切换到用户模式,保证其他位不变
@ 修改之后,CPSR中的值应该为
@ I[7] = 1 F[6] = 1 T[5] = 0 M[4:0] = 10000
@ 方式1:之间修改CPSR的模式位,前提必须的确定其他位的值
@ msr cpsr, #0xD0 @ 1101 0000
@ 方式2: 只修改CPSR的模式位,其他位不变
@ 1. 先将cpsr中的值读到普通寄存器中
mrs r0, cpsr
@ 2. 修改普通寄存器中的[4:0]位
bic r0, r0, #0x1F
orr r0, r0, #0x10
@ 3. 将结果写回到cpsr中
msr cpsr, r0
7. Load/Store内存读写指令
7.1 单寄存器指令
7.1.1 指令码
ldr/str : 一次读写1个字空间的大小的数据
ldrh/strh :一次读写半个字空间大小的数据
ldrb/strb :一次读写一个字节空间大小的数据
ld : Load(加载)
st : Store(存储)
r : Register
h : Half
b : Byte
7.1.2 指令格式
ldr/ldrh/ldrb Rd, [Rm]
1> 将Rm寄存器中的数据当成一个内存的地址,
2> 将[Rm]指向的空间的数据读到Rd寄存器。
int a = 100;
int *p = &a;
int b = *p;
----------------------
Rm <======> p
[Rm] <======> *p;
ldr Rd, [Rm] <=====> b = *p;
str/strh/strb Rn, [Rm]
1> 将Rm寄存器中的数据当成一个内存的地址,
2> 将Rn寄存器中的数据写到[Rm]指向的内存空间中。
int a = 100;
int *p = &a;
int b = 200;
*p = b;
----------------
Rm <======> p
[Rm] <======> *p
str Rn, [Rm] <=====> *p = b;
7.1.3 测试代码
ldr r0, =0x12345678
ldr r1, =0x40000800
@ 将R0寄存器中的数据写到[R1]指向的地址空间中
str r0, [R1]
@ 将[R1]指向的地址空间中的数据读到R2寄存器中
ldr r2, [R1]
7.1.4 练习
ldr r0, =0x40000800
ldr r1, =0x11111111
ldr r2, =0x22222222
ldr r3, =0x33333333
@ 将r1寄存器中的数据写到[R0+4]指向的地址空间中,
@ R0寄存器中的地址不变。
@ [0x40000804] = 0x11111111 R0 = 0x40000800
str r1, [r0, #4]
@ 将R2寄存器中的数据写到[R0]指向的地址空间中,
@ 同时更新R0寄存器中的地址,R0 = R0 + 4;
@ [0x40000800] = 0x22222222 R0 = 0x40000804
str r2, [r0], #4
@ 将r3寄存器中的数据写到[R0+4]指向的地址空间中,
@ 同时更新R0寄存器中的地址,R0 = R0 + 4;
@ ! : 更新地址
@ [0x40000808] = 0x33333333 R0 = 0x40000808
str r3, [r0, #4]!
@ 以上三种写法同样适用于ldr str ldrh strh ldrb strb
@ 偏移地址必须是自己操作空间整数倍。
@ 使用汇编验证大小端,
@ 使用str存储数据,使用ldrb分别读取数据。
ldr r0, =0x40000800
ldr r1, =0x12345678
str r1, [r0]
ldrb r2, [r0, #0]
ldrb r3, [r0, #1]
ldrb r4, [r0, #2]
ldrb r5, [r0, #3]
ldrb r2, [r0], #1
ldrb r3, [r0], #1
ldrb r4, [r0], #1
ldrb r5, [r0], #1
ldr r0, =0x40000800
ldrb r2, [r0]
ldrb r3, [r0, #1]!
ldrb r4, [r0, #1]!
ldrb r5, [r0, #1]!
7.2 多寄存器操作指令
7.2.1 指令码
stm : 将多个寄存器中的值写到连续的地址空间中
ldm :将连续的地址空间中的数据读到多个寄存器中
m : multi
7.2.2 指令格式
stm Rm, {寄存器列表}
1> 将Rm寄存器中的数据当成一个地址看待
2> 多个不同编号的寄存器
ldm Rm, {寄存器列表}
1> 将Rm寄存器中的数据当成一个地址看待
2> 多个不同编号的寄存器
寄存器列表的书写方式:
1> 如果寄存器列表中的寄存器编号连续使用“-”隔开, 比如:r0-r5
2> 如果寄存器列表中的寄存器编号不连续使用“,”隔开,比如:r0-r5, lr
3> 寄存器列表中的寄存器要求从小到大依次书写。
4> 如果寄存器列表中的寄存器,按照从大到小进行书写,所有的寄存器必须单独书写,
不可以使用"-",只能使用“,”,并且编译时会报警告。
比如:
r5-r0 : 错误书写格式
r5,r4,r3,r2,r1,r0 : 正确,但是报警告。
7.2.3 测试代码
ldr r0, =0x40000800
ldr r1, =0x11111111
ldr r2, =0x22222222
ldr r3, =0x33333333
ldr r4, =0x44444444
ldr r5, =0x55555555
@ 将r1-r5寄存器中的数据写到R0指向的连续的20字节内存空间中
stm r0, {r1-r5}
@ 将r0指向的连续的20字节空间的数据读到r6-r10寄存器中。
@ ldm r0, {r6-r9,r10}
ldm r0, {r10,r9,r8,r7,r6}
7.3 栈操作指令
7.3.1 栈的类型
增栈:压栈之后,栈指针向高地址方向移动。
减栈:压栈指针,栈指针向低地址方向移动。
满栈:当前栈指针指向的空间右有效的数据,需要先移动栈指针,让栈指针指向一个没有有效数据的空间,然后再压入数据,压入数据之后,栈指针指向的空间又包含有效数据了。
空栈:当前栈指针指向的栈空间,没有有效的数据,可以先进行压栈,压入数据之后栈指针指向的空间就有有效的数据了,因此需要移动栈指针,让栈指针再次指向一个没有有效数据的空间。
将定义两两结合就行成了:
满减栈
满增栈
空减栈
空增栈
7.3.2 栈空间的操作方式
满增栈 :Full Ascending stmfa/ldmfa
满减栈 :Full Descending stmfd/ldmfd
空增栈 :Empty Ascending stmea/ldmea
空减栈 :Empty Descending stmed/ldmed
ARM处理器中默认使用的满减栈,stmfd/ldmfd。
7.3.3 指令格式
stmfd sp!, {寄存器列表}
ldmfd sp!, {寄存器列表}
1> 将sp寄存器中的数据当成一个地址看待
2> 多个不同编号的寄存器
3> ! : 作用,每次压栈或者出栈完成之后都需要跟新栈指针寄存器中的地址。
寄存器列表的书写方式:
1> 如果寄存器列表中的寄存器编号连续使用“-”隔开, 比如:r0-r5
2> 如果寄存器列表中的寄存器编号不连续使用“,”隔开,比如:r0-r5, lr
3> 寄存器列表中的寄存器要求从小到大依次书写。
4> 如果寄存器列表中的寄存器,按照从大到小进行书写,所有的寄存器必须单独书写,
不可以使用"-",只能使用“,”,并且编译时会报警告。
比如:
r5-r0 : 错误书写格式
r5,r4,r3,r2,r1,r0 : 正确,但是报警告。
5> 不管寄存器列表中的寄存器如何书写,永远都是小编号的寄存器对应这低地址,
大编号的寄存器对应这高地址。
7.3.4 测试代码
@ 1. 初始化栈指针
ldr sp, =0x40000820
@ 2. 初始化r0,r1寄存器
mov r0, #3
mov r1, #4
bl add_func
add r2, r0, r1 @ r2 = r0 + r1 = 0x7
nop
nop
b loop
add_func:
stmfd sp!, {r0-r1,lr} @ 压栈保存现场
mov r0, #5
mov r1, #6
bl sub_func
add r3, r0, r1 @ r3 = r0 + r1 = 0xB
ldmfd sp!, {r0-r1,pc} @ 出栈恢复现场
@ mov pc, lr
sub_func:
stmfd sp!, {r0-r1} @ 压栈保存现场
mov r0, #10
mov r1, #7
sub r4, r0, r1 @ r4 = r0 - r1 = 0x3
ldmfd sp!, {r0-r1} @ 出栈恢复现场
mov pc, lr