3、ARM64指令集入门:数据处理指令与立即数编码

各位好,欢迎来到第三章。前两章我们把ARM64的寄存器体系和内存模型理清了。从这章开始,咱们真正上手——学指令。

ARM64的指令集,说多不多,说少不少。但核心的数据处理指令,其实就那么几类。我当年刚转ARM64时,看着指令手册上千页,心里也发怵。后来发现,日常反汇编和调优中,80%的代码都在用这章要讲的指令。先把这些吃透,后面就顺了。

本章核心脉络:算术运算 → 逻辑运算 → 移位操作 → 比较指令 → 立即数编码。最后一张SVG图帮你串起整个知识体系。

3.1 算术运算指令:ADD / SUB / MUL

算术指令是CPU最基础的肌肉。ARM64的ADD、SUB、MUL,用法上跟x86有相似之处,但寄存器模型完全不同。

3.1.1 ADD — 加法指令

基本格式:ADD Xd, Xn, Xm。把Xn和Xm相加,结果存入Xd。

// 简单加法
ADD X0, X1, X2      // X0 = X1 + X2

// 加立即数
ADD X0, X1, #42     // X0 = X1 + 42

// 带移位(后面细讲)
ADD X0, X1, X2, LSL #2  // X0 = X1 + (X2 << 2)

我个人习惯,在反汇编时看到ADD指令,第一反应是看第三个操作数是不是立即数。如果是立即数,要留意它的编码范围——这个后面3.5节专门讲。

小技巧:ADD指令的第三个操作数可以是寄存器、立即数,也可以是「寄存器+移位」的组合。但注意,ARM64里没有x86那种「ADD mem, reg」的格式——内存操作必须通过LOAD/STORE指令。

3.1.2 SUB — 减法指令

格式跟ADD完全对称:SUB Xd, Xn, Xm。

SUB X0, X1, X2      // X0 = X1 - X2
SUB X0, X1, #100    // X0 = X1 - 100
SUBS X0, X1, X2     // 带标志位更新的减法

这里有个关键点:SUBS(带S后缀)会更新NZCV标志寄存器。普通SUB不更新。我曾在调试一个循环边界条件时,发现标志位总不对,查了半天才发现用的是SUB而不是SUBS——嗯,这种坑踩过一次就记住了。

3.1.3 MUL — 乘法指令

乘法稍微特殊一点:MUL Xd, Xn, Xm。结果只保留低64位。

MUL X0, X1, X2      // X0 = X1 * X2 (低64位)

// 乘加指令(非常实用)
MADD X0, X1, X2, X3 // X0 = X1 * X2 + X3

MADD是我个人非常喜欢的一条指令。很多算法里都有「乘积累加」的操作,一条MADD搞定,比先MUL再ADD省一条指令,还少一次寄存器写回。

指令 格式 说明
ADD ADD Xd, Xn, Xm Xn + Xm → Xd
SUB SUB Xd, Xn, Xm Xn - Xm → Xd
MUL MUL Xd, Xn, Xm Xn * Xm (低64位) → Xd
MADD MADD Xd, Xn, Xm, Xa Xn * Xm + Xa → Xd

3.2 逻辑运算指令:AND / ORR / EOR

逻辑指令在底层编程中出场率极高。位掩码、标志位操作、权限检查,都离不开它们。

3.2.1 AND — 按位与

AND X0, X1, X2      // X0 = X1 & X2
AND X0, X1, #0xFF   // X0 = X1 & 0xFF (取低8位)

AND最常见的用途是位掩码提取。比如你想取一个寄存器的低8位,用AND X0, X1, #0xFF就行。我在做网络协议解析时,经常用AND来提取包头里的标志位字段。

3.2.2 ORR — 按位或

ORR X0, X1, X2      // X0 = X1 | X2
ORR X0, X1, #0x3    // X0 = X1 | 0x3 (置低2位为1)

ORR用来置位。你想把某个寄存器的第3位和第5位置1,用ORR X0, X0, #(1<<3 | 1<<5)就行。

3.2.3 EOR — 按位异或

EOR X0, X1, X2      // X0 = X1 ^ X2
EOR X0, X1, #-1     // X0 = ~X1 (按位取反)

EOR有个经典用法:寄存器清零。EOR X0, X0, X0 比 MOV X0, #0 在某些微架构上更快。为什么?因为EOR不依赖立即数生成器,硬件上更简单。

注意:ARM64里没有专门的NOT指令。按位取反用 EOR Xd, Xn, #-1 实现。或者用 MVN Xd, Xn(按位取反移动)。我个人更习惯用MVN,语义更清晰。

3.3 移位指令:LSL / LSR / ASR

移位操作在ARM64里不是独立的指令,而是作为操作数修饰符出现的。你可以在ADD、SUB、AND等指令的第三个操作数后面加移位。

3.3.1 LSL — 逻辑左移

左移n位,低位补0。等价于乘以2ⁿ。

ADD X0, X1, X2, LSL #3   // X0 = X1 + (X2 << 3)
LSL X0, X1, #4            // X0 = X1 << 4 (独立移位指令)

3.3.2 LSR — 逻辑右移

右移n位,高位补0。等价于无符号除以2ⁿ。

AND X0, X1, X2, LSR #2   // X0 = X1 & (X2 >> 2)
LSR X0, X1, #8            // X0 = X1 >> 8 (高位补0)

