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控制器。后面学习 UbootLinux 内核的时候汇编是必须要会的。

官方文档?

我们在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
2
3
4
5
6
7
8
9
10
.text				@ 表示当前段为代码段 
.global _start @ 声明_start为全局符号
_start: @ 汇编程序的入口

@ 中间这里是我们要写的指令

STOP: @ 死循环,防止程序跑飞
B STOP

.end @ 汇编程序的结束

5. GNU函数语法

在汇编中同时也支持函数的定义使用,一般定义格式如下:

1
2
3
函数名 : 
函数体
返回语句

【注意】GNU汇编函数返回语句不是必须的。

点击查看实例
1
2
3
4
/* 未定义中断 */ 
Undefined_Handler: @ 函数名
LDR R0, = Undefined_Handler @ 函数体
BX R0 @ 返回语句,并非必须

二、数据处理指令

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数据处理指令编码格式如下图:

image-20220728071703224

【注意】对于ARMCotex-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相等0000Z==1
NE不相等0001Z==0
CS无符号大于或等于 0010C==1
CC无符号小于0011C==0
MI负数0100N==1
PL正数或零0101N==0
VS溢出0110V==1
VC未溢出0111V==0
HI无符号大于1000C==1 Z==0
LS无符号小于或等于 1001C==0 Z==1
GE带符号大于或等于 1010N==V
LT带符号小于1011N!=V
GT带符号大于1100Z==0 N==V
LE带符号小于或等于 1101Z==1 N!=V
AL无条件执行1110忽略

1.2.2 CMP指令

条件码的使用是需要有CPSR寄存器相关位变化的,这个时候我们就需要使用CMP指令来把一个寄存器的数据和另一个寄存器的数据或一个立即数进行比较,同时更新CPSR中条件标志位的值。CMP指令的格式如下:

1
2
CMP Rd, Rn          @ 相当于 Rd - Rn 
CMP Rd, #immediate @ 相当于 Rd - immediate, immediate表示立即数,后边会学到

实际上,CMP指令所做的事情就是将RdRn相减,这实质上就是一个减法指令,但是运算的结果不会存入寄存器,只会通过CPSR的相关标志位表示出来。

1.2.3 实例说明

后边我们将学习到的大多数指令都可以加上条件码,例如SUBB等。这里放一个实例,具体的其他指令后边会学习到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text				@ 表示当前段为代码段 
.global _start @ 声明_start为全局符号
_start: @ 汇编程序的入口
MOV R0, #6
MOV R1, #15
START:
CMP R0, R1
BEQ STOP @ R0 == R1, 程序退出
SUBGT R0, R0, R1 @ R0 > R1, R0 = R0 - R1
SUBLT R1, R1, R0 @ R0 < R1, R1 = R1 - R0
B START

STOP: @ 死循环,防止程序跑飞
B STOP

.end @ 汇编程序的结束

这段汇编表示的含义用C语言表示出来就是下边的意思:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(void)
{
int a = 6;
int b = 15;
while(1)
{
if(a == b) return 0;
if(a > b) a = a - b;
if(a < b) b = b - a;
}
return 0;
}

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
2
3
immediate  : 0x104 @ 0000 0000 0000 0000   0000 0001 0000 0100
immed_8 : 0x41 @ 0000 0000 0000 0000 0000 0000 0100 0001
rotate_imm : 15 @ 将会循环右移2x15=30位

2. 数据搬移指令

2.1 MOV

MOV指令用于将数据从一个寄存器拷贝到另外一个寄存器,或者将一个立即数传递到寄存器里面,使用示例如下:

1
2
3
MOV R1, R0
MOV R2, #3
MOV R2, #0xFF

【注意】这里我们通过MOV指令,只能将一个立即数存入寄存器,那如果想存任意32位数据据呢?里边肯定有很多都不是立即数,这个时候我们可以用LDR指令,后边会学习到。

2.2 MVN

MVN指令用于将一个数据按位取反后从一个寄存器拷贝到年另一个寄存器,或者将一个立即数按位取反后传递到寄存器中:

1
2
MVN R4, R0
MVN R4, #0xFF @ 将数据按位取反后放入寄存器R4

3. 数据运算指令

一般数据运算指令都符合以下格式:

