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)名称描述
31N负号标志位。当最近一次算术或逻辑操作的结果为负数时,N=1;否则N=0。
30Z零标志位。当最近一次算术或逻辑操作的结果为零时,Z=1;否则Z=0。
29C进位/借位标志位。用于表示最近一次算术或逻辑操作是否产生了进位或借位。
28V溢出标志位。用于表示最近一次算术操作是否产生了溢出。
27Q粘性溢出标志位。在执行饱和运算后,用于表示是否发生了溢出。
26-8-保留位。这些位被保留为0,用于未来的扩展或特定实现。
7-0M模式位。用于表示当前处理器的模式,如用户模式、系统模式、中断模式等。

指令分类

  1. 数据处理指令:这类指令用于执行数据的算术和逻辑运算。包括数据传送指令(如MOV、MVN),算术逻辑运算指令(如ADD, SUM, AND),以及比较指令(如CMP、TST)等。这些指令直接对寄存器的内容进行操作,可以对寄存器中的数据进行各种计算、比较和修改。
  2. 跳转指令:跳转指令用于控制程序的流程,根据某些条件或者无条件地跳转到代码的其他部分执行。例如,B指令用于无条件跳转,而BEQ和BNE等指令则用于在满足特定条件(如相等或不等)时进行跳转。
  3. 程序状态寄存器(PSR)传输指令:这类指令用于访问和修改程序状态寄存器(如CPSR或SPSR),这些寄存器存储了处理器的状态信息,如条件标志、模式位等。
  4. Load/Store指令:这类指令用于从内存中加载数据到寄存器,或者将寄存器中的数据存储到内存中。例如,LDR指令用于加载数据,而STR指令则用于存储数据。
  5. 协处理器指令:协处理器指令用于与ARM处理器的协处理器进行交互,执行特定的数学运算或其他任务。
  6. 异常中断指令:这类指令用于处理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) 程序计数寄存器