3.3.3 ASR — 算术右移

右移n位,高位补符号位。用于有符号数的除法。

ASR X0, X1, #3            // X0 = X1 >> 3 (高位补符号位)
移位类型 高位补什么 等价操作 适用场景
LSL 补0 乘以2ⁿ 无符号/有符号左移
LSR 补0 无符号除以2ⁿ 无符号右移
ASR 补符号位 有符号除以2ⁿ 有符号右移

经验之谈:反汇编时看到 LSL #3 或 LSR #2,基本可以断定是在做乘以8或除以4的优化。编译器特别喜欢用移位代替乘除法——因为移位只要1个周期,乘法要3-4个周期,除法更慢。

3.4 比较指令:CMP / CMN

比较指令本质上是减法,但不保存结果,只更新标志位。

3.4.1 CMP — 比较

CMP Xn, Xm 等价于 SUBS XZR, Xn, Xm。用Xn减Xm,结果丢弃,只更新NZCV。

CMP X0, X1           // 比较X0和X1
CMP X0, #100         // 比较X0和100

比较之后,通常跟着条件分支指令(B.EQ、B.NE、B.LT等)。

3.4.2 CMN — 负数比较

CMN Xn, Xm 等价于 ADDS XZR, Xn, Xm。用Xn加Xm,结果丢弃,只更新NZCV。

CMN X0, X1           // 比较X0和 -X1 (等价于 CMP X0, -X1)

CMN用得不多,但当你需要判断「两个数相加是否为零」时,它比CMP更高效——因为CMP做减法,CMN做加法,硬件路径不同。

关键区别:CMP做减法,CMN做加法。CMP X0, X1 判断 X0 > X1?CMN X0, X1 判断 X0 + X1 == 0?

3.5 立即数编码规则

这是ARM64里最容易让人困惑的地方。为什么MOV X0, #0x12345678 会报错?为什么有些立即数能用,有些不行?

ARM64的立即数编码,说白了就是:不是所有32位或64位数都能直接编码。它有一套特殊的编码规则。

3.5.1 立即数编码格式

ARM64的算术指令(ADD、SUB、AND等)中的立即数,占12位。但这12位不是直接表示数值,而是编码成:

  • 4位移位量(0-15,表示右移位数,每次移2位)
  • 8位核心值(0-255)

最终数值 = 核心值 循环右移 (移位量 × 2) 位。

举个例子:

// 立即数 0xFF
// 核心值 = 0xFF, 移位量 = 0
// 结果 = 0xFF 循环右移 0 位 = 0xFF

// 立即数 0xFC000000
// 核心值 = 0xFF, 移位量 = 6
// 结果 = 0xFF 循环右移 12 位 = 0xFC000000

3.5.2 哪些立即数能用?

简单判断:

  • 0-255(0x00-0xFF):都能用
  • 256-65535:大部分不能用,除非符合循环右移模式
  • 大于65535:极少数能用

我当年在写一个驱动时,想用 ADD X0, X1, #0x10000,结果汇编器报错。查了半天手册才明白——0x10000不符合编码规则。最后改成 ADD X0, X1, #0x1, LSL #16 才通过。

避坑指南:如果你需要加载一个无法直接编码的立即数,用以下方法:

  • 小数值(0-65535):用 MOV Xd, #imm(MOV有专门的立即数编码,范围更大)
  • 大数值:用 MOVZ + MOVK 组合,或者从内存加载
  • 或者用 LDR Xd, =imm 让汇编器帮你处理

3.6 知识体系总览

下面这张SVG图,把本章所有指令类型和它们的关系串起来了。建议你仔细看看,理解每类指令在什么场景下用。

ARM64数据处理指令知识体系 ARM64 数据处理指令知识体系 数据处理指令 算术运算 逻辑运算 移位操作 比较指令 ADD SUB MUL / MADD AND ORR EOR LSL LSR ASR CMP CMN 💡 立即数编码:12位编码 = 4位移位量 + 8位核心值,数值 = 核心值 循环右移 (移位量×2) 位 不是所有32/64位数都能直接编码,不符合规则时用 MOVZ/MOVK 或 LDR 加载 典型应用场景 🔹 算术运算:数值计算、地址偏移、循环计数 🔹 逻辑运算:位掩码提取、标志位置位/清零、权限检查 🔹 移位操作:乘除法优化、位域操作、对齐计算 🔹 比较指令:条件判断、循环边界、状态检测

3.7 实战要点总结

最后,我把自己在实际项目中积累的几个要点列出来,希望能帮你少走弯路:

  1. 能用移位就别用乘除:编译器会自动优化,但手写汇编时也要有这个意识。
  2. 注意SUBS和SUB的区别:需要更新标志位时用SUBS,否则用SUB。用错了,条件分支就全乱了。
  3. 立即数编码是ARM64的「特色」:写代码时如果遇到「invalid operand」错误,先检查立即数是否符合编码规则。
  4. EOR X0, X0, X0 比 MOV X0, #0 快:在性能敏感的代码段里,用EOR清零。
  5. MADD是性能利器:乘积累加场景,一条MADD顶两条指令。

推荐练习:打开一个编译好的ARM64二进制文件(比如Android的libc.so),用objdump反汇编,找找看里面用了哪些数据处理指令。我当年就是这么学的——看真实代码比看手册管用多了。