1
操作码 目标寄存器, 第一操作寄存器,第二操作数
  • 操作码:就是表示执行哪种操作;
  • 目标寄存器:用于存储目标寄存器;
  • 第一操作寄存器 :存储第一个参数运算的数据,这里不可以是一个立即数,只能写寄存器
  • 第二操作数:第二个参与运算的数据,可以是立即数,也可以是寄存器

3.1 加法运算

3.1.1 ADD

指令计算公式
ADD Rd, Rn, RmRd = Rn + Rm
ADD Rd, Rn, #immediateRd = Rn + #immediate

【指令说明】ADD不带进位的加法指令。

【使用实例】

1
2
3
4
5
MOV R1, #1
MOV R2, #2

ADD R3, R1, R2 @ R3 = R1 + R2 = 3
ADD R3, R2, #3 @ R3 = R2 + 3 = 5

3.1.2 ADC

指令计算公式
ADC Rd, Rn, RmRd = Rn + Rm + 进位
ADC Rd, Rn, #immediateRd = Rn + #immediate + 进位

【指令说明】ADC带进位的加法指令。

【注意】进行带进位的加法指令时,若有用到ADD,则需要加上S后缀,这样才会使CPSR进位标志C产生变化,才可以真正加上进位。

【使用实例】

1
2
3
4
5
6
7
8
9
10
11
12
@ 6.1第一个数 0000 0002 FFFF FFFF
MOV R2, #0xFFFFFFFF @ 第一个数低32位
MOV R3, #0x00000002 @ 第一个数高32位
@ 6.2第二个数 0000 0004 0000 0003
MOV R4, #0x00000003 @ 第二个数低32位
MOV R5, #0x00000004 @ 第二个数高32位
@ 6.3不带进位加法结果 0000 0006 0000 0010
ADD R6, R2, R4 @ 结果的低32位
ADD R7, R3, R5 @ 结果的高32位
@ 6.4带进位加法结果 0000 0007 0000 0010
ADDS R6, R2, R4 @ 结果的低32位
ADC R7, R3, R5 @ 结果的高32位(R7 = R3 + R5 + CPSR[C])

3.1.3 使用实例

【题目】使用ARM寄存器完成两个128位数的加法运算

【解答】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ 1.1第一个数 0000 0002 FFFF FFFF 0000 0002 FFFF FFFF
MOV R0, #0xFFFFFFFF
MOV R1, #0x00000002
MOV R2, #0xFFFFFFFF
MOV R3, #0x00000002
@ 1.2第二个数 0000 0004 0000 0003 0000 0004 0000 0003
MOV R4, #0x00000003
MOV R5, #0x00000004
MOV R6, #0x00000003
MOV R7, #0x00000004
@ 1.3开始加法运算
ADDS R8, R0, R4
ADCS R9, R1, R5
ADCS R10, R2, R6
ADC R11, R3, R7

3.2 减法运算

3.2.1 SUB

指令计算公式
SUB Rd, Rn, RmRd = Rn - Rm
SUB Rd, Rn, #immediateRd = Rn - #immediate
SUB Rd, #immediateRd = Rd - #immediate
SUB Rd, RnRd = Rd - Rn

【指令说明】SUB不带借位的减法指令(前减后)。

【使用实例】

1
2
3
4
5
6
MOV R1, #1
MOV R2, #4

SUB R3, R2, R1 @ R4 = R2 - R1 = 2 - 1 = 1
SUB R2, R1 @ R2 = R2 - R1 = 2 - 1 = 1
SUB R2, #1 @ R2 = R2 - 1 = 2 - 1 = 1

3.2.2 RSB

指令计算公式
RSB Rd, Rn, RmRd = Rm - Rn
RSB Rd, Rn, #immediateRd = #immediate - Rn

【指令说明】RSB逆向减法指令(后减前)。

3.1.2 SUC

指令计算公式
SBC Rd, Rn, #immediateRd = Rn - #immediate – 借位
SBC Rd, Rn, RmRd = Rn - Rm – 借位

【指令说明】SUBC带借位的减法指令。

【注意】进行带借位的减法指令时,若有用到SUB,则需要加上S后缀,这样才会使CPSR进位标志C产生变化,才可以真正实现借位。

【使用实例】

