电脑基础 · 2023年3月25日

ARM汇编寄存器和常用指令详解

文章目录

  • AAPCS关于ARM寄存器的定义
  • 寄存器
    • R0~R12 通用寄存器
    • R13-SP(Stack Pointer) 栈寄存器
    • R14-LR(Link Register) 链接寄存器
    • R15-PC(Program Counter) 程序计数器
  • 指令
    • ADD 加法指令
    • SUB 减法指令
    • MOV 数据搬移指令(复制)
    • LDR 将内存数据加载到寄存器
    • LDM (LDR增强版,将多个连续数据存入到一组寄存器中)
    • STM (将一组寄存器中的数据存入到栈中)
    • PUSH 压栈指令
    • POP 出栈指令
    • MRS
    • MSR
    • AND 与
    • BX 跳转
    • CBZ
    • SVC 软中断指令
  • 汇编代码示例

AAPCS关于ARM寄存器的定义

ARM汇编寄存器和常用指令详解

对于32位及其以下的ARM处理器来说,函数调用规则如下:

  1. 父函数与子函数的入口参数以此通过 R0~R3 这4个寄存器传递。 父函数在调用子函数前先将子函数入口参数存入 R0~R3 寄存器中,若只有一个入口参数则使用 R0 寄存器传递,若有2个入口参数则使用 R0 和 R1 寄存器传递,以此类推。。当超过4个参数时,其余的入口参数则以此压入当前栈通过栈传递。子函数运行时,其将根据自身参数个数从 R0~R3 或者栈中读取入口参数
  2. 子函数通过 R0 寄存器将函数返回值传递给父函数。子函数返回时,将返回值存入 R0寄存器,当返回到父函数时,父函数读取 R0 寄存器就可以获得子函数的返回值。
  3. AAPCS规定,发生函数调用前由父函数备份 R0~R3 寄存器中有用的数据,若没有有用的数据则可以不处理,然后才能调用子函数,以防止父函数保存在 R0~R3 寄存器中有用的数据在子函数使用这些寄存器时被破坏。因此,无论父函数是否通过 R0~R3 寄存器向子函数传递入口参数,子函数都可以直接使用 R0~R3 寄存器,无需考虑改写 R0~R3 寄存器会破坏父函数存储在它们中的数值,子函数返回时也无需恢复 R0~R3 寄存器中的数值。
  4. R4~R11 寄存器为普通的通用寄存器。AAPCS规定,发生函数调用时,父函数无需对这些寄存器进行备份处理,若子函数需要使用这些寄存器,则由子函数负责备份(需要使用哪个就备份哪个),以防止破坏父函数保存在 R4~R11 寄存器中的数据。子函数返回父函数前需先恢复 R4~R11 寄存器中的数值(使用了哪个就恢复哪个),恢复到父函数调用子函数这一时刻的数值,然后再返回到父函数。
  5. R12 寄存器在某些版本的编译下另有他用,在函数调用时需要备份,它的用法等同于 R0~R3 寄存器。
  6. R13 寄存器时栈寄存器(SP),用来保护栈的当前指针,函数存储在栈中的数据就是通过这个寄存器来寻址的。函数返回时需要保证 SP 指向 调用 该函数时的栈地址。
  7. R14 寄存器时链接寄存器(LR),用来保存函数的返回地址。父函数调用子函数时,父函数会将调用子函数指令的下一条指令地址存入到 LR 寄存器中,当子函数返回时只需要跳转到 LR 寄存器里的地址就会返回父函数继续执行。父函数调用子函数将子函数返回地址存入 LR 寄存器前, LR 寄存器中保存的可能是父函数返回其上一级函数的地址或其他有用的数据,因此需要先备份 LR 寄存器然后才能调用子函数。
  8. R15 寄存器时程序寄存器(PC),正在执行的指令地址就存储在 PC 寄存器中,更改 PC 寄存器的数值就会执行这个数值所对应的地址中的指令。

寄存器

ARM内核有 18 个寄存器,包括 R0~R12,2个R13,R14,R15和 XPSR 寄存器。
其中 R13 寄存器又叫 SP 寄存器, R14 寄存器又叫 LR 寄存器, R15 寄存器也叫 PC 寄存器。

R0~R12 通用寄存器

