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 位。不难看出这个设计导致了如下问题:

  1. 在单条数据处理指令中,通常无法直接编码任意 32 位立即数
  2. 不同指令格式中,操作码、寄存器等字段占用位数不同,因此可用于立即数编码的位数也不同。

因此,ARM 指令集不得不作出了一定的妥协:不同的指令对能接受的立即数的范围不同。这也是 RISC 指令集相比于 CISC 指令集的一点小“缺陷”。

条件码

条件码的助记符由两个英文符号表示,它们与 CPSR 的条件标志位相关:

助记符CPSR 标志位值含义全称
EQZ=1相等Equal
NEZ=0不相等Not Equal
CS/HSC=1无符号数大于等于Carry Set/unsigned Higher or Same
CC/LOC=0无符号数小于Carry Clear/unsigned LOwer
MIN=1负数MInus
PLN=0正数或零PLus
VSV=1溢出oVerflow Set
VCV=0没有溢出oVerflow Clear
HIC=1 且 Z=0无符号数大于unsigned HIgher
LSC=0 或 Z=1无符号数小于等于unsigned Lower or Same
GEN=V有符号数大于等于signed Greater Than or Equal
LTN$\ne$V有符号数小于signed Less Than
GTN=V 且 Z=0有符号数大于signed Greater Than
LEN$\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 类:

  1. 跳转指令
  2. 数据处理指令
  3. 程序状态加载指令
  4. 加载/存储指令
  5. 协处理器指令
  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,LRBX LR 返回主程序(ARM v7 还没有 RET 指令)。

除此之外,还有 BLXBX。它们在跳转后,还会转换 ARM 的工作状态,这里就不赘述了。

数据处理指令

数据处理指令主要完成寄存器中数据的各种运算操作,需要注意:

  • 所有操作数都是 32 位,可以是寄存器或立即数
  • 如果数据操作有结果,结果也为 32 位,放在目的寄存器
  • 指令使用“两操作数”或“三操作数”方式,即每一个操作数寄存器和目的寄存器分别指定
  • 数据处理指令只能对寄存器的内容进行操作;指令后都可以选择 S 后缀来影响标志位
  • 比较指令不需要后缀 S,这些指令执行后都会影响标志位

这里涉及到的指令比较多,而且具有较明显的共性,这里就不再一一介绍了。

这些指令默认不会影响标志位:

  • MOVMVN
  • ADDSUBRSB
  • ADCSBCRSC
  • ANDORREORBIC

下面是一些比较指令,也不详细展开了:

  • CMPCMNTSTTEQ

程序状态寄存器处理指令

这里主要涉及两条指令,MRSMSR,它们用于在状态寄存器和通用寄存器间传输数据。下面是一个基本的状态寄存器修改步骤:

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

加载/存储指令

加载/存储指令用于在寄存器和存储器之间传输数据。

最基础的是 LDRSTR,它们分别用于将 32 位字数据传输到目的寄存器和从源寄存器中将一个 32 位字数据写入存储器中:

LDR     R1,[R0,#0x12]   /* 将存储器地址为 R0 + 0x12 的字数据写入 R1 */
STR     R0,[R1,#12]     /* 将 R0 中的字数据写入以 R1 + 12 为地址的存储器中 */

接下来的是 LDMSTM 指令,它们实现了一组寄存器和一片连续存储空间之间的数据传输,其中 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 处理器有两条异常产生指令:

  1. SWI:软中断指令3
  2. BKPT:断点中断指令

  1. 现在主流的 ARM 指令集(AArch64)基本没有这个机制了,更推荐使用专门的条件指令。 ↩︎

  2. 这个后缀放在条件码的后面。 ↩︎

  3. 现在已被 SVC 取代。 ↩︎