news 2026/10/22 4:24:01

ARM汇编全景解析:寄存器模型、指令系统与函数调用约定

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ARM汇编全景解析:寄存器模型、指令系统与函数调用约定

想弄明白 ARM 汇编的人,可能是在调试一个诡异的崩溃栈,也可能是在做底层性能优化,又或者是想搞懂编译器生成的二进制到底是怎么跑起来的。这件事确实不该绕开——ARM 架构的手机、路由器、嵌入式设备遍地都是,而汇编是唯一能让你看清“机器到底在按什么规则运转”的视角。这篇内容会从寄存器模型、指令分类、寻址方式一路讲到函数调用约定和反汇编实操,配合具体指令序列和调试经验,帮你在头脑里建立一张 ARM 汇编的全景图。

1. 为什么 ARM 汇编跟 x86 完全不是一回事

先放下指令细节,说说思维模型上的差异。如果你之前只接触过 x86 汇编,上手 ARM 的第一感觉应该是“别扭”,但这种别扭恰恰是 ARM 设计哲学的直接体现:精简指令集,每条指令干一件简单明确的事,大量操作通过通用寄存器完成,而不是背着沉重的内存寻址包袱。

1.1 CISC 与 RISC 的分水岭

x86 属于 CISC(复杂指令集计算机),一条指令能完成加载、计算、存储好几个动作,指令长度不固定,历史包袱重。ARM 属于 RISC(精简指令集计算机),指令长度固定(ARM 模式 4 字节,Thumb 模式 2 字节),指令格式规整,绝大多数指令在一个周期内可以完成执行。

