ARM统一汇编语言指令集

从立即数编码、条件执行到桶形移位器,逐条拆解 ARMv7-A 指令集的底层机制与设计决策。

1. 概览

ARMv7-A 架构引入了一个重要概念:统一汇编语言(UAL, Unified Assembly Language)。在此之前,ARM 代码和 Thumb 代码使用两套语法,编写和维护都极其繁琐。UAL 通过一种统一的语法形式,同时覆盖 ARM 32 位指令集和 Thumb-2 16/32 位混合指令集。

从功能视角看,ARM 汇编指令可以分为以下类别:

  • 数据处理指令ADDSUBANDORRMOV 等 —— 算术和逻辑运算
  • 内存访问指令LDRSTRLDMSTM —— 寄存器和内存之间的数据搬运
  • 分支指令BBLBXCBZ 等 —— 控制流跳转
  • 饱和算术指令QADDQSUBSSAT —— 数字信号处理核心
  • 杂项/系统指令:协处理器、SVC、缓存预取、字节反转等

阅读本章之前,需要先熟悉 ARM 处理器模式、寄存器组(R0-R15、CPSR/SPSR)以及汇编语法基础。

本章近一半的篇幅都在讨论指令集的三个基础构件:立即数编码、条件执行和状态标志。这三个概念是理解后续所有指令族的前提。

1.1 ARM 与 Thumb 代码的识别

拿到一段 ARM 汇编代码,先判断它编译为 32 位 ARM 指令还是 16/32 位 Thumb 指令,这是阅读和调试的第一步。有以下几类典型特征可做判断:

