ARM 汇编
寄存器
寄存器 | 描述 | 详细 |
---|---|---|
R0 - R7 | 通用寄存器 | low register |
R8 - R12 | 通用寄存器 | high register |
SR (R13) | 堆栈指针寄存器 | stack pointer |
LR (R14) | 链接寄存器 | link register |
PC (R15) | 程序计数器 | program counter |
CPSR | 程序状态寄存器 | program status register |
详细见(#过程调用标准) |
CPSR程序状态寄存器
CPSR程序状态寄存器
提供了丰富的处理器状态和控制信息,使得程序可以根据处理器的当前状态执行条件分支,控制中断,以及切换处理器模式等。在编写ARM汇编代码或进行底层系统开发时,对CPSR寄存器的理解和正确使用是非常重要的。详细如下:
位(Bit) | 名称 | 描述 |
---|---|---|
31 | N | 负号标志位。当最近一次算术或逻辑操作的结果为负数时,N=1;否则N=0。 |
30 | Z | 零标志位。当最近一次算术或逻辑操作的结果为零时,Z=1;否则Z=0。 |
29 | C | 进位/借位标志位。用于表示最近一次算术或逻辑操作是否产生了进位或借位。 |
28 | V | 溢出标志位。用于表示最近一次算术操作是否产生了溢出。 |
27 | Q | 粘性溢出标志位。在执行饱和运算后,用于表示是否发生了溢出。 |
26-8 | - | 保留位。这些位被保留为0,用于未来的扩展或特定实现。 |
7-0 | M | 模式位。用于表示当前处理器的模式,如用户模式、系统模式、中断模式等。 |
指令分类
- 数据处理指令:这类指令用于执行数据的算术和逻辑运算。包括数据传送指令(如MOV、MVN),算术逻辑运算指令(如ADD, SUM, AND),以及比较指令(如CMP、TST)等。这些指令直接对寄存器的内容进行操作,可以对寄存器中的数据进行各种计算、比较和修改。
- 跳转指令:跳转指令用于控制程序的流程,根据某些条件或者无条件地跳转到代码的其他部分执行。例如,B指令用于无条件跳转,而BEQ和BNE等指令则用于在满足特定条件(如相等或不等)时进行跳转。
- 程序状态寄存器(PSR)传输指令:这类指令用于访问和修改程序状态寄存器(如CPSR或SPSR),这些寄存器存储了处理器的状态信息,如条件标志、模式位等。
- Load/Store指令:这类指令用于从内存中加载数据到寄存器,或者将寄存器中的数据存储到内存中。例如,LDR指令用于加载数据,而STR指令则用于存储数据。
- 协处理器指令:协处理器指令用于与ARM处理器的协处理器进行交互,执行特定的数学运算或其他任务。
- 异常中断指令:这类指令用于处理ARM处理器上的异常和中断事件,包括软件中断和硬件中断。
指令统计
指令 | 描述 | 示例代码 |
---|---|---|
ADD | 将两个值相加,并将结果存储在目标寄存器中 | ADD R0, R1, R2 |
AND | 执行位与操作,并将结果存储在目标寄存器中 | AND R0, R1, #0xFF |
B | 根据条件标志进行无条件分支跳转 | B location |
BEQ | 当相等时跳转 | BEQ equal_label |
BNE | 当不相等时跳转 | BNE not_equal_label |
BL | 执行子程序调用,并将返回地址存储在链接寄存器中 | BL subroutine |
BLX | 执行子程序调用,并可能在返回时切换指令集 | BLX subroutine |
CBZ | 如果寄存器值为零,则跳转 | CBZ R0, zero_label |
CBNZ | 如果寄存器值非零,则跳转 | CBNZ R0, non_zero_label |
CMP | 比较两个值,并根据结果设置条件标志 | CMP R0, R1 |
DIV | 将第一个值除以第二个值,并将结果存储在目标寄存器中 | DIV R0, R1, R2 |
EOR | 执行位异或操作,并将结果存储在目标寄存器中 | EOR R0, R1, R2 |
LDR | 从内存中加载一个值到寄存器中 | LDR R0, [R1] |
LDM | 从内存中加载多个值到寄存器列表中 | LDM R0!, {R1-R3} |
MOV | 将一个值复制到寄存器中 | MOV R0, #10 |
MUL | 将两个值相乘,并将结果存储在目标寄存器中 | MUL R0, R1, R2 |
ORR | 执行位或操作,并将结果存储在目标寄存器中 | ORR R0, R1, #0xF0 |
POP | 从堆栈中弹出值到寄存器中 | POP {R0, R1} |
PUSH | 将寄存器中的值压入堆栈 | PUSH {R0, R1} |
RET | 从子程序返回,使用链接寄存器中的地址 | RET |
STM | 将多个寄存器中的值存储到内存中 | STM R0!, {R1-R3} |
STR | 将寄存器中的值存储到内存中 | STR R0, [R1] |
SUB | 从第一个值中减去第二个值,并将结果存储在目标寄存器中 | SUB R0, R1, R2 |
寻址模式
寻址方式 | 说明 | 示例 |
---|---|---|
立即寻址 | 操作数(常数、变量或表达式)直接包含在指令中 | MOV R0, #0x1234 |
寄存器寻址 | 操作数存放在寄存器中、速度快 | MOV R0, R2 |
寄存器移位寻址 | 寄存器内容经过移位操作后得到操作数 | MOV R0, R1, LSL #2 |
寄存器间接寻址 | 寄存器内容作为地址指针,操作数存放在存储器中 | LDR R0, [R1] |
基址寻址 | 基址寄存器内容与偏移量相加,得到操作数地址 | LDR R0, [R1, #4] |
多寄存器寻址 | 一条指令可完成多个寄存器的操作 | LDMIA R0!, {R1-R4} |
堆栈寻址 | 使用堆栈指针和偏移量访问堆栈中的数据 | PUSH {R0, R1} |
块拷贝寻址 | 用于连续内存块的数据拷贝 | LDMIA R0!, {R1-R4} |
相对寻址 | 基于当前指令地址的相对偏移量来定位操作数 | BEQ label |
指令后缀
指令后缀 | 说明 | 示例 | 解释 |
---|---|---|---|
B | 操作一个字节(8位)的数据 | LDRB R0, [R1] | 从R1指向的地址加载一个字节到R0 |
H | 操作半个字(16位)的数据 | LDRH R0, [R1] | 从R1指向的地址加载半个字到R0 |
S | 表示有符号操作,影响条件标志位 | ADDS R0, R1, R2 | 将R1和R2相加,并将结果存储在R0中,同时设置条件标志位 |
! | 强制更新存储器中的数据 | STR R0, [R1]! | 将R0中的值存储到R1指向的地址,并将R1的值增加4 |
? | 测试条件执行。根据条件标志位的值来决定是否执行指令。 | ADDEQ R0, R1, R2 |
CPS指令
# 指令格式
CPS #mode
CPSIE iflags{, #mode}
CPSID iflags{, #mode}
# 使能中断
CPSIE I
# 禁止中断
CPSID I
# 使能异步终止和快中断
CPSIE AF
# 禁止异步终止和快中断
CPSID AF
# 使能中断并切换到SYS模式
CPSIE I, #0x1f
伪指令
编译后不生成机器码,不同编译器实现方式可能不同
指令 | 说明 | 示例 |
---|---|---|
.align | 定义对齐方式 | .align 4 |
.ascii | 定义一个字符串常量 | .ascii "Hello World!" |
.arm | 指定ARM指令集 | .arm |
.balign | 定义对齐方式 | .balign 16 |
.byte | 定义一个字节常量 | .byte 0x12 |
.code | 指定16位指令集 | .code 16 |
.data | 指定数据段 | .data |
.equ | 定义一个常数 | .equ PI, 3.14 |
.float | 定义一个浮点数常量 | .float 3.14 |
.global | 定义一个外部链接符号 | .global _start |
.section | 定义一个段 | .section .text |
.set | 定义一个符号 | .set counter, 0 |
.short | 定义一个短整数常量 | .short 0x1234 |
.string | 定义一个字符串常量 | .string "Hello World!" |
.text | 指定代码段 | .text |
.thumb | 指定THUMB指令集 | .thumb |
.word | 定义一个字常量 | .word 0x12345678 |
过程调用标准
- R0 - R3
用于传入函数参数,其中R0用于函数返回值。(写到这里,突然感觉没必要详细描述了。因为本质都是一样的,想尽办法保护传入和传出参数。)
比如KEIL是如下处理方式:
传入参数当作局部变量,采用R4-Rx进行保护(当然了,这是取传参变量地址的的缘故,若取地址。则只能压栈了)。便于后续R0-R3作为被调用子函数的传入参数和不存在子函数的时候随便使用。
保护方式可能并不唯一。此处仅做示例记录。
void sub(int a,int b)
{
int c = 0;
int d = 0;
int e = 0;
int f = 0;
// d = sub_sub(a,b,c);
}
sub:
0x00002348 B570 PUSH {r4-r6,lr}
0x0000234A 4604 MOV r4, r0 ; 相当于看作局部变量处理
0x0000234C 460D MOV r5, r1 ;
0x0000234E 2000 MOVS r0, #0x00
0x00002350 2100 MOVS r1, #0x00
0x00002352 2200 MOVS r2, #0x00
0x00002354 2300 MOVS r3, #0x00
0x00002356 BD70 POP {r4-r6,pc}
- R4 - R11 用来存放函数的局部变量。被调用函数中使用了谁,则需要保护谁。
int add_test(void)
{
int a = 2;
int b = 3;
int c = 0;
c = add(a,b);
return c;
}
Cortex-M0+ 平台KEIL编译后对应的汇编如下:
add_test:
0x00002290 B570 PUSH {r4-r6,lr}
0x00002292 2402 MOVS r4,#0x02
0x00002294 2503 MOVS r5,#0x03
0x00002296 2600 MOVS r6,#0x00
0x00002298 4629 MOV r1,r5
0x0000229A 4620 MOV r0,r4
0x0000229C F7FFFF50 BL.W add (0x00002140)
0x000022A0 4606 MOV r6,r0
0x000022A2 4630 MOV r0,r6
0x000022A4 BD70 POP {r4-r6,pc}
R12 内部调用暂时寄存器IP
R13 (SP) 栈指针,SP中存放的值在推出被调用函数时,必须和进入时的值相同。
int add_test(void)
{
int a = 2;
int b = 3;
int c = 0;
int data[7] = {7,8,9,10,11};
c = add(a,b);
return c;
}
add_test:
0x000022C8 B570 PUSH {r4-r6,lr}
0x000022CA B088 SUB sp,sp,#0x20 ; 不理解为什么是0x20而不直接0x1C?
0x000022CC 2402 MOVS r4,#0x02
0x000022CE 2503 MOVS r5,#0x03
0x000022D0 2600 MOVS r6,#0x00
0x000022D2 221C MOVS r2,#0x1C
0x000022D4 4905 LDR r1,[pc,#20] ; @0x000022EC
0x000022D6 A801 ADD r0,sp,#0x04 ; 可能和keil的编译底层有关吧?
0x000022D8 F7FFFF38 BL.W __aeabi_memcpy4 (0x0000214C)
0x000022DC 4629 MOV r1,r5
0x000022DE 4620 MOV r0,r4
0x000022E0 F7FFFF2E BL.W add (0x00002140)
0x000022E4 4606 MOV r6,r0
0x000022E6 4630 MOV r0,r6
0x000022E8 B008 ADD sp,sp,#0x20
0x000022EA BD70 POP {r4-r6,pc}
0x000022EC 0000 DCW 0x0000
0x000022EE 4023 DCW 0x4023
- R14 (LR) 链接寄存器,用于存储函数返回地址。
若被调用函数中R14已经被压入栈中,则在被调用函数中可以将该寄存器用于其他用途。当然了,一般很少这么做。
- R15 (PC) 程序计数寄存器