这个差异带来的直接后果是:x86 的汇编代码看起来“每条指令干很多事”,而 ARM 的汇编代码看起来“指令很多,但每一条都特别单纯”。举个例子,x86 里一条add eax, [ebx+4]既做了内存读取又做了加法,ARM 里你得先ldr r0, [r1, #4]把内存读到寄存器,再add r0, r0, r2做加法。

1.2 条件执行:ARM 最出名的差异化设计

ARM 处理器里有一个状态寄存器叫 CPSR(当前程序状态寄存器),其中 NZCV 四个标志位分别表示结果为负数(Negative)、为零(Zero)、产生进位(Carry)、发生溢出(oVerflow)。x86 也有标志位,但只能在少数跳转指令里用到。

ARM 不一样,绝大多数指令都可以带条件码。比如addeq r0, r1, r2,只有当上一次比较结果相等(Z=1)时才执行加法。这意味着编译器可以省掉很多分支跳转,把短小的if-else直接转换成条件执行指令序列,避免流水线冲刷,这也是 ARM 在低功耗场景下表现出色的原因之一。

1.3 理解移位操作的内置特性

ARM 的另一个特色是“操作数灵活”:数据处理指令的第二个操作数可以先经过桶形移位器再参与运算。mov r0, r1, lsl #3表示把 r1 逻辑左移 3 位后放入 r0,等效于乘以 8。这在做数组索引、位图处理时特别方便,一条指令完成“移位+运算”。

这种设计背后是硬件上的“桶形移位器”(barrel shifter),它可以在一个时钟周期内完成任意位数的移位。理解这一点,你就能明白为什么 ARM 汇编里经常看到ldr r0, [r1, r2, lsl #2]这种写法——它在读取内存的同时完成基址+偏置的计算,高效且省电。

2. 寄存器模型与处理器模式:先搞懂手里的牌

ARM 是加载/存储架构,运算都发生在寄存器里,所以你把寄存器模型背下来,汇编就算学会了一半。32 位 ARM 有 16 个通用寄存器(r0-r15),加上 CPSR,一共 17 个程序员可见的寄存器。名字上偶尔会看到别名写法,比如 r13 常叫 sp,r14 常叫 lr,r15 常叫 pc,但本质就是同一组物理寄存器。

2.1 通用寄存器的作用分工

寄存器别名主要用途
r0-r3无传参、返回值,函数调用时前四个参数按顺序放入 r0-r3
r4-r11无函数内部的局部变量、临时值,被调用方需保存(callee-saved)
r12ip函数内部的临时寄存器,也被称为 intra-procedure-call scratch register
r13sp栈指针,指向当前栈顶
r14lr链接寄存器,保存函数返回地址
r15pc程序计数器,指向当前取指地址

这里有一个特别容易让人困惑的细节:ARM 的 pc 在 ARM 模式下指向的是“当前指令 + 8”,也就是向后两条指令的地址。这是因为 ARM 采用三级流水线:取指、译码、执行并行推进,pc 读到的其实是取指阶段的地址。老版本的 ARM 文档专门强调过这个偏移,新的 ARMv7 文档说 pc 指向当前指令地址,但实际上读 pc 仍然会有 +8(ARM 状态)或 +4(Thumb 状态)的偏移量。写汇编时如果依赖 pc 做位置无关计算(PIC),必须把这个偏移算进去。

2.2 处理器模式与寄存器备份

ARM 处理器不止一种工作模式,常见的有用户模式(USR)、系统模式(SYS)、中断模式(IRQ)、快速中断模式(FIQ)、管理模式(SVC)等。除了用户模式和系统模式,其他特权模式都有自己独立的 sp 和 lr 备份。

为什么要做寄存器备份?假设内核运行在 SVC 模式下,外部中断来了,处理器切到 IRQ 模式,IRQ 模式有自己的 sp 和 lr,不会覆盖 SVC 模式正在使用的栈指针和返回地址。这种硬件级隔离让操作系统的上下文切换变得简单可靠。你在用户态写普通应用程序,接触到的通常只是用户模式,但如果做裸机开发或者写内核驱动,就必须理解模式切换时寄存器的变化。

2.3 CPSR 与程序状态控制

CPSR 的低 8 位是控制位,包含工作模式位(M[4:0])、中断屏蔽位(I 和 F)、Thumb 状态位(T)。NZCV 四个条件标志位在高四位区域。修改 CPSR 的指令通常是msr(把通用寄存器写入状态寄存器)和mrs(把状态寄存器读到通用寄存器)。

调试时最常用的操作是mrs r0, cpsr然后把 r0 打出来看标志位,特别是刚执行完比较指令后,通过检查 Z、C 标志位来确认跳转条件是否符合预期。

3. 指令系统拆解:数据处理、访存与分支

ARM 指令按功能可以大致分为数据处理指令、访存指令、分支指令、协处理器指令和异常相关指令。对初学者来说,先吃透前三类就够了,它们覆盖了 95% 以上的日常分析场景。

3.1 数据处理指令

这类指令的特点是“操作数来自寄存器或立即数,结果写回寄存器”,不访问内存。核心指令包括:

  • mov:赋值,mov r0, #0x10把立即数 0x10 装入 r0
  • add/sub:加减法,add r0, r1, r2表示 r0 = r1 + r2
  • and/orr/eor:按位与、或、异或
  • cmp:比较,执行 r1 - r2 但不写回结果,只更新 CPSR 标志位
  • tst:测试,执行 r1 & r2 但不写回结果,只更新标志位
  • mul:乘法,注意 32 位 ARM 的乘法指令对操作数有限制,乘积寄存器不能跟第一个操作数相同

让我用实际的指令序列说明一下:

; 计算 r0 = (r1 * 8) + (r2 & 0xFF) mov r3, r1, lsl #3 ; r3 = r1 * 8,利用桶形移位器 and r4, r2, #0xFF ; r4 = r2 & 0xFF add r0, r3, r4 ; r0 = r3 + r4

这里mov r3, r1, lsl #3就是 ARM 独有的风格,x86 里你得先移位再移动,这里一条指令搞定。

3.2 访存指令

ARM 的访存指令只有两类核心:ldr(load register,从内存读入寄存器)和str(store register,从寄存器写入内存)。按访问宽度细化有ldrb(读一个字节)、ldrh(读半个字,2 字节)、ldr(读一个字,4 字节),写内存对应的有strb、strh、str。

基本的寻址格式是ldr r0, [r1, #offset]。这里的含义是:有效地址 = r1 + offset,然后把该地址处的一个字读入 r0。偏移量可以是正数也可以是负数,比如ldr r0, [r1, #-4]就是读取 r1-4 处的内容。

一个新手常犯的错误是混淆ldr r0, =0x40000000和ldr r0, [r1, #0x10]。前者是“伪指令”,汇编器会把立即数放到常量池里,然后生成一条真正读取常量池的ldr;后者是标准的基于寄存器偏移的访存指令。两者的语法只差一个方括号,含义完全不同。

批量访存指令ldm/stm是一次操作多个寄存器的指令,多用于函数开头保存现场和结尾恢复现场。举个例子:

stmfd sp!, {r4-r11, lr} ; 把 r4 到 r11 以及 lr 压入栈 ldmfd sp!, {r4-r11, pc} ; 从栈弹出,同时把 lr 的值赋给 pc 实现返回

fd是 full descending 的意思,表示栈从高地址向低地址增长,sp 指向栈顶已使用的位置。这是 ATPCS(ARM 的调用约定)里默认的栈规则。

3.3 分支指令与 IT 块

分支指令包括b(无条件跳转)、bl(跳转并保存返回地址到 lr)、bx(跳转并切换指令集)、blx(跳转、保存返回地址并切换指令集)。

b label是最基本的跳转;bl func是“调用函数”,等于mov lr, pc-8; b func的合体;bx lr是“从函数返回”,因为 lr 里存的就是调用方下一条指令的地址。

Thumb-2 指令集引入了it(if-then)指令块,用来在 Thumb 模式下实现条件执行。例如:

cmp r0, #0 itte eq ; IF-THEN-THEN-ELSE:接下来四条指令,前两条 eq 时执行,最后一条 ne 时执行 moveq r1, #1 ; 如果 r0 == 0,则 r1 = 1 addeq r2, r2, #1 ; 如果 r0 == 0,则 r2 = r2 + 1 movne r1, #0 ; 如果 r0 != 0,则 r1 = 0

裸机开发里经常见这类代码,读反汇编的时候看到ittt、itete之类的毫秒级符号,别困惑,它的正确读法是“if-then”的缩写,后面跟的每个t代表一条条件执行指令,e代表 else 分支指令。

4. 寻址方式与常见陷阱:别在地址计算上翻车

ARM 的寻址方式不算多,但还是那句话,细节里全是坑。掌握寻址方式的核心在于:偏移量到底是加在访存之前还是之后。

4.1 前变址、后变址与自动回写

先看三类基础寻址:

  1. 前变址(pre-indexed):ldr r0, [r1, #4],有效地址 = r1 + 4,然后访问内存。指令执行完后 r1 的值不变。
  2. 后变址(post-indexed):ldr r0, [r1], #4,有效地址 = r1,访问内存,然后 r1 = r1 + 4。这种写法常用于数组遍历,一条指令既读取数据又移动指针。
  3. 自动回写(writeback):ldr r0, [r1, #4]!,有效地址 = r1 + 4,访问内存,然后把 r1 更新为 r1 + 4。感叹号表示“回写基址寄存器”。

自动回写的底层的机制是:CPU 先算有效地址,再决定是否把计算结果写回基址寄存器,这个回写动作是硬件完成的,不需要额外指令。用!的写法能让代码更紧凑,但代价是语义不那么直观,读代码时要特别留神。

这三类寻址看起来简单,实际调试时很容易搞混。我见过很多次这样的错误:想遍历一个数据块,写成了ldr r0, [r1], #4,但后面又用ldr r0, [r1]重复读同一个位置,因为编译器生成的代码跟自己手写的不一致,行为就对不上。用调试器单步执行时,一定要盯住 sp、r1 这类基址寄存器的实时变化。

4.2 立即数的合法范围:一个让许多编译器新人懵圈的规则

ARM 的数据处理指令支持立即数,但立即数不是随意给的。ARM 立即数的编码格式是“8 位常数 + 4 位循环右移量”,也就是说,合法的立即数是通过把一个 8 位数值(0-255)循环右移任意偶数位得到的。

举例来说,0x3FC是合法的,因为 0xFF 循环右移 2 位就是 0x3FC(实际是 0x000003FC,移位逻辑上是 8 位循环右移加零扩展)。但0x12345678就不是合法的立即数,你需要先ldr r0, =0x12345678(汇编器自动放入常量池)或者用多条指令拼接。判断时有个速算法则:立即数的二进制表示中出现“1”的位宽跨度,如果超过了 8 位(在循环移位意义下),基本就得走常量池了。

这个规则对逆向分析影响很大:你在反汇编里看到ldr r0, [pc, #offset],往下翻数据段常能看到一个 32 位完整数值,这个就是常量池条目,不是真正的基于 pc 的访存指令。

4.3 PC 相对寻址与位置无关代码

ldr r0, [pc, #offset]这条指令用于读取与当前 pc 相对偏移处的数据。因为偏移量是相对固定的,代码被加载到任意内存地址都能正确运行,与它的加载地址无关。这就是位置无关代码(PIC)的基本原理,常用于共享库、启动代码。

计算偏移时要记住 pc 的 +8 偏移(ARM 状态)。假设某条指令地址是 0x8000,执行这条指令时 pc 实际值是 0x8008,如果偏移是 #4,那么读取的是 0x800C 处的数据。反汇编工具一般会帮你算好注释里的实际地址,但你自己写汇编或用脚本分析 raw binary 时,这个 +8 偏移就会冒出来坑你一次。

5. 函数调用约定:读懂 callee-saved 与 caller-saved

在 ARM 世界里,函数调用的规则被定义在 AAPCS(ARM Architecture Procedure Call Standard)里。理解这套规则,是读懂任何一段 ARM 反汇编代码的钥匙。

5.1 寄存器角色再梳理

AAPCS 把通用寄存器分成三个阵营:

  • 参数寄存器 r0-r3:调用方把前四个参数放进 r0-r3,如果参数超过四个,多余参数按顺序压栈。返回值放在 r0(32 位值)或 r0-r1(64 位值)。
  • 被调用方保存寄存器 r4-r11:被调用的函数如果要用 r4-r11,必须先保存原值(通常压栈),返回前恢复原值。对于调用方来说,调用完一个函数后,r4-r11 的值不会变。
  • 临时寄存器 r12(ip)以及 r0-r3:调用方不保证这些寄存器在函数返回后保持不变,被调用方可以随意使用。

这个设计的本质是:谁用谁保存。参数寄存器是“易失的”,因为函数返回值本来就要覆盖 r0,而 r4-r11 是“非易失的”,因为被调用方有义务保持它们不被破坏。

5.2 一段典型函数的反汇编阅读

来看一段经典的 C 代码编译产物(简化):

int add_and_multiply(int a, int b) { return (a + b) * 2; }

对应 ARM 汇编可能是:

add_and_multiply: add r0, r0, r1 ; r0 = a + b mov r0, r0, lsl #1 ; r0 = r0 * 2 bx lr ; 返回,lr 里是调用方的下一条指令地址

没有栈操作!因为参数直接在 r0、r1 里,局部计算也用不上 r4-r11,所以不需要保存任何寄存器。这是 ARM 函数里最常见的形态:短小、无栈、直接返回。

再看一个需要栈操作的函数:

int complex_calc(int a, int b, int c, int d, int e) { int local = a + b; return local * c + d - e; }

这是一个超过 4 个参数的函数,第 5 个参数 e 会被调用方压栈。汇编可能长这样:

complex_calc: push {r4, r5, lr} ; 保存 r4、r5 和返回地址 ldr r4, [sp, #12] ; 读取第 5 个参数 e(在栈上) add r5, r0, r1 ; local = a + b mul r5, r5, r2 ; local = local * c add r5, r5, r3 ; local = local + d sub r0, r5, r4 ; r0 = local - e,作为返回值 pop {r4, r5, pc} ; 恢复 r4、r5,同时把 lr 弹入 pc 返回

注意这里push {r4, r5, lr}压入 3 个寄存器(12 字节),第 5 个参数 e 在调用方眼里位于“sp + 4 + 8”处——因为调用方执行bl complex_calc时,pc 压栈的关系和栈布局不同。实际调试时,第 5 个参数的位置要用 sp 偏移一点点推算,这是新手最容易算错的地方。

5.3 栈帧布局:高地址到低地址

AAPCS 要求栈是满递减的(full descending),sp 指向栈顶元素。函数入口push {r4-r11, lr}后,sp 向低地址移动,栈上的寄存器布局从高地址到低地址依次是:lr、r11、r10、...、r4。图 1 的思维模型是:越晚压栈的寄存器越靠近低地址(因为栈向下增长)。

注意:AAPCS 规定在函数入口处 sp 必须 8 字节对齐(因为要兼容某些依赖双字加载的指令)。如果函数只压栈了奇数个寄存器,编译器会在push指令里多压一个不用的寄存器,或者用额外的sub sp, sp, #4保持对齐。反汇编时看到多压入一个看似没用的 r7,别觉得奇怪,这是对齐要求。

6. 实操:从 ARM 汇编到逆向调试的心法

纸上谈兵到此为止。接下来分享的是我平时看 ARM 反汇编和调崩溃的一些实际套路。

6.1 把 C 和汇对照着看的习惯

学习阶段最有用的方法,是把一段 C 代码编译成 ARM 汇编,再一行行对回去。在 Linux 下可以用:

# 编译成汇编,不链接 arm-linux-gnueabi-gcc -S -O2 -marm example.c -o example.s # 或者直接反汇编目标文件 arm-linux-gnueabi-objdump -d example.o

如果用-O0优化关闭,生成的汇编会又长又啰嗦,不适合学习;用-O2优化开启的真实汇编才能真正反映 CPU 实际执行的指令。

一个很好的练习是把a[i] += 1这样的代码编译成汇编,观察索引计算(通常就是ldr加移位)、读入寄存器、自增、写回的过程。你会惊讶地发现,一次高级语言操作在底层要展开成 4-5 条指令,而每条指令都那么机械、那么清晰。

6.2 崩溃栈回溯的汇编视角

程序崩溃时,调试器给出的 backtrace 本质上是沿着栈帧里的“保存的 lr/pc 链”回溯调用历史。所以理解函数调用的汇编级实现,对你定位崩溃点有直接帮助。

比如在 GDB 里看到这样的输出:

#0 0x000103f8 in crash_func () #1 0x000104cc in caller_func ()

你可以查看caller_func的汇编,看 0x000104cc 是哪一条bl调用的下一条指令。如果崩溃发生在crash_func内,把 PC 对应到具体指令上,再看这条指令访问了什么内存区域,往往就能定位是空指针、非法地址还是越界写。

6.3 我踩过的几个真实深坑

坑一:Thumb 与 ARM 状态混用。某些工具链默认生成 Thumb-2 指令,你反汇编时如果不指定指令集模式,会出现一堆乱码。用objdump -d -M force-thumb或-M force-arm可以强制切换,但最重要的是理解函数地址的最低有效位:如果函数指针 LSB 是 1,说明目标函数是 Thumb 代码,bx跳转时会自动切换状态。手工写汇编时,bx lr能正确返回就是因为 lr 的低位被bl指令正确写入了对应的状态位。

坑二:立即数溢出误判。在写mov r0, #300时,有些汇编器会报错,有些会静默改成ldr r0, [pc, #...]+ 常量池条目,不会报错。你以为是合法的立即数,其实生成的是内存读取指令。调试时单步执行,看到 pc 跳到自己写的数据区域,才反应过来是常量池在作怪。

坑三:用 sp 保存临时值,忘了恢复。新手写汇编容易图省事,直接str r0, [sp]把临时值放到栈顶,但在函数返回时忘记调整 sp,导致pop {r4, r5, pc}从错误的位置恢复寄存器,程序直接跑飞。正确做法是:push/pop严格配对,或用sub sp, sp, #N预留栈空间,并在返回前add sp, sp, #N恢复。

坑四:confusing 的ldr伪指令与常量池。我在前面提到过ldr r0, =0x12345678和ldr r0, [r1]的区别。还有一个边角情况是汇编器会让伪指令开辟的常量池自动对齐到 4 字节边界,在分析裸机固件时,看到.word 0x12345678出现在代码段里不要惊讶,那就是某个ldr =常量池条目。

7. 条件标志位与分支优化:编译器为什么这样写

阅读编译优化后的 ARM 代码,你经常会看到很多让人“迷糊”的条件执行序列。这里讲清楚背后的逻辑,能让你省去大量猜测。

7.1 条件码后缀速查

ARM 指令的条件后缀有几十个,但最常用的就那么几个:

后缀条件标志位判定
eq相等Z = 1
ne不相等Z = 0
cs/hs无符号大于等于C = 1
cc/lo无符号小于C = 0
mi负数N = 1
pl正数或零N = 0
vs有符号溢出V = 1
vc无溢出V = 0
gt有符号大于Z = 0 且 N = V
le有符号小于等于Z = 1 或 N != V
ls无符号小于等于C = 0 或 Z = 1
hi无符号大于C = 1 且 Z = 0

要特别注意有符号和无符号的区分:同样两个数比较,如果按无符号处理用cmphs,按有符号处理用cmpgt。ARM 的cmp指令本身并不知道操作数是“有符号”还是“无符号”,它只是做减法并更新标志位,后续用什么条件后缀完全由程序员/编译器决定。

7.2 条件执行与分支预测的权衡

现代 ARM 处理器普遍采用分支预测,无条件跳转的成本越来越低,但条件执行仍然有价值,尤其是在需要隐藏控制流信息的安全场景,以及极端短小的分支中能避免流水线冲刷。

举一个常见模式:

if (a > b) { c = 1; } else { c = 2; }

编译器可能生成:

cmp r0, r1 movgt r2, #1 movle r2, #2

两条mov都是条件执行,没有跳转指令,流水线完全不打断。但如果分支内部逻辑比较复杂,条件执行会白白浪费指令周期(因为条件失败的那些指令还是要走取指和译码阶段),编译器会改为传统跳转。

理解这个权衡,读优化代码时就多了一条线索:看到一串条件执行指令,说明编译器认为分支很短;看到常规跳转,说明分支体量大,或者预测成功率高。

7.3 标志位污染问题

条件执行有个隐含风险:两条连续的条件指令中,第一条执行指令可能改变标志位,影响后续条件判断。ARM 对此有精细的规定:数据处理指令默认更新标志位,但可以通过加s后缀控制更新的指令很少。实际上 ARM 的数据处理指令在带条件后缀时也能写标志位,这导致编译器必须精心编排指令顺序,避免标志位被意外覆盖。

比如:

cmp r0, r1 addeq r2, r3, #1 ; 这条指令会更新 NZCV 标志位 moveq r4, #5 ; 此时 eq 条件检查的是 addeq 的结果,不是 cmp 的结果

这是新手最容易困惑的点。解决思路是:需要保留“比较结果”时要立刻使用,或者重新比较;如果后续条件跟当前计算无关,别在那两条指令之间塞一个会改标志位的指令。某些 ARM 汇编器(如高版本的 Gas)会对这种“标志位污染”的用法给警告,但默认情况它还是放行。

8. 常用工具链与上手建议

最后聊一下怎么把知识落地。工具选择上,不同平台各有侧重,但核心套路是一样的:写一段 C,编译成汇编,对照观察。

8.1 本机实验最快路径

如果你的开发机是 ARM 架构(比如树莓派或 ARM 云主机),直接用系统自带工具就行:

gcc -S -O2 example.c # 生成汇编 gcc -c example.c objdump -d example.o # 反汇编目标文件

如果机器是 x86,则用交叉工具链:

sudo apt install gcc-arm-linux-gnueabi arm-linux-gnueabi-gcc -S -O2 example.c arm-linux-gnueabi-objdump -d example.o

注意不同交叉工具链的默认指令集可能不一样,有些默认生成 Thumb,有些默认生成 ARM 模式,建议用-marm或-mthumb显式指定。

8.2 读反汇编的三个技巧

第一,看结构先看函数边界。反汇编器会用push和pop/bx lr来标记函数边界,找到匹配关系,一段代码的画风就清晰了。

第二,看数据流先找 str/ldr。一条str r0, [sp, #8]说明这里把计算结果写到了栈上的局部变量区,顺着这个地址找对应的ldr,能还原变量在内存中的位置。

第三,看控制流先找 bl。bl到哪个地址,就是调用哪个函数。结合符号表或字符串引用,能很快还原函数调用关系。

8.3 从汇编“逆推”C 代码的练习

一个高效的练习是把一段编译器生成的汇编尝试还原成等价的 C 代码,并注意保持语义一致但不苛求一模一样。举个例子,看到这段:

ldr r0, [r3, #4] cmp r0, #0 beq .L_false mov r0, #1 b .L_end .L_false: mov r0, #0 .L_end:

基本可以还原成:

if (arr[1] != 0) { return 1; } else { return 0; }

但更贴近原始代码的可能是一个布尔表达式return !!arr[1]或者return arr[1] != 0,具体形式取决于编译器优化选项和源码。多练几次这样的“逆推”,你会对高级语言到机器指令的映射形成肌肉记忆。

9. 结语之外:一份属于自己的速查卡

写到最后,我把自己平时放在手边的 ARM 汇编速查要点再整理一份,希望对你有用。

  • ARM 是 RISC,指令定长,运算全在寄存器里进行,访存必须显式用ldr/str。
  • r0-r3 传参,r0 返回值,r4-r11 被调用方保存,r13 是 sp,r14 是 lr,r15 是 pc。
  • bl调用把返回地址写入 lr,返回用bx lr。
  • ARM 状态 pc = 当前指令 + 8,PIC 计算偏移时要算上这个偏移量。
  • 数据处理指令的第二个操作数可以经过桶形移位器,一条指令完成移位+运算。
  • 立即数必须是“8 位值循环右移偶数位”,否则放入常量池。
  • 条件后缀加在指令尾部,cmp不写回结果,只更新标志位。
  • ldr r0, [r1, #4]和ldr r0, [r1], #4的区别是基址回写时机,一个前变址一个后变址,调试时盯住 sp/r1 的变化。
  • Thumb-2 的it块在反汇编里很常见,ittt eq后面的三条指令都要按 eq 条件解读。
  • 编译器会为了 8 字节栈对齐“多压”一个寄存器,别误读成有额外变量。

从第一次在调试器里看清一条ldr的访存地址,到能流畅阅读整段函数的反汇编,这个过程不需要死记硬背全部指令,关键是抓住“数据从哪来、运算在哪做、结果存到哪、控制流怎么走”这条主线。ARM 汇编看着指令多,实际上你日常用到的类型就那十几种,反复接触几周,就能形成稳定的阅读直觉。我也建议你挑一个真实项目——比如自己写过的排序算法、CRC 校验函数——强制自己读一读对应的反汇编代码,这种感觉会比任何教程都直观。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 6:27:35

操作系统原理入门:从资源管理到内核态与用户态的全面解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 16:54:35

NB-IoT嵌入式实战:模组选型、信号实测与端到云协议栈设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 13:56:58

基于MP2642GR-Z与STM32F765ZI的两串电池主动均衡控制系统设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 13:28:05

AD2S1210模拟SPI通信与高精度旋变解码实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 10:36:32

一阶低通滤波的位移法实现:单片机ADC去毛刺的轻量级方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 3:04:18

两节串联电池主动均衡:基于MP2642GR-Z与MK51的工程实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华