ARM汇编语言入门

深入探讨 ARM 汇编语言入门,涵盖指令集设计、GNU 汇编器以及 Interworking 的核心机制与最佳实践。

1. RISC vs CISC:ARM 指令集的架构哲学

ARM 是 RISC(精简指令集计算机)处理器。与 x86 这类 CISC(复杂指令集计算机)相比,核心差异在于:

维度 CISC (x86) RISC (ARM)
指令集规模 丰富,单条指令可完成复杂任务 精简,更通用的指令
内部实现 微码解码——指令被译为内部微操作序列 硬连线——晶体管更少,功耗更低
寄存器 较少 大量通用寄存器(16×32-bit)
指令周期 可变 大多数单周期

ARM 架构的几条根本约束塑造了所有后续设计决策:

Load/Store 架构。 数据运算指令只能操作寄存器,不能直接访问内存。要在内存中递增一个值,必须经过三步:

1
2
3
LDR  r0, [r1]       @ 从 [r1] 加载到 r0
ADD r0, r0, #1 @ 寄存器内递增
STR r0, [r1] @ 写回内存

其结果是:没有 x86 的 INC [ebx] 这样的单指令内存操作。代价是多了几条指令,收益是整个数据通路更简单、频率更高。

三操作数格式。 ARM 和 x86 的目标操作数放在最前面(68K 相反):

1
2
3
4
5
@ ARM (目标在前)
add r0, r1, r2 @ r0 = r1 + r2

@ 对比: 68K (目标在后)
ADD D0, D1 @ D0 = D0 + D1

ARM 选择目标寄存器在前的理由很实际:与 x86 Intel 语法保持一致,降低跨架构开发者的迁移成本;同时编译器中间表示(如 GCC RTL)的 (set (dest) (op src1 src2)) 恰好也是目标在左,寄存器分配策略更直观。

内嵌移位。 ARM 可以在算术指令中附带一次移位操作,零额外周期:

1
ADD  r0, r1, r2, LSL #3   @ r0 = r1 + (r2 << 3)

条件执行。 几乎所有 ARM 指令都可以条件执行(Thumb 中用 IT 指令实现),消除了短分支的流水线冲刷。Load/Store Multiple(LDM/STM)实现多寄存器批量加载,极大的加速了堆栈操作和块拷贝。

以上设计原则构成了 ARM 指令集的骨架。但 ARM 架构还有一个独特之处:它维护了两套指令集——不只是编译器选项的区别,而是硬件层面的”语言切换”。下一节展开这个机制。


2. ARM 与 Thumb 两套指令集的设计取舍

ARMv7 架构支持两套指令集,当前处理器状态由 CPSR[5](T bit) 决定:

T bit 状态 指令位宽
0 ARM state 32-bit 固定
1 Thumb state 16-bit(Thumb)/ 16+32-bit 混合(Thumb-2)

2.1 Thumb 的诞生:代码密度优先

ARM7TDMI 首次引入 Thumb 指令集。每条 Thumb 指令 16 位长,功能上是 ARM 指令集的子集。Thumb 代码体积通常比 ARM 版本小约 1/3。在物理内存有限或直连 16-bit 存储总线的系统中,Thumb 还可每周期取一条指令(ARM 需要两拍取一条 32-bit 指令)。

代价是单条指令能力弱,需要用更多条指令完成任务。手册给出了分支范围的具体对比:

分支类型 ARM Thumb-2 纯 16-bit Thumb
相对分支 ±32MB ±16MB ±2048 字节(无条件)
条件分支 ±32MB ±16MB ±256 字节

16-bit Thumb 的条件分支仅有 256 字节范围——几乎只够跳几条指令。

2.2 Thumb-2:混合长度打破两难

Cortex-A 系列全线支持的 Thumb-2 技术将 Thumb 扩展为 16-bit 和 32-bit 混合长度指令集。性能接近 ARM 的同时代码密度接近 Thumb。手册一针见血地说:*”it is increasingly common for all code to be compiled or assembled to take advantage of Thumb-2 technology.”*

汇编程序员可以用宽度修饰符选择编码:

1
2
BCS.W   label      @ 强制 32-bit 编码
B.N label @ 强制 16-bit 编码,超出范围即汇编错误

2.3 状态切换的硬件约束

从一个状态切到另一个状态时,必须同时刷新指令流水线——因为流水线中正在解码的指令可能与新状态不匹配。ARM 不会自动切换。这引出了后续章节中 Interworking 的核心问题:如何安全地改变 T bit 并保证程序继续正确执行。

