简介:面向从事底层开发、嵌入式系统、性能优化与逆向工程的工程师与学习者,这份资源系统整理了 Armv8-A 架构下 Aarch64 汇编指令集的官方技术参考,可用来解决指令查询、寄存器使用、寻址方式选择与异常中断处理等具体问题。压缩包仅含 1 个 PDF 文件,大小约 9.7MB,内容为完整的 A64 指令集手册,便于离线检索和长期查阅。资源已有 2986 人学习下载,是较受关注的参考资料。手册覆盖基础算术与逻辑指令、比较分支、内存访问与控制流,并延伸至 SIMD 与 SVE 可伸缩向量扩展、SME 安全内存加密等高级特性;同时讲解 X0–X31 通用寄存器、V0–V31 向量寄存器的组织方式,以及立即寻址、寄存器间接寻址、预增预减、偏移与基址加索引等多种寻址模式,还涉及异常级别管理、中断向量表与陷阱处理机制。对于需要编写或阅读汇编代码、分析编译产物、理解 ABI 调用约定的读者,这份手册可作为案头工具书反复查阅。
1. aarch64 汇编指令集:一份能当字典翻的 A64 规范参考
反汇编窗口里蹦出一行autiasp,紧跟着ret就把进程带回内核,栈上明明有合法返回地址却认证失败;或者编译器把cset w0, eq编成csinc w0, wzr, wzr, ne,单步跟到一半就对不上号。这类问题靠搜索框翻不出来,得有本能按字母序查的指令字典。这份 aarch64 汇编指令集就是 arm64 的规范参考手册(编号 DDI 0596 / ID032522,2022-03 版),按助记符字母序列出基础整数、浮点与 SIMD 指令,并单独覆盖 SVE、SME、指针认证、内存标记这些特性扩展,每条给出操作语义伪代码、编码位域和汇编符号限制。适合做逆向、写裸机启动代码、调编译器后端、盯 crash 栈的从业者。
2. 手册的四层结构:基础整数、SIMD、SVE、SME 各管哪一段
拿到一本厚规范,最容易犯的错是从第一页硬读。这份文档的组织方式其实很工程化:正文主体是按助记符字母序排列的指令条目,每条格式高度统一;SVE、SME、指针认证、内存标记这些扩展则按特性单独成章。先把分层搞清楚,查一条指令的平均时间能从十分钟压到一分钟以内。更重要的是,分层决定了你该不该去翻它——一条add不需要查手册,一条cpyfprtn就必须查。
2.1 别名机制:CMP、MOV、CSET 背后是同一批编码
手册条目里大量标着 “an alias of XXX”,这是 A64 最容易吃亏的地方。A64 的助记符分两类:真实指令有独立编码,别名是汇编器的语法糖,只固定某些操作数位置,最终编到某条真实指令上。这意味着你按助记符做指令匹配的工具——inline hook、热补丁、二进制特征扫描——只要漏掉别名就会静默失效。
| 别名 | 真实指令 | 等价展开 |
|---|---|---|
CMP Xn, Xm | SUBS | SUBS XZR, Xn, Xm |
CMN Xn, Xm | ADDS | ADDS XZR, Xn, Xm |
TST Xn, Xm | ANDS | ANDS XZR, Xn, Xm |
MOV Xd, Xn | ORR | ORR Xd, XZR, Xn |
MOV Xd, #imm | MOVZ / MOVN | 视立即数形态选其一 |
LSL Xd, Xn, #n | UBFM | 由位域操作拼出移位语义 |
CSET Wd, cond | CSINC | CSINC Wd, WZR, WZR, invert(cond) |
CSETM Wd, cond | CSINV | 条件成立给全 1,否则给 0 |
CINC Xd, Xn, cond | CSINC | 两个源都取 Xn |
CSET值得单独说一句:它把条件取反、两个源都设成零寄存器、再让CSINC加一,正好凑出 0/1。所以cset w0, eq落成csinc w0, wzr, wzr, ne不是编译器抽风,是编码层的必然。
cmp w1, #0 cset w0, eq // 汇编器接受,实际编码是 CSINC 家族 // 真实的等价写法: // csinc w0, wzr, wzr, ne ret看反汇编时想绕开别名,让输出和手册编码页一一对齐,加一个开关就够:
# -M no-aliases 让反汇编器输出真实指令,而不是 CMP/CSET 这类糖 aarch64-linux-gnu-objdump -d -M no-aliases ./demo.o | sed -n '1,40p'-M no-aliases是 AArch64 后端支持的选项,作用是抑制助记符替换;-d只反汇编可执行段;sed那段只是截断输出,换成grep -A2 "<func>:"定位单个函数更实用。做补丁比对时,输入侧和输出侧必须统一口径,一边带别名一边不带,比对结果永远对不上。
2.2 SIMD 与 SVE 的分工:定长向量和可伸缩向量不是一回事
手册里向量相关条目铺得很开,因为 A64 上同时存在三套并行的向量体系,寄存器命名还各不相同。先把这张对照表记住,能省掉大量翻页:
| 寄存器组 | 宽度 | 数量 | 归属 |
|---|---|---|---|
| X0–X30 | 64 位 | 31 | 基础整数指令 |
| W0–W30 | 32 位 | 31 | X 寄存器的低 32 位视图 |
| SP / XZR | 64 位 | 编码复用 31 号 | 由指令语义决定是哪个 |
| V0–V31 | 128 位 | 32 | 浮点与 Advanced SIMD,可按 B/H/S/D/Q 解释 |
| Z0–Z31 | 128–2048 位可变 | 32 | SVE |
| P0–P15 | 按位谓词,长度随 VL | 16 | SVE 谓词与聚集/分散 |
| ZA | 实现定义 | 一组 tile | SME 的矩阵累加器 |
选型逻辑很直接。Advanced SIMD 是定长 128 位,寄存器宽度和 lane 数在编译期就定死,工具链成熟、跨核行为一致,做图像处理、音视频解码这类规整数据最稳。SVE 是向量长度无关的,同一份二进制在 VL 不同的核上都能跑,尾部处理靠谓词寄存器而不是标量收尾循环,代价是你不能假设向量有多宽。
这条约束会直接改变写法:不能写死 lane 数,元素计数要用CNTB/CNTW/CNTD系列现算,循环入口用whilelt生成谓词。判断目标机到底有没有 SVE,看/proc/cpuinfo的 Features 里有没有sve,或者读ID_AA64PFR0_EL1对应字段,别靠芯片型号猜。
另外手册里 SME 指的是可伸缩矩阵扩展(Scalable Matrix Extension),和内存加密不是一回事,做内存标记的是 MTE 那套IRG/ADDG/LDG/STG。这两个缩写在讨论里经常被混着用,查条目之前先确认自己在找哪个特性,否则翻半天翻到不相干的章节。
2.3 定位一条陌生指令:从助记符到编码页的五步
手册条目格式统一,读法也可以固定下来,我一般按这个顺序走:
- 先拆后缀再查。
ldrb、ldrh、ldrsb、ldrsw是各自独立的条目,按前缀猜会查到错的那条;b.eq的条件写在助记符里,主体条目是B.cond。 - 扫一眼有没有 “an alias of”。有就别读 Operation 了,直接跳到它指向的真实指令,否则你会对着一份转述理解语义。
- 读 Operation 伪代码,只盯三件事:读不读 SP、有没有写回基址、条件不成立时目标寄存器是否仍然被写。第三条最阴,
CSEL族在条件不成立时照样写目标寄存器。 - 看 Encoding 页的位域。
sf位决定 32 位还是 64 位,立即数字段宽度决定你能不能直接用常量,移位字段决定缩放系数。 - 最后看 Assembler symbols。立即数范围、对齐要求、哪些寄存器组合非法,全写在这儿,报错信息基本对得上这一段。
这五步里第三步最值钱。伪代码是手册里唯一不带歧义的部分,汇编语法、别名、示例代码都可能省略细节,只有 Operation 段把“读哪些寄存器、写哪些寄存器、什么时候写”讲全了。写内联汇编或者手改机器码之前,把这一段逐行对着看一遍,能省掉一整天的调试。
3. 寄存器与寻址:把 31 个通用寄存器和四种寻址方式吃透
指令条目查得再熟,落到具体代码上还是两个问题:操作数放哪、地址怎么算。A64 在这两块的设计和 32 位时代差别很大,尤其是“31 号寄存器”这个坑,几乎每个人都要踩一次。
3.1 编号规则与调用约定
通用寄存器是 X0 到 X30,没有 X31。编码里确实留了 31 号,但它的身份随指令语境变化:做数据运算时它是 XZR,读恒为 0、写直接丢弃;做地址计算或栈操作时它是 SP。这就是为什么add x0, x0, x31汇编不过,而add sp, sp, x0完全合法——前者在语法层就没有 x31 这个名字,后者走的是允许 SP 的那套编码。
写汇编 stub 或者手写函数序言时,调用约定比指令语法更容易出错:
| 寄存器 | 角色 | 谁负责保存 |
|---|---|---|
| X0–X7 | 参数与返回值 | 调用者 |
| X8 | 间接结果地址 | 调用者 |
| X9–X15 | 临时寄存器 | 调用者 |
| X16 / X17 | 链接器暂存(IP0 / IP1) | 调用者,且不要跨调用持有 |
| X18 | 平台寄存器,部分系统保留 | 视平台约定 |
| X19–X28 | 通用变量 | 被调用者 |
| X29 | 帧指针 | 被调用者 |
| X30 | 链接寄存器 LR | 被调用者,调子函数前必须自己存 |
| SP | 栈指针,须 16 字节对齐 | — |
血泪经验:写 trampoline 时只压自己真正改过的寄存器就够了,但 X30 必须存。A64 的bl会把返回地址写进 X30,你在 stub 里再调一次bl就把外层返回地址冲掉了,表现是函数返回后跳到随机地址,栈回溯看起来完全正常,只有最外层帧是坏的。
3.2 四种寻址模式与回写语义
A64 的 load/store 寻址就这几套写法,但偏移范围和回写时机差别很大:
| 写法 | 名称 | 有效地址 | 基址回写 |
|---|---|---|---|
[Xn, #imm] | 无偏移 / 立即数偏移 | Xn + imm | 不回写 |
[Xn, #imm]! | 前索引 | Xn + imm | Xn ← Xn + imm,先算地址再访问 |
[Xn], #imm | 后索引 | Xn | Xn ← Xn + imm,先访问再改 |
[Xn, Xm, LSL #s] | 寄存器偏移 | Xn + (Xm << s) | 不回写 |
[Xn, Wm, SXTW #s] | 扩展寄存器偏移 | Xn + SignExtend(Wm << s) | 不回写 |
立即数范围是硬约束,写超了汇编器直接报错:无偏移形式用 12 位无符号缩放立即数,实际可用范围是 4096 乘以访问宽度(按字节访问就是 0 到 4095,按 8 字节访问能到 32KB 左右);前索引和后索引用 9 位有符号立即数,范围是 -256 到 255 字节。想在一条指令里跨几兆的偏移,只能先把地址算出来。
// 逐字节拷贝:后索引写法每条省一条 add // x0 = src, x1 = dst, x2 = count copy_loop: cbz x2, done ldrb w3, [x0], #1 // 先读 [x0],再把 x0 自增 1 strb w3, [x1], #1 // 同上,先写后加 subs x2, x2, #1 b.ne copy_loop done: ret后索引的好处是省指令、循环体紧凑,坏处是基址寄存器在你还没反应过来时就已经变了。如果这段会被异常或信号打断、并且需要精确恢复现场,前索引或者显式add更安全——至少 Xn 指向的是已经访问过的那一块,而不是下一条待访问的地址。上面这段里subs直接改标志位、b.ne复用,比sub加cbnz少一条指令,这个小习惯在热路径里积少成多。
3.3 手写一段并交叉验证
写完的汇编不能只看,要让工具链把它翻回机器码再对一遍。日常验证流程固定成三条命令:
# 1. 只汇编不链接,保留符号表 aarch64-linux-gnu-gcc -c -O2 -march=armv8-a demo.S -o demo.o # 2. 反汇编,关掉别名,逐条和手册 Encoding 页对位域 aarch64-linux-gnu-objdump -d -M no-aliases demo.o # 3. 打印 .text 原始字节,核对 4 字节对齐和小端序 aarch64-linux-gnu-objdump -s -j .text demo.o参数含义得说清:-march=armv8-a决定允许哪些特性,不加后缀就只能用 8.0 基线指令;想验证 LSE 原子指令要写-march=armv8.1-a+lse,验证指针认证要写-march=armv8.3-a+pauth,特性没开汇编器会直接拒绝,这反而是最省事的一道防线。-M no-aliases决定输出是subs还是cmp。第三步用-s看节内容,是为了排查指令流错位——A64 指令固定 4 字节、必须对齐,一旦混进.inst伪指令或者数据字,后面的反汇编会整体错位,表现是一串看起来像指令的垃圾,很多人误以为是反汇编器有 bug。
4. 条件与原子:从 CSEL 到 LSE 的选型与写法
分支和并发是汇编层最容易写出隐蔽 bug 的两块。前者错在标志位,后者错在内存序,两者的共同点是错误不一定当场报,可能在高负载下才显形。
4.1 条件码与条件选择族
条件码一共 16 个,由 NZCV 四个标志位组合出来,手册的条件分支条目和条件选择条目共用这一套:
| 条件码 | 含义 | 条件码 | 含义 |
|---|---|---|---|
| EQ / NE | 相等 / 不等 | HI / LS | 无符号高 / 无符号低或同 |
| CS / CC | 进位 / 无进位 | GE / LT | 有符号大于等于 / 小于 |
| MI / PL | 负数 / 非负 | GT / LE | 有符号大于 / 小于等于 |
| VS / VC | 溢出 / 未溢出 | AL / NV | 恒真 / 恒假(少用) |
条件选择只有四条真实指令:CSEL(条件成立取第一个源,否则取第二个)、CSINC(否则取第二个源加一)、CSINV(否则取第二个源取反)、CSNEG(否则取第二个源取负)。CSET、CINC、CINV、CNEG全是它们的别名。
// 无分支取较大值 // w0 = a, w1 = b cmp w0, w1 csel w0, w0, w1, gt // gt 成立取 a,否则取 b ret有个细节手册写得很清楚但很容易被略过:CSEL无论条件是否成立都会写目标寄存器。如果目标恰好是某个只读的映射位置,或者你指望“条件不成立就保持原值”,逻辑就错了——要保持原值,得把原值作为第二个源显式传进去。另外这四条指令都读 NZCV,紧跟在会改标志位的指令后面时,别指望 CPU 帮你重排语义。
4.2 独占访问与 LSE 原子指令怎么选
原子操作在 A64 上有两代实现。8.0 时代靠 load-exclusive / store-exclusive 配对,8.1 起引入 LSE 扩展,给了一整套单指令原子操作。手册两组条目都收,选型依据是你对目标核的最低要求:
| 场景 | 无 LSE(8.0 基线) | 有 FEAT_LSE |
|---|---|---|
| 原子加 | LDXR/STXR循环 | LDADD加 A/L/AL 后缀 |
| 比较交换 | LDXR+ 比较 +STXR | CAS/CASA/CASL/CASAL |
| 交换 | LDXR/STXR循环 | SWP家族 |
| 位清除 | LL/SC 循环 | LDCLR家族 |
| 半字 / 字节 | 需按宽度选对应条目 | 加 B / H 后缀即可 |
// LL/SC 版本原子加:没有 LSE 时的兜底实现 // x0 = &counter, w1 = 增量 1: ldxr w2, [x0] // 独占读,标记监视区间 add w2, w2, w1 stxr w3, w2, [x0] // 独占写,w3 = 0 表示成功 cbnz w3, 1b // 失败就重来 dmb ish // 见 4.3 retstxr的第一个参数是状态输出,0 才是成功,非 0 必须重试。重试循环要写得极紧凑,中间别插函数调用、别做printf式的调试输出,否则监视区间被清掉,循环可能一直失败。想搬多字数据,用LDXP/STXP成对操作,单条LDXR管不了跨字的一致性。
选 LSE 的理由很实在:单条指令、硬件保证原子性、代码短、吞吐好,在高竞争场景下比 LL/SC 循环稳定得多。代价是它属于 8.1 之后的特性,跑在 8.0 的核上直接是非法指令。稳妥做法是编译期用__ARM_FEATURE_ATOMICS分支,运行期再用HWCAP_ATOMICS之类的特性位确认一遍,两条路都留,别赌目标机。
4.3 内存序三件套:DMB、DSB、ISB 什么时候必须上
屏障指令是另一处“不写不报错、写了才知道为什么之前会错”的地方。三条指令语义完全不同:
| 指令 | 保证什么 | 典型场景 |
|---|---|---|
DMB | 屏障前后的内存访问顺序 | 发布指针、释放锁、无锁队列 |
DSB | 屏障前的访问真正完成(含到达一致性点) | cache 维护、页表更新后、DMA 交接 |
ISB | 冲刷流水线,让后续指令看到新的上下文 | 改系统寄存器后、切换页表基址后 |
常见参数有sy(全系统)、ish(内部共享域)、ishst/ishld(只限存储或加载)、nsh、osh。经验是别一律dsb sy——那是把整颗核的流水线按到底,代价比你想的大。纯 load/store 的顺序问题用dmb ish就够;写SCTLR_EL1、TTBR0_EL1这类系统寄存器之后必须isb,否则后续指令仍按旧上下文取指;改完页表要dsb ish→ TLBI →dsb ish→isb走完整套,少一步都可能留下陈旧 TLB 条目。
5. 避坑与排查:用这份手册最容易翻车的五个现场
手册本身不会错,错的是读法。下面五条按“现象 → 原因 → 解决”记,都是翻手册时反复撞上的。
5.1 汇编与编码层面的三个现场
现象:mov x0, #0x12345678直接报立即数非法,可换个小的常量又能过。原因:MOV立刻数是个别名,能覆盖的编码有限,只能表达移位后 16 位模式那类立即数,超出范围汇编器就拒绝。解决:翻MOVZ/MOVN/MOVK三条条目,大常量拆成 16 位一段用movz+ 若干movk拼;数据常量更省事的做法是丢进字面量池用ldr x0, =0x12345678,让汇编器自己选。
现象:循环里ldr x0, [x1], #8跑出来的数据整体错位一条,前八个字节对、后面全偏。原因:后索引是先用 Xn 访问、再把 Xn 加偏移,有人按“前索引”的心智模型理解,多写了一条add,等于加了两遍。解决:把两种写法并排写注释,[Xn, #imm]!先加后用、[Xn], #imm先用后加;循环体里只在最后一条 load/store 上带后索引,别一条指令既做地址推进又做数据搬运还指望可读。
现象:给某个函数打二进制补丁,按cmp的机器码去扫,扫不到。原因:cmp是subs的别名,编译器产出的编码里根本没有叫cmp的东西,按助记符匹配等于按注释匹配。解决:一律用-M no-aliases反汇编后再提取特征,或者干脆按 Encoding 页的位域写掩码匹配,位域是稳定的,助记符不是。
5.2 手册查阅与特性层面的两个现场
现象:崩溃栈里ret之前的autiasp认证失败,栈内容看起来完好。原因:指针认证的签名和 SP 绑在一起,只要你在签名和认证之间动过 SP(哪怕只是 push 了一个寄存器没配对 pop),认证就会失败。解决:翻PACIASP/AUTIASP这两条条目,确认签名与认证之间 SP 必须保持一致;排查时看函数序言有没有中途调整栈,或者有没有alloca式的动态栈分配。
现象:照着某章节实现 SVE 相关逻辑,实际硬件行为和小节描述对不上。原因:2022-03 这一版里,涉及 Armv9-A 的部分(含 RME、SME)标注为 Beta 质量,性能监视器那个可选的 64 位外部接口更是 Alpha 质量。Beta 的意思是主要特性写全了,细节点可能缺。解决:翻特性章节之前先看文档开头的质量声明,落到 Beta 段落就同步查同一版本对应的机器可读规范文件(Release Notes 里提到的 A64 ISA XML),以那份为准,别把 Beta 描述当最终定论。
6. 反查习惯:把陌生指令三分钟内落到手册某一页
我现在处理陌生指令的动作已经固化成三步,几乎不再靠搜索。第一步拿地址:aarch64-linux-gnu-objdump -d --start-address=0x... --stop-address=0x... ./bin,只反汇编崩的那几条,避免在几万行输出里找一行。第二步关别名再反一次,加-M no-aliases,因为崩溃现场打印出来的助记符往往是糖,真实指令才是手册里有编码图的那条。第三步按助记符首字母去手册正文字母序翻,先读 Operation 伪代码,再对 Encoding 的sf位和立即数字段。
| 步骤 | 命令或动作 | 目的 |
|---|---|---|
| 定位 | objdump -d --start-address/--stop-address | 只看出问题的那几条指令 |
| 去糖 | objdump -d -M no-aliases | 拿到有真实编码的助记符 |
| 查义 | 手册 Operation 伪代码 | 确认读了哪些寄存器、写不写回 |
| 对码 | 手册 Encoding 位域 | 确认位宽、缩放、扩展方式 |
上个月有个典型现场,某图像处理 Demo 在一个循环里偶发访问越界,反汇编出来是ldr x0, [x1, w2, sxtw #3]。第一眼看像普通数组下标,实际是 W2 为负时先符号扩展成 64 位再左移三位,指针直接算到低地址去。翻到那条条目的 Encoding 页,option字段写着 110 就是 SXTW,S位标着是否符号扩展——两分钟定位,比在源码里找半天强。
从那以后我每次改完手写汇编或者处理一个陌生崩溃栈,都强制走一遍这三步,尤其是“关别名”那一步,哪怕觉得没必要。省下来的那点时间,远不够填一次误判的坑。希望帮到你。
本文还有配套的精品资源,点击获取