3. ARM汇编基础:寄存器组、指令集分类、寻址方式、Thumb/Thumb-2指令集

好,咱们进入第三章。ARM汇编,这是Bootloader和内核裁剪绕不开的坎儿。很多人一看到汇编就头疼,其实说白了,ARM汇编比x86规整得多。你只要摸清它的寄存器、指令套路和寻址方式,后面看启动代码就跟看说明书一样。

我个人习惯,学汇编先别急着背指令,先把“家底”——寄存器组搞清楚。寄存器是CPU的贴身口袋,数据放口袋里操作最快。

3.1 ARM寄存器组

ARM处理器有37个寄存器,但咱们平时打交道最多的就那16个(R0-R15)。这16个是“可见”的,剩下的用于异常模式,后面讲异常时会提到。

寄存器 别名 功能说明
R0-R3 参数寄存器 传参、返回值。函数调用时前4个参数放这里
R4-R11 变量寄存器 保存局部变量。被调用函数必须保护R4-R11
R12 IP (Intra-Procedure) 用于函数调用时的临时跳转
R13 SP (Stack Pointer) 栈指针。指向当前栈顶
R14 LR (Link Register) 保存函数返回地址
R15 PC (Program Counter) 程序计数器。指向当前指令+8(流水线原因)
CPSR 状态寄存器 保存条件标志(N/Z/C/V)、模式位、中断使能位
避坑指南: 我曾经在调试一个Bootloader时,发现函数返回后PC跑飞了。查了半天,原来是某个中断服务程序里改了R14(LR)没恢复。记住:R0-R3可以随便用,但R4-R11和R14用之前一定要压栈保护。

这里有个细节——PC的值。ARM状态下,PC总是指向“当前指令地址+8”。为什么?因为ARM用了三级流水线(取指、译码、执行)。你执行第N条指令时,PC已经指向N+2了。写代码时要注意这个偏移量。

3.2 指令集分类

ARM指令集按功能分,其实就几大类。我整理了个表格,方便你对照着看。

类别 代表指令 用途
数据处理 MOV, ADD, SUB, AND, ORR 寄存器间的算术逻辑运算
数据加载/存储 LDR, STR, LDM, STM 内存与寄存器之间的数据搬运
分支跳转 B, BL, BX, BLX 改变程序执行流
协处理器 MCR, MRC 操作系统控制协处理器(如MMU、Cache)
异常产生 SWI (SVC) 触发软中断,进入内核态

你想想看,Bootloader里最常用的是哪几类?加载/存储和分支跳转。LDR加载外设寄存器地址,B/BL跳转到C函数入口。数据处理反而用得少,因为复杂运算都交给C了。

3.3 寻址方式

ARM的寻址方式,说白了就是“怎么找到操作数”。我把它归纳为8种,但常用的就前5种。

  1. 立即数寻址:操作数直接写在指令里。
    MOV R0, #0xFF — 把0xFF赋给R0
  2. 寄存器寻址:操作数在寄存器里。
    MOV R0, R1 — 把R1的值赋给R0
  3. 寄存器移位寻址:先移位再操作。
    MOV R0, R1, LSL #2 — R1左移2位后赋给R0
  4. 寄存器间接寻址:寄存器里存的是地址。
    LDR R0, [R1] — 从R1指向的内存读数据到R0
  5. 基址变址寻址:基址+偏移量。
    LDR R0, [R1, #4] — 从R1+4的地址读数据
  6. 多寄存器寻址:一次操作多个寄存器。
    LDMIA R0!, {R1-R4} — 从R0指向的内存连续读4个字
  7. 堆栈寻址:操作栈。
    STMFD SP!, {R0-R3, LR} — 压栈保护
  8. 相对寻址:PC+偏移量。
    B label — 跳转到label处
实战经验: 我在写Bootloader的MMU初始化代码时,经常用“基址变址寻址”来访问页表。比如 LDR R0, [R1, R2, LSL #2],R1是页表基址,R2是索引,左移2位是因为每个页表项占4字节。这种写法比手动计算地址高效得多。

3.4 Thumb/Thumb-2指令集

ARM指令每条32位,占空间大。早期嵌入式设备ROM小,ARM推出了16位的Thumb指令集。Thumb指令密度高,但功能受限——只能操作R0-R7,不能条件执行。

后来ARM在Cortex-M系列上推出了Thumb-2。这是个混合指令集,16位和32位指令混着用。Thumb-2既保留了Thumb的高密度,又补上了功能短板。现在Cortex-M处理器只支持Thumb-2,不支持ARM指令。

特性 ARM Thumb Thumb-2
指令长度 32位固定 16位固定 16/32位混合
代码密度 低 高(约节省30%) 高
可访问寄存器 R0-R15 R0-R7(受限) R0-R14
条件执行 支持 仅B指令支持 支持(IT指令)
适用处理器 Cortex-A/R ARM7/9/11 Cortex-M全系列
注意: 写Bootloader时,如果目标芯片是Cortex-A系列(比如树莓派的BCM2837),启动代码必须用ARM指令。因为Cortex-A上电后默认执行ARM指令,切换到Thumb需要额外指令。而Cortex-M芯片(比如STM32)只认Thumb-2,你写ARM指令它根本不执行。

嗯,这里有个常见的坑。有些同学在Cortex-M上写启动代码,直接抄了Cortex-A的汇编,结果芯片不跑。为什么?因为Cortex-M的向量表格式和Cortex-A不一样,而且指令集也不兼容。所以写代码前,先确认你的芯片是Cortex-A还是Cortex-M。

下面这张图展示了ARM指令集的发展脉络和选择逻辑。

ARM指令集分类与选择流程图 ARM指令集分类与选择流程 ARM指令集 处理器架构类型? Cortex-A/R ARM指令(32位) Cortex-M Thumb-2(16/32位混合) Bootloader/内核启动 裸机/RTOS应用 选择建议 • Cortex-A/R:启动代码用ARM指令,性能优先 • Cortex-M:只能用Thumb-2,代码密度优先 • 老ARM7/9:可用Thumb(16位)节省空间

最后说个实用技巧。在Bootloader里,经常需要判断当前是ARM状态还是Thumb状态。你可以读CPSR的T位(bit 5)。T=0是ARM,T=1是Thumb/Thumb-2。不过Cortex-M的CPSR里T位永远是1,因为只支持Thumb-2。

调试小技巧: 用GDB调试Bootloader时,如果反汇编出来的指令看起来不对(比如全是16位指令),检查一下当前处理器模式。我遇到过好几次,明明写的是ARM指令,GDB却按Thumb反汇编,结果全是乱码。这时候用 info registers cpsr 看一眼T位就明白了。