传统 ARM 汇编(32 位指令):操作数可以有三到四个(如 ADD R0, R1, R2),非分支指令也可带条件后缀(如 ADDNE R0, R0, #1),文件名通常为 .s.S

UAL 统一汇编:文件头部有 .syntax unified 伪指令。语法上看起来和传统 ARM 汇编相似,但条件执行必须用 IT 指令包裹。根据 .thumb.arm 伪指令的不同,会被汇编为 32 位 ARM 指令或 16/32 位混合 Thumb 指令。

16 位 Thumb 汇编:包含 .code 16.thumb.thumb_func 伪指令,但没有 .syntax unified。多数指令只有两个操作数(ADDSUB 可有三个)。只有分支指令才可以条件执行。

GCC 内联汇编:C/C++ 源文件中的内联汇编(.c.h.cpp 等),构造为 ARM 还是 Thumb 取决于 GCC 的命令行参数 -marm-mthumb

此外,运行时区分 ARM 和 Thumb 状态有两种方法:

  • CPSR 的 T 位:T = 0 表示 ARM 状态,T = 1 表示 Thumb 状态
  • BX/BLX 的 bit[0] 约定:所有函数指针的 bit[0] 在 Thumb 状态下必须为 1,ARM 状态下必为 0。BX Rm 根据 Rm 的 bit[0] 决定目标指令集,这也是反汇编工具判断状态的依据

2. 指令集基础要素

任何架构的汇编语言都有几个”元规则”——掌握了它们,其余指令不过是规则的组合与应用。对 ARMv7-A 而言,这三个元规则是:立即数编码方式、条件执行机制、以及四位状态标志。本章先逐一拆解这三个基石,为后续所有指令族铺平道路。

2.1 立即数编码

ARM 和 Thumb 指令长度只有 16 或 32 位。拿 32 位的 ARM 指令来说,条件码(4 位)、操作码、源/目的寄存器已经占去大半空间,留给立即数的只有区区 12 位。

ARM or Thumb assembly language instructions have a length of only 16 or 32 bits. This presents something of a problem. It means that you cannot encode an arbitrary 32-bit value within the opcode.

如果用 12 位直接表示常数,范围只有 -2048 到 +2047,实在不够用。ARM 的设计方案很巧妙:12 位 = 8 位立即数 + 4 位循环右移位数。4 位右移位数的步长为 2(即 0, 2, 4, …, 30),所以可以用 8 位数值构造出分散在高位的常数。

实例:要生成 0x23000000,表达为 0x23 ROR 8 即可。ARM 汇编器会自动帮我们做这个转换。

<0xFF 这种值没办法在单条指令中产生,因为它不满足”8 位值循环右移偶数位”的约束。遇到这种情况,要么用多指令拼接,要么从内存中的文字池(literal pool)加载。

Thumb-2 对立即数的支持更丰富,允许三种额外模式:

  • 0x00XY00XY 形式(如 0x00FF00FF
  • 0xXY00XY00 形式
  • 0xXYXYXYXY 形式(如 0xFFFFFFFF 用于 MVN

MOVW 和 MOVT 的引入是 ARMv7-A 的重大改进。MOVW(Move Wide)将 16 位常数装入寄存器低 16 位并清零高 16 位;MOVT(Move Top)将 16 位常数装入高 16 位,不影响低 16 位。两者配合就能构造出任意 32 位常数:

1
2
MOVW R0, #:lower16:label
MOVT R0, #:upper16:label

MOVWlabel 的低 16 位地址装入 R0 并清零高 16 位;MOVT 再把高 16 位地址写入 R0[31:16],不改变 R0 的低半部分。两条指令配合,不耗任何内存读取即可构造完整 32 位地址。

在此之前,唯一的手段是伪指令 LDR Rn, =<constant>,汇编器会自行选择用 MOV/MVN 编码还是从文字池加载。

反事实推理:如果没有 MOVW/MOVT,加载一个 32 位地址常数就需要从文字池做一次内存读取。这不仅占用内存空间(Literal Pool 区域),还在数据缓存中产生一次访问——对频繁访问缓存的代码路径来说,这是完全可以避免的开销。

2.2 条件执行

A feature of the ARM instruction set is that nearly all instructions can be conditional. On most other architectures, only branches or jumps can be executed conditionally.

这是 ARM 与 x86 最核心的差异之一。绝大多数 ARM 指令都可以附加一个两位字母的条件后缀,当条件满足时才执行。而 x86 只有 Jcc(条件跳转)指令有类似能力。

考虑一段”比较 R0 和 R1,把较小值放入 R2”的代码。用传统分支写法:

1
2
3
4
5
6
7
CMP     R0, R1
BLT .Lsmaller
MOV R2, R1
B .Lend
.Lsmaller:
MOV R2, R0
.Lend:

用条件执行写同样的逻辑:

1
2
3
CMP     R0, R1
MOVGE R2, R1 @ 若 R0 >= R1,R2 = R1
MOVLT R2, R0 @ 若 R0 < R1,R2 = R0

后者更短小,而且在老式 ARM 处理器(如 ARM7TDMI,三级流水线)上也更快——因为规避了流水线刷新(pipeline flush)的代价。

但需要注意:在 Cortex-A9 这类现代处理器上,条件执行未必比分支快。原因有三:第一,指令间数据依赖可能导致更长的停滞;第二,分支预测器可以大幅消减分支成本;第三,分支预测命中时,条件执行的指令即使被跳过也占用了发射槽位。

完整的 15 组条件码如下:

后缀 含义 标志条件 助记
EQ Equal Z = 1 相等
NE Not equal Z = 0 不等
CS / HS Carry Set / Unsigned higher or same C = 1 无符号 >=
CC / LO Carry Clear / Unsigned lower C = 0 无符号 <
MI Minus N = 1 结果为负
PL Plus N = 0 结果非负
VS Overflow V = 1 有符号溢出
VC No overflow V = 0 无符号溢出
HI Unsigned higher C=1 AND Z=0 无符号 >
LS Unsigned lower or same C=0 OR Z=1 无符号 <=
GE Signed >= N = V 有符号 >=
LT Signed < N != V 有符号 <
GT Signed > Z=0 AND N=V 有符号 >
LE Signed <= Z=1 OR N!=V 有符号 <=
AL Always 默认条件(可省略)

要点总结:

  • 无符号比较只用 C 和 Z 标志
  • 有符号比较需要同时考察 N 和 V 标志(溢出会破坏符号位的含义)
  • HICS 的区别在于是否排除相等情况
  • AL 是默认值,不需要显式写出

Thumb-2 的 IT 指令:Thumb 模式下没有 ARM 那样”每条指令自带条件码”的编码空间。Thumb-2 通过 IT{x{y{z}}} 指令为后续 1~4 条指令赋予条件执行能力,其中 x/y/zT(Then)或 E(Else)表示条件取反。例如:

1
2
3
ITT   EQ
SUBEQ r1, r1, #1
ADDEQ r0, r0, #60

IT 块通常由汇编器自动生成,但在阅读反汇编输出时经常遇到。

2.3 状态标志

每条可设定标志的指令都直接影响 CPSR(Current Program Status Register)中的四个标志位:

标志 位号 名称 设置逻辑
N 31 Negative 等于结果的 bit[31](对有符号数来说就是符号位)
Z 30 Zero 结果为零时置 1
C 29 Carry 无符号运算的进位/借位,或移位操作中最后移出的位
V 28 Overflow 有符号运算的溢出标志

理解 C 和 V 各自对应的”算术世界”很关键:

  • C 标志服务于无符号运算。比如 0xFFFFFFFF + 0x1 = 0x00000000,无符号意义上溢出了,C=1。C 标志常常用于实现 64 位算术——用两条 32 位 ADC/SBC 指令串起来。
  • V 标志服务于有符号运算。比如 0x7FFFFFFF + 0x2 = 0x80000001,有符号意义上正数加正数得到了负数,V=1。

设置标志的方法是给指令附加 S 后缀:ADDSMOVSSUBS。比较指令 CMPCMNTSTTEQ 唯一的副作用就是设置标志——它们隐式带有 S 语义,不要再加后缀。

需要注意的是:Load 和 Store 指令从不设定标志

有了这三个基石的知识铺垫——立即数编码规则、15 组条件码、以及 N/Z/C/V 标志的设置逻辑——现在可以进入 ARM 汇编中数量最多的一族:数据处理指令。

3. 数据处理指令

ARM 核心只能对寄存器执行 ALU 操作,不能直接操作内存。数据处理指令的基本格式是:

1
Operation{cond}{S} Rd, Rn, Operand2

其中 {cond} 是可选的 15 组条件码,{S} 表示是否更新 CPSR 标志。

完整的数据处理指令表如下:

操作码 功能 公式
ADC 带进位加 Rd = Rn + Op2 + C
ADD 加法 Rd = Rn + Op2
MOV 传送 Rd = Op2
MVN 取反传送 Rd = ~Op2
RSB 反向减 Rd = Op2 - Rn
RSC 带借位反向减 Rd = Op2 - Rn - !C
SBC 带借位减 Rd = Rn - Op2 - !C
SUB 减法 Rd = Rn - Op2
AND 按位与 Rd = Rn & Op2
BIC 位清除 Rd = Rn & ~Op2
EOR 异或 Rd = Rn ^ Op2
ORR 按位或 Rd = Rn | Op2
CMP 比较 Rn - Op2(仅更新标志)
CMN 比较负 Rn + Op2(仅更新标志)
TEQ 测试相等 Rn ^ Op2(仅更新标志)
TST 测试位 Rn & Op2(仅更新标志)

以下指令需要特别关注:

RSB(Reverse Subtract) 的存在是因为 Op1 只能是一个寄存器。要写 R0 = 100 - R1SUB R0, #100, R1 是非法的(第一操作数不能是立即数),必须用 RSB R0, R1, #100

BIC(Bit Clear) 是一个非常优雅的位清除手段。比如要清除 R0 的 bit[11]:BIC R0, R0, #0x800。#0x800 的只有 bit[11] 为 1,取反后只有 bit[11] 为 0,AND 操作后 bit[11] 就被清零了。

ORR 而非 OR:三个字母的助记符是历史遗留——最初的 ARM 指令集中所有数据处理操作都是三字母。这个约定延续至今。

3.1 桶形移位器与第二操作数

数据处理指令的 Operand2 可以不仅仅是寄存器或立即数。桶形移位器(Barrel Shifter)允许在第二个操作数进入 ALU 之前,先做一次移位/旋转。五种移位操作如下:

操作 全称 行为
LSL Logical Shift Left 左移,低位补 0
LSR Logical Shift Right 右移,高位补 0(无符号)
ASR Arithmetic Shift Right 右移,高位补符号位(有符号)
ROR Rotate Right 循环右移
RRX Rotate Right with Extend 带 C 标志的循环右移

这意味着 ARM 汇编中不存在独立的移位指令。MOV R0, R1, LSL #2 就是 R0 = R1 << 2。更重要的是,移位可以与算术运算融合在一条指令中:

1
2
3
4
ADD     R0, R1, #1             @ R0 = R1 + 1
ADD R0, R1, R2 @ R0 = R1 + R2
ADD R0, R1, R2, LSL #4 @ R0 = R1 + (R2 << 4)
ADD R0, R1, R2, LSR R3 @ R0 = R1 + (R2 >> R3)

一个经典用法是快速乘法:ADD R0, R0, R0, LSL #2 等价于 R0 = R0 * 5。这是因为 R0 × 5 = R0 + (R0 × 4) = R0 + (R0 << 2)。

又如数组索引计算。假设 R1 指向一个 int 数组的首地址,R2 是下标(0-based)。那么 LDR R0, [R1, R2, LSL #2] 就能直接取到 arr[R2],因为地址偏移 = R2 × sizeof(int) = R2 × 4。

对比 x86:x86 汇编中移位和算术运算通常是分开的——SHL + ADD 需要两条指令。ARM 的桶形移位器把两步合为一步,在高密度信号处理代码中优势明显。

3.2 乘法操作

ARM 乘法操作的最大限制是不能使用立即数——除数必须是寄存器。核心乘法指令如下:

操作码 功能 公式
MUL 32位乘法 Rd = Rn × Rm
MLA 乘加 Rd = Ra + (Rn × Rm)
MLS 乘减 Rd = Ra - (Rn × Rm)
UMULL 无符号 64 位乘法 RdHiLo = Rn × Rm
SMULL 有符号 64 位乘法 RdHiLo = Rn × Rm
UMLAL 无符号 64 位乘加 RdHiLo += Rn × Rm
SMLAL 有符号 64 位乘加 RdHiLo += Rn × Rm

64 位乘法需要两个目标寄存器(RdLo 和 RdHi)来存放结果。下面用具体寄存器值演示几种常见乘法模式:

1
2
3
4
5
6
7
8
9
@ 32-bit 乘法:R0 = R1 × R2(结果放在单一寄存器)
MUL R0, R1, R2

@ 乘加:R0 = R2 + (R0 × R1)
MLA R0, R0, R1, R2

@ 无符号 64-bit 乘加:(R3,R2) = (R3,R2) + R4 × R5
@ R2 存低 32 位,R3 存高 32 位
UMLAL R2, R3, R4, R5

MUL 是最简单的 32×32 → 32 乘法。MLA 把乘积累加到第三个寄存器——这是 FIR 滤波器内积运算的标准构件。UMLAL 则把 32×32 乘积累加到 64 位寄存器对 (R3,R2),是长整数算术的基本单元。

UMULLUMLAL 在实现大整数运算时不可或缺——两个寄存器的串联构成了 64 位累加器。

扩展乘法:ARMv6 之后增加了 SMMUL/SMMLA/SMMLS 系列,它们执行 32 × 32 乘法,但只保留高 32 位结果。加 R 后缀表示舍入而非截断。UMAAL(Unsigned Multiply Accumulate Accumulate Long)则执行 32 × 32 乘法后,再将两个 32 位寄存器的值相加——一次完成乘法+两次累加。

3.3 整数 SIMD 指令

These must not be confused with the significantly more powerful Advanced SIMD (NEON) operations that were introduced in the ARMv7 architecture.

ARMv6 引入了整数 SIMD 指令,让单个 32 位寄存器可以同时容纳 4 个 8 位或 2 个 16 位数据,并行处理。这不是 NEON——NEON 拥有独立的 64/128 位寄存器组,性能远超这里的”轻量 SIMD”。但整数 SIMD 的低延迟特性使其在简单 DSP 场景中仍有吸引力。

这些指令使用 CPSR 中的 GE[3:0] 标志(与常规 N/Z/C/V 标志独立),四个标志分别对应字内四个字节的溢出状态。典型指令包括:

  • **SADD16/UADD16**:两对 16 位半字分别做有符号/无符号加法
  • **SSUB8/USUB8**:四对 8 位字节分别做有符号/无符号减法
  • **QADD16/QSADD16**:饱和版 16 位加法
  • ASX/SAX:交换一个操作数的半字,然后并行加/减
  • **SEL**:根据 GE 标志,逐个字节从两个源中选择

SMUAD/SMUSD 系列更强大——它们在一个寄存器内完成两对 16 位乘法的SMUSD(SIMD Multiply and Subtract)被用来做复数乘法:top x top - bottom x bottom,恰好对应于复数乘积的实部计算。

另一个关键指令是 USADA8(Sum of Absolute Differences),它在视频编码的运动估计中不可或缺。一次指令完成:四个字节的绝对差求和 + 累加到累加器。指令 USADA8 Rd, Rn, Rm, Ra 把 Rn 和 Rm 的四个对应字节分别求差、取绝对值、求和,再累加 Ra 的值。

数据打包/解包PKHBT/PKHTBUXTH/UXTB 系列让 SIMD 流水线真正成型——先用双字加载(LDRD)从内存读入密集打包数据,解包为独立寄存器,并行处理,再用 PKHBT 打包回寄存器,最后用双字存储(STRD)写回内存。

SIMD 的打包/解包流水线最终依赖的正是 LDRDSTRD —— 双字(64 位)加载与存储。这自然引出下一个主题:ARM 如何处理寄存器和内存之间的数据搬运。

4. 内存访问指令

ARM 是 Load/Store 架构。ALU 不能直接操作内存——必须先用 LDR 把数据搬进寄存器,运算完再用 STR 写回去:

1
2
LDR    R0, [R1]         @ 从 R1 指向的地址读 32 位到 R0
STR R0, [R1] @ 把 R0 的值写入 R1 指向的地址

操作宽度用后缀控制:B = 字节、H = 半字、D = 双字(64位)。对加载而言,额外的 S 表示有符号扩展——LDRSB 加载一个字节并将符号扩展到全 32 位。

if you load an 8-bit or 16-bit quantity into a 32-bit register you must decide what to do with the most significant bits of the register.

无符号加载(LDRBLDRH)将高位清零;有符号加载(LDRSBLDRSH)将符号位复制到高位。

4.1 寻址模式

ARM 提供了五种寻址方式:

模式 语法 行为
寄存器寻址 LDR Rd, [Rn] 直接用 Rn 的值作为地址
预索引 LDR Rd, [Rn, Op2] 地址 = Rn + Op2,Rn 不变
预索引 + 回写 LDR Rd, [Rn, Op2]! 地址 = Rn + Op2,然后 Rn = Rn + Op2
后索引 + 回写 LDR Rd, [Rn], Op2 地址 = Rn,然后 Rn = Rn + Op2
PC 相对 LDR Rd, label 汇编器转换为 [PC, #offset]

注意 !] 的区别极为关键:

1
2
LDR     R0, [R1, #32]!     @ 预索引+回写:从 [R1+32] 加载,然后 R1 += 32
LDR R0, [R1], #32 @ 后索引+回写:从 [R1] 加载,然后 R1 += 32

[R1, #32]!! 放在方括号内,表示先用 R1+32 做地址,执行完再写回 R1;[R1], #32 的偏移在括号外,表示直接用 R1 做地址,执行完再写回 R1。前者常用于顺序遍历数组(每次基址前进),后者常用于从固定位置反复读取(基址不动,偏移单独递增)。

Op2 可以是立即数、寄存器、或带移位操作的寄存器——与数据处理指令的 Operand2 共享同一个桶形移位器。

4.2 多寄存器传输与栈操作

LDM(Load Multiple)和 STM(Store Multiple)可以一次性读/写连续内存中的多个 32 位字。操作数包含基址寄存器(可选 ! 回写)和花括号包裹的寄存器列表:

1
LDMIA   R10!, { R0-R3, R12 }

关键要点:寄存器的内存映射顺序由寄存器编号(而非列表中的书写顺序)决定。编号最小的寄存器映射到最低地址。上面这条指令中,R0 被写入最低地址,R12 被写入最高地址,R10 最后自增 20(5 × 4 字节)。

访问方向有四种变体:

变体 含义 栈别名
IA Increment After FD (Full Descending)
IB Increment Before FA (Full Ascending)
DA Decrement After ED (Empty Descending)
DB Decrement Before EA (Empty Ascending)

ARM 系统统一采用 FD(Full Descending)栈约定,即栈指针指向最后一个已占用的栈槽,向低地址增长。对应的指令对:

1
2
STMFD   sp!, {r0-r5}     @ 压栈(等同于 PUSH {r0-r5})
LDMFD sp!, {r0-r5} @ 出栈(等同于 POP {r0-r5})

By convention, only the Full Descending (FD) option is used for stacks in ARM processor based systems.

栈操作的内存布局:压栈前 SP 指向最后一个被占用的字;STMFD sp!, {R1, R2} 执行后,SP 减 8(两个字),R1 存入低地址,R2 存入高地址。


5. 分支与饱和算术

数据处理和数据搬运之后,下一个自然的问题是:程序的控制流如何跳转?ARM 提供了从简单 PC 相对跳转到表驱动 switch 的完整分支体系。而饱和算术虽然属于数据处理范畴,但它与音频视频编码的控制逻辑密不可分——许多编解码器的核心循环中,分支和饱和运算交替出现,因此将两者放在同一章讨论。

5.1 分支指令

ARM 分支指令的家族比较庞大:

指令 用途
B label 无条件跳转(PC 相对偏移)
BL label 子程序调用(LR = 返回地址,然后跳转)
BX Rm 跳转并可切换指令集(ARM ↔ Thumb,由 Rm 的 bit[0] 决定)
BLX label/Rm 调用 + 切换指令集
CBZ Rn, label Thumb 专属:Rn == 0 则跳转(+4~130 字节前向范围)
CBNZ Rn, label Thumb 专属:Rn != 0 则跳转
TBB [Rn, Rm] Thumb 表分支(字节偏移表)
TBH [Rn, Rm, LSL #1] Thumb 表分支(半字偏移表)

下面几行给出典型用法:

1
2
3
4
5
6
7
8
9
10
11
12
@ 简单相对跳转
B skip @ 无条件跳到 label skip

@ 子程序调用(返回地址自动存入 LR)
BL strlen @ LR = PC+4, 跳转到 strlen

@ Thumb 下的零值检测分支(不修改 CPSR)
CBZ R2, notfound @ 若 R2 == 0, 跳到 notfound
CBNZ R3, process @ 若 R3 != 0, 跳到 process

@ ARM/Thumb 切换 - BX 通过 Rm 的 bit[0] 决定目标指令集
BX LR @ 从 bit[0] 判断返回 ARM 还是 Thumb 态

BX 指令的核心机制:ARM 状态下 bit[0] 总是 0,Thumb 状态下 bit[0] 总是 1。BX Rm 通过检查 Rm 的 bit[0] 来决定跳转到 ARM 还是 Thumb 状态,实际跳转地址会取 bit[0]=0 的值。这也是 BLX 把返回地址的 bit[0] 设为 1 的原因——确保 BX LR 能正确返回 Thumb 调用者。

CBZ/CBNZ 是比较和分支的融合指令:一次完成零值检验+条件跳转,不修改 CPSR 标志。

TBB/TBH 用于实现 switch-case。偏移表紧跟在指令后,指令根据索引读出偏移量,将 PC 前移 2×偏移量。字节表(TBB)支持 256 个分支目标(0-510 字节跳转范围),半字表(TBH)支持更大范围。

5.2 饱和算术指令

Saturated arithmetic is commonly used in audio and video codecs.

32 位加法中,0x7FFFFFFF + 1 = 0x80000000 是一个合法的算术结果,但数学意义上它是不对的(两个正数相加得负数)。在音频处理中,这种溢出会产生刺耳的爆音。饱和算术说:0x7FFFFFFF + 1 = 0x7FFFFFFF(保持最大值)。

核心指令:

指令 行为
QADD 饱和加法(word)
QSUB 饱和减法(word)
QDADD 二操作数加倍后饱和加
QDSUB 二操作数加倍后饱和减
QADD8 / QADD16 并行饱和加法(字节/半字)
SSAT 有符号饱和到指定位宽
USAT 无符号饱和到指定位宽

溢出时设置 CPSR 的 Q 标志(sticky——一旦置位就保持到显式写 CPSR 清除)。

QDADD/QDSUB 是为 Q15 或 Q31 定点算术设计的:先将第二操作数×2,再饱和加/减。这是定点 FFT 蝶形运算中频繁出现的模式。

SSAT/USAT 可以将任意值饱和到一个指定位宽。比如 SSAT R0, #12, R1 将 R1 饱和到有符号 12 位范围 [-2048, 2047]。SSAT16/USAT16 则对寄存器内的两路 16 位半字同时应用饱和。

CLZ(Count Leading Zeros)虽然不是严格意义上的饱和指令,但与归一化算法紧密相关——很多浮点模拟实现先用 CLZ 确定前导零个数,再用移位操作做位宽归一化。


6. 杂项指令

前面各章覆盖了 ARM 指令集的主流类别。本章汇总余下的杂项指令,包括协处理器、SVC、PSR 操作、位域操作、缓存预取以及字节反转等,适合按需查阅。

6.1 协处理器指令

ARM 支持最多 16 个协处理器(CP0-CP15),通过专用接口连接。在 Cortex-A 系列中,CP15 是系统控制协处理器(缓存/MMU 配置),CP14 是硬件调试协处理器,CP10/CP11 是 VFP/NEON。

五种协处理器指令类型:

  • CDP:启动协处理器数据处理
  • MCR/MRC:ARM 寄存器 ↔ 协处理器寄存器
  • STC/LDC:协处理器寄存器 ↔ 内存
  • MCRR/MRRC:一对 ARM 寄存器 ↔ 协处理器寄存器
  • STCL/LDCL:协处理器多寄存器 ↔ 内存

执行不存在的协处理器指令会触发未定义指令异常——操作系统可以借此做软件仿真(比如在没有 VFP 硬件的系统中模拟浮点运算)。

6.2 SVC 超级调用指令

SVC(Supervisor Call)是用户态代码请求操作系统服务的唯一合法入口。指令中内嵌一个 24 位(ARM)或 8 位(Thumb)的数字,SVC 异常处理程序可以通过读取指令编码来获取这个参数。

Linux 系统调用的标准调用约定:

1
2
3
4
5
MOV     R0, #1              @ STDOUT 文件描述符
ADR R1, msgtext @ 字符串地址
MOV R2, #13 @ 字符串长度
MOV R7, #4 @ sys_write 系统调用号
SVC #0 @ 进入内核

R7 存放系统调用号,R0-R6 存放参数。SVC 处理程序(在异常向量表中)检查指令中的立即数并分发到对应内核函数。

This instruction was originally called SWI (Software Interrupt). ——历史命名能帮你理解旧的代码库。

6.3 PSR 修改与位域操作

PSR 访问

  • MRS Rd, CPSR/SPSR:将状态寄存器值读入通用寄存器
  • MSR CPSR/SPSR_f, Rn:将通用寄存器值写入状态寄存器(_f 表示仅修改标志域)
  • 用户在 User 模式只能修改条件标志;特权模式可以修改整个 PSR
  • CPS:在特权模式下直接切换模式或开关中断(I/F 位)
  • SETEND:动态切换小端/大端数据访问(仅修改 E 位)
1
2
3
4
5
6
7
@ 读取 CPSR 到 R0,检查当前模式位
MRS R0, CPSR
AND R0, R0, #0x1F @ 提取 mode bits[4:0]

@ 切换到 IRQ 模式并禁用 FIQ
CPSID if @ 注意: CPS 只能在特权模式使用
NOP @ 填充 (CPS 需在特权模式执行)

MRS/MSR 是上下文切换中保存/恢复标志和模式位的核心指令。CPS 提供比 MSR 更简洁的中断开关方式,但仅对 FIQ 和 IRQ 两个中断位有效。

位域操作

  • BFI(Bit Field Insert):从源寄存器底部取出指定位宽(width+LSB),插入目标寄存器任意位置
  • BFC(Bit Field Clear):将寄存器内连续的若干位清零
  • SBFX/UBFX(Bit Field Extract):取出连续位并做有符号/无符号扩展到 32 位
  • RBIT:将寄存器内 32 位的顺序完全反转(bit 31 ↔ bit 0, bit 30 ↔ bit 1, …)
1
2
3
4
5
6
7
8
@ 将 R1 的 bits[7:0] 插入 R0 的 bits[15:8]
BFI R0, R1, #8, #8 @ width=8, lsb=8

@ 清除 R2 的 bits[11:0]
BFC R2, #0, #12 @ lsb=0, width=12

@ 位反转: R0[31]→R1[0], R0[0]→R1[31]
RBIT R1, R0

BFI 常用于构造硬件寄存器写入值——从分散的字段合成一个字。BFCBIC 的推广:一次操作可以清零任意位置的连续位域。RBIT 在 CRC 校验等位序相关算法中特别有用。

6.4 缓存预取与字节反转

PLD/PLIPLD 提示数据缓存将访问指定地址;PLI 提示指令缓存将访问指定地址。两者都是暗示(hint)——不产生异常,实现可以将其视为 NOP。但在 Cortex-A 系列中它们能显著减少缓存缺失(cache miss)导致的停滞。

REV 系列

  • REV:反转字内四个字节的顺序(处理大小端转换)
  • REV16:反转字的每半字内的两个字节
  • REVSH:反转低半字的两个字节并做有符号扩展到 32 位

BKPT/WFI/WFE/SEV

  • BKPT:断点指令,进入调试状态或触发预取异常
  • WFI:Wait For Interrupt → 核心进入待机,中断或调试事件唤醒
  • WFE:Wait For Event → 核心进入待机,SEV 指令或中断唤醒
  • SEV:发送事件(唤醒执行了 WFE 的其他核心)
  • NOP:不保证执行时间,仅用于填充/对齐

7. 总结

  1. ARM 立即数不是随便写的。只有形如”8 位值循环右移偶数位”的常数才能单指令编码。遇到非法立即数时,用 MOVW+MOVT 或文字池加载。汇编器会自动选择策略,但理解底层机制有助于调试”invalid constant”错误。

  2. 条件执行是双刃剑。在无分支预测的老处理器上是最优选择;在 Cortex-A9 等现代处理器上,分支预测器可能更快。性能取舍不再有固定答案——需要根据目标处理器做 profiling 判断。

  3. S 后缀控制标志设定ADD 不影响标志,ADDS 影响。CMP/TST 等比较指令必须设标志——这是它们的唯一目的。Load/Store 指令永远不动标志。

  4. 桶形移位器是免费的。移位操作不独立占指令槽——它能融入 ALU 操作的 Operand2 中,单周期完成”移位+运算”。这个设计在实现快速乘法和数组索引时特别高效。

  5. 多寄存器传输的顺序由硬件决定LDMIA R0, {R3, R0, R1} 的映射顺序是 R0→最低地址、R1→中间地址、R3→最高地址——与你写的顺序无关。

  6. ARM 栈统一用 FD(Full Descending)STMFD = 压栈,LDMFD = 出栈。PUSH/POP 在 UAL 中是为此约定的语法糖。

  7. 整数 SIMD 不等于 NEON。ARMv6 的整数 SIMD 在 32 位通用寄存器内操作,延迟低但吞吐量有限。NEON 有独立寄存器组和更宽的向量宽度。先用 SIMD 写,瓶颈后再迁到 NEON 是务实的路径。

  8. SVC 是唯一的合法 OS 入口。用户态不能直接调用内核函数——必须通过 SVC #0 触发异常进入特权态。Linux 用 R7 传系统调用号。

  9. 饱和算术的 Q 标志是 sticky 的。一旦溢出,保持置位直到你显式写 CPSR 清除它。检查一次 Q 标志可以知道整个计算序列中是否出现过溢出。

  10. PLD/PLI 是暗示不是命令。它们不产生异常——即使传了非法地址也不会数据中止。但它们能显著改善缓存的预取行为。

  11. 与 x86 的关键差异:ARM 是 Load/Store 架构,x86 的 ALU 可以直接操作内存;ARM 的条件执行覆盖几乎所有指令,x86 只有条件跳转;ARM 的桶形移位器融合了移位和运算,x86 需要单独的移位指令。

  12. 统一汇编语言(UAL)是实用指南的核心价值。同一套语法同时覆盖 ARM 32 位和 Thumb-2 16/32 位指令集——不需要记忆两套不同的写法。这对现代的 Cortex-A 程序员来说是巨大的生产力提升。


本文内容整理自 ARM Cortex-A Series Programmer’s Guide (Version 4.0, DEN0013_0400_en) 第六章。