在进入运行时状态切换之前,先回答一个前置问题:这些汇编指令最终是如何变成可执行代码的? 接下来的两节分别介绍 GNU Assembler 和 ARM 工具链——两种最常用的 ARM 汇编器。


3. GNU 汇编器(GAS)语法

GNU Assembler 是 GNU 工具链的汇编器,将 .s 源文件转为 .o 目标文件。

3.1 调用与链接

1
2
arm-none-eabi-as -g -o file.o file.s    # 汇编,-g 嵌入调试信息
arm-none-eabi-ld -o program.elf file.o # 链接为 ELF

工具名前缀 arm-none-eabi- 表示目标为裸机 ARM EABI 系统(无 OS)。手册指出 GNU Assembler 文档在 http://sourceware.org/binutils/docs/as/index.html 或 Ubuntu 的 /gcc-doc/ 包中。

3.2 语句格式

1
label:  instruction  @ comment

GNU Assembler 面向多架构,因此语法与 ARM 专用工具链不同:注释符为 @(ARM 工具链用 ;)。语句的三个部分(标签、指令、注释)均可选。标签后跟冒号。

3.3 段布局

用途
.text 可执行代码段(汇编指令必须在 .text 中)
.data 可读写段(已初始化全局变量)
.rodata 只读常量段
.bss 零初始化静态数据(Block Started by Symbol)

3.4 核心伪指令

伪指令以 . 开头,不生成机器码而是命令汇编器本身执行操作:

指令 功能 示例
.align n 对齐至 2^n 边界,代码段填 NOP、数据段填零 .align 2(4-byte 对齐)
.ascii "s" 插入字符串(无 NUL 终止) .ascii "Hello"
.asciz "s" 同上,加 NUL 终止 .asciz "Hello"
.word expr 插入 32-bit 值 .word 0x12345678
.byte expr 插入 8-bit 值 .byte 0xFF
.global sym 符号对链接器全局可见 .global _start
.extern sym 声明符号在其他文件 .extern printf
.equ sym, val 定义符号常量 .equ STACK_SZ, 0x1000
.macro / .endm 宏定义开始/结束
.include "f" 包含文件 .include "regs.h"

3.5 GNU Assembler 与 armasm 对照

手册表 5-1 给出了常用伪指令的完整对照(此处选出关键差异):

GNU Assembler armasm 功能
@ ; 注释
#0xFF 0xFF 立即数(GAS 中 # 可选)
.global IMPORT / EXPORT 全局可见性
.word DCD 32-bit 数据字
.byte DCB 8-bit 数据
.short DCW 16-bit 数据
.equ EQU 常量符号
.include "f" INCLUDE f 文件包含(GAS 需引号)
.macro / .endm MACRO / ENDM 宏定义
.if / .else / .endif IFDEF / ELSE / ENDIF 条件汇编

实例——同一段逻辑在两种语法下的对照:

1
2
3
4
5
6
7
8
@===== GNU Assembler =====       @===== armasm =====
.text AREA |.text|, CODE
.global main EXPORT main
main: main
LDR r0, =message LDR r0, =message
BX lr BX lr
message: message
.asciz "Hello\n" DCB "Hello\n", 0

注意注释符(@ vs ;)、段声明(.text vs AREA)、字符串定义(.asciz vs DCB)和立即数符号(=message vs =message,此处一致)的差异。

3.6 寄存器命名

GNU 工具链中支持别名(SP=R13, LR=R14, PC=R15, FP=R11)。状态寄存器通过 xPSR 及其子字段(xPSR_all, xPSR_f, xPSR_ctl 等)访问,其中 xC(当前 CPSR)或 S(保存的 SPSR)。AAPCS 进一步规定了寄存器在函数调用中的保存责任分配。

3.7 表达式

汇编器对立即数支持十进制、十六进制(0x)、二进制(0b)和单引号 ASCII 字符。算术/逻辑表达式在汇编时求值,产生绝对(位置无关)或相对值(链接时写入,如分支偏移)。

GNU Assembler 面向多架构,语法是”最小公分母”风格。ARM 自家工具链有一套更贴近 ARM 特性的语法——UAL——解决了跨 ARM/Thumb 指令集的源码统一问题。


4. ARM 工具链统一汇编语言(UAL)