1
2
3
4
5
6
7
8
9
10
@ 8.带借位的减法
@ 8.1第一个数 0000 0002 0000 0001
MOV R0, #0x00000001 @ 第一个数低32位
MOV R1, #0x00000002 @ 第一个数高32位
@ 8.2第二个数 0000 0001 0000 0005
MOV R2, #0x00000005 @ 第二个数低32位
MOV R3, #0x00000001 @ 第二个数高32位
@ 8.3带借位减法结果
SUBS R6, R0, R2
SBC R7, R1, R3 @ (R7 = R1 - R3 - CPSR[!C]) C取反

3.4 乘法运算

3.4.1 MUL

指令计算公式
MUL Rd, Rn, RmRd = Rn * Rm

【指令说明】MUL是(32位)乘法指令。

【使用实例】

1
2
3
4
MOV R1, #1
MOV R2, #2

MUL R3, R1, R2 @ R3 = R1 x R2 = 1 x 2 = 2

3.5 除法运算

【注意】ARM不支持除法运算

3.5.1 UDIV

指令计算公式
UDIV Rd, Rn, RmRd = Rn / Rm

【指令说明】UDIV是无符号除法指令。

3.5.2 SDIV

指令计算公式
SDIV Rd, Rn, RmRd = Rn / Rm

【指令说明】UDIV是有符号除法指令。

4. 逻辑运算指令

4.1按位与运算

4.1.1AND

指令计算公式
AND Rd, RnRd = Rd & Rn
AND Rd, Rn, #immediateRd = Rn & #immediate
AND Rd, Rn, RmRd = Rn & Rm

【指令说明】AND是按位与运算指令。

【使用实例】

1
2
3
MOV R1, #1      @ R1 = 0000 0001
MOV R2, #3 @ R1 = 0000 0011
AND R3, R1, R2 @ R3 = 0000 0001

4.2 按位或运算

4.2.1 ORR

指令计算公式
ORR Rd, RnRd = Rd | Rn
ORR Rd, Rn, #immediateRd = Rn | #immediate
ORR Rd, Rn, RmRd = Rn | Rm

【指令说明】ORR是按位或运算指令。

【使用实例】

1
2
3
MOV R1, #1      @ R1 = 0000 0001
MOV R2, #3 @ R1 = 0000 0011
ORR R4, R1, R2 @ R4 = 0000 0011

4.3 位清除

4.3.1 BIC

指令计算公式
BIC Rd, RnRd = Rd & (~Rn)
BIC Rd, Rn, #immediateRd = Rn & (~#immediate)
BIC Rd, Rn, RmRd = Rn & (~Rm)

【指令说明】BIC是位清除指令,第二操作数中哪一位为1,就将第一操作数哪一位清0,然后将结果存入目标寄存器。

【使用实例】

1
2
3
MOV R1, #0xAA   @ R1 = 1010 1010
MOV R2, #0x0F @ R2 = 0000 1111
BIC R3, R1, R2 @ R3 = 1010 0000

4.4 按位异或

4.4.1 EOR

指令计算公式
EOR Rd, RnRd = Rd ^ Rn
EOR Rd, Rn, #immediateRd = Rn ^ #immediate
EOR Rd, Rn, RmRd = Rn ^ Rm

【指令说明】EOR是按位异或指令。

【使用实例】

1
2
3
MOV R1, #1      @ R1 = 0000 0001
MOV R2, #3 @ R1 = 0000 0011
EOR R5, R1, R2 @ R5 = 0000 0010

4.5 左移

4.5.1 LSL

指令计算公式
LSL Rd, Rn, RmRd = Rn << Rm(左移Rm位)

【指令说明】LSL是左移指令。

【使用实例】

1
2
3
MOV R1, #1      @ R1 = 0000 0001
MOV R2, #3 @ R1 = 0000 0011
LSL R6, R1, R2 @ R6 = 0000 1000 (R1 << R2)

4.6 右移

4.6.1 LSR

指令计算公式
LSR Rd, Rn, RmRd = Rn >> Rm(右移Rm位)

【指令说明】LSR是右移指令。

【使用实例】

1
2
3
MOV R1, #1      @ R1 = 0000 0001
MOV R2, #3 @ R1 = 0000 0011
LSR R7, R6, R2 @ R7 = 0000 0001 (R6 >> R2)

5. 三种相关寻址实例

这部分主要是介绍数据处理指令相关的ARM指令寻址方式。

5.1 立即数寻址

立即数寻址,也叫立即寻址,寻找数据时直接从机器码中获取。

