ARM汇编基础:指令集概述与核心操作

ARM汇编,说白了就是跟芯片硬件对话的底层语言。我刚开始接触ARM时,总觉得汇编离我太远——写驱动不都用C吗?直到有一次调试一个中断响应延迟的问题,C代码怎么优化都压不下去,最后反汇编一看,编译器生成了一段冗余的栈操作。嗯,从那以后我老老实实把ARM指令集啃了一遍。

这一章我们聚焦ARM指令集的核心:数据处理、加载存储、分支,以及Thumb/Thumb-2。你不需要背下所有指令,但得理解它们的设计思路——这对后面做内核裁剪和驱动优化至关重要。

ARM指令集概述

ARM处理器采用RISC(精简指令集计算机)架构。什么叫精简?就是每条指令只做一件事,而且大部分指令在一个时钟周期内完成。跟x86那种一条指令能绕三圈的做法完全不同。

ARM指令集的核心特征:

  • 固定32位指令长度(ARM模式)
  • Load/Store架构:只有加载和存储指令能访问内存
  • 条件执行:几乎所有指令都可以带条件码
  • 桶形移位器:数据处理前可对操作数进行移位

我个人习惯把ARM指令分成三大类:数据处理、内存访问、程序控制。下面这张图可以帮你快速建立整体认知:

ARM指令集分类结构图 ARM指令集 数据处理指令 加载/存储指令 分支指令 算术运算 逻辑运算 LDR/STR LDM/STM B/BL BX/BLX ARM模式:32位指令,所有指令可条件执行 Thumb模式:16位指令,代码密度高,适合嵌入式

数据处理指令

数据处理指令只操作寄存器,不碰内存。你想想看,如果一条加法指令还要去内存读数据,那CPU得等多久?所以ARM把内存访问和计算彻底分开。

算术运算指令

指令 功能 示例
ADD 加法 ADD R0, R1, R2 ; R0 = R1 + R2
SUB 减法 SUB R0, R1, #5 ; R0 = R1 - 5
RSB 反向减法 RSB R0, R1, R2 ; R0 = R2 - R1
MUL 乘法 MUL R0, R1, R2 ; R0 = R1 * R2

个人经验:我早期写驱动时,经常用ADD R0, R0, #1做循环计数。后来发现SUBS指令可以同时减法和更新标志位,一条指令搞定循环判断——代码量直接砍半。

逻辑运算与移位

逻辑运算在驱动开发里用得特别多。比如你要操作GPIO的某个位,就得用AND和ORR来置位或清零。

; 将R0的第3位置1
ORR R0, R0, #(1 << 3)

; 将R0的第5位清零
BIC R0, R0, #(1 << 5)

; 左移4位
LSL R0, R1, #4

这里有个坑——BIC指令是ARM的特色,它相当于"位清除"。x86里你得先取反再AND,ARM一条指令搞定。我在项目中遇到过有人用AND加MVN来实现,结果多花了两个时钟周期。

加载存储指令

ARM是Load/Store架构,这个设计理念你一定要刻在脑子里:只有LDR和STR能访问内存。其他指令只能在寄存器之间倒腾数据。

单数据传输

指令 功能 寻址模式示例
LDR 加载字(32位) LDR R0, [R1, #4]
STR 存储字 STR R0, [R1, R2]
LDRB 加载字节 LDRB R0, [R1]
STRH 存储半字 STRH R0, [R1, #2]!

注意:ARM要求内存访问必须对齐。LDR加载字时地址必须是4的倍数,否则会触发异常。我曾经在移植一个网络驱动时,因为结构体成员没对齐,导致LDR指令一直报对齐错误——查了整整一个下午。

多数据传输

LDM和STM指令可以一次加载或存储多个寄存器。这在函数调用和中断处理中特别有用——你想想看,进入中断时要保存十几个寄存器,如果用LDR一条一条存,那得写多少行代码?

; 保存R4-R7到栈上
STMDB SP!, {R4-R7, LR}

; 从栈上恢复
LDMIA SP!, {R4-R7, PC}

这里DB是Decrement Before,IA是Increment After。我个人习惯用STMDB和LDMIA配对,这样栈指针管理最清晰。

分支指令

分支指令控制程序的执行流。ARM的分支指令有个特点——几乎所有指令都可以条件执行。这意味着你可以省掉很多分支判断。

无条件分支与条件分支

指令 条件码 说明
B 无条件 直接跳转
BEQ EQ (Z=1) 相等时跳转
BNE NE (Z=0) 不等时跳转
BL 无条件 跳转并保存返回地址到LR
BX 无条件 跳转并切换指令集

条件执行示例:

; 比较R0和R1
CMP R0, R1
; 如果相等,执行加法
ADDEQ R2, R2, #1
; 如果不等,执行减法
SUBNE R2, R2, #1

这段代码没有分支跳转,CPU流水线不会被冲刷。在性能敏感的场景下,这种写法比B跳转快得多。

Thumb/Thumb-2指令集

ARM模式每条指令32位,功能强大但代码密度低。对于嵌入式设备,Flash空间寸土寸金,这时候Thumb就派上用场了。

Thumb指令集

Thumb是ARM的16位指令集。它把常用的ARM指令压缩成16位,代码密度能提升30%左右。但代价是功能受限——比如条件执行被砍掉了,大部分指令只能操作低8个寄存器。

我记得第一次用Thumb是在一个Flash只有64KB的MCU项目上。ARM模式的代码编译出来68KB,塞不进去。切换到Thumb后,直接降到45KB——嗯,那一刻我彻底服了。

Thumb-2指令集

Thumb-2是ARMv6T2之后引入的改进版。它混合了16位和32位指令,既保留了Thumb的高密度,又恢复了ARM模式的性能。

特性 ARM Thumb Thumb-2
指令长度 32位固定 16位固定 16/32位混合
代码密度 低 高 高
条件执行 全部指令 仅分支指令 部分指令
寄存器访问 全部16个 低8个为主 全部16个
适用场景 高性能 小容量Flash 性能与密度平衡

避坑指南:我曾经在Thumb模式下写了一个中断服务函数,忘了加.thumb_func指令。结果编译器把函数入口标记成了ARM模式,中断一来直接跳转到错误地址——系统死机。记住:Thumb模式下定义函数一定要加.thumb_func。

指令集切换机制

ARM和Thumb模式之间通过BX指令切换。核心原理是:PC寄存器的bit[0]决定当前模式——0表示ARM模式,1表示Thumb模式。

; 从ARM模式跳转到Thumb函数
LDR R0, =thumb_func_addr
BX R0   ; 如果R0的bit[0]=1,自动切换到Thumb模式

你想想看,这个设计多巧妙——不需要专门的模式切换指令,一个BX就搞定了。ARM的工程师在指令集设计上确实下了功夫。

好了,这一章的内容就到这里。ARM指令集虽然看起来指令很多,但核心就那几类:数据处理、加载存储、分支。你先把这三类吃透,后面写驱动和裁剪内核时,遇到汇编代码就不会发怵了。