13 minute read

I.MX6ULL 的核心是 ARM Cortex-A7,它基于 ARMv7-A 架构。

要真正理解指令集,就不能只是背诵汇编手册,而是要回答一个核心问题:如果不看代码,只看电路,CPU 在做什么?

本质上,每一条指令都是对 CPU 内部寄存器状态或外部总线(内存/外设)的一次原子操作

总线(Bus)通俗理解就是主板上的 “高速公路”。它的硬件本质则是一组并排的导线,通过这组导线,连接 CPU 内部各部分或连接 CPU 和外设。

总线分为数据总线(传数据)、地址总线(告诉内存我要读哪一个单元)和控制总线(读还是写)。

原子操作(Atomic Operation)通俗理解就是,要么做完,要么完全不做,中间绝不允许被打断的操作。

在 ARM 中,单条读写指令通常是原子的,但 “读-改-写” 三步操作默认不是原子的,需要特殊指令(ldrex / strex) 保证。

1. 物理本质

在 ARM 状态下(Cortex-A7 上电默认状态),每一条指令都是严格的 32 位(4 字节)二进制码。

与 x86(变长指令)不同,ARM 的定长指令让译码器设计更加简洁高效。一条典型的 ARM 指令在物理电路上被切割为以下结构:

Instruction = [Codition Code (4 bits)] + [OpCode & Flags] + [Operands (Registers/Immediates)]

1.1 核心差异:条件执行(Conditional Execution)

这是 ARM 架构最吸引人的设计之一。几乎每一条指令的高 4 位(Bits 31-28)都是条件码。这意味着:任何指令都可以是条件跳转指令的变体

条件后缀 标志位要求(CPSR) 解释 汇编举例 C 语言映射
eq Z == 1 Equal(相等) addeq r0, r1, r2
(如果相等,则相加)
if (a == b)
ne Z == 0 Not Equal(不想等) movne r0, #1
(如果不等,则赋值)
if (a != b)
al Ignored Always(默认) add r0, r1, r2
(指令不写后缀默认就是 AL)
(无条件执行)
gt/lt   大于 / 小于 subgt ... ><

2. 寄存器模型

在执行指令前,必须知道指令操作的对象。I.MX6ULL 是 Load/Store 架构。

  • 原则:ALU(算术逻辑单元)只能处理通用寄存器中的数据。
  • 即不能对内存里的变量做运算,必须:搬运到寄存器 $\rightarrow$ 计算 $\rightarrow$ 搬回内存。

I.MX6ULL 有 16 个核心寄存器(R0-R15):

  • R0-R12:通用寄存器。
  • R13(SP):栈指针(当前的内存栈顶)。
  • R14(LR):链接寄存器(保存函数调用的返回地址)。
  • R15(PC):程序计数器(指向当前正在取指的地址)。修改 PC 就是跳转

在 ARM 开发中,寄存器最重要的属性是它的 AAPCS 角色(ARM Architecture Procedure Call Standard)。这决定了编译器如何使用它们。

寄存器 别名 硬件功能 AAPCS 谁负责保存?
R0 a1 通用 参数 1 / 返回值 调用者(Caller)
R1 a2 通用 参数 2 / 返回值(64 位时) 调用者(Caller)
R2 a3 通用 参数 3 调用者(Caller)
R3 a4 通用 参数 4 调用者(Caller)
R4 v1 通用 局部变量 被调用者(Callee)
R5 v2 通用 局部变量 被调用者(Callee)
R6 v3 通用 局部变量 被调用者(Callee)
R7 v4 通用 局部变量(在 Thumb 模式下常用做栈帧指针) 被调用者(Callee)
R8 v5 通用 局部变量 被调用者(Callee)
R9 v6/SB 通用 局部变量(静态基址,平台相关) 被调用者(Callee)
R10 v7 通用 局部变量 被调用者(Callee)
R11 v8/FP 通用 帧指针(Frame Pointer) 被调用者(Callee)
R12 IP 通用 过程调用中间寄存器(Intra-Procedure-call scratch) 调用者(Caller)
R13 SP 栈指针 指向当前栈顶(Stach Pointer) -
R14 LR 链接寄存器 保存函数返回地址(Link Register) -
R15 PC 程序计数器 指向取指地址(Program Counter) -