1
2
MOV R0, #1
ADD R1, R0, #2

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
2
3
4
5
6
7
8
9
10
MAIN2:
MOV R1, #1
MOV R2, #2
B FUNC2 @ 不带返回的跳转指令
MOV R3, #3

FUNC2:
MOV R4, #1
MOV R5, #2
MOV R6, #3

3. BL

指令说明
BL <label>跳转到标号地址,并将返回地址保存在LR中。

【指令说明】BL是带返回的跳转指令,实现程序的跳转,本质是将PC寄存器的值修改成跳转标号下第一条指令的地址,同时将跳转指令的下一条指令的地址保存到LR寄存器中。需要注意的是,想要返回到跳转之前的位置,还需要将LR寄存器的值给到PC寄存器。

【使用实例】

1
2
3
4
5
6
7
8
9
10
11
MAIN3:
MOV R1, #1
MOV R2, #2
BL FUNC3 @ 带返回的跳转指令
MOV R3, #3

FUNC3:
MOV R4, #1
MOV R5, #2
MOV R6, #3
MOV PC,LR

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 可从 Rmbit[0]推算出目标状态。

(2)如果 Rmbit[0]0,则处理器的状态会更改为(或保持在)ARM 状态;如果 Rmbit [0]1,则处理器的状态会更改为(或保持在)Thumb 状态。

6. 使用实例

【题目】使用汇编计算 1 + 2 + 3 + … + 98 + 99 + 100

【解答】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ ARM指令条件码实现求100以内的正整数之和
@ 1 + 2 +3 + ... + 98 + 99 + 100 = 50 x 101 = 5050 = 13BA

.text @ 表示当前段为代码段
.global _start @ 声明_start为全局符号
_start: @ 汇编程序的入口

MOV R0, #100 @ R0存储结束的条件
MOV R1, #0 @ R1存储当前要加的数
FUNC:
ADD R2, R2, R1 @ R2存储 R2 + R1 的值
ADD R1, R1, #1 @ R1自增1
CMP R1, R0 @ 比较R0和R1
BGT STOP @ R1 > R0 时,也就是R1 > 100了,循环结束
BLE FUNC


STOP: @ 死循环,防止程序跑飞
B STOP

.end @ 汇编程序的结束

四、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位置。
**【指令说明】**`STR`指令将寄存器中的数据写入到存储器中(一次性写入`4`个字节),`STRB`可以向存储器中写入`1`个字节数据,`STRH`可以向存储器写入`2`字节的数据。

【注意】

(1)#offset不写的时候,将直接从Rd位置开始存储。

(2)对于小端模式的ARM处理器来说,写入1字节或者2字节的时候,都是从低字节开始写的。

【使用实例】(下边的例子提前用到了LDR,后边在说明)

1
2
3
4
5
6
7
8
9
10
11
MOV R0, #0x40000000             @ 要写入的内存地址
MOV R1, #0xFF000000 @ 要写入的数据
STR R1, [R0] @ 将R1寄存器中的4字节数据存储到R0指向的内存空间
@ 3.写1个字节到内存
@ MOV R3, 0x12345678 @ 这样是会报错的,立即数的问题,网上说超过8位的立即数最好用LDR
LDR R3, = 0x12345678 @ 将 0x12345678 写入R3寄存器
STRB R3, [R0] @ 写一个字节到R0指向的内存空间
@ 4.写2个字节到内存
LDR R4, = 0x12345678 @ 将数据 0x12345678 写入到 R4 寄存器
LDR R5, = 0x40000004 @ 将内存地址 0x40000004 写入到 R5 寄存器
STRH R4, [R5] @ 写两个字节到R5指向的内存空间

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寄存器。
**【指令说明】**`LDR`指令将存储器中的数据读取到寄存器(一次性读取`4`个字节),`LDRB`可以从存储器读取`1`个字节数据,`LDRH`可以从存储器读取`2`字节的数据。

【注意】

