概述
分类
汇编中的符号:
- 指令:能够编译生成一条32位机器码,并且能被处理器识别和执行
- 伪指令:本身不是指令,编译器可以将其替换成若干条指令
- 伪操作:不会生成指令,只是在编译阶段告诉编译器怎么编译
对于不同的CPU,指令和伪指令不同。因为指令和伪指令都由CPU来直接识别。
对于不同的编译器,伪操作不同,因为伪操作是控制编译器编译的。
ARM指令集的分类:
- 数据处理指令:进行数学运算、逻辑运算
- 跳转指令:实现程序的跳转,本质就是修改了PC寄存器
- Load/Store指令:访问(读写)内存
- 状态寄存器传送指令:用于访问(读写)CPSR寄存器
- 软中断指令:触发软中断
- 协处理器指令:操作协处理器的指令
其中数据处理指令、跳转指令、Load/Store指令是通用指令,任何一个CPU都具有该指令。并且这些指令在C语言中都有对应的语句。
指令格式:
汇编语言不区分大小写,但是不要出现一个指令里面既有大写也有小写。比如:可以写MOV或mov,但不要写成Mov
立即数
什么是立即数:
立即数就是能够写在指令后面的数,例如 " MOV R0 #1 " 这个#1当中的数就是立即数。
立即数的本质就是包含在指令当中的数,属于指令的一部分。
注意:立即数不能写到32位,因为汇编指令转为机器码后是32位,这32位中有几位用来表示指令含义和寄存器含义,因此数据位并没有32位。
立即数与变量的区别:
立即数是指令的一部分,在使用立即数赋值时,CPU直接从机器码中就可以获取数据,不需要从内存中读取相应的值。
变量是存放在内存中,通过变量赋值,CPU必须先从内存中取出,再进行赋值。
32位非立即数却可赋值的情况:
当我们使用MOV R0, #0xFFFFFFFF这个指令时,0xFFFFFFFF是一个32位的数,一定不是一个立即数,但最终编译未出错。具体的编译器处理结果如下:
在这里,编译器将MOV R0, #0xFFFFFFFF 替换成了 MVN R0,#00000000,这个MVN的指令就是CPU可以识别执行的指令,原先的MOV就被称作伪指令。
条件码
条件码就是一个条件,当条件满足时执行这个操作,条件不满足时不执行该操作。
大多数的指令后都可以添加条件,具体示例见"跳转指令" - "2、B"
在使用条件码时,需要先使用CMP进行比较,之后再使用条件码进行条件执行。
条件码如下:
数据处理指令
1、数据搬移指令
1.1 MOV
将指定的数据搬移(赋值)到指定的寄存器中。
格式:
MOV 目标寄存器, 源操作数
目标寄存器: 可以为通用寄存器R0~R12,也可以是PC
源操作数:可以是立即数,也可以是寄存器
示例:
将立即数3搬移到寄存器R1,即:令R1=3
MOV R1, #3 @ #3代表十进制的3,#0x3代表十六进制的3
将R2寄存器的值搬移到寄存器R1,即:令R1=R2
MOV R1, R2
1.2 MVN
将指定的数据按位取反后,搬移(赋值)到指定的寄存器中。
格式:
MVN 目标寄存器, 源操作数
示例:
将立即数0xFF取反后的值搬移到寄存器R1,即:令R1 = ~0xFF。
注意:这时R1存放的值为0xFFFFFF00,而不是0,因为ARM的寄存器是32位,所以在ARM寄存器中0xFF的存放值是0x000000FF,因此按位取反后为0xFFFFFF00,而不是0。
MVN R1 #0xFF
2、数值运算指令
一般格式:<操作码> <目标寄存器> <第一操作寄存器> <第二操作数>
- 操作码:执行什么操作,如加减乘除
- 目标寄存器:用于存放运算结果
- 第一操作寄存器:必须是一个寄存器,是参与运算的数。
- 第二操作数:可以是寄存器,也可以是立即数,这也是参与运算的数。
2.1 ADD
加法指令,将指定的两个数进行相加,结果存入指定的寄存器。
格式:
ADD 目标寄存器, 源寄存器, 源操作数
目标寄存器: 可以为通用寄存器R0~R12,也可以是PC
源寄存器:只能是寄存器
源操作数:可以是立即数,也可以是寄存器
示例:
已知R2=5,R3=3,实现R1=R2+R3
MOV R2,#5
MOV R3,#3
ADD R1,R2,R3 @ R1 = R2 + R3
已知R2=5,实现R1=R2+5
MOV R2,#5
ADD R1,R2,#5 @ R1 = R2 + 5
注意:这里不能写成ADD R1,#5,R2 对应ADD只有最后一个为源操作数
2.2 ADC
带进位的加法指令,将指定的两个数进行相加,并且会加上CPSR的进位。
ADC实现64位运算:
实现:
0x00000001 00000002 + 0x00000003 00000004
思路:
将第一个数的低位和高位分别存放在R1、R2;将第二个数的低位和高位分别存放在R3、R4;将结果的低位和高位分别存放在R5、R6
运算代码:
MOV R1,0x00000002
MOV R2,0x00000001
MOV R3,0x00000004
MOV R4,0x00000003
ADDS R5,R1,R3 @将两个低位相加并允许设置CPSR
ADC R6,R2,R4 @将两个高位相加并加上CPSR的C位(溢出进位)
ADCS实现128位运算:
实现:
0x00000001 00000002 00000003 00000004 + 0x00000005 00000006 00000007 00000008
运算代码:
MOV R1,0x00000004
MOV R2,0x00000003
MOV R3,0x00000002
MOV R4,0x00000001
MOV R5,0x00000008
MOV R6,0x00000007
MOV R7,0x00000006
MOV R8,0x00000005
ADDS R9,R1,R5 @将两个低位相加并允许设置CPSR
ADCS R10,R2,R6 @将两个高位相加并加上CPSR的C位(溢出进位)并允许设置CPSR
ADCS R11,R3,R7 @将两个高位相加并加上CPSR的C位(溢出进位)并允许设置CPSR
ADC R12,R4,R8 @将两个高位相加并加上CPSR的C位(溢出进位)
2.3 SUB
减法指令,将指定的两个数进行相减,结果存入指定的寄存器。
该指令只能实现 "寄存器 - 寄存器" 或者 "寄存器 - 立即数" 但实现不了 "立即数 - 寄存器"
格式:
SUB 目标寄存器, 源寄存器, 源操作数
目标寄存器: 可以为通用寄存器R0~R12,也可以是PC
源寄存器:只能是寄存器
源操作数:可以是立即数,也可以是寄存器
注意:SUB减法中有顺序,源寄存器存放的是被减数,源操作数存放的是减数。
示例:
已知R2=5,R3=3,实现R1=R2-R3
MOV R2,#5
MOV R3,#3
SUB R1,R2,R3 @ R1 = R2 - R3
已知R2=5,实现R1=R2-5
MOV R2,#5
SUB R1,R2,#5 @ R1 = R2 - 5
注意:这里不能写成SUB R1,#5,R2 对应SUB只有最后一个为源操作数
2.4 SBC
带借位的减法指令,将指定的两个数进行相减,并且会减去CPSR的借位(C位取反)。
SBC实现64位运算:
实现:
0x00000002 00000001 - 0x00000001 00000002
运算代码:
MOV R1,0x00000001
MOV R2,0x00000002
MOV R3,0x00000002
MOV R4,0x00000001
SUBS R5,R1,R4 @将两个低位相减并允许设置CPSR
SBC R6,R2,R5 @将两个高位相减并减去CPSR的C位取反(借位)
2.5 RSB
逆向减法指令,将指定的两个数进行相减,结果存入指定的寄存器。
该指令通过被减数与减数调换,解决了SUB不能实现"立即数 - 寄存器"的问题。
格式:
RSB 目标寄存器, 源寄存器, 源操作数
目标寄存器: 可以为通用寄存器R0~R12,也可以是PC
源寄存器:只能是寄存器
源操作数:可以是立即数,也可以是寄存器
注意:RSB减法中有顺序,源寄存器存放的是减数,源操作数存放的是被减数。这与SUB相反。
示例:
实现R1 = 3 - R2。
RSB R1,R2,#3 @ R1 = 3 - R2
2.6 MUL
乘法指令,将指定的两个数进行相乘,结果存入指定的寄存器。
格式:
MUL 目标寄存器, 源寄存器, 源寄存器
目标寄存器: 可以为通用寄存器R0~R12,也可以是PC
源寄存器:只能是寄存器
注意:乘法指令中只能是两个寄存器相乘,不能使用立即数。
示例:
实现R1 = R2 * R3。
MUL R1,R2,R3 @ R1 = R2 * R3
3、位运算指令
3.1 AND
按位与指令,将指定的两个数进行按位与,结果存入指定的寄存器。
格式:
AND 目标寄存器, 源寄存器, 源操作数
目标寄存器: 可以为通用寄存器R0~R12,也可以是PC
源寄存器:只能是寄存器
源操作数:可以是立即数,也可以是寄存器
示例:
实现R1 = R2 & R3。
AND R1,R2,R3 @R1 = R2 & R3
3.2 ORR
按位或指令,将指定的两个数进行按位或,结果存入指定的寄存器。
格式:
ORR 目标寄存器, 源寄存器, 源操作数
目标寄存器: 可以为通用寄存器R0~R12,也可以是PC
源寄存器:只能是寄存器
源操作数:可以是立即数,也可以是寄存器
示例:
实现R1 = R2 | R3。
ORR R1,R2,R3 @R1 = R2 | R3
3.3 EOR
按位异或指令,将指定的两个数进行按位异或,结果存入指定的寄存器。
格式:
EOR 目标寄存器, 源寄存器, 源操作数
目标寄存器: 可以为通用寄存器R0~R12,也可以是PC
源寄存器:只能是寄存器
源操作数:可以是立即数,也可以是寄存器
示例:
实现R1 = R2 ^ R3。
EOR R1,R2,R3 @R1 = R2 ^ R3
3.4 LSL
左移指令,将指定的数左移指定的位,结果存入指定的寄存器。
格式:
LSL 目标寄存器, 源寄存器, 源操作数
目标寄存器: 可以为通用寄存器R0~R12,也可以是PC
源寄存器:只能是寄存器
源操作数:可以是立即数,也可以是寄存器
示例:
实现R1 = R2 << 3
方式1:寄存器法
MOV R3,#3
LSL R1,R2,R3 @R1 = R2 << 3
方式2:立即数法
LSL R1,R2,#3 @R1 = R2 << 3
3.4 LSR
右移指令,将指定的数右移指定的位,结果存入指定的寄存器。
格式:
LSR 目标寄存器, 源寄存器, 源操作数
目标寄存器: 可以为通用寄存器R0~R12,也可以是PC
源寄存器:只能是寄存器
源操作数:可以是立即数,也可以是寄存器
示例:
实现R1 = R2 >> 3
方式1:寄存器法
MOV R3,#3
LSR R1,R2,R3 @R1 = R2 >> 3
方式2:立即数法
LSR R1,R2,#3 @R1 = R2 >> 3
3.5 BIC
位清零指令,将指定的位进行清零,结果存入指定的寄存器。
格式:
BIC 目标寄存器, 源寄存器, 源操作数
目标寄存器: 可以为通用寄存器R0~R12,也可以是PC
源寄存器:只能是寄存器
源操作数:可以是立即数,也可以是寄存器
注意:清零源操作数为1的位数,为0的位数不进行操作。但源寄存器的数值不进行改变
示例:
已知R2=0xFF,实现将R2低四位清零,并把结果写入到R1中,并且R2值不进行改变。
MOV R2,#0xFF
BIC R1,R2,#0x0F @0x0F的低四位为1,所以R2的低4位被清零,结果存入到R1
@运算之后R1=0xF0,R2=0xFF(值不改变)
4、其他
4.1 格式扩展
格式扩展示例:
MOV R3, R0, LSL #3 ; 将R0的值左移3位后存入R3
格式扩展指令的看法:
首先这里面有MOV、LSR这两个指令,最前面的MOV是操作码,所以这个整体指令的功能是数据搬移功能,因此R0为第一操作数,LSL #3为第二操作数。
这里的LSL #3并不是一个指令,含义是左移3,操作的数据源就是前面的R0,返回的结果存放在前面的R3。因此最终功能是将R0的值左移3位后存入R3。
4.2 运算影响CPSR
指令后加S影响CPSR:
默认情况下,数据运算不会对CPSR的条件位产生影响。当指令加上后缀 "S" 后,即可影响CPSR
MOV R1,#3
SUB R2,R1,#5 @这计算是一个负数,但没有加S后缀,所以CPSR的N位不置1
MOV R1,#3
SUBS R2,R1,#5 @这计算是一个负数,加了S后缀,所以CPSR的N位置1
跳转指令
1、CMP指令
比较指令,比较两个寄存器值中的关系,比较的结果存储再CPSR的NZCV中。
CMP的本质就是一条减法指令(SUBS),只是没有将运算结果存入寄存器。
格式:
CMP 源寄存器1 源寄存器2
当源寄存器1 == 源寄存器2时,CPSR中的 Z = 1。
当源寄存器1 != 源寄存器2时,CPSR中的 Z = 0。
当源寄存器1 < 源寄存器2时,CPSR中的 C = 0。
当源寄存器1 <= 源寄存器2时,CPSR中的 C = 0 或者 Z = 1。
当源寄存器1 > 源寄存器2时,CPSR中的 C = 1 且 Z = 0(相等时也不产生借位)。
当源寄存器1 >= 源寄存器2时,CPSR中的 C = 1。
示例:
实现1~100求和,结果存入R2。
2、B
跳转到指定的位置。跳转指令的本质就是修改PC指向。
注意:默认情况下LR不会自动保存下一条指令的地址,加上后缀 "L" 可以允许LR保存下一条指令的地址。
B称为不带返回的跳转指令,BL称为带返回的跳转指令。
格式:
B 条件码 标签
条件码可以不写,当不写时为无条件跳转,当写时为有条件跳转。 条件码如下:
示例:
1、无返回跳转到FUNC标号下的第一条指令位置。
当执行 " B FUNC " 之后,PC将指向 "MOV R6,#6" 并在下一次执行该指令。
2、有返回跳转到FUNC标号下的第一条指令位置,执行之后再返回。
当执行 " BL FUNC " 之后,PC将指向 "MOV R6,#6" 并且 LR存储了 "MOV R4,#4"的位置。
当执行 " MOV PC,LR "之后,PC将指向 "MOV R4,#4" 实现了返回。
3、条件跳转,当R1=R2时,跳转到FUNC
当指向 "BEQ FUNC" 后,因为R1==R2不成立,所以不跳转,继续执行 "MOV R3,#3" 指令
4、跳转到自身
指令 "B ." 代表跳转到自身。
比如 "B ." 存在0x00000004处,执行完该指令后PC依旧指向0x00000004
Load/Store指令
1、单寄存器内存访问
1.1 STR
将指定寄存器中的数据存储到指定的内存地址中。
指令与存放数据的字节数:
- STR -- 4字节
- STRH -- 2字节
- STRB -- 1字节
格式:
STR 源寄存器 [目标寄存器]
目标寄存器中存放内存的地址
注意:源寄存器中数据为多少字节,那么目标寄存器存放的地址就得为多少的整数倍。
示例:
将R1的数据写入到R2存放的地址的位置。
这里R2存放的0x40000000为内存,这是因为地址映射时规定了这个地址是内存的起始地址。
这里R1的数据为0xFF000000是4字节,因此R2存放的地址应该为4的整数倍,所以如果R2的值为0x40000001,这就是错误的地址。
1.2 LDR
将指定的内存地址中的数据读取到指定寄存器中。
指令与取出数据的字节数:
- LDR -- 4字节
- LDRH -- 2字节
- LDRB -- 1字节
格式:
LDR 目标寄存器 [源寄存器]
源寄存器存放的是所需取数据的数据地址。
示例:
将R2中存放的地址的数据读出来,存放到R3当中。
该代码运行结束后,R3就存放了0xFF000000
2、多寄存器内存访问
2.1 STM
将多个寄存器中的数据存储到指定的内存地址中。
格式:
STM 目标地址 {源寄存器}
源寄存器:当寄存器连续时,使用 "-"链接;当寄存器不连续时,使用 ","分隔。
如 "STM R11,{R1-R4}" 这操作的时R1、R2、R3、R4
如 "STM R11,{R1,R4}" 这操作的时R1、R4
示例:
将R1~R4寄存器的值存放到以某个地址为起点的地址空间
这里的R11没有加 "[ ]",但它的数据含义代表地址
列表顺序并不是真正的存储顺序, 存储顺序一定是小编号的寄存器存低地址数据。
如上述STM指令写成 "STM R11,{R1,R4,R2,R3}",存储顺序依旧是和 "STM R11,{R1-R4}"一致,而不是0x40000020存放R1,0x40000024存放R4....
2.2 LDM
将指定的内存地址中的数据读取到多个寄存器中。
格式:
LDM 读取起始地址 {目标寄存器}
目标寄存器:当寄存器连续时,使用 "-"链接;当寄存器不连续时,使用 ","分隔。
如 "LDM R11,{R6-R9}" 这操作的时R6、R7、R8、R9
如 "LDM R11,{R6,R9}" 这操作的时R6、R9
示例:
将某个地址为起点的地址空间的数据读取到R6~R9寄存器中
这里的R11没有加 "[ ]",但它的数据含义代表地址
列表顺序并不是真正的存储顺序, 存储顺序一定是小编号的寄存器存低地址数据。
如上述LDR指令写成 "LDR R11,{R6,R9,R7,R8}",存储顺序依旧是和 "LDR R11,{R6-R9}"一致。
状态寄存器传送指令
该指令常用于操作系统内部,比如刚上电时,初始化系统时使用该指令。
1、MRS
将CPSR的值读取到指定寄存器
格式:
MRS 目标寄存器 CPSR
示例:
2、MSR
将指定数据写入到CPSR中
注意:特权模式下可以只写改变CPSR的值,User模式下不能直接改变CPSR的值。
格式:
MSR CPSR 源操作数
示例:
软中断指令
1、SWI
该指令常用于操作系统内部,比如在User模式下调用系统调用时,Linux就会写一个SWI的指令将User模式转换成SVC模式,从而有足够的权限处理硬件。这个过程其实就是使用SWI指令将用户态切换到内核态。
格式:
SWI #参数
参数:这个值用于区分不同的硬件。用户态切换到内核态都是调用SWI来实现,但究竟调用哪一个硬件CPU并不清楚,此时CPU就需要该参数来得知调用哪一个硬件。
2、模拟实现软中断处理过程
流程描述:
- 当程序复位,PC指向0x00处,0x00处存放了异常向量表Reset,跳转到MAIN函数。
- 执行MAIN函数到SWI语句,产生了软中断异常。软中断异常是由SVC模式进行处理,所以CPU自动拷贝CPSR到SPSR_svc中,并且修改CPSR、保存返回地址到LR_svc,最后将PC指向异常向量表对应位置。对于软中断,对应地址为0x80,因此PC跳转到0x80。
- PC到达0x80后执行跳转指令,跳转到中断处理函数。
- 中断处理函数也是一个函数,因此需要先入栈保存现场,之后进行相关的操作,最后出栈恢复现场。在出栈时,加上 "^" 符号可以自动将SPSR_svc的数据拷贝到CPSR恢复现场,同时出栈数据可以直接写到PC中,一步实现出栈、恢复现场、跳转。
实验代码如下:
.text
.global _start
_start:
@异常向量表
B MAIN @Reset 0x00
B . @Undef 0x04
B SWI_Handler @SWI 0x08
B . @Prefetch Abort 0x0C
B . @Data Abort 0x10
B . @Reserved 0x14
B . @IRQ 0x18
B . @FIQ 0x20
@MAIN中实现R3=R1+R2,并调用一次SWI
MAIN:
MOV SP,#0x40000020 @初始化SP,这里是SVC模式下的SP
MSR CPSR,#0x10 @将模式改为User模式,IRQ/FIQ使能
MOV R1,#1
MOV R2,#2
SWI #1 @调用SWI,#1用于区分硬件
ADD R3,R1,R2
B STOP
@SWI_Handler是SWI的中断处理函数,这里也实现R3=R1+R2的功能
SWI_Handler:
STMFD SP!,{R1,R2,LR} @入栈保护现场
MOV R1,#20
MOV R2,#30
ADD R3,R1,R2
LDMFD SP!,{R1,R2,PC}^ @将LR的数据出栈到PC,并把SPSR的值拷贝到CPSR(^的作用)
STOP:
B STOP
.end
协处理器指令
协处理器指令的分类可以分为:
- 数据运算指令CDP
- 存储器访问指令STC、LDC
- 寄存器传送指令MRC、MCR,用于在协处理器和ARM处理器中的寄存器进行数据交互。
伪指令
1、NOP
NOP是让程序什么都不干,暂停一个机器周期。最终编译之后NOP会被编译成MOV R0,R0
2、LDR
LDR的格式不同,含义也不同。当为 "LDR R1,[R2]" 这种格式时,LDR是一个指令;伪指令的情况有以下情况:
1、赋值任意32位数
当为 "LDR R1,=0x12345678" 这种格式时,LDR是一个伪指令,代表R1=0x12345678。该伪指令可以将任意32位数搬移到寄存器。LDR R1,=0x12345678的转换过程如下:
- 0x12345678将作为一个不被执行的机器码存放到内存中,这条机器码被存放在全部有效指令之后,在下图中存放在0x58处,因为0x54是最后一条有效指令。
- LDR的指令将被译码为 "LDR R1,[PC,#偏移值]" 这种格式,在下图中指令被存放在0x30,因为三级流水线的原因,此时PC=0x30+8,因此偏移值 = 0x20。
- 最终拿到的值是0x58地址当中的数据,就是0x12345678,实现了任意32位数据的赋值。
2、通过标号将地址写入寄存器
当为 "LDR R1,=STOP" 这种格式时,LDR是一个伪指令,代表将STOP标号的地址值存入R1中。
编译后的指令同样是转换成PC加偏移量模式,PC偏移量计算与LDR R1,=0x12345678这种计算方法一样。
3、通过标号将地址当中的值写入寄存器
当为 "LDR R1,STOP" 这种格式时,LDR是一个伪指令,代表将STOP标号的地址当中的值存入R1中。
编译后的指令同样是转换成PC加偏移量模式,PC偏移量计算与LDR R1,=0x12345678这种计算方法一样。
伪操作
伪操作通常以 "." 开头。
1、全局/局部声明
格式:
.global <符号名>
.local <符号名>
作用:
将符号声明为全局/局部。
当声明为全局时,所有.s都能访问;当声明为局部时,只有当前.s能访问。
2、宏
格式:
.equ <宏名>,值
作用:
与C语言的#define一样,原值替代。
3、语句封装
格式:
.macro <符号>
@汇编语句
.endm
作用:
类似函数,函数名就是.macro后面跟的符号
4、条件编译
格式:
.if
@汇编语句
.endif
作用:
类似#if #endif,条件满足时才进行编译。
5、重复编译
格式:
.rept <次数>
@汇编语句
.endr
作用:
将区域中的汇编语句重复编译指定次数。
6、弱化符号
格式:
.weak <符号>
作用:
弱化指定的符号,当使用到该符号但未定义该符号时,编译器依旧不报错。
当弱化了符号,并且符号未定义时,编译器会将调用该符号的指令变为NOP,即:一个空操作。
7、申请空间
格式:
.word <初始化值> @32位,一个字
.byte <初始化值> @8位,一个字节
.align <以多少字节对齐> @写3,代表以2^3对齐
.space <申请字节数> <初始化值> @申请指定字节存放数据
.word作用:
在当前指令的地址位置申请空间,并初始化。
.byte作用:
注意:.byte申请空间后,需要再申请冗余空间来进行地址对齐。
.space作用:
注意:.space申请空间后,不一定需要.align来对齐,比如上述是12给字节空间,申请之后地址就是对齐的。
8、标注
格式:
.arm @告诉编译器这之后是arm指令
@ARM汇编指令
.thumb @告诉编译器这之后是thumb指令
@Thumb汇编指令
.text @告诉编译器这之后是代码段
.end @告诉编译器汇编代码已结束