除此之外还有两个非常重要的特殊状态寄存器 CPSR 和 SPSR。

寄存器 全称 解释 关键位域
CPSR Current Program Status Register 当前程序状态寄存器 N, Z, C, V(条件标志位)
I, F(中断屏蔽位)
Mode(运行模式位:User, SVC, IRQ 等)
SPSR Saved Program Status Register 备份程序状态寄存器 仅在异常模式下存在
当中断发生时,硬件自动把 CPSR 里的值拷贝到 SPSR
当中断返回时,再从 SPSR 恢复到 CPSR

ARMv7-A 实际上不止上述的 18 个寄存器,还有影子寄存器(Banked Registers)。

2.1 为什么要区分调用者保存和被调用者保存?

简单来说,这是 ARM 官方制定的规范,叫做 AAPCS(Procedure Call Standard for the ARM Architecture,ARM 架构过程调用标准)。ARM 公司不仅设计 CPU 硬件架构,还定义了软件应如何与这些硬件交互的 ABI(Application Binary Interface,应用程序二进制接口)。

制定这一规范的核心目的在于统一标准。它确保了二进制层面的兼容性——无论代码是使用 GCC 还是 ARM Compiler 编译的,只要遵循 AAPCS,它们就能无缝协作。你无需担心不同编译器在参数传递或堆栈管理上的差异,从而可以放心地调用第三方提供的预编译库。

当 Caller 调用 Callee 时,参数通过 R0-R3 传递。调用结束后,返回值通常保存在 R0 或 R0-R1 中。这就带来了一个问题:Callee 执行时不可避免地会使用寄存器,Caller 如何确保自己的数据不被覆盖?

AAPCS 为此制定了明确的规范:

  • 易失性寄存器(Volatile Registers, R0-R3, R12):AAPCS 规定 Callee 可以随意修改它们。因此,如果 Caller 在 R0-R3 中存放了重要数据且在调用后仍需要使用,Caller 必须自己负责在调用前将它们压栈保存(Caller-saved)。
  • 非易失性寄存器(Non-volatile Registers, R4-R11):AAPCS 规定 Callee 必须保证函数返回时,这些寄存器的值与进入函数时完全一致。如果 Callee 内部需要使用 R4-R11,Callee 必须负责先将其压栈保存,并在返回前恢复(Callee-saved)。这样 Caller 就可以放心地将关键数据放在这里,无需担心被篡改。

上述规则解决了函数间的协作问题,但中断(Interrupt)打破了这种默契。中断是异步突发的,主程序(Caller)根本不知道何时会被打断,因此无法预先保存 R0-R3 等易失性寄存器。

为了解决这个问题,ARM Cortex-M 架构引入了硬件压栈(Hardware Stacking)机制。当 CPU 响应中断时,硬件会自动将 Caller 上下文(包括 R0-R3、R12、LR、PC 和 xPSR)。这一过程无需软件干预。

因此,进入中断服务函数(ISR)后,寄存器已经被硬件保护好了。ISR 此时只需像普通函数一样,遵守 AAPCS 规则处理 R4-R11(即如果用到则保存)即可。这种硬件机制极大降低了中断延迟,也简化了软件设计。

2.2 R12(IP)是干什么的?

它是一个被很多人容易忽略的寄存器,主要用于链接器(Linker)插入的代码。当你的程序非常大,bl target 跳转的目标太远(超过 $\pm 32\text{MB}$)时,bl 指令够不着。此时,链接器会自动插入一段小代码(Veneer/Trampoline),先跳转到 R12 指向的中转站,再由 R12 赋值给 PC 进行长跳转。

2.3 R15(PC)为什么有 +8 现象?

在 ARM 状态(32 位指令)下,读取 PC 的值,通常是当前指令地址 + 8。这则是因为 CPU 的流水线(Pipeline)设计,当 CPU 执行当前指令时,取指单元已经在取下下条指令了(当前指令地址 +8 地址处)。