这些通用寄存器用来临时存放数据,供处理器运行程序时使用。

其中某些寄存器在程序调用时还会有其他专用功能,
比如说 R0~R3 寄存器在程序调用时可以用来传递函数参数和返回值,R12 寄存器在某些情况下可以保存子程序调用的中间值。

R13-SP(Stack Pointer) 栈寄存器

在ARM内核中有 2个 SP,分别是 MSP 和 PSP,根据模式不同使用的 SP 寄存器不同,不能同时存在。

R14-LR(Link Register) 链接寄存器

用来保存跳转后返回的地址。
当发生函数调用时,LR 寄存器中保存着函数返回后需要执行的指令地址。

R15-PC(Program Counter) 程序计数器

存放的是当前所执行指令所在的地址。
当在C语言中调用函数或者产生跳转时,实际上就是通过改变PC寄存器的值实现的。

指令

数据计算指令: ADD ADR SUB
数据搬移指令: MOV LDR LDM STM PUSH POP
状态寄存器操作指令: MRS MSR
逻辑计算指令: AND
跳转指令: BX CBZ
软中断指令: SVC

ADD 加法指令

SUB 减法指令

MOV 数据搬移指令(复制)

LDR 将内存数据加载到寄存器

指令格式有两种:
LDR 目的寄存器,=常量
LDR 目的寄存器,[源寄存器]

第一种格式,是将常量值存入目的寄存器
第二种格式,是将源寄存器中的数据指向的内存地址中的数据存入目的寄存器

例:

LDR R0,=globalVariate ;globalVariate表示全局变量,变量名对应它的地址
LDR R3,[R0]

对应 C语言:

R0 = &globalVariate
R3 = *R0

LDM (LDR增强版,将多个连续数据存入到一组寄存器中)

根据对栈指针不同的操作方式可以有4中划分:
满栈(Full):栈指针指向栈顶最后一个入栈的位置,此时栈指针指向的栈空间是满的。
空栈(Empty):栈指针指向栈顶将要入栈的位置,此时栈指针指向的栈空间是没用过的,是空的。
递减栈(Descending):向栈内存储数据时栈指针向着内存地址减少的方向移动。
递增栈(Ascending):向栈内存储数据时栈指针向着内存地址增加的方向移动。
综合栈的空满和增减特性,栈可分为FD ED FA EA 这4种类型。

汇编指令提供了4种对栈的操作方式,分别是DB(Decrement Before)、DA(Decrement After)、IB(Increment Before)和IA(Increment After)。

  • DB意为栈指针先减少再操作
  • DA意为栈指针先操作再减少
  • IB意为栈指针先增加再操作
  • IA意为栈指针先操作再增加
    这4种操作方式都可以与 LDM 指令组合,形成 LDMDB、LDMDA、LDMIB和LDMIA 指令

LDM经常在栈操作中使用,有2中指令格式,以LDMIA为例:
LDMIA 源寄存器,{一组目的寄存器} ;目的寄存器之间可以用 ','分开,也可以用'-'表示一个范围的寄存器
LDMIA 源寄存器!,{一组目的寄存器}

第一种指令格式,从源寄存器指定的站地址开始,将栈中的一组数据存入到目的寄存器组中;
第二种指令格式,除了完成第一种指令格式功能外,还将源寄存器操作后指向的栈地址保存到源寄存器中。

例:
LDMIA R14,{R0-R3,R12}
LDMIA R1!,{R4-R7}

对应 C语言:

/* No.1 */
R0 = *R14
R1 = *(R14 + 4)  //32位,4字节
R2 = *(R14 + 8)  //32位,4字节
R3 = *(R14 + 12)  //32位,4字节
R12 = *(R14 + 16)  //32位,4字节
//操作完成,R14寄存器的数值仍不变
/* No.2 */
R4 = *R1
R5 = *(R1 + 4)  //32位,4字节
R6 = *(R1 + 8)  //32位,4字节
R7 = *(R1 + 12)  //32位,4字节
R1 = R1 + 16  //32位,4字节
//操作完成,将R1寄存器的数值更新为当前指向的栈地址

STM (将一组寄存器中的数据存入到栈中)