(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
2
3
LDR R4, = 0x40000000
LDR R5, = 0x12345678
STR R5, [R4] @ 通过寄存器操作内存

1.3.2 寄存器基址变址寻址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LDR R4, = 0x40000000
MOV R5, #4
LDR R6, = 0x12345678
STR R6, [R4, R5] @ 将数据写入到 R4 + R5 所指向的内存地址,这里就是0x40000004

STR R6, [R4, R5, LSL #1] @ 将数据写入到 R4 + (R5 << 1) 所指向的内存地址,这里就是0x40000008

STR R6, [R4, #12] @ 前索引,将数据写入到 R4 + 12 所指向的内存地址,这里就是0x4000000C

LDR R6, = 0x87654321
STR R6, [R4], #8 @ 后索引,将数据写入到 R4 所指向的内存地址,这里就是0x40000000,然后将R4再加8

LDR R4, = 0x40000000
LDR R6, = 0x1234ABCD
STR R6, [R4, #8]! @ 自动索引,将数据写入到 R4 +8 所指向的内存地址,这里就是0x40000008,然后将R4再加8

2. 多寄存器内存访问

2.1 STM

指令说明
STM {cond} Rd{!}, {reglist}{^}将寄存器列表reglist中的所有数据写入到 Rd 所指向的存储器的地址中。

【指令说明】

  • STM:表示执行将多个寄存器数据写入存储器操作,是一种数据块传输指令;
  • cond:表示条件码,后边学习到寻址方式的时候会用到;
点击查看条件码
IAIncrease After,每次传送后地址加4,其中的寄存器从左到右执行,例如:STMIA R0,{R1,LR} 先存R1,再存LR
IBIncrease Before,每次传送前地址加4,其中的寄存器从左到右执行,例如:STMIA R0,{R1,LR} 先存R1,再存LR
DADecrease After,每次传送后地址减4,其中的寄存器从右到左执行,例如:STMDA R0,{R1,LR} 先存LR,再存R1
DBDecrease 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
2
3
4
5
6
7
8
9
10
LDR R0, = 0x40000000
MOV R1, #1
MOV R2, #2
MOV R3, #3
MOV R4, #4
STM R0, {R1-R4} @ 将R1到R4这四个寄存器中的数据写入到R0寄存器指向的内存地址为起始地址的内存空间中

STM R0, {R1, R3, R4}

STM R0!, {R1-R4} @ 数据存储完毕后,R0自动增加,存多少字节就增加多少字节。在这里就是增加16字节,即0x40000010

2.2 LDM

指令说明
LDM {cond} Rd{!}, {reglist}{^}将 Rd 所指向的存储器的地址中的数据依次读取到寄存器列表reglist。
**【指令说明】**
  • LDM:表示执行将存储器中的多个数据读取到多个寄存器操作,是一种数据块传输指令;
  • cond:表示条件码,后边学习到寻址方式的时候会用到;
点击查看条件码
IAIncrease After,每次传送后地址加4,其中的寄存器从左到右执行,例如:STMIA R0,{R1,LR} 先存R1,再存LR
IBIncrease Before,每次传送前地址加4,其中的寄存器从左到右执行,例如:STMIA R0,{R1,LR} 先存R1,再存LR
DADecrease After,每次传送后地址减4,其中的寄存器从右到左执行,例如:STMDA R0,{R1,LR} 先存LR,再存R1
DBDecrease 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
2
LDR R0, = 0x40000000
LDM R0, {R11, R10}

2.3 一种相关寻址方式实例

2.3.1 多寄存器寻址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LDR R1, = 0x11112222
LDR R2, = 0x33334444
LDR R3, = 0x55556666
LDR R0, = 0x40000000
@ 1.Increase After (先存储数据后增加地址)从R0指向的内存空间地址开始向高地址存储R1,R2,R3中数据,顺序是 R1->R2->R3
STMIA R0, {R1, R2, R3}
@ 2.Increase Before(先增加地址后存储数据)从R0 + 4 指向的内存空间地址开始向高地址存储R1,R2,R3中数据,顺序是 R1->R2->R3
LDR R0, = 0x40000020
STMIB R0, {R1, R2, R3}
@ 3.Decrease After (先存储数据后减小地址)从R0指向的内存空间地址开始向低地址存储R1,R2,R3中数据,顺序是 R3->R2->R1(正着看的话其实也是R1->R2->R3)
LDR R0, = 0x40000040
STMDA R0, {R1, R2, R3}
@ 4.Decrease Before(先减小地址后存储数据)从R0 - 4 指向的内存空间地址开始向低地址存储R1,R2,R3中数据,顺序是 R3->R2->R1(正着看的话其实也是R1->R2->R3)
LDR R0, = 0x40000060
STMDB R0, {R1, R2, R3}
@ 总的来说,存的时候都是小编号寄存器放在低字节

3. 栈

3.1 栈的概念

栈的本质就是一段内存,程序运行时用于保存一些临时数据,如局部变量、函数的参数、返回值、以及程序跳转时需要保护的寄存器等。

3.2 栈的分类

先来了解几个概念:

增栈压栈时栈指针越来越大,出栈时栈指针越来越小
减栈压栈时栈指针越来越大,出栈时栈指针越来越小
满栈栈指针指向最后一次压入到栈中的数据,压栈时需要先移动栈指针到相邻位置然后再压栈
空栈栈指针指向最后一次压入到栈中的数据的相邻位置,压栈时可直接压栈,之后需要将栈指针移动到相邻位置

这样的话,栈其实可以分为四种:空增(EA)、空减(ED)、满增(FA)、满减(FD)。在ARM处理器一般使用满减栈。各种栈的实现其实都是通过STM/LDM两个指令来实现的,只是不同条件码对应不同类型的栈。对于ARM中常用的满减栈,我们使用的指令如下:

1
2
3
4
5
6
STMDB R0!, {R1-R4}
LDMIA R0!, {R5-R8}

@ 就等价于下边的写法,而且下边的写法更清晰
STMFD R0!, {R1-R4}
LDMFD R0!, {R5-R8}

3.3 栈的使用

3.3.1 叶子函数

叶子函数就是不再调用其他函数的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.text				@ 表示当前段为代码段 
.global _start @ 声明_start为全局符号
_start: @ 汇编程序的入口
@ 初始化栈指针SP,也就是给R13寄存器存上一个栈地址
LDR SP, = 0x40000020
@ 叶子函数:不再调用其他函数的函数

MAIN:
MOV R1, #3
MOV R2, #5
BL FUNC
ADD R3, R1, R2
B STOP

FUNC: @ 叶子函数
STMFD SP!, {R1, R2} @ 将R1和R2的值压入栈中,保护现场
MOV R1, #10
MOV R2, #20
SUB R3, R2, R1
LDMFD SP!, {R1, R2} @ 将R1和R2的值出栈,恢复现场
MOV PC, LR

STOP: @ 死循环,防止程序跑飞
B STOP

.end @ 汇编程序的结束

3.3.2 非叶子函数

非叶子函数就是在函数中还调用了其他的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@ 栈的应用2
.text @ 表示当前段为代码段
.global _start @ 声明_start为全局符号
_start: @ 汇编程序的入口
@ 初始化栈指针SP,也就是给R13寄存器存上一个栈地址
LDR SP, = 0x40000020
@ 非叶子函数:函数中调用了其他函数

MAIN:
MOV R1, #3
MOV R2, #5
BL FUNC1
ADD R3, R1, R2
B STOP

FUNC1: @ 非叶子函数
STMFD SP!, {R1, R2, LR} @ 将R1和R2的值压入栈中,保护现场
MOV R1, #10
MOV R2, #20
BL FUNC2
SUB R3, R2, R1
LDMFD SP!, {R1, R2, LR} @ 将R1和R2的值出栈,恢复现场
MOV PC, LR

FUNC2: @ 叶子函数
STMFD SP!, {R1, R2} @ 将R1和R2的值压入栈中,保护现场
MOV R1, #7
MOV R2, #8
MUL R3, R1, R2
LDMFD SP!, {R1, R2} @ 将R1和R2的值出栈,恢复现场
MOV PC, LR

STOP: @ 死循环,防止程序跑飞
B STOP

.end @ 汇编程序的结束

五、状态寄存器传送指令

前边我们学习了CPSR寄存器,这是ARM的控制寄存器,我们是无法使用MOV指令来修改CPSR的各个位的,我们只能通过状态寄存器传送指令来读写CPSR寄存器。C语言写的语句都不会被编译成状态寄存器传送指令,一般由操作系统内部使用。

1. MRS

指令说明
MRS Rd, CPSRCPSR中的数据读取到Rd寄存器。

【使用实例】

1
MRS R0, CPSR 

2. MSR

指令说明
MSR CPSR, Rn将Rn中的数据写入CPSR寄存器。
MSR CPSR, #immediate将 immediate 写入CPSR寄存器。
**【注意】**`User`模式下是无法修改`CPU`工作模式的。

【使用实例】

1
MRS R0, CPSR 

六、软中断指令

有时候我们需要从用户模式下切换工作模式,但是我们知道在用户模式下是无法切换工作模式的,而软中断异常的产生可以进入SVC模式,所以我们在需要修改ARM工作模式的时候就可以发出一个软中断。

1. SWI

指令说明
SWI{cond} immed_24产生一个软中断异常。
**【指令说明】**`SWI`指令可以产生一个软中断异常,从而使`CPU`进入管理员模式,指令中`immed_24`是一个`24`位立即数。

SWI指令后面的24立即数是干什么用的呢?

用户程序通过SWI指令切换到特权模式,进入软中断处理程序,但是软中断处理程序不知道用户程序到底想要做什么,SWI指令后面的24位立即数用来做用户程序和软中断处理程序之间的信号。通过该软中断立即数来区分用户不同操作,执行不同内核函数。

【使用实例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
.text				@ 表示当前段为代码段 
.global _start @ 声明_start为全局符号
_start: @ 汇编程序的入口

@ 异常向量表
B MAIN @ 0x00 Reset
B . @ 0x02 Undefined instruction
B SWI_HANDLER @ 0x08 Software interrupt(SWI)
B . @ 0x0C Prefetch Abort
B . @ 0x10 Data Abort
B . @ 0x14 Reserved
B . @ 0x18 IRQ
B . @ 0x1C FIQ

@ 应用程序

MAIN:
LDR SP, = 0x40000020 @ 初始化SVC模式下的栈指针
MSR CPSR, #0x10 @ 修改CPU为User模式

@ 将会初始化User模式下的栈指针
@ LDR SP, = 0x40000020

MOV R1, #1
MOV R2, #2
SWI #1 @ 软中断指令,触发异常
ADD R3, R1, R2
B STOP

@ SWI软件中断异常处理
SWI_HANDLER:
STMFD SP!, {R1, R2, LR} @ 压栈,保护现场
MOV R1, #10
MOV R2, #20
SUB R3, R2, R1
LDMFD SP!, {R1, R2, PC}^ @ 出栈,恢复现场,直接将LR读入PC, ^ 表示直接将SPSR值传递给CPSR

STOP: @ 死循环,防止程序跑飞
B STOP

.end @ 汇编程序的结束

七、协处理器指令

重点目前不在这里,这里就简单了解一下。

1. 协处理器简介

协处理器是一种芯片,用于减轻系统微处理器的特定处理任务。例如,数学协处理器可以控制数字处理;图形协处理器可以处理视频绘制。协处理器可以通过一组专门的、提供load-store类型接口的ARM指令来访问。例如协处理器15CP15),ARM处理器使用协处理器15的寄存器来控制cacheTCM和存储器管理。

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包含1632位的寄存器,其编号为0~15。而访问CP15寄存器的指令主要是MCRMRC这两个指令。

2.3.1 MRC

一般格式如下:

1
2
3
MRC{条件} 协处理器编码, 协处理器操作码1, 目的寄存器, 源寄存器1, 源寄存器2, 协处理器操作码2 @ 读协处理器寄存器
@ 一般用于 CP15 协处理器,使用格式如下:
MRC{cond} P15, <Opcode_1>, <Rd>, <CRn>, <CRm>, <Opcode_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
2
MRC P3, 3, R0, C4, C5, 6   @ 将协处理器 P3 的寄存器 C4 和 C5 中的数据传送到 ARM 处理器寄存器 R0 中
MRC P15, 0, R0, C1, C0, 0 @ 将 CP15 的寄存器 C1 的值读到 r0 中

2.3.2 MCR

一般格式如下:

1
2
3
4
MCR{条件} 协处理器编码, 协处理器操作码1, 源寄存器, 目的寄存器1, 目的寄存器2, 协处理器操作码2 @ 写协处理器寄存器

@ 一般用于 CP15 协处理器,使用格式如下:
MCR{cond} P15, <Opcode_1>, <Rd>, <CRn>, <CRm>, <Opcode_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
2
MCR P3, 3, R0, C4, C5, 6   @ 将 ARM 处理器寄存器 R0 中的数据传送到协处理器 P3 的寄存器 C4 和 C5 中
MCR P15, 0, R0, C8, C7, 0 @ 使无效整个数据 TLB 和指令 TLB2

八、伪指令

1. NOP

指令说明
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中。
**【指令说明】**`LDR`在此处用作伪指令用途。

【使用实例】

1
2
3
4
5
6
7
LDR R1, = 0x12345678    @ 这是一条伪指令,可以将一个任意的32位数据放到寄存器

LDR R1, = STOP @ 将STOP的地址写入到寄存器
LDR R1, STOP @ 将STOP地址中的内容写入到寄存器

STOP: @ 死循环,防止程序跑飞
B STOP

九、伪操作

前边已经简单了解了伪操作的概念,这里我们来学习一下常见的伪操作。需要注意的是,GNU中的伪操作一般都以.开头。

1. .byte

定义单字节数据。例如,

1
.byte 0x12

2. .short

定义双字节数据。例如,

1
.short 0x1234

3. .long

定义四字节数据。例如,

1
.byte 0x12345678

4. .equ

相当于C语言中的宏定义,它不会生成机器码,该语句的语法格式如下:

1
.equ 变量名, 表达式

【使用实例】

1
2
.equ DATA, 0x12  @ 表示 num = 0x12。
MOV R0, #DATA

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
2
.weak main
B 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
2
3
.if <expression>  @ 0 or 1
... @ 语句
.endif @ 结束标志

【使用实例】

1
2
3
4
.if 0
MOV R3, #3
MOV R4, #4
.endif

13. .macro

类似于C语言中的定义函数,语法格式为:

1
2
3
.macro name
...
.endm

【使用实例】

1
2
3
4
5
.macro FUNC
MOV R1, #1
MOV R2, #2
.endm
FUNC

14. .rept

此伪操作会生成多次相同的机器码,语法格式为:

1
2
3
.rept num
...
.endr

【使用实例】

1
2
3
4
.rept 3                    @ 编译3次,下边两条将会生成6次机器码
MOV R5, #5
MOV R6, #6
.endr

十、C与汇编混合编程

1. 汇编中调用C

我们可以在汇编语言中调用(跳转)C语言,需要注意的是,在汇编中C语言中的函数将会被当做标号来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
.text				@ 表示当前段为代码段 
.global _start @ 声明_start为全局符号
_start: @ 汇编程序的入口
MOV R1, #1
MOV R2, #2
@ 1.汇编语言调用(跳转)C语言
BL func_c
MOV R3, #3

STOP: @ 死循环,防止程序跑飞
B STOP

.end @ 汇编程序的结束
1
2
3
4
5
void func_c(void)
{
int a = 3;
a++;
}

2. C中调用汇编

我们可以在C语言中调用(跳转)汇编,需要注意的是,在C语言中,汇编的函数被当做函数处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text				@ 表示当前段为代码段 
.global _start @ 声明_start为全局符号
_start: @ 汇编程序的入口
MOV R1, #1
MOV R2, #2
@ 1.汇编语言调用(跳转)C语言
BL func_c
MOV R3, #3

.global FUNC_ASM
FUNC_ASM:
MOV R4, #4

STOP: @ 死循环,防止程序跑飞
B STOP

.end @ 汇编程序的结束
1
2
3
4
5
6
7
void func_c(void)
{
int a = 3;
a++;
FUNC_ASM();
a--;
}

3. C内嵌汇编

我们可以在C语言中直接执行汇编指令,使用的格式为:

1
asm("ARM 汇编指令\n");
1
2
3
4
5
6
7
8
9
10
11
12
13
.text				@ 表示当前段为代码段 
.global _start @ 声明_start为全局符号
_start: @ 汇编程序的入口
MOV R1, #1
MOV R2, #2
@ 1.汇编语言调用(跳转)C语言
BL func_c
MOV R3, #3

STOP: @ 死循环,防止程序跑飞
B STOP

.end @ 汇编程序的结束
1
2
3
4
5
6
7
8
9
10
11
12
13
void func_c(void)
{
int a = 3;
a++;

asm
(
"MOV R6, #6\n"
"MOV R7, #7\n"
);
FUNC_ASM();
a--;
}

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位整数时,可以通过R0R1返回,依此类推。
  • 结果为一个浮点数时,可以通过浮点运算部件的寄存器f0d0或者s0来返回。
  • 结果为一个复合的浮点数时,可以通过寄存器f0-fN或者d0~dN来返回。
  • 对于位数更多的结果,需要通过调用内存来传递。