ARM汇编语言入门
深入探讨 ARM 汇编语言入门,涵盖指令集设计、GNU 汇编器以及 Interworking 的核心机制与最佳实践。
1. RISC vs CISC:ARM 指令集的架构哲学
ARM 是 RISC(精简指令集计算机)处理器。与 x86 这类 CISC(复杂指令集计算机)相比,核心差异在于:
| 维度 | CISC (x86) | RISC (ARM) |
|---|---|---|
| 指令集规模 | 丰富,单条指令可完成复杂任务 | 精简,更通用的指令 |
| 内部实现 | 微码解码——指令被译为内部微操作序列 | 硬连线——晶体管更少,功耗更低 |
| 寄存器 | 较少 | 大量通用寄存器(16×32-bit) |
| 指令周期 | 可变 | 大多数单周期 |
ARM 架构的几条根本约束塑造了所有后续设计决策:
Load/Store 架构。 数据运算指令只能操作寄存器,不能直接访问内存。要在内存中递增一个值,必须经过三步:
1 | LDR r0, [r1] @ 从 [r1] 加载到 r0 |
其结果是:没有 x86 的 INC [ebx] 这样的单指令内存操作。代价是多了几条指令,收益是整个数据通路更简单、频率更高。
三操作数格式。 ARM 和 x86 的目标操作数放在最前面(68K 相反):
1 | @ ARM (目标在前) |
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 | BCS.W label @ 强制 32-bit 编码 |
2.3 状态切换的硬件约束
从一个状态切到另一个状态时,必须同时刷新指令流水线——因为流水线中正在解码的指令可能与新状态不匹配。ARM 不会自动切换。这引出了后续章节中 Interworking 的核心问题:如何安全地改变 T bit 并保证程序继续正确执行。
在进入运行时状态切换之前,先回答一个前置问题:这些汇编指令最终是如何变成可执行代码的? 接下来的两节分别介绍 GNU Assembler 和 ARM 工具链——两种最常用的 ARM 汇编器。
3. GNU 汇编器(GAS)语法
GNU Assembler 是 GNU 工具链的汇编器,将 .s 源文件转为 .o 目标文件。
3.1 调用与链接
1 | arm-none-eabi-as -g -o file.o file.s # 汇编,-g 嵌入调试信息 |
工具名前缀 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 | @===== GNU Assembler ===== @===== armasm ===== |
注意注释符(@ 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 等)访问,其中 x 为 C(当前 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 | Loop MUL R5, R5, R1 |
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 | ; ARM state (T=0) |
实例:Thumb 代码调用 ARM 函数
1 | ; Thumb state (T=1) |
5.2 非叶子函数的返回模式
会调用子函数的非叶子函数不能直接用 BX LR 返回——LR 会被 BL sub_func 覆盖。标准模式是先压栈再弹出到 PC:
1 | nonleaf PUSH {LR} ; 保护返回地址 |
手册强调 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 | .syntax unified |
判别过程: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 | @ Core 0 (Producer): 写入共享数据后释放 |
在 ARMv7 上 LDA/STL 实际汇编为 LDR/STR + DMB 组合;在 ARMv8 上利用硬件 acquire/release 语义减少了显式屏障。
这些指令实现 C11/C++11 的 release/acquire 内存顺序语义,在 ARMv8 上利用硬件特性提供更强的保证。
8. 学习要点总结
Load/Store 架构是所有 ARM 汇编理解的起点:运算不进内存,一旦违反思维模型就会写出错误的优化假设。三步 load→运算→store 是 ARM 编程的基本操作模式。
Thumb-2 终结了”ARM or Thumb”的二选一。当前的默认策略是全部编译为 Thumb-2,在 16-bit 和 32-bit 编码间自动选择。只有特殊情况才需要
.W/.N修饰符。**T bit (CPSR[5]) 是处理器的”语言开关”**。它不是软件标志——修改它必须伴随流水线刷新,因此必须通过 BX/BLX(或写入 PC 的 LDR/LDM/POP)来安全切换。
bit[0] = 指令集指示器。这可能是 ARM 汇编中最反直觉的设计:函数指针的最低位不是地址的一部分,而是告诉处理器该函数被编译为哪种指令集。链接器自动设置此位。
**GNU Assembler 和 armasm 的差异不只是
@vs;**。.wordvsDCD、.equvsEQU、.include "f"vsINCLUDE f——在两种工具链间迁移代码时必须全局替换伪指令。UAL 统一了源码层:
.syntax unified让同一份汇编源可同时编译为 ARM 或 Thumb,汇编时通过.arm/.thumb伪指令选择目标。这是编译器生成的汇编代码的默认格式。非叶子函数的 PUSH {LR} / POP {PC} 模式是 ARM 汇编的标准模板——不是
CALL→RET,而是显式的栈操作。理解这一点才能顺利阅读 boot 代码和内核上下文切换的汇编实现。