STM功能正好和LDM功能相反,同样,STM也有4种操作方式:STMDB、STMDA、STMIB和STMIA,分别与LDM对应
指令格式:
STMIA 目的寄存器,{一组源寄存器}
STMIA 目的寄存器!,{一组源寄存器}

例:
STMIA R13,{R0}

PUSH 压栈指令

指令格式:
PUSH {一组寄存器}

PUSH指令其实就是如下指令的简写:
STMDB SP!,{一组寄存器} ;先减少后操作

POP 出栈指令

POP指令可以将数据从栈中弹出到寄存器中
指令格式:
POP {一组寄存器}

PUSH指令其实就是如下指令的简写:
LDMIA SP!,{一组寄存器} ;先操作后增加

MRS

将 XPSR寄存器中的数据保存到通用寄存器中,格式为:
MRS 寄存器,XPSR

MSR

将通用寄存器中的数据保存到 XPSR寄存器,格式为:
MSR XPSR,寄存器

AND 与

将源寄存器中的数据和目的寄存器中的数据进行与操作,并将结果存入到目的寄存器中
指令格式:
AND 目的寄存器,源寄存器

BX 跳转

BX指令除了可以跳转到目的寄存器指向的地址外,还可以改变处理器运行的指令集
指令格式:
BX 寄存器

CBZ

CBZ指令判断 R0 寄存器中的数值是否为0,如果是0,则跳转到 __BACKUP_REG地址,其中__BACKUP_REG是一个地址常数
指令格式:
CBZ R0,__BACKUP_REG
即:

if(0 == R0) {
    goto __BACKUP_REG
}

SVC 软中断指令

指令格式:
SVC 立即数

例:
SVC #0

这条指令将产生一次软中断,并将立即数0存入指令中,这个立即数可以作为软中断号使用,软中断服务程序可以通过这个立即数区分不同的软中断服务。

汇编代码示例

父函数向子函数传6个参数,子函数将其参数之和返回,父函数将返回值 +7后的值,当做自己的返回值返回。

TestFunc1

TestFunc1
    ;需使用R4,R5寄存器,为避免破坏R4,R5寄存器中的数据,将其压栈
    PUSH {R4 - R5}
    LDR R0, =1     ;相当于 R0 = 1
    LDR R1, =2
    LDR R2, =3     ;R0~R3寄存器作为接口寄存器,可直接使用
    LDR R3, =4
    LDR R4, =5
    LDR R5, =6
    ;R4,R5寄存器压栈,是因为它们是作为第5,6个入口参数,需要栈传递
    ;R14(LR)寄存器压栈,是因为TestFunc2的返回地址将被存入LR寄存器,防止 LR 寄存器中现有数据破坏
    PUSH {R4 - R5, R14}
    BL TestFunct2      ;跳转到 TestFunc2函数
    ADD R0, #7      ;TestFunc2函数的返回值保存在R0寄存器,将其 +7
    ;恢复进入TestFunt1函数现场
    ;将 SP指针向栈顶方向移动 8个字节,跳过原来存入栈中的2个入口参数
    ;此时 SP指针指向的栈中存放的是调用TestFunc2函数前存入的 LR寄存器
    ADD SP, #8
    POP {R14}      ;弹出 LR寄存器,将其恢复到进入 TestFunc1函数前的数值
    POP {R4 - R5}  ;弹出 R4,R5寄存器,将其恢复到进入 TestFunc1函数前的数值
    BX R14     ;函数执行完毕,此时 R14保存的是TestFunc1函数的返回地址

TestFunc2

TestFunc2
    ADD R0, R1
    ADD R0, R2
    ADD R0, R3
    POP {R1, R2}  ;从栈中取出第5,6参数,将其值赋给R1,R2寄存器
    ADD R0, R1
    ADD R0, R2
    SUB SP, #8   ;POP时移动栈指针,现在恢复到进入此函数之前栈指针指向
    BX R14

C语言原型

char TestFunc1() {
    return TestFunc2(1, 2, 3, 4, 5, 6) + 7;
}
char TestFunc2(char p1, char p2, char p3, char p4, char p5, char p6) {
    return p1 + p2 + p3 + p4 + p5 + p6;
}

本文内容为阅读《嵌入式操作系统内核调度-底层开发者手册》笔记,如有侵权,请联系删除。