视频加载失败

ARMv7-A 指令体系全解:数据处理、访存、分支与同步

4324 字
22 分钟
ARMv7-A 指令体系全解:数据处理、访存、分支与同步

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

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

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

Note

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

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

Note

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

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

1. 物理本质#

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

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

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

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

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

条件后缀标志位要求(CPSR)解释汇编举例C 语言映射
eqZ == 1Equal(相等)addeq r0, r1, r2
(如果相等,则相加)
if (a == b)
neZ == 0Not Equal(不想等)movne r0, #1
(如果不等,则赋值)
if (a != b)
alIgnoredAlways(默认)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谁负责保存?
R0a1通用参数 1 / 返回值调用者(Caller)
R1a2通用参数 2 / 返回值(64 位时)调用者(Caller)
R2a3通用参数 3调用者(Caller)
R3a4通用参数 4调用者(Caller)
R4v1通用局部变量被调用者(Callee)
R5v2通用局部变量被调用者(Callee)
R6v3通用局部变量被调用者(Callee)
R7v4通用局部变量(在 Thumb 模式下常用做栈帧指针)被调用者(Callee)
R8v5通用局部变量被调用者(Callee)
R9v6/SB通用局部变量(静态基址,平台相关)被调用者(Callee)
R10v7通用局部变量被调用者(Callee)
R11v8/FP通用帧指针(Frame Pointer)被调用者(Callee)
R12IP通用过程调用中间寄存器(Intra-Procedure-call scratch)调用者(Caller)
R13SP栈指针指向当前栈顶(Stach Pointer)-
R14LR链接寄存器保存函数返回地址(Link Register)-
R15PC程序计数器指向取指地址(Program Counter)-

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

寄存器全称解释关键位域
CPSRCurrent Program Status Register当前程序状态寄存器N, Z, C, V(条件标志位)
I, F(中断屏蔽位)
Mode(运行模式位:User, SVC, IRQ 等)
SPSRSaved 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 跳转的目标太远(超过 ±32MB\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)#

Note

核心逻辑: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, r2unsigned a = b / c;
sdiv
(Signed Divided)
有符号除法。sdiv r0, r1, r2int a = b / c;

3.1.2 逻辑类#

指令解释汇编举例C 语言映射
and
(Logical AND)
按位与。
常用于提取某几位。
and r0, r1, #0xffa = b & 0xff;
orr
(Logical OR)
按位或。
常用于置位(设为 1)。
orr r0, r0, #(1<<3)`r0
eor
(Exclusive OR)
按位异或。
常用于翻转某几位。
eor r0, r0, #1a = 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, r1a = b;
mvn
(Move Not)
按位取反后传送。mvn r0, #0
(r0 = 0xffffffff)
a = ~0;
Warning

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

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

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

3.1.4 比较指令#

Note

不保存运算结果,只更新 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 的物理含义是:

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

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

模式物理动作C 标志位更新数学含义C 语言映射典型应用场景
lsl
(Logical Shift Left)
左移
低位补 0,高位移出
最后移出的位×2n\times 2^nx << n乘法优化、位字段对齐、生成掩码
lsr
(Logical Shift Right)
右移
高位补 0,低位移出
最后移出的位/2n/2^n
(无符号)
(unsigned)x >> n无符号数除法、读取特定位段
asr
(Arithmetic Shift Right)
算术右移
高位补符号位,低位移出
最后移出的位/2n/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], #4r0 = *(ptr++);访问内存r1 更新为 r1 + 4

3.2.3 多寄存器#

Note

这是 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)
相对跳转(范围 ±32MB\pm 32\text{MB}b labelgoto label;
bl
(Branch with Link)
带连接跳转。
保存下一条指令地址到 lr
bl funcfunc();
bx
(Branch Exchange)
跳转并根据目标地址最低位切换状态
(ARM \rightarrow Thumb)。
bx lrreturn;(函数返回)
blx
(Branch Link Exchange)
bl + bx 的结合。
调用不同指令集的函数。
blx r0函数指针调用
Note

还可以直接修改 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)。

Note

协处理器(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)
清除监视器状态。上下文切换时清理残余状态
Note

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

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

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

Note

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

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

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

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
ARMv7-A 指令体系全解:数据处理、访存、分支与同步
https://bfmhno3.github.io/posts/armv7-a-isa/
作者
bfmhno3
发布于
2026-04-05
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
bfmhno3
Hello, I'm bfmhno3.
公告
本站曾使用 Jekyll + Minimal Mistakes 主题,现已迁移至 Astro + Firefly。博客内容未变,但文章链接有所调整,请自行查找需要的文章。
分类
标签
最新动态
站点统计
文章
36
分类
2
标签
58
总字数
136,569
运行时长
0
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.16.7
文章许可
CC BY-NC-SA 4.0
文章目录