LV08-01-ARM体系-03-ARM汇编基础
本文主要是ARM基础知识——ARM的汇编语言相关的基础知识相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
| Windows | windows11 |
| Ubuntu | Ubuntu16.04的64位版本 |
| VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
| SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
| 开发板 | 正点原子 i.MX6ULL Linux阿尔法开发板 |
| uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
| linux内核 | linux-4.15(NXP官方提供) |
| STM32开发板 | 正点原子战舰V3(STM32F103ZET6) |
| Keil(MDK) | Keil uVision V4.54.0.0 |
点击查看本文参考资料
为什么要学ARM汇编?
我们在学习 STM32的时候几乎没有用到过汇编,但是我们在进行嵌入式Linux开发的时候是绝对要掌握基本的 ARM 汇编,对于Cortex-A芯片来讲,大部分芯片在上电以后 C语言环境还没准备好,所以第一行程序肯定是汇编的,至于要写多少汇编程序,那就看我们能在哪一步把 C语言环境准备好。
所谓的 C 语言环境就是保证 C 语言能够正常运行。 C语言中的函数调用涉及到出栈入栈,出栈入栈就要对堆栈进行操作,所谓的堆栈其实就是一段内存,这段内存比较特殊,由 SP 指针访问, SP 指针指向栈顶。芯片一上电 SP 指针还没有初始化,所以 C 语言没法运行,对于有些芯片还需要初始化 DDR,因为芯片本身没有 RAM,或者内部 RAM不开放给用户使用,用户代码需要在DDR中运行,因此一开始要用汇编来初始化 DDR控制器。后面学习 Uboot和 Linux 内核的时候汇编是必须要会的。
官方文档?
我们在ARM的官网上可以找到很多的芯片手册以及编程指南,关于这篇笔记中的ARM指令,我们可以参考这篇文档:《ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition》的A4章节,我们可以在这一章节系统的了解到Cotex-A7的所有汇编指令。我使用的开发板使用的Cotex-A系列的ARM处理器,所以这份文档就可以了。
一、GNU汇编语法
1. 伪指令和伪操作
1.1 伪指令
伪指令(Pseudo Instruction)是用于对汇编过程进行控制的指令,该类指令并不是可执行指令,没有机器代码,只用于汇编过程中为汇编程序提供汇编信息。例如,提供如下信息:哪些是指令、哪些是数据及数据的字长、程序的起始地址和结束地址等。伪指令有2个特点:
(1)由于是伪“指令”,因而它只存在于汇编语言中。高级语言中不叫指令,叫语句;
(2)由于是“伪”指令,也即“假”指令,因而不是可执行指令,不会产生机器代码,不会占用ROM空间,只用于汇编过程中为汇编程序提供汇编信息。
1.2 伪操作
汇编语言程序语句除指令以外还可以由伪操作和宏指令组成。伪操作不像机器指令那样是在程序运行期间由CPU来执行的,它是在汇编程序对源程序汇编期间由汇编程序处理的操作,它们可以完成如数据定义、分配存储区、指示程序结束等功能。
2. 汇编语法格式
GNU汇编语法适用于所有的架构,并不是 ARM 独享的, GNU汇编由一系列的语句组成,每行一条语句,每条语句有三个可选部分:
1 | label: instruction @ comment |
【语句说明】
label:标号,表示地址位置,有些指令前面可能会有标号,这样就可以通过这个标号得到指令的地址,标号也可以用来表示数据地址。注意 label后面的:,任何以:结尾的标识符都会被识别为 一个标号。instruction:指令,也就是汇编指令或伪指令。@:表示后面的是注释,就跟C语言里面的“/*和*/一样,其实在GNU汇编文件中我们也可以使用/*和*/来注释。comment:就是注释的内容了。
【注意】
(1)ARM中的指令、伪指令、伪操作、寄存器名等可以全部使用大写,也可以全部使用小写,但是不能大小写混用。
(2)汇编指令每行结束不需要带;或者其他任何符号。
3. 预定义段
我们可以使用以下伪操作来定义一个段:
1 | .section .UserSection @定义一个 UserSection段 |
在汇编系统中预定义了一些段名:
| .text | 表示代码段。 |
| .data | 初始化的数据段。 |
| .bss | 未初始化的数据段。 |
| .rodata | 只读数据段。 |
4. 汇编的开始与结束
汇编程序的默认入口标号是 _start,不过我们也可以在链接脚本中使用 ENTRY来指明其它的入口点,一般还是使用 _start作为入口标号,下边就是一个完整的GNU汇编程序开始与结束的格式:
1 | .text @ 表示当前段为代码段 |
5. GNU函数语法
在汇编中同时也支持函数的定义使用,一般定义格式如下:
1 | 函数名 : |
【注意】GNU汇编函数返回语句不是必须的。
点击查看实例
1 | /* 未定义中断 */ |
二、数据处理指令
1. 数据处理指令格式
1.1 编码格式
1 | <opcode> {cond} {S} <Rd>,<Rn>,<shifter_operand> |
【格式说明】
opcode:指令助记符,如MOV等;cond:条件码助记符,如EQ(0000)、NE(0001))等;S:如果指令有S后缀,则该指令的操作会影响CPSR的值;Rd:目标寄存器;Rn:包含第一个源操作数的寄存器;shifter_operand: 表示第二个源操作数,可以为立即数。
【格式图示】在网上查询了下,一般来说ARM数据处理指令编码格式如下图:
【注意】对于ARM的Cotex-A7系列的指令编码格式可以看这篇文档《ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition》的A5章节
1.2 条件码
1.2.1 条件码种类
当处理器工作在ARM状态时,几乎所有的指令均根据CPSR中条件码的状态和指令的条件域有条件的执行。当指令的执行条件满足时,指令被执行,否则指令被忽略。每一条ARM指令包含4位的条件码,位于指令的最高4位[31:28]。
条件码共有16种,每种条件码可用两个字符表示,这两个字符可以添加在指令助记符的后面和指令同时使用。例如,跳转指令B可以加上后缀EQ变为BEQ表示相等则跳转,即当CPSR中的Z标志置位时发生跳转。
| 助记符 | 含义 | 条件码 | CPSR变化标志 |
| EQ | 相等 | 0000 | Z==1 |
| NE | 不相等 | 0001 | Z==0 |
| CS | 无符号大于或等于 | 0010 | C==1 |
| CC | 无符号小于 | 0011 | C==0 |
| MI | 负数 | 0100 | N==1 |
| PL | 正数或零 | 0101 | N==0 |
| VS | 溢出 | 0110 | V==1 |
| VC | 未溢出 | 0111 | V==0 |
| HI | 无符号大于 | 1000 | C==1 Z==0 |
| LS | 无符号小于或等于 | 1001 | C==0 Z==1 |
| GE | 带符号大于或等于 | 1010 | N==V |
| LT | 带符号小于 | 1011 | N!=V |
| GT | 带符号大于 | 1100 | Z==0 N==V |
| LE | 带符号小于或等于 | 1101 | Z==1 N!=V |
| AL | 无条件执行 | 1110 | 忽略 |
1.2.2 CMP指令
条件码的使用是需要有CPSR寄存器相关位变化的,这个时候我们就需要使用CMP指令来把一个寄存器的数据和另一个寄存器的数据或一个立即数进行比较,同时更新CPSR中条件标志位的值。CMP指令的格式如下:
1 | CMP Rd, Rn @ 相当于 Rd - Rn |
实际上,CMP指令所做的事情就是将Rd和Rn相减,这实质上就是一个减法指令,但是运算的结果不会存入寄存器,只会通过CPSR的相关标志位表示出来。
1.2.3 实例说明
后边我们将学习到的大多数指令都可以加上条件码,例如SUB、B等。这里放一个实例,具体的其他指令后边会学习到:
1 | .text @ 表示当前段为代码段 |
这段汇编表示的含义用C语言表示出来就是下边的意思:
1 |
|
1.3 立即数
在学习指令之前,需要了解一下立即数。关于立即数,百度百科是这样解释的:立即数_通常是指在立即寻址方式指令中给出的数。可以是8位、16位或32位,该数值紧跟在操作码之后。
其实看完后不是很理解,后来查了其他的一些资料,发现立即数可以和汇编指令存储在一起,在汇编之后会和指令共同构成机器码。而且并不是随便什么数都可以是立即数,例如0x4000000就是一个立即数,但是0x12345678就不是立即数,当我们直接使用MOV搬移非立即数时,会报以下错误:
1 | error: immediate expression requires a # prefix -- `mov R0,0x12345678' |
在ARM汇编指令中,MOV指令将会将常用到立即数,立即数有如下语法格式:
1 | <immediate> = immed_8 循环右移 (2 * rotate_imm) |
【格式说明】
immediate:表示有效的立即数;immed_8:表示一个普通的8位常数;rotate_imm:表示循环右移的位数,和2相乘之后就变成了偶数次位移(偶数表示的时候可以用2n,所以这里也很好理解)。
【说明】上边的格式就决定了立即数必须可以由一个8位常数循环右移偶数位得到。
为什么要这样?
我们可以看一下shifter_operand所占的位数是12位。要用一个12位的编码来表示任意的32位数是绝对不可能的。但是又要用12位的编码来表示32位数,这又该怎么办呢?那就只有在表示数的数量上做限制。通过编码来实现用12位的编码来表示32位数。于是在12位的shifter_operand中:8位存数据,4位存移位的次数,这样就导致了立即数有了限制。
点击查看实例
0x104
1 | immediate : 0x104 @ 0000 0000 0000 0000 0000 0001 0000 0100 |
2. 数据搬移指令
2.1 MOV
MOV指令用于将数据从一个寄存器拷贝到另外一个寄存器,或者将一个立即数传递到寄存器里面,使用示例如下:
1 | MOV R1, R0 |
【注意】这里我们通过MOV指令,只能将一个立即数存入寄存器,那如果想存任意32位数据据呢?里边肯定有很多都不是立即数,这个时候我们可以用LDR指令,后边会学习到。
2.2 MVN
MVN指令用于将一个数据按位取反后从一个寄存器拷贝到年另一个寄存器,或者将一个立即数按位取反后传递到寄存器中:
1 | MVN R4, R0 |
3. 数据运算指令
一般数据运算指令都符合以下格式:
1 | 操作码 目标寄存器, 第一操作寄存器,第二操作数 |
- 操作码:就是表示执行哪种操作;
- 目标寄存器:用于存储目标寄存器;
- 第一操作寄存器 :存储第一个参数运算的数据,这里不可以是一个立即数,只能写寄存器;
- 第二操作数:第二个参与运算的数据,可以是立即数,也可以是寄存器。
3.1 加法运算
3.1.1 ADD
| 指令 | 计算公式 |
| ADD Rd, Rn, Rm | Rd = Rn + Rm |
| ADD Rd, Rn, #immediate | Rd = Rn + #immediate |
【指令说明】ADD是不带进位的加法指令。
【使用实例】
1 | MOV R1, #1 |
3.1.2 ADC
| 指令 | 计算公式 |
| ADC Rd, Rn, Rm | Rd = Rn + Rm + 进位 |
| ADC Rd, Rn, #immediate | Rd = Rn + #immediate + 进位 |
【指令说明】ADC是带进位的加法指令。
【注意】进行带进位的加法指令时,若有用到ADD,则需要加上S后缀,这样才会使CPSR进位标志C产生变化,才可以真正加上进位。
【使用实例】
1 | @ 6.1第一个数 0000 0002 FFFF FFFF |
3.1.3 使用实例
【题目】使用ARM寄存器完成两个128位数的加法运算
【解答】
1 | @ 1.1第一个数 0000 0002 FFFF FFFF 0000 0002 FFFF FFFF |
3.2 减法运算
3.2.1 SUB
| 指令 | 计算公式 |
| SUB Rd, Rn, Rm | Rd = Rn - Rm |
| SUB Rd, Rn, #immediate | Rd = Rn - #immediate |
| SUB Rd, #immediate | Rd = Rd - #immediate |
| SUB Rd, Rn | Rd = Rd - Rn |
【指令说明】SUB是不带借位的减法指令(前减后)。
【使用实例】
1 | MOV R1, #1 |
3.2.2 RSB
| 指令 | 计算公式 |
| RSB Rd, Rn, Rm | Rd = Rm - Rn |
| RSB Rd, Rn, #immediate | Rd = #immediate - Rn |
【指令说明】RSB是逆向减法指令(后减前)。
3.1.2 SUC
| 指令 | 计算公式 |
| SBC Rd, Rn, #immediate | Rd = Rn - #immediate – 借位 |
| SBC Rd, Rn, Rm | Rd = Rn - Rm – 借位 |
【指令说明】SUBC是带借位的减法指令。
【注意】进行带借位的减法指令时,若有用到SUB,则需要加上S后缀,这样才会使CPSR进位标志C产生变化,才可以真正实现借位。
【使用实例】
1 | @ 8.带借位的减法 |
3.4 乘法运算
3.4.1 MUL
| 指令 | 计算公式 |
| MUL Rd, Rn, Rm | Rd = Rn * Rm |
【指令说明】MUL是(32位)乘法指令。
【使用实例】
1 | MOV R1, #1 |
3.5 除法运算
【注意】ARM不支持除法运算。
3.5.1 UDIV
| 指令 | 计算公式 |
| UDIV Rd, Rn, Rm | Rd = Rn / Rm |
【指令说明】UDIV是无符号除法指令。
3.5.2 SDIV
| 指令 | 计算公式 |
| SDIV Rd, Rn, Rm | Rd = Rn / Rm |
【指令说明】UDIV是有符号除法指令。
4. 逻辑运算指令
4.1按位与运算
4.1.1AND
| 指令 | 计算公式 |
| AND Rd, Rn | Rd = Rd & Rn |
| AND Rd, Rn, #immediate | Rd = Rn & #immediate |
| AND Rd, Rn, Rm | Rd = Rn & Rm |
【指令说明】AND是按位与运算指令。
【使用实例】
1 | MOV R1, #1 @ R1 = 0000 0001 |
4.2 按位或运算
4.2.1 ORR
| 指令 | 计算公式 |
| ORR Rd, Rn | Rd = Rd | Rn |
| ORR Rd, Rn, #immediate | Rd = Rn | #immediate |
| ORR Rd, Rn, Rm | Rd = Rn | Rm |
【指令说明】ORR是按位或运算指令。
【使用实例】
1 | MOV R1, #1 @ R1 = 0000 0001 |
4.3 位清除
4.3.1 BIC
| 指令 | 计算公式 |
| BIC Rd, Rn | Rd = Rd & (~Rn) |
| BIC Rd, Rn, #immediate | Rd = Rn & (~#immediate) |
| BIC Rd, Rn, Rm | Rd = Rn & (~Rm) |
【指令说明】BIC是位清除指令,第二操作数中哪一位为1,就将第一操作数哪一位清0,然后将结果存入目标寄存器。
【使用实例】
1 | MOV R1, #0xAA @ R1 = 1010 1010 |
4.4 按位异或
4.4.1 EOR
| 指令 | 计算公式 |
| EOR Rd, Rn | Rd = Rd ^ Rn |
| EOR Rd, Rn, #immediate | Rd = Rn ^ #immediate |
| EOR Rd, Rn, Rm | Rd = Rn ^ Rm |
【指令说明】EOR是按位异或指令。
【使用实例】
1 | MOV R1, #1 @ R1 = 0000 0001 |
4.5 左移
4.5.1 LSL
| 指令 | 计算公式 |
| LSL Rd, Rn, Rm | Rd = Rn << Rm(左移Rm位) |
【指令说明】LSL是左移指令。
【使用实例】
1 | MOV R1, #1 @ R1 = 0000 0001 |
4.6 右移
4.6.1 LSR
| 指令 | 计算公式 |
| LSR Rd, Rn, Rm | Rd = Rn >> Rm(右移Rm位) |
【指令说明】LSR是右移指令。
【使用实例】
1 | MOV R1, #1 @ R1 = 0000 0001 |
5. 三种相关寻址实例
这部分主要是介绍数据处理指令相关的ARM指令寻址方式。
5.1 立即数寻址
立即数寻址,也叫立即寻址,寻找数据时直接从机器码中获取。
1 | MOV R0, #1 |
5.2 寄存器寻址
1 | ADD R2, R1, R0 |
5.3 寄存器移位寻址
1 | MOV R3, R2, LSL #1 @ 将 R2 左移1位后的结果存入 R3 |
三、跳转指令
1. 直接修改PC
我们可以直接修改程序计数器PC的值来实现程序的跳转。
1 | MOV PC, #0x10 |
像这样,我们便可以跳转到相应的地址去执行相应的指令了,但是,有一个问题就是我们在写代码的时候并不知道跳转的目标地址是多少。
2. B
| 指令 | 说明 |
| B <label> | 跳转到label,如果跳转范围超过了 +/-2KB,可以指定 B.W <label>使用 32位版本的跳转指令, 这样可以得到较大范围的跳转 |
【指令说明】B是不带返回的跳转指令,实现程序的跳转,本质是将PC寄存器的值修改成跳转标号下第一条指令的地址。如果要调用的函数不会再返回到原来的执行处,那就可以用 B指令。
【使用实例】
1 | MAIN2: |
3. BL
| 指令 | 说明 |
| BL <label> | 跳转到标号地址,并将返回地址保存在LR中。 |
【指令说明】BL是带返回的跳转指令,实现程序的跳转,本质是将PC寄存器的值修改成跳转标号下第一条指令的地址,同时将跳转指令的下一条指令的地址保存到LR寄存器中。需要注意的是,想要返回到跳转之前的位置,还需要将LR寄存器的值给到PC寄存器。
【使用实例】
1 | MAIN3: |
4. BX
| 指令 | 说明 |
| BX Rm | 带状态切换的跳转指令,若 Rm 的 bit[0] 为1,切换到 Thumb 指令执行;若 Rm 的 bit[0] 为0,切换到 ARM 指令执行。 |
【指令说明】BX是带状态切换的跳转指令,此条指令与B一样,是不带返回的。
【使用实例】
1 | BX R0 @ 跳转到R0寄存器中存储的地址,如果R0[0]=1,则进入Thumb状态。 |
5. BLX
| 指令 | 说明 |
| BLX Rm | 结合 BX和 BL的特点,跳转到 Rm指定的地址,并将返回地址保存在 LR 中,并切换指令集。 |
| BLX <label> |
【指令说明】BLX是带状态切换和带返回的跳转指令。
【注意】
(1)BLX <label>无论何种情况,始终会更改处理器的状态;BLX Rm 可从 Rm 的bit[0]推算出目标状态。
(2)如果 Rm 的bit[0] 为 0,则处理器的状态会更改为(或保持在)ARM 状态;如果 Rm 的bit [0] 为 1,则处理器的状态会更改为(或保持在)Thumb 状态。
6. 使用实例
【题目】使用汇编计算 1 + 2 + 3 + … + 98 + 99 + 100
【解答】
1 | @ ARM指令条件码实现求100以内的正整数之和 |
四、Load/Store指令
ARM不能直接访问存储器,当我们需要使用存储器来存储数据,或者从存储器读取数据的时候,就需要使用内存访问指令来实现对内存的读写。
1. 单个寄存器内存访问
1.1 STR
| 指令 | 说明 |
| STR Rn, [Rd, #offset] | 将寄存器Rn中的4个字节数据写入到存储器中的 Rd + offset位置。 |
| STRB Rn, [Rd, #offset] | 将寄存器Rn中的1个字节数据写入到存储器中的 Rd + offset位置。 |
| STRH Rn, [Rd, #offset] | 将寄存器Rn中的2个字节数据写入到存储器中的 Rd + offset位置。 |
【注意】
(1)#offset不写的时候,将直接从Rd位置开始存储。
(2)对于小端模式的ARM处理器来说,写入1字节或者2字节的时候,都是从低字节开始写的。
【使用实例】(下边的例子提前用到了LDR,后边在说明)
1 | MOV R0, #0x40000000 @ 要写入的内存地址 |
1.2 LDR
1.2.1 用法一:读取数据
| 指令 | 说明 |
| LDR Rd, [Rn, #offset] | 从存储器地址为 Rd + offset 的位置读取4个字节数据到Rd寄存器。 |
| LDRB Rd, [Rn, #offset] | 从存储器地址为 Rd + offset 的位置读取1个字节数据到Rd寄存器。 |
| LDRH Rd, [Rn, #offset] | 从存储器地址为 Rd + offset 的位置读取2个字节数据到Rd寄存器。 |
【注意】
(1)#offset不写的时候,将直接从Rd位置开始读取。
(2)对于小端模式的ARM处理器来说,读取1字节或者2字节的时候,也是从低字节开始读的。
1.2.2 用法二:伪指令
| 指令 | 说明 |
| LDR Rd, = #data | 此指令并非ARM指令,而是一条伪指令(后边会再学习),它会将data存储到Rd寄存器,这时候它与MOV很像,只是MOV指令后会有立即数的限制,而LDR不会。 |
| LDR Rd, <label> | 把label当做地址,然后将label指向的地址中的值加载到寄存器Rd中。 |
| LDR Rd, = <label> | 把label的值加载到寄存器Rd中。 |
【指令说明】LDR在此处用作伪指令用途。
1.3 两种相关寻址方式实例
1.3.1 寄存器间接寻址
1 | LDR R4, = 0x40000000 |
1.3.2 寄存器基址变址寻址
1 | LDR R4, = 0x40000000 |
2. 多寄存器内存访问
2.1 STM
| 指令 | 说明 |
| STM {cond} Rd{!}, {reglist}{^} | 将寄存器列表reglist中的所有数据写入到 Rd 所指向的存储器的地址中。 |
【指令说明】
STM:表示执行将多个寄存器数据写入存储器操作,是一种数据块传输指令;cond:表示条件码,后边学习到寻址方式的时候会用到;
点击查看条件码
| IA | Increase After,每次传送后地址加4,其中的寄存器从左到右执行,例如:STMIA R0,{R1,LR} 先存R1,再存LR |
| IB | Increase Before,每次传送前地址加4,其中的寄存器从左到右执行,例如:STMIA R0,{R1,LR} 先存R1,再存LR |
| DA | Decrease After,每次传送后地址减4,其中的寄存器从右到左执行,例如:STMDA R0,{R1,LR} 先存LR,再存R1 |
| DB | Decrease Before,每次传送前地址减4,其中的寄存器从右到左执行,例如:STMDA R0,{R1,LR} 先存LR,再存R1 |
| FD | 满递减堆栈 (每次传送前地址减4) |
| FA | 满递增堆栈 (每次传送后地址减4) |
| ED | 空递减堆栈 (每次传送前地址加4) |
| EA | 空递增堆栈 (每次传送后地址加4) |
Rd:基址寄存器,表示要将寄存器列表中的数据写入到存储器的目标地址;!:可选后缀,当数据传送完毕之后,将最后的地址写入到基址寄存器(Rn)中reglist:表示需要传输的寄存器的列表,注意要使用花括号{}括起来,当寄存器连续时,可以使用-连接,如R1-R4,当寄存器不连续时,寄存器列表之间用逗号,分隔开,并且寄存器编号顺序不会影响存储到存储器的顺序(小端模式下的ARM中,寄存器编号小的存储到低地址)。^:可选后缀,当指令为LDM且寄存器列表中包含R15,选用该后缀时表示:除了正常的数据传送外,还将SPSR复制到CPSR中。同时,该后缀还表示传入或传出的是用户模式下的寄存器,而不是当前模式下的寄存器。
【注意】不按顺序写寄存器列表的话,可能会有如下警告:
1 | warning: register range not in ascending order |
【使用实例】
1 | LDR R0, = 0x40000000 |
2.2 LDM
| 指令 | 说明 |
| LDM {cond} Rd{!}, {reglist}{^} | 将 Rd 所指向的存储器的地址中的数据依次读取到寄存器列表reglist。 |
LDM:表示执行将存储器中的多个数据读取到多个寄存器操作,是一种数据块传输指令;cond:表示条件码,后边学习到寻址方式的时候会用到;
点击查看条件码
| IA | Increase After,每次传送后地址加4,其中的寄存器从左到右执行,例如:STMIA R0,{R1,LR} 先存R1,再存LR |
| IB | Increase Before,每次传送前地址加4,其中的寄存器从左到右执行,例如:STMIA R0,{R1,LR} 先存R1,再存LR |
| DA | Decrease After,每次传送后地址减4,其中的寄存器从右到左执行,例如:STMDA R0,{R1,LR} 先存LR,再存R1 |
| DB | Decrease Before,每次传送前地址减4,其中的寄存器从右到左执行,例如:STMDA R0,{R1,LR} 先存LR,再存R1 |
| FD | 满递减堆栈 (每次传送前地址减4) |
| FA | 满递增堆栈 (每次传送后地址减4) |
| ED | 空递减堆栈 (每次传送前地址加4) |
| EA | 空递增堆栈 (每次传送后地址加4) |
Rd:基址寄存器,表示要将寄存器列表中的数据写入到存储器的目标地址;!:可选后缀,当数据传送完毕之后,将最后的地址写入到基址寄存器(Rn)中reglist:表示需要传输的寄存器的列表,注意要使用花括号{}括起来,当寄存器连续时,可以使用-连接,如R1-R4,当寄存器不连续时,寄存器列表之间用逗号,分隔开,并且寄存器编号顺序不会影响从存储器读取到寄存器的顺序(小端模式下的ARM中,低地址中的数据存储到小编号的寄存器中)。^:可选后缀,当指令为LDM且寄存器列表中包含R15,选用该后缀时表示:除了正常的数据传送外,还将SPSR复制到CPSR中。同时,该后缀还表示传入或传出的是用户模式下的寄存器,而不是当前模式下的寄存器。
【注意】不按顺序写寄存器列表的话,可能会有如下警告:
1 | warning: register range not in ascending order |
【使用实例】
1 | LDR R0, = 0x40000000 |
2.3 一种相关寻址方式实例
2.3.1 多寄存器寻址
1 | LDR R1, = 0x11112222 |
3. 栈
3.1 栈的概念
栈的本质就是一段内存,程序运行时用于保存一些临时数据,如局部变量、函数的参数、返回值、以及程序跳转时需要保护的寄存器等。
3.2 栈的分类
先来了解几个概念:
| 增栈 | 压栈时栈指针越来越大,出栈时栈指针越来越小 |
| 减栈 | 压栈时栈指针越来越大,出栈时栈指针越来越小 |
| 满栈 | 栈指针指向最后一次压入到栈中的数据,压栈时需要先移动栈指针到相邻位置然后再压栈 |
| 空栈 | 栈指针指向最后一次压入到栈中的数据的相邻位置,压栈时可直接压栈,之后需要将栈指针移动到相邻位置 |
这样的话,栈其实可以分为四种:空增(EA)、空减(ED)、满增(FA)、满减(FD)。在ARM处理器一般使用满减栈。各种栈的实现其实都是通过STM/LDM两个指令来实现的,只是不同条件码对应不同类型的栈。对于ARM中常用的满减栈,我们使用的指令如下:
1 | STMDB R0!, {R1-R4} |
3.3 栈的使用
3.3.1 叶子函数
叶子函数就是不再调用其他函数的函数。
1 | .text @ 表示当前段为代码段 |
3.3.2 非叶子函数
非叶子函数就是在函数中还调用了其他的函数。
1 | @ 栈的应用2 |
五、状态寄存器传送指令
前边我们学习了CPSR寄存器,这是ARM的控制寄存器,我们是无法使用MOV指令来修改CPSR的各个位的,我们只能通过状态寄存器传送指令来读写CPSR寄存器。C语言写的语句都不会被编译成状态寄存器传送指令,一般由操作系统内部使用。
1. MRS
| 指令 | 说明 |
| MRS Rd, CPSR | CPSR中的数据读取到Rd寄存器。 |
【使用实例】
1 | MRS R0, CPSR |
2. MSR
| 指令 | 说明 |
| MSR CPSR, Rn | 将Rn中的数据写入CPSR寄存器。 |
| MSR CPSR, #immediate | 将 immediate 写入CPSR寄存器。 |
【使用实例】
1 | MRS R0, CPSR |
六、软中断指令
有时候我们需要从用户模式下切换工作模式,但是我们知道在用户模式下是无法切换工作模式的,而软中断异常的产生可以进入SVC模式,所以我们在需要修改ARM工作模式的时候就可以发出一个软中断。
1. SWI
| 指令 | 说明 |
| SWI{cond} immed_24 | 产生一个软中断异常。 |
SWI指令后面的24立即数是干什么用的呢?
用户程序通过SWI指令切换到特权模式,进入软中断处理程序,但是软中断处理程序不知道用户程序到底想要做什么,SWI指令后面的24位立即数用来做用户程序和软中断处理程序之间的信号。通过该软中断立即数来区分用户不同操作,执行不同内核函数。
【使用实例】
1 | .text @ 表示当前段为代码段 |
七、协处理器指令
重点目前不在这里,这里就简单了解一下。
1. 协处理器简介
协处理器是一种芯片,用于减轻系统微处理器的特定处理任务。例如,数学协处理器可以控制数字处理;图形协处理器可以处理视频绘制。协处理器可以通过一组专门的、提供load-store类型接口的ARM指令来访问。例如协处理器15(CP15),ARM处理器使用协处理器15的寄存器来控制cache、TCM和存储器管理。
ARM 微处理器可支持多达 16 个协处理器,用于各种协处理操作,在程序执行的过程中,每个协处理器只执行针对自身的协处理指令,忽略 ARM 处理器和其他协处理器的指令。
ARM 的协处理器指令主要用于:ARM 处理器初始化,ARM 协处理器的数据处理操作,以及在ARM 处理器的寄存器和协处理器的寄存器之间传送数据,和在 ARM 协处理器的寄存器和存储器之间传送数据。
2. 协处理器指令简介
2.1 协处理器数据运算指令
2.1.1 CDP
一般格式如下:
1 | CDP{条件} 协处理器编码, 协处理器操作码1, 目的寄存器, 源寄存器1, 源寄存器2, 协处理器操作码2 |
【指令说明】CDP 指令用于ARM 处理器通知ARM协处理器执行特定的操作,若协处理器不能成功完成特定的操作,则产生未定义指令异常。其中协处理器操作码1 和协处理器操作码2 为协处理器将要执行的操作,目的寄存器和源寄存器均为协处理器的寄存器,指令不涉及ARM 处理器的寄存器和存储器。
【使用实例】
1 | CDP P3, 2, C12, C10, C3, 4 @ 该指令完成协处理器 P3 的初始化 |
2.2 协处理器存储器访问指令
2.2.1 LDC
一般格式如下:
1 | LDC{条件}{L} 协处理器编码, 目的寄存器,[源(ARM)寄存器] @ ARM处理器->协处理器 |
【指令说明】LDC 指令用于将源寄存器所指向的存储器中的字数据传送到目的寄存器中,若协处理器不能成功完成传送操作,则产生未定义指令异常。其中,{L}选项表示指令为长读取操作,如用于双精度数据的传输。
【注意】[]中的为ARM处理器中的寄存器。
【使用实例】
1 | LDC P3, C4, [R0] @ 将 ARM 处理器的寄存器 R0 所指向的存储器中的字数据传送到协处理器 P3 的寄存器 C4 中 |
2.2.2 STC
一般格式如下:
1 | STC{条件}{L} 协处理器编码, 源寄存器, [目的(ARM)寄存器] @ 协处理器->ARM处理器 |
【指令说明】STC 指令用于将源寄存器中的字数据传送到目的寄存器所指向的存储器中,若协处理器不能成功完成传送操作,则发生未定义指令反常。其中,{L}选项表明指令为长读取操作,如用于双精度数据的传输。
【注意】[]中的为ARM处理器中的寄存器。
【使用实例】
1 | STC P3, C4, [R0] @ 将协处理器 P3 的寄存器 C4 中的字数据传送到 ARM 处理器的寄存器 R0 所指向的存储器中 |
2.3 协处理器寄存器访问指令
在基于ARM的嵌入式应用系统中,存储系统的操作通常是由协处理器CP15完成的。CP15包含16个32位的寄存器,其编号为0~15。而访问CP15寄存器的指令主要是MCR和MRC这两个指令。
2.3.1 MRC
一般格式如下:
1 | MRC{条件} 协处理器编码, 协处理器操作码1, 目的寄存器, 源寄存器1, 源寄存器2, 协处理器操作码2 @ 读协处理器寄存器 |
【指令说明】MRC 指令用于将协处理器寄存器中的数据传送到ARM 处理器寄存器中,若协处理器不能成功完成操作,则产生未定义指令异常。其中协处理器操作码1 和协处理器操作码2为协处理器将要执行的操作,目的寄存器为ARM 处理器的寄存器,源寄存器1 和源寄存器2 均为协处理器的寄存器。
【参数说明】
cond:为指令执行的条件码,当cond忽略时指令为无条件执行。Opcode_1:协处理器的特定操作码,对于CP15寄存器来说,opcode1=0。Rd:作为源寄存器的ARM寄存器,将协处理器寄存器的值传送到该寄存器里面 ,通常为R0。CRn:作为目标寄存器的协处理器寄存器,其编号是C0 ~ C15。CRm:协处理器中附加的目标寄存器或源操作数寄存器。如果不需要设置附加信息,将CRm设置为c0,否 则结果未知。Opcode_2:可选的协处理器特定操作码。(用来区分同一个编号的不同物理寄存器,当不需要提供附加信息时,指定为0)。
【使用实例】
1 | MRC P3, 3, R0, C4, C5, 6 @ 将协处理器 P3 的寄存器 C4 和 C5 中的数据传送到 ARM 处理器寄存器 R0 中 |
2.3.2 MCR
一般格式如下:
1 | MCR{条件} 协处理器编码, 协处理器操作码1, 源寄存器, 目的寄存器1, 目的寄存器2, 协处理器操作码2 @ 写协处理器寄存器 |
【指令说明】MCR 指令用于将 ARM 处理器寄存器中的数据传送到协处理器寄存器中,若协处理器不能成功完成操作,则产生未定义指令异常。其中协处理器操作码1 和协处理器操作码 2 为协处理器将要执行的操作,源寄存器为 ARM 处理器的寄存器,目的寄存器1和目的寄存器 2 均为协处理器的寄存器。
【参数说明】
cond:为指令执行的条件码,当cond忽略时指令为无条件执行。Opcode_1:协处理器的特定操作码,对于CP15寄存器来说,opcode1=0。Rd:作为源寄存器的ARM寄存器,其值将被传送到协处理器寄存器中,通常为R0。CRn:作为目标寄存器的协处理器寄存器,其编号是C0 ~ C15。CRm:协处理器中附加的目标寄存器或源操作数寄存器。如果不需要设置附加信息,将CRm设置为c0,否 则结果未知。Opcode_2:可选的协处理器特定操作码。(用来区分同一个编号的不同物理寄存器,当不需要提供附加信息时,指定为0)。
【使用实例】
1 | MCR P3, 3, R0, C4, C5, 6 @ 将 ARM 处理器寄存器 R0 中的数据传送到协处理器 P3 的寄存器 C4 和 C5 中 |
八、伪指令
1. NOP
| 指令 | 说明 |
| NOP | 空操作 |
2. LDR
当我们使用LDR指令来将一个数据存储到寄存器时,它就属于一个伪指令了。
| 指令 | 说明 |
| LDR Rd, = #data | 此指令并非ARM指令,而是一条伪指令(后边会再学习),它会将data存储到Rd寄存器,这时候它与MOV很像,只是MOV指令后会有立即数的限制,而LDR不会。 |
| LDR Rd, <label> | 把label当做地址,然后将label指向的地址中的值加载到寄存器Rd中。 |
| LDR Rd, = <label> | 把label的值加载到寄存器Rd中。 |
【使用实例】
1 | LDR R1, = 0x12345678 @ 这是一条伪指令,可以将一个任意的32位数据放到寄存器 |
九、伪操作
前边已经简单了解了伪操作的概念,这里我们来学习一下常见的伪操作。需要注意的是,GNU中的伪操作一般都以.开头。
1. .byte
定义单字节数据。例如,
1 | .byte 0x12 |
2. .short
定义双字节数据。例如,
1 | .short 0x1234 |
3. .long
定义四字节数据。例如,
1 | .byte 0x12345678 |
4. .equ
相当于C语言中的宏定义,它不会生成机器码,该语句的语法格式如下:
1 | .equ 变量名, 表达式 |
【使用实例】
1 | .equ DATA, 0x12 @ 表示 num = 0x12。 |
5. .align
数据字节对齐方式,语法格式为:
1 | .align n @ n表示对齐的字节数 |
【使用实例】
1 | .align 4 @ 4字节对齐 |
6. .end
表示源汇编文件结束,语法格式为:
1 | .end @ 表示源文件结束 |
7. .global
定义一个全局符号,语法格式为:
1 | .global symbol @ 声明 symbol 为全局符号 |
【使用实例】
1 | .global _start @ 声明_start为全局符号 |
8. .local
定义一个局部符号,只能在当前文件使用,语法格式为:
1 | .local symbol @ 声明 symbol 为局部符号,只能在当前文件使用 n @ n表示对齐的字节数 |
9. .weak
定义一个局部符号,只能在当前文件使用,语法格式为:
1 | .weak symbol @ 弱化一个符号 |
【使用实例】
1 | .weak main |
10. .word
在当前地址空间申请一个字的空间并初始化,语法格式为:
1 | .word value |
【使用实例】
1 | .word 0xFFFFFFFF |
11. .space
在当前地址空间申请多个字节的空间并初始化,语法格式为:
1 | .space n, value @ 在当前地址空间申请 n 个字节的空间并初始化为value |
【使用实例】
1 | .space 12, 0x12.word 0xFFFFFFFF |
12. .if
条件编译,这与C语言中的条件编译效果一样,语法格式为:
1 | .if <expression> @ 0 or 1 |
【使用实例】
1 | .if 0 |
13. .macro
类似于C语言中的定义函数,语法格式为:
1 | .macro name |
【使用实例】
1 | .macro FUNC |
14. .rept
此伪操作会生成多次相同的机器码,语法格式为:
1 | .rept num |
【使用实例】
1 | .rept 3 @ 编译3次,下边两条将会生成6次机器码 |
十、C与汇编混合编程
1. 汇编中调用C
我们可以在汇编语言中调用(跳转)C语言,需要注意的是,在汇编中C语言中的函数将会被当做标号来处理。
1 | .text @ 表示当前段为代码段 |
1 | void func_c(void) |
2. C中调用汇编
我们可以在C语言中调用(跳转)汇编,需要注意的是,在C语言中,汇编的函数被当做函数处理。
1 | .text @ 表示当前段为代码段 |
1 | void func_c(void) |
3. C内嵌汇编
我们可以在C语言中直接执行汇编指令,使用的格式为:
1 | asm("ARM 汇编指令\n"); |
1 | .text @ 表示当前段为代码段 |
1 | void func_c(void) |
4. ATPCS协议
4.1 ATPCS简介
ATPCS,即ARM-Thumb Procedure Call Standard,翻译过来就是ARM-Thumb过程调用标准。为了使单独编译的C语言程序和汇编程序之间能够相互调用,必须为子程序之间的调用规定一定的规则。ATPCS就是ARM程序和THUMB程序中子程序调用的基本规则。
4.2 ATPCS主要内容
4.2.1 寄存器使用规则
R15:程序计数器,只能用于存储程序的指针,不能作为其他用途;R14:连接寄存器,只能用于存储返回地址,不能用作其他用途;R13:栈指针,只能用于存储栈指针,不能用作其他用途;R0-R3:当函数的参数少于4个的时候使用R0-R3传参,多出4个的参数部分使用栈传递函数返回值使用R0传递;R4-R12:主要用于存储局部变量。
4.2.2 堆栈规则
ATPCS规定堆栈为FD类型,即满递减堆栈。并且堆栈的操作是8字节对齐。而对于汇编程序来说,如果目标文件中包含了外部调用,则必须满足以下条件:
- 外部接口的数据栈一定是
8位对齐的,也就是要保证在进入该汇编代码后,直到该汇编程序调用外部代码之间,数据栈的栈指针变化为偶数个字; - 在汇编程序中使用
PRESERVE8伪操作告诉连接器,本汇编程序是8字节对齐的.
4.2.3 参数传递规则
根据参数个数是否固定,可以将子程序分为参数个数固定的子程序和参数个数可变的子程序。这两种子程序的参数传递规则是不同的。
- 参数个数可变的子程序参数传递规则
对于参数个数可变的子程序,当参数不超过4个时,可以使用寄存器R0~R3来进行参数传递,当参数超过4个时,还可以使用数据栈来传递参数。
在参数传递时,将所有参数看做是存放在连续的内存单元中的字数据。然后,依次将各名字数据传送到寄存器R0,R1,R2,R3; 如果参数多于4个,将剩余的字数据传送到数据栈中,入栈的顺序与参数顺序相反,即最后一个字数据先入栈。
按照上面的规则,一个浮点数参数可以通过寄存器传递,也可以通过数据栈传递,也可能一半通过寄存器传递,另一半通过数据栈传递。
- 参数个数固定的子程序参数传递规则
对于参数个数固定的子程序,如果系统包含浮点运算的硬件部件,参数传递与参数个数可变的子程序参数传递规则不同。浮点参数将按照下面的规则传递:
(1)各个浮点参数按顺序处理;
(2)为每个浮点参数分配FP寄存器;
(3)分配的方法是:满足该浮点参数需要的且编号最小的一组连续的FP寄存器。第一个整数参数通过寄存器R0~R3来传递,其他参数通过数据栈传递。
4.2.4 子程序结果返回规则
- 结果为一个
32位的整数时,可以通过寄存器R0返回。 - 结果为一个
64位整数时,可以通过R0和R1返回,依此类推。 - 结果为一个浮点数时,可以通过浮点运算部件的寄存器
f0,d0或者s0来返回。 - 结果为一个复合的浮点数时,可以通过寄存器
f0-fN或者d0~dN来返回。 - 对于位数更多的结果,需要通过调用内存来传递。