3. 指令集讲解

3.1 数据处理指令(Data Processing)

核心逻辑:ALU 运算,结果写回寄存器,并可选更新 CPSR(如果指令后加 s,如 adds

3.1.1 算术类

指令 解释 汇编举例 C 语言映射
add
(Add)
加法。 add r0, r1, r2
(R0 = R1 + R2)
a = b + c;
sub
(Subtract)
减法。 sub r0, r1, #1
(R0 = R1 - 1)
a = b - 1;
rsb
(Reverse Substract)
逆向减法。
用于 R0 = 常数 - Rx 的场景。
rsb r0, r1, #0
(R0 = 0 - R1)
a = -b;
adc
(Add with Carry)
带进位加法。
用于实现 64 位加法的高 32 位运算。
adc r0, r1, r2
(R0 = R1 + R2 + Carry)
(long long )a + (long long)b 的高位部分
sbc
(Subtract with Carry)
带借位减法。
用于 64 位减法。
sbc r0, r1, r2
(R0 = R1 - R2 - !Carry)
64 位减法的高位部分
mul
(Multiply)
32 位乘法(结果取低 32 位) mul r0, r1, r2
(R0 = R1 * R2)
int a = b * c;
mla
(Multiply Accumulate)
乘法运算。
DSP 算法核心指令。
mul r0, r1, r2, r3
(R0 = R1 * R2 + R3)
a = (b * c) + d;
udiv
(Unsigned Divide)
无符号除法。 udiv r0, r1, r2 unsigned a = b / c;
sdiv
(Signed Divided)
有符号除法。 sdiv r0, r1, r2 int a = b / c;

3.1.2 逻辑类

指令 解释 汇编举例 C 语言映射
and
(Logical AND)
按位与。
常用于提取某几位。
and r0, r1, #0xff a = b & 0xff;
orr
(Logical OR)
按位或。
常用于置位(设为 1)。
orr r0, r0, #(1<<3) r0 | = (1 << 3);
eor
(Exclusive OR)
按位异或。
常用于翻转某几位。
eor r0, r0, #1 a = b ^ 1;
bic
(Bit Clear)
位清除
and 的反向操作:dest = op1 and (not op2)
bic r0, r0, #(1<<3) r0 &= ~(1 << 3);

3.1.3 数据搬运

指令 解释 汇编举例 C 语言映射
mov
(Move)
寄存器间传值,或立即数赋值。 mov r0, r1 a = b;
mvn
(Move Not)
按位取反后传送。 mvn r0, #0
(r0 = 0xffffffff)
a = ~0;

ARM 指令是定长 32 位的。除去操作码和条件码,留给立即数的空间只有 12 位。并且这 12 位还不是直接存数字,而是被拆分位 8 位常数 + 4 位循环右移。

这就意味着在向寄存器写入某些立即数时,例如 mov r0, #0x12345678 是会失败的,因为 0x12345678 这个立即数太乱了,无法通过 8 位常数 + 4 位循环右移凑出来。

解决办法是使用 ldr r0, =0x12345678。此时,汇编器会悄悄在代码段的末尾(Literal Pool)找个地方放 0x12345678,然后把这条指令翻译成去那个地址读数据。

3.1.4 比较指令

不保存运算结果,只更新 CPSR 的标志位(N, Z, C, V),为后面的条件执行做铺垫。

指令 解释 汇编举例 C 语言映射
cmp
(Compare)
本质是 sub
比较大小。
cmp r0, #5
(r0 - 5,更新 Z 标志位)
if (a >= 5)
tst
(Test)
本质是 and
测试某位是否为 1。
tst r0, #(1<<3)
(r0 & 0x8,更新 Z 标志位)
if (a & (1<<3))
teq
(Test Equivalence)
本质是 eor
测试两个数是否相等(不影响进位标志)。
teq r0, r1 常用于汇编手写检查

3.1.5 桶形移位器(Barrel Shifter)

在 ARM 中,移位操作不是独立指令,而是数据进入 ALU 之前的预处理。它是免费的。指令 add r0, r1, r2, lsl #2 的物理含义是:

$$ \text{R0} = \text{R1} + (\text{R2} \ll 2) $$

这在单周期内完成,这就是为什么 ARM 处理数组索引(地址 = 基址 + 索引 $\times$ 4)特别快。

模式 物理动作 C 标志位更新 数学含义 C 语言映射 典型应用场景
lsl
(Logical Shift Left)
左移
低位补 0,高位移出
最后移出的位 $\times 2^n$ x << n 乘法优化、位字段对齐、生成掩码
lsr
(Logical Shift Right)
右移
高位补 0,低位移出
最后移出的位 $/2^n$
(无符号)
(unsigned)x >> n 无符号数除法、读取特定位段
asr
(Arithmetic Shift Right)
算术右移
高位补符号位,低位移出
最后移出的位 $/2^n$
(有符号)
(signed)x >> n 有符号数除法
ror
(Rotate Right)
循环右移
低位移出后,填补到高位
最后移出的位 - (x >> n) | (x << (32-n)) 加密算法、哈希函数、位图操作
rrx
(Rotate Right with Extend)
带扩展循环右移
33 位循环
(32 位寄存器 + 1 位 Carry)
寄存器移出的 LSB - 大数运算、多精度算术

3.2 内存访问指令(Memory Access)

这是 CPU 与外部世界(DDR 内存、GPIO 控制器等)交互的唯一桥梁。

3.2.1 单寄存器

指令 解释 汇编举例 C 语言映射
ldr
(Load Register)
从内存读 4 字节到寄存器。 ldr r0, [r1] r0 = *r1;
str
(Store Register)
把寄存器的值写入内存 4 字节。 str r0, [r1] *r1 = r0;
ldrb (8 bit)
(Load Byte)
读 1 字节,高 24 位补零 ldrb r0, [r1] r0 = (unsigned char)*r1;
ldrh (16 bit)
(Load Halfword)
读 2 字节,高 16 位补零 ldrh r0, [r1] r0 = (unsigned short)*r1;
ldrsb
(Load Signed Byte)
读 1 字节,高 24 位补符号位 ldrsb r0, [r1] r0 = (char)*r1;
ldrsh
(Load Signed Halfword)
读 2 字节,高 16 位补符号位 ldrsh r0, [r1] r0 = (short)*r1;

3.2.2 寻址模式

寻址模式 汇编举例 C 语言映射 状态
立即数偏移
(Offset)
ldr r0, [r1, #4] r0 = *(r1 + 1); r1 保持不变
前索引
(Pre-indexed)
ldr r0, [r1, #4]! r0 = *(++ptr); r1 在访问内存更新为 r1 + 4
后索引
(Post-indexed)
ldr r0, [r1], #4 r0 = *(ptr++); 访问内存r1 更新为 r1 + 4

3.2.3 多寄存器

这是 ARM 实现(Stack)和上下文切换(Context Switch)的基石。

指令 解释 汇编举例 场景
ldm
(Load Multiple)
内存 $\rightarrow$ 多个寄存器。 ldm r0, {r4-r6, pc} 恢复现场并返回
stm
(Store Multiple)
多个寄存器 $\rightarrow$ 内存。 stm r0, {r4-r6, lr} 保存现场

说明:

  • ldm r0, {r4-r6, pc}:从以 r0 为基址的内存地址开始,依次加载数据到寄存器 r4r5r6pc
  • stm r0, {r4-r6, lr}:从寄存器 r4r5r6lr 中,依次将数据存储到以 r0 为基址的内存地址。
后缀模式

虽然有 iaibdadb 四种,但 ARM 官方推荐在操作栈时使用别名,方便记忆:

  • stmfd (Full Descending) = stmdb (Decrement Before):入栈(压栈)。
  • ldmfd (Full Descending) = ldmia (Increment After):出栈。
  • push {reglist} 等价于 stmdb sp!, {reglist}
  • pop {reglist} 等价于 ldmia sp!, {reglist}

3.3 分支与控制流(Branch & Control)

打破顺序执行,实现 ifforfunction call

指令 解释 汇编举例 C 语言映射
b
(Branch)
相对跳转(范围 $\pm 32\text{MB}$) b label goto label;
bl
(Branch with Link)
带连接跳转。
保存下一条指令地址到 lr
bl func func();
bx
(Branch Exchange)
跳转并根据目标地址最低位切换状态
(ARM $\rightarrow$ Thumb)。
bx lr return;(函数返回)
blx
(Branch Link Exchange)
bl + bx 的结合。
调用不同指令集的函数。
blx r0 函数指针调用

还可以直接修改 PC 寄存器实现跳转,例如 mov pc, lr,这是函数返回的底层实现。

3.4 系统与状态指令(System & Status)

这部分在使用上层语言实现业务逻辑时几乎看不到,但是对于操作系统而言至关重要。

3.4.1 状态寄存器访问

指令 解释 汇编举例 场景
mrs
(Move Register from Status)
读取 CPSR/SPSR 到通用寄存器。 mrs r0, cpsr 读取当前中断状态
msr
(Move Status from Register)
将通用寄存器写会 CPSR/SPSR。 msr cpsr_c, r0 开/关中断

3.4.2 异常与中断生成

指令 解释 场景
svc
(Supervisor Call)
软中断
CPU 进入 SVC 模式。
Linux 中用于从用户态(User)陷入内核态(Kernel)。
系统调用(openread 等)。
bkpt
(Breakpoint)
断点指令。
执行到此时 CPU 暂停并通知调试器。
JTAG/GDB 调试。

3.4.3 协处理器指令

I.MX6ULL 中,CP15 协处理器控制着 MMU、Cache 和中断向量表基地址。

指令 解释 助记法
mcr
(Move to Coprocessor from Register)
CPU $\rightarrow$ CP15
(写配置)
R(Register)在最后,表示源
mrc
(Move to Register from Coprocessor)
CP15 $\rightarrow$ CPU
(读状态)
C(Coprocessor)在最后,表示源

例如:mcr p15, 0, r0, c1, c0, 0(开启 MMU)。

协处理器(Coprocessor)时依附于 CPU 主核的独立电路单元。ARM 主核(ALU)只管算逻辑和搬砖,像 MMU(内存映射管理)、Cache 配置这些脏活累活,都丢给 CP15 协处理器;浮点运算丢给 CP10/CP11。

3.4.4 内存屏障(Memory Barriers)

指令 解释 场景
dmb
(Data Memory Barrier)
确保指令前的内存访问在指令后的内存访问之前完成。 操作 DMA 描述符时
dsb
(Data Synchronization Barrier)
比 DMB 更强。
确保前面的访存全部完成,CPU 才会执行下一条指令。
修改页表后
isb
(Instruction Synchronization Barrier)
冲刷流水线。确保重新取指。 修改了代码段或中断向量表后

3.5 同步与原子操作(Synchronization)

对于多任务操作系统,需要处理竞态条件。

指令 解释 C 语言映射
ldrex
(Load Register Exclusive)
读取并设置独占监视器 atomic_load (C11)
strex
(Store Register Exclusive)
如果监视状态未被破坏,则写入。
返回 0 成功,1 失败。
atomic_compare_exchange (C11)
clrex
(Clear Exclusive)
清除监视器状态。 上下文切换时清理残余状态

ARM 没有 x86 的 lock 总线锁,它使用 “独占监视器” 硬件。

  • ldrex 读取数据的同时标记该地址为 “被我监视”。
  • strex 尝试写入。如果期间没有其他核心 / DMA 修改过该地址,写入成功;否则写入失败。

这会实现 mutex(互斥锁)和 semaphore(信号量)的基石。

互斥锁(Mutex)& 信号量(Semaphore):

  • 互斥锁(厕所钥匙):只有一个资源(厕所)。你拿了钥匙进去锁门,别惹只能在门口排队(阻塞),直到你出来还钥匙。用户保护临界区
  • 信号量(餐厅叫号):有 $N$ 个资源(空桌子)。来一个人 $N-1$,走一个人 $N+1$。当 $N=0$ 时,新来的人必须等到。

这两个都是软件概念,但底层必须由原子指令支持。

Comments