UAL(Unified Assembly Language)允许同一份汇编源码在汇编时通过伪指令或命令行开关选择生成 ARM 或 Thumb 指令——不再需要维护两份源码。

4.1 语句与标签

ARM 工具链用 ; 注释,标签必须从行首开始(无首字符缩进)。无标签行需空格/Tab 开头:

1
2
3
Loop    MUL   R5, R5, R1
SUBS R1, R1, #1
BNE Loop ; 汇编器自动将偏移编码为指向 Loop

4.2 数据定义与伪指令

指令 含义 示例
DCD 32-bit 字数据 Masks DCD 0x100, 0x80, 0x40
DCB 8-bit 字节数据 MESSAGE DCB "Hello", 0
DCW 16-bit 半字数据
EQU 常量符号(不产生数据) CtrlD EQU 4
AREA 定义逻辑段,供链接器放置
ALIGN n 对齐到 2^n 边界 ALIGN 5(cache line 对齐)
END 源文件结束(不写会报错)
INCLUDE 包含文件(用于共享定义) INCLUDE regs.inc

汇编器和指令集都有了。现在回到 §3 留下的问题:ARM 和 Thumb 代码如何在同一个程序中无缝调用? UAL 统一了源码层,但运行时仍需显式切换——这就是 Interworking。


5. Interworking:ARM ↔ Thumb 运行时切换

核心问题:ARM 和 Thumb 指令集不同,处理器如何安全地从一种状态切换到另一种?答案是 BX/BLX 指令——它们不光跳转,还负责改变处理器的 T bit。

5.1 bit[0] 作为状态位

ARM 指令对齐在 4-byte 边界,Thumb 对齐在 2-byte 边界。两者的 bit[0] 都不参与实际地址计算——这是 Interworking 能运作的关键硬件前提。ARM 复用 bit[0] 来承载状态信息:

  • Rn[0] = 0 → 目标执行 ARM 指令(T bit 被清零)
  • Rn[1] = 1 → 目标执行 Thumb 指令(T bit 被置 1)
指令 全称 行为
BX Rn Branch and eXchange 跳至 Rn,按 Rn[0] 切状态
BLX Rn Branch and Link and eXchange 同上,且 LR = 返回地址
BLX label 立即数偏移版本(链接器自动补状态切换)

实例:ARM 代码调用 Thumb 函数

1
2
3
4
5
6
7
8
9
        ; ARM state (T=0)
BLX thumb_func ; LR = 返回地址 (bit[0]=0,指示返回 ARM)
; 跳转至 thumb_func,T=1,进入 Thumb 状态
; 返回后继续执行 ARM 指令
...
thumb_func PROC
; Thumb state (T=1)
BX LR ; LR bit[0]=0 → T 被清零,回到 ARM 状态
ENDP

实例:Thumb 代码调用 ARM 函数

1
2
3
4
5
6
7
8
9
        ; Thumb state (T=1)
LDR r0, =arm_func ; arm_func bit[0]=0
BLX r0 ; 切换至 ARM (T=0),LR bit[0]=1 (返回 Thumb)
; 返回后继续 Thumb
...
arm_func PROC
; ARM state (T=0)
BX LR ; LR bit[0]=1 → T 置 1,回到 Thumb 状态
ENDP

5.2 非叶子函数的返回模式

会调用子函数的非叶子函数不能直接用 BX LR 返回——LR 会被 BL sub_func 覆盖。标准模式是先压栈再弹出到 PC:

1
2
3
nonleaf  PUSH  {LR}          ; 保护返回地址
BL sub_func ; 调用子函数 (LR 被覆盖)
POP {PC} ; 恢复返回地址并跳回——POP {PC} 同样根据 bit[0] 切换状态

手册强调 LDR PC, ... 和 POP/LDM 中写入 PC 的操作同样具有 bit[0] 状态切换能力。汇编程序员需遵循 ATPCS(ARM-Thumb 过程调用标准)确保混合代码正确。

理解了 ARM ↔ Thumb 的切换机制后,在实际阅读汇编代码时还需先分辨这段代码属于哪种方言。手册 §5.6 给出了直接可用的区分方法。


6. 识别汇编代码的方言

面对不确定的 ARM 汇编源文件,手册 §5.6 给出了三条快速区分依据:

特征 传统 ARM 传统 16-bit Thumb UAL
操作数个数 3–4 多数 2(ADD/SUB 可 3) 2–4
条件执行 几乎所有指令可 *NE/*EQ 仅分支可条件 IT 指令引导条件块
关键指令 .syntax unified .code 16 / .thumb_func .syntax unified
立即数 # 可省略 可省略
文件扩展名 .s, .S .s, .S .s, .S

实例:辨别一段未知汇编代码

1
2
3
4
5
6
7
8
9
10
11
        .syntax unified
.cpu cortex-a8
func PROC
CMP r0, #0
ITTEE EQ @ 三个特征:IT 指令 = UAL
MOVEQ r1, #1 @ 条件执行靠 IT 引导而非后缀
LDREQ r2, [r3]
MOVNE r1, #0
STRNE r2, [r3]
BX lr
ENDP

判别过程:IT 指令 → 排除传统 ARM 和纯 16-bit Thumb;.syntax unified → 确认为 UAL 格式;.cpu cortex-a8 → 可编译为 ARM 或 Thumb(取决于 .arm/.thumb 指令或命令行)。

GCC 内联汇编(.c 文件中的 __asm__ 块)通过 -marm-mthumb 命令行开关选择生成 ARM 或 Thumb 代码。


7. ARMv8-A 兼容性新增指令

手册最后指出,ARMv8-A 引入后,以下 Load-Acquire / Store-Release 系列指令被反向追加到 ARMv7 ISA(在 ARMv7 中等价于 LDR/STR + DMB 组合):

指令 含义
LDA / LDAB / LDAH Load-Acquire(字/字节/半字)
STL / STLB / STLH Store-Release(字/字节/半字)
LDAEX / LDAEXB / LDAEXD / LDAEXH Load-Acquire Exclusive
STLEX / STLEXB / STLEXD / STLEXH Store-Release Exclusive

实例:用 LDA/STL 实现跨核心消息通知

1
2
3
4
5
6
7
8
9
10
11
@ Core 0 (Producer): 写入共享数据后释放
STR r1, [shared_data] @ 先写数据
DMB ISH @ 内存屏障:保证数据写入先于锁释放
MOV r0, #0
STL r0, [shared_flag] @ Store-Release:释放标志,视作"写入完成"

@ Core 1 (Consumer): 等待标志后读取数据
wait: LDA r1, [shared_flag] @ Load-Acquire:获取标志,视作"可读"
CMP r1, #0
BEQ wait @ 标志未置位,自旋等待
LDR r2, [shared_data] @ 标志已置位,保证能看到 Producer 写入的数据

在 ARMv7 上 LDA/STL 实际汇编为 LDR/STR + DMB 组合;在 ARMv8 上利用硬件 acquire/release 语义减少了显式屏障。

这些指令实现 C11/C++11 的 release/acquire 内存顺序语义,在 ARMv8 上利用硬件特性提供更强的保证。


8. 学习要点总结

  1. Load/Store 架构是所有 ARM 汇编理解的起点:运算不进内存,一旦违反思维模型就会写出错误的优化假设。三步 load→运算→store 是 ARM 编程的基本操作模式。

  2. Thumb-2 终结了”ARM or Thumb”的二选一。当前的默认策略是全部编译为 Thumb-2,在 16-bit 和 32-bit 编码间自动选择。只有特殊情况才需要 .W/.N 修饰符。

  3. **T bit (CPSR[5]) 是处理器的”语言开关”**。它不是软件标志——修改它必须伴随流水线刷新,因此必须通过 BX/BLX(或写入 PC 的 LDR/LDM/POP)来安全切换。

  4. bit[0] = 指令集指示器。这可能是 ARM 汇编中最反直觉的设计:函数指针的最低位不是地址的一部分,而是告诉处理器该函数被编译为哪种指令集。链接器自动设置此位。

  5. **GNU Assembler 和 armasm 的差异不只是 @ vs ;**。.word vs DCD.equ vs EQU.include "f" vs INCLUDE f——在两种工具链间迁移代码时必须全局替换伪指令。

  6. UAL 统一了源码层.syntax unified 让同一份汇编源可同时编译为 ARM 或 Thumb,汇编时通过 .arm/.thumb 伪指令选择目标。这是编译器生成的汇编代码的默认格式。

  7. 非叶子函数的 PUSH {LR} / POP {PC} 模式是 ARM 汇编的标准模板——不是 CALLRET,而是显式的栈操作。理解这一点才能顺利阅读 boot 代码和内核上下文切换的汇编实现。