ARM 处理器的指令集
| 指令集 | 特点 | 存储方式 | 优缺点 |
|---|---|---|---|
| ARM 指令集 | 原生 32 位指令 | 以字对齐(4 字节边界对齐) | 效率高,代码密度较低 |
| Thumb 指令集 | 16 位指令集,是 ARM 指令集的子集 | 2 字节边界对齐 | 具有较高的代码密度,保持 ARM 的大多数性能优势 |
| Thumb 2 指令集 | 具有 16 位和 32 位指令,提供几乎与 ARM 指令集完全相同的功能 | 2 字节边界对齐,16 位和 32 位指令可自由混合 | 继承了 Thumb 指令集的高代码密度,又能实现 ARM 指令集的高性能 |
| Thumb EE 指令集(2011 起被废弃) | Thumb 2 指令集的一个变体,用于动态产生的代码 | 不能与 ARM 指令集和 Thumb 指令集混合在一起。 |
我们只涉及 ARM 指令集。
ARM 指令的一般规范
指令格式
ARM 指令集的指令基本格式如下:
<opcode>{<cond>}{S} <Rd>,<Rn>,<shift_operand>
其中{}内的项为可选项。各符号的含义如下表所示:
| 符号 | 说明 |
|---|---|
opcode | 操作码,即指令助记符,如 MOV |
cond | 条件码,描述指令执行的条件1 |
S | 可选后缀,加上表示该指令正常执行且生效后会自动更新 CPSR 寄存器中的条件标志位 |
Rd | 目的寄存器 |
Rn | 存放第 1 个操作数的寄存器 |
shift_operand | 第 2 个操作数,可以是寄存器、立即数等 |
指令编码
本门课程中涉及的 ARM 指令集的每条指令长度都是 32 位。不难看出这个设计导致了如下问题:
- 在单条数据处理指令中,通常无法直接编码任意 32 位立即数
- 不同指令格式中,操作码、寄存器等字段占用位数不同,因此可用于立即数编码的位数也不同。
因此,ARM 指令集不得不作出了一定的妥协:不同的指令对能接受的立即数的范围不同。这也是 RISC 指令集相比于 CISC 指令集的一点小“缺陷”。
条件码
条件码的助记符由两个英文符号表示,它们与 CPSR 的条件标志位相关:
| 助记符 | CPSR 标志位值 | 含义 | 全称 |
|---|---|---|---|
EQ | Z=1 | 相等 | Equal |
NE | Z=0 | 不相等 | Not Equal |
CS/HS | C=1 | 无符号数大于等于 | Carry Set/unsigned Higher or Same |
CC/LO | C=0 | 无符号数小于 | Carry Clear/unsigned LOwer |
MI | N=1 | 负数 | MInus |
PL | N=0 | 正数或零 | PLus |
VS | V=1 | 溢出 | oVerflow Set |
VC | V=0 | 没有溢出 | oVerflow Clear |
HI | C=1 且 Z=0 | 无符号数大于 | unsigned HIgher |
LS | C=0 或 Z=1 | 无符号数小于等于 | unsigned Lower or Same |
GE | N=V | 有符号数大于等于 | signed Greater Than or Equal |
LT | N$\ne$V | 有符号数小于 | signed Less Than |
GT | N=V 且 Z=0 | 有符号数大于 | signed Greater Than |
LE | N$\ne$V 或 Z=1 | 有符号数小于等于 | signed Less Than or Equal |
AL | 任何 | 无条件执行(指令默认条件) | ALways |
NV | 任何 | 历史上表示从不执行(现代架构中通常保留) | NeVer |
汇编书写风格
当前常见的 ARM 汇编书写风格主要有两类:ARM/Keil 官方工具链(armasm)风格和 GNU 汇编器(gas)风格。
下面是 ARM/Keil 工具链的汇编书写风格的例子(这里好像高亮有问题):
; 文件名:TEST1.S
; 功能:实现两个寄存器相加
AREA TEST1, CODE, READONLY ; 声明代码段 TEST1
ENTRY ; 标识代码段
CODE32 ; 声明 32 位 ARM 指令
START
MOV R0, #0 ; 设置参数
MOV R1, #10
LOOP
BL ADD_SUB ; 调用子程序 ADD_SUB
B LOOP ; 跳转到 loop
ADD_SUB
ADDS R0, R0, R1 ; R0 = R0 + R1
MOV PC, LR ; 子程序返回
END ; 文件结束
与之对应的 GNU 汇编器的风格:
/* 文件名:TEST1.S
功能:实现两个寄存器相加 */
.text
.arm /* 声明 32 位 ARM 指令 */
.global _start
_start:
MOV R0,#0 /* 设置参数 */
MOV R1,#10
loop:
BL add_sub /* 调用子程序 add_sub */
B loop /* 转到 loop */
add_sub:
ADDS R0,R0,R1 /* R0 = R0 + R1 */
MOV PC,LR /* 子程序返回 */
.end /* 文件结束 */
ARM 指令的寻址方式
寻址方式(Addressing mode)是指处理器根据指令中给出的地址信息,找出操作数所存放的物理地址,实现对操作数的访问。
注意:以下提及的寻址方式并非 ARM 官方的说法。
立即寻址(Immediate addressing)
立即寻址指令中的操作码字段后面的地址码部分即是操作数本身。包含在指令中的数据称为立即数,用前缀 # 区分。
比如:
MOV R0,#0xFF00
关于立即数的范围前面已经提及,这里不再赘述。
寄存器寻址(Register addressing)
操作数的值存在寄存器中,指令中的地址码字段指出的是寄存器编号,指令执行时直接取出寄存器值来操作。这是各类微处理器常用的一种有较高执行效率的寻址方式。
比如:
MOV R1,R2
寄存器移位寻址(Shift register addressing)
这是 ARM 指令集独有的寻址方式。第 2 个寄存器操作数与第 1 个操作数结合之前,进行移位操作。
比如:
MOV R0,R2,LSL #3
这里的意思是先把 R2 的值左移 3 位,结果放入 R0。
可以采用的移位操作有:
| 移位操作 | 描述 |
| LSL | 逻辑左移,寄存器值低端空出的补零 |
| LSR | 逻辑右移,寄存器值高端空出的位补零 |
| ASR | 算术右移,算术移位操作对象是有符号数,移位过程保证操作数的符号不变 |
| ROR | 循环右移,从低端移出的位填入高端空出的位中 |
| RRX | 带扩展的循环右移,操作数右移 1 位,高端空出的位用 C 标志位填充 |
寄存器间接寻址(Scaled register addressing)
在这种形式下,指令中的地址码给出的是一个通用寄存器的编号,所需的操作数保存在寄存器指定地址的存储单元中。换句话说,寄存器的值为操作数的地址指针。ARM 采用 load/store 架构:数据处理指令通常只接受寄存器或立即数操作数,不能像 x86 那样直接使用内存操作数,因此这种寻址方式主要出现在特定的访存指令中。
比如:
LDR R1,[R2]
这里将 R2 指向的存储单元的数据读出,保存在 R1 中。
基址变址寻址(Offset addressing)
基址变址寻址就是将基址寄存器的内容与指令中给出的偏移量相加,形成操作数的有效地址。它主要用于访问基址附近的存储单元,常用于查表、数组操作、功能部件寄存器访问等。
比如:
LDR R2,[R3,#0x0C]
这里读取 R3+0x0C 地址上的存储单元的内容,放入 R2
相对寻址(PC-relative addressing)
相对寻址是基址寻址的一种变体。由程序计数器 PC 提供基准地址,指令中的地址码字段作为偏移量,两者相加后得到的地址即为操作数的有效地址。
比如:
BL SUBR1
BEQ LOOP
...
LOOP
MOV R6,#1
...
SUBR1
...
ARM 指令简介
ARM 指令集主要有 6 类:
- 跳转指令
- 数据处理指令
- 程序状态加载指令
- 加载/存储指令
- 协处理器指令
- 异常展示指令
ARM 指令集是加载/存储型的,具体体现在:
- 指令的操作数都存储在寄存器中
- 处理结果直接放入到目的寄存器中
- 采用专门的加载/存储指令来访问系统寄存器
跳转指令
ARM 程序中有两种方式实现程序流程的跳转:
第一种,直接向程序计数器 PC 中写入地址,可以实现 4G 地址空间(虚拟内存)内的任意跳转,如:
LDR PC,[PC,#+0x00FF]
第二种,使用专门的跳转指令。先介绍最简单的 B 指令,它直接跳转到给定的目标地址,从那里继续执行。不过,因为指令长度的限制,B 指令的地址只有 24 比特。然而,由于 ARM 要求指令是字对齐的,计算地址将 24 位左移 2 位,变成 26 位,其中一位为符号位,因此能表示 $\pm$ 32 $\mathrm{MB}$ 的地址范围。下面是 B 指令的简单用法:
B 0x1234
B FUNC0
然后是 BL 指令,它主要用于子程序调用。相比 B 指令,BL 指令在跳转之前,会将下一条指令的地址复制到链接寄存器 R14(LR)中,然后跳转到指定地址执行。在子程序末尾用 MOV PC,LR 或 BX LR 返回主程序(ARM v7 还没有 RET 指令)。
除此之外,还有 BLX 和 BX。它们在跳转后,还会转换 ARM 的工作状态,这里就不赘述了。
数据处理指令
数据处理指令主要完成寄存器中数据的各种运算操作,需要注意:
- 所有操作数都是 32 位,可以是寄存器或立即数
- 如果数据操作有结果,结果也为 32 位,放在目的寄存器
- 指令使用“两操作数”或“三操作数”方式,即每一个操作数寄存器和目的寄存器分别指定
- 数据处理指令只能对寄存器的内容进行操作;指令后都可以选择
S后缀来影响标志位 - 比较指令不需要后缀
S,这些指令执行后都会影响标志位
这里涉及到的指令比较多,而且具有较明显的共性,这里就不再一一介绍了。
这些指令默认不会影响标志位:
MOV、MVNADD、SUB、RSBADC、SBC、RSCAND、ORR、EOR、BIC
下面是一些比较指令,也不详细展开了:
CMP、CMN、TST、TEQ
程序状态寄存器处理指令
这里主要涉及两条指令,MRS 和 MSR,它们用于在状态寄存器和通用寄存器间传输数据。下面是一个基本的状态寄存器修改步骤:
MRS R0,CPSR /* 将 CPSR 的值复制到 R0 中 */
ORR R0,R0,#C0 /* R0 的位 6 和位 7 置 1,即屏蔽外部中断和快速中断 */
MSR CPSR,R0 /* 将 R0 值写回到 CPSR 中 */
在执行 MSR 指令时,可能只设置程序状态寄存器的部分位,这时可以利用“域”的概念:
| 位 | 名称 | 符号 |
|---|---|---|
| [31:24] | 条件标志域 | f |
| [23:16] | 状态位域 | s |
| [15:8] | 扩展位域 | x |
| [7:0] | 控制位域 | c |
比如:
MSR CPSR_cxsf,R3
加载/存储指令
加载/存储指令用于在寄存器和存储器之间传输数据。
最基础的是 LDR 和 STR,它们分别用于将 32 位字数据传输到目的寄存器和从源寄存器中将一个 32 位字数据写入存储器中:
LDR R1,[R0,#0x12] /* 将存储器地址为 R0 + 0x12 的字数据写入 R1 */
STR R0,[R1,#12] /* 将 R0 中的字数据写入以 R1 + 12 为地址的存储器中 */
接下来的是 LDM 和 STM 指令,它们实现了一组寄存器和一片连续存储空间之间的数据传输,其中 LDM 用于加载多个寄存器,STM 用于存储多个寄存器。它们都有 8 种模式,由后缀区分2:
| 后缀 | 说明 |
|---|---|
IA | 每次传送后地址加 4 |
IB | 每次传送前地址加 4 |
DA | 每次传送后地址减 4 |
DB | 每次传送前地址减 4 |
FD | 满递减堆栈 |
ED | 空递减堆栈 |
FA | 满递增堆栈 |
EA | 空递增堆栈 |
这类指令常用于现场保护、数据复制、参数传输等。下面是一些例子
LDMIA R1!,{R2-R7,R12} ; 将 R1 指向的单元中的数据读出到 R2 ~ R7、R12 中(R1 之后自动加 4 个字节)
这里出现了新的后缀 !,它被称为前索引取址(Pre-indexed Addressing),说明如下:
- 在寄存器操作数后加上
!后缀,当数据传输完成后,将最后地址写入寄存器中,否则其值不更新。 R15(PC)不能加上该后缀
顺便再补充一下之后可能会遇到的其他格式,比如:
STR R0,[R1],#12 ; 将 R0 中的字数据写入以 R1 为地址的寄存器中,并将新地址 R1+12 写入 R1
这个被称作后索引取址(Post-indexed Addressing)。除此之外,还有一个 ^ 后缀,它的作用是当指令为 LDM 且寄存器列表中有 R15 时,在完成数据传输后将 SPSR 复制到 CPSR。
在加载/存储指令中最后介绍的是 SWP 指令,顾名思义,它用于完成数据的交换,具体用法如下:
SWP R1,R1,[R0] ; 将 R1 的内容与 R0 指向的存储单元的内容进行交换
SWP R1,R2,[R0] ; 将 R0 指向的存储单元的内容写入到 R1 中,并将 R2 的内容写入到该内存单元中
协处理指令
课程不要求,先跳过了。
异常产生指令
ARM 处理器有两条异常产生指令:
SWI:软中断指令3BKPT:断点中断指令