ARM统一汇编语言指令集
从立即数编码、条件执行到桶形移位器,逐条拆解 ARMv7-A 指令集的底层机制与设计决策。
1. 概览
ARMv7-A 架构引入了一个重要概念:统一汇编语言(UAL, Unified Assembly Language)。在此之前,ARM 代码和 Thumb 代码使用两套语法,编写和维护都极其繁琐。UAL 通过一种统一的语法形式,同时覆盖 ARM 32 位指令集和 Thumb-2 16/32 位混合指令集。
从功能视角看,ARM 汇编指令可以分为以下类别:
- 数据处理指令:
ADD、SUB、AND、ORR、MOV等 —— 算术和逻辑运算 - 内存访问指令:
LDR、STR、LDM、STM—— 寄存器和内存之间的数据搬运 - 分支指令:
B、BL、BX、CBZ等 —— 控制流跳转 - 饱和算术指令:
QADD、QSUB、SSAT—— 数字信号处理核心 - 杂项/系统指令:协处理器、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。多数指令只有两个操作数(ADD 和 SUB 可有三个)。只有分支指令才可以条件执行。
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 | MOVW R0, #:lower16:label |
MOVW 将 label 的低 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 | CMP R0, R1 |
用条件执行写同样的逻辑:
1 | CMP R0, R1 |
后者更短小,而且在老式 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 标志(溢出会破坏符号位的含义)
HI和CS的区别在于是否排除相等情况AL是默认值,不需要显式写出
Thumb-2 的 IT 指令:Thumb 模式下没有 ARM 那样”每条指令自带条件码”的编码空间。Thumb-2 通过 IT{x{y{z}}} 指令为后续 1~4 条指令赋予条件执行能力,其中 x/y/z 用 T(Then)或 E(Else)表示条件取反。例如:
1 | ITT EQ |
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 后缀:ADDS、MOVS、SUBS。比较指令 CMP、CMN、TST、TEQ 唯一的副作用就是设置标志——它们隐式带有 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 - R1,SUB 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 | ADD R0, R1, #1 @ R0 = R1 + 1 |
一个经典用法是快速乘法: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 | @ 32-bit 乘法:R0 = R1 × R2(结果放在单一寄存器) |
MUL 是最简单的 32×32 → 32 乘法。MLA 把乘积累加到第三个寄存器——这是 FIR 滤波器内积运算的标准构件。UMLAL 则把 32×32 乘积累加到 64 位寄存器对 (R3,R2),是长整数算术的基本单元。
UMULL 和 UMLAL 在实现大整数运算时不可或缺——两个寄存器的串联构成了 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/PKHTB 和 UXTH/UXTB 系列让 SIMD 流水线真正成型——先用双字加载(LDRD)从内存读入密集打包数据,解包为独立寄存器,并行处理,再用 PKHBT 打包回寄存器,最后用双字存储(STRD)写回内存。
SIMD 的打包/解包流水线最终依赖的正是 LDRD 和 STRD —— 双字(64 位)加载与存储。这自然引出下一个主题:ARM 如何处理寄存器和内存之间的数据搬运。
4. 内存访问指令
ARM 是 Load/Store 架构。ALU 不能直接操作内存——必须先用 LDR 把数据搬进寄存器,运算完再用 STR 写回去:
1 | LDR R0, [R1] @ 从 R1 指向的地址读 32 位到 R0 |
操作宽度用后缀控制: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.
无符号加载(LDRB、LDRH)将高位清零;有符号加载(LDRSB、LDRSH)将符号位复制到高位。
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 | LDR R0, [R1, #32]! @ 预索引+回写:从 [R1+32] 加载,然后 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 | STMFD sp!, {r0-r5} @ 压栈(等同于 PUSH {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 | @ 简单相对跳转 |
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 | MOV R0, #1 @ STDOUT 文件描述符 |
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 | @ 读取 CPSR 到 R0,检查当前模式位 |
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 | @ 将 R1 的 bits[7:0] 插入 R0 的 bits[15:8] |
BFI 常用于构造硬件寄存器写入值——从分散的字段合成一个字。BFC 是 BIC 的推广:一次操作可以清零任意位置的连续位域。RBIT 在 CRC 校验等位序相关算法中特别有用。
6.4 缓存预取与字节反转
PLD/PLI:PLD 提示数据缓存将访问指定地址;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. 总结
ARM 立即数不是随便写的。只有形如”8 位值循环右移偶数位”的常数才能单指令编码。遇到非法立即数时,用
MOVW+MOVT或文字池加载。汇编器会自动选择策略,但理解底层机制有助于调试”invalid constant”错误。条件执行是双刃剑。在无分支预测的老处理器上是最优选择;在 Cortex-A9 等现代处理器上,分支预测器可能更快。性能取舍不再有固定答案——需要根据目标处理器做 profiling 判断。
S 后缀控制标志设定。
ADD不影响标志,ADDS影响。CMP/TST等比较指令必须设标志——这是它们的唯一目的。Load/Store 指令永远不动标志。桶形移位器是免费的。移位操作不独立占指令槽——它能融入 ALU 操作的 Operand2 中,单周期完成”移位+运算”。这个设计在实现快速乘法和数组索引时特别高效。
多寄存器传输的顺序由硬件决定。
LDMIA R0, {R3, R0, R1}的映射顺序是 R0→最低地址、R1→中间地址、R3→最高地址——与你写的顺序无关。ARM 栈统一用 FD(Full Descending)。
STMFD= 压栈,LDMFD= 出栈。PUSH/POP在 UAL 中是为此约定的语法糖。整数 SIMD 不等于 NEON。ARMv6 的整数 SIMD 在 32 位通用寄存器内操作,延迟低但吞吐量有限。NEON 有独立寄存器组和更宽的向量宽度。先用 SIMD 写,瓶颈后再迁到 NEON 是务实的路径。
SVC 是唯一的合法 OS 入口。用户态不能直接调用内核函数——必须通过
SVC #0触发异常进入特权态。Linux 用 R7 传系统调用号。饱和算术的 Q 标志是 sticky 的。一旦溢出,保持置位直到你显式写 CPSR 清除它。检查一次 Q 标志可以知道整个计算序列中是否出现过溢出。
PLD/PLI 是暗示不是命令。它们不产生异常——即使传了非法地址也不会数据中止。但它们能显著改善缓存的预取行为。
与 x86 的关键差异:ARM 是 Load/Store 架构,x86 的 ALU 可以直接操作内存;ARM 的条件执行覆盖几乎所有指令,x86 只有条件跳转;ARM 的桶形移位器融合了移位和运算,x86 需要单独的移位指令。
统一汇编语言(UAL)是实用指南的核心价值。同一套语法同时覆盖 ARM 32 位和 Thumb-2 16/32 位指令集——不需要记忆两套不同的写法。这对现代的 Cortex-A 程序员来说是巨大的生产力提升。
本文内容整理自 ARM Cortex-A Series Programmer’s Guide (Version 4.0, DEN0013_0400_en) 第六章。