
简介Aarch64Armv8-A 64位汇编指令集参考手册面向从事ARM平台底层开发、驱动移植、性能优化与逆向分析的中高级工程师及嵌入式学习者用于系统查阅指令编码、语义与架构特性解决开发中指令用法、寄存器约定与寻址方式不清的问题。压缩包共1个PDF文件约9.7MB按指令类别组织涵盖基础算术与逻辑运算、比较分支、加载存储、原子操作、内存访问及多种寻址模式并延伸至SIMD、SVE可伸缩向量扩展、SME内存加密等高级特性同时说明X0–X31通用寄存器、V0–V31向量寄存器与异常中断处理机制。正文以指令条目为主体逐条给出汇编语法、操作数约束、伪代码语义及标志位影响便于按助记符快速检索定位。已有2993人学习下载适合作为案头工具书配合GNU as、LLVM等工具链实践查阅。1. 一条 ADD 拆成四种编码A64 指令表为什么长这样反直觉的地方在于AArch64 里写add x0, x1, x2和add x0, x1, #1落到机器码上是两条完全不同的指令——编码格式不同、位域布局不同、能带的操作数也不同。指令集手册把它们分别列为 ADD (shifted register)、ADD (immediate)、ADD (extended register)再加上会改写标志位的 ADDS一条助记符直接扩展成六个条目。第一次翻这份按字母序排布的 A64 完整指令集很容易觉得它啰嗦直到在反汇编里看见add x0, sp, x1, lsl #3才发现它走的根本不是你以为的那条路。这份 arm64 指令表真正的价值不在“查指令怎么拼”而在于把每条指令的编码格式、别名归属、特性门控摊在同一页上等于给了你一张 A64 解码器的思维导图。2. 寄存器堆的实际结构X0-X30、SP 与 ZR 的边界条件A64 的寄存器故事比“64 个 64 位通用寄存器”这个说法复杂得多。实际能数的只有 X0 到 X30 共 31 个加上一个身份不固定的第 31 号槽位再加上 SP、PC、PSTATE 和一堆系统寄存器。写汇编时最容易出错的地方几乎都集中在这个“第 31 号槽位”到底算谁上。把寄存器的账本理清后面看寻址和条件执行才不会一路踩空。2.1 第 31 号寄存器的双重身份SP 还是 XZR指令编码里 Rn、Rd、Rm 这类字段只有 5 位能表示 0 到 31。0 到 30 对应 X0 到 X30 没有歧义31 号则要看具体指令在绝大多数数据处理指令里它是 XZR读恒为 0写直接丢弃在访存指令的基址位置、以及 ADD/SUB 的立即数形式里它被解释成 SP。判断依据不在助记符里而在每条指令的编码表位域说明中会标注“31 means SP”还是“31 means ZR”。// 同样是 31 号语义完全不同 add sp, sp, #16 // ADD (immediate)31 解释为 SP栈指针真的动了 add x0, x1, xzr // ADD (shifted register)31 解释为 XZR加 0 mov x0, sp // 注意这条不是 mov是 add x0, sp, #0 的别名第三行是典型的认知盲点。mov x0, sp看起来像寄存器搬运实际汇编成 ADD (immediate) 的别名形式因为 A64 的 MOV 只在“寄存器到寄存器”时是 ORR 的别名一旦源是 SP 就必须走加法路径。这也解释了为什么mov sp, x0在某些汇编器上直接报错——没有对应的编码能表达“写 SP”这个动作只能写成add sp, x0, #0。2.2 W 寄存器写入为什么等价于零扩展A64 规定任何对 Wn 的写入都会把对应 Xn 的高 32 位清零而不是保留。这不是某个实现的优化而是架构级约定。它让 32 位运算天然具备“结果可预测”的性质代价是从 AArch32 迁过来的代码会出现意料之外的行为——AArch32 里mov r0, #-1之后把 r0 当 32 位用没问题迁到 A64 写成mov w0, #-1得到的是0x00000000FFFFFFFF与mov x0, #-1的0xFFFFFFFFFFFFFFFF完全不同。mov w0, #-1 // x0 0x00000000FFFFFFFF高 32 位被清零 mov x0, #-1 // x0 0xFFFFFFFFFFFFFFFF movz w0, #0x1234 // 只写低 16 位其余清零 movk w0, #0x5678, lsl #16 // 保留其他位只改 lsl 指定的那 16 位这里顺带说明定长编码带来的连锁后果。MOVZ、MOVN、MOVK 的立即数字段只有 16 位加一个 2 位的移位选择0/16/32/48所以一条指令最多只能写 16 位。想构造任意 64 位常量就得拆成最多四条 MOVZ/MOVK 组合编译器在常量折叠阶段会做这个拆分。所谓“A64 代码体积比 AArch32 大 20%~30%”很大一部分就来自这里定长 32 位换来了译码简单和流水线友好代价是立即数表达力被削平。2.3 寄存器角色约定与典型栈帧硬件层面对 X0 到 X30 一视同仁但 AAPCS64 调用约定给它们分配了角色X0-X7 传参和返回X8 传间接结果地址X9-X15 是调用者保存的临时寄存器X16/X17 是 PLT 桩专用的 IP0/IP1X18 被平台保留X19-X28 由被调用者保存X29 是帧指针X30 是链接寄存器。这套约定不是强制但整个工具链、调试器和异常展开都依赖它违反之后崩溃现场会非常难读。// 典型的函数序言与尾声pre-index 与 post-index 各出现一次 func: stp x29, x30, [sp, #-32]! // 预减先 sp - 32再把 x29/x30 存到新 sp 处 mov x29, sp // 建立帧指针便于回溯 str x19, [sp, #16] // 保存被调用者寄存器 // ... 函数体 ... ldr x19, [sp, #16] ldp x29, x30, [sp], #32 // 后增先从 sp 加载再 sp 32 ret // 等价于 br x30stp ... [sp, #-32]!那个感叹号是写回标记地址表达式里的偏移先作用到基址寄存器上再做访存。[sp], #32则是后变址先按当前 sp 访存再把偏移加到 sp 上。两种写法在异常展开时语义不同——前者在指令执行前就改 sp若中途被中断展开器看到的 sp 已经是新值后者相反。写内核或信号处理相关代码时这个差别会直接影响栈回溯是否正确。2.4 SIMD 与 SVE 共用同一片寄存器文件V0 到 V31 是 128 位向量寄存器供 NEON 使用Z0 到 Z31 是 SVE 的可伸缩向量寄存器长度在 128 到 2048 位之间由实现决定。关键在于它们复用同一片物理寄存器堆的低 128 位所以V0与Z0的低半部分在语义上是重叠的。P0 到 P15 是 SVE 的谓词寄存器每个位对应向量中一个元素是否参与运算这让 SVE 有了按元素粒度的掩码能力而不是 NEON 那种整寄存器操作。这个设计带来的实操后果从 NEON 迁到 SVE 的代码不能假设向量长度固定为 128 位必须用cntb、whilelt这类指令在运行时探测长度并生成循环。常见的实现思路是把循环写成“按向量长度步进 谓词收尾”的形式第一条whilelt p0.s, x0, x1同时承担了边界检查和生成掩码两个职责省掉了单独的尾循环。3. 寻址与访存LDR 的三种编码和原子指令的分水岭访存是 A64 里编码分叉最多的部分光 LDR 就分 immediate、literal、register 三种形态每种形态的偏移范围、是否缩放、能不能写回都不一样。更麻烦的是同一段源码在不同优化级别下可能生成完全不同的访存指令序列。搞清楚这些编码边界才能解释“为什么编译器在这里多插了一条 add”这类问题。3.1 LDR 三种形态的偏移边界LDR (immediate) 用 12 位无符号偏移并且按访问宽度缩放字节访问范围是 0 到 40958 字节访问乘 8 之后是 0 到 32760。这就是大结构体成员访问会翻车的地方——偏移量稍微超一点汇编器直接报“immediate out of range”。LDR (literal) 是 PC 相对寻址范围 ±1MB常用于加载全局变量的地址。LDR (register) 则是基址加索引寄存器支持扩展和移位适合数组遍历。ldr x0, [x1, #32760] // 合法12 位 * 8 的上限 ldr x0, [x1, #32768] // 汇编失败超出 scaled offset add x1, x1, #32768 ldr x0, [x1] // 手动拆成两条编译器通常也这么干 ldur x0, [x1, #-8] // 9 位有符号非缩放偏移范围 -256..255 ldr x0, [x1, x2, lsl #3] // 基址 索引 * 8遍历指针数组的常见形态这里有个取舍值得说清楚。为什么不做一套统一的大偏移编码因为指令定长 32 位操作码、寄存器号占掉大半留给立即数的位宽是硬约束。设计者的选择是把“常用小偏移”做成缩放形式范围扩大 8 倍但只对对齐访问有效把“任意偏移”留给非缩放的 LDUR 和手动地址计算。这套组合在实际代码里覆盖率很高代价是汇编器和反汇编器要做更多形态判别。ADRP 是同一思路的延伸。它把 PC 所在 4KB 页的基址取出加上 21 位页偏移得到 ±4GB 范围内的页对齐地址adrp x0, symbol // 取 symbol 所在 4KB 页的基址PC 相对 add x0, x0, :lo12:symbol // 补上页内偏移:lo12: 是链接器重定位修饰符两条指令加起来才凑出一个完整地址原因还是那个32 位编码塞不下 33 位以上的地址偏移。位置无关代码基本都长这样看到adrp就该意识到后面大概率跟一条add两者间距过远或者中间插了会改标志位的指令就要怀疑是不是手写汇编时漏了配对。3.2 独占访存与 LSE 原子两条技术路线实现原子读改写有两条路。老路线是 LL/SCload-linked / store-conditionalLDXR 读并置监视器STXR 写回并把成功与否写入状态寄存器失败就重试。新路线是 LSE 扩展的单指令原子LDADD、LDCLR、LDEOR、CAS、SWP一条指令完成“读-改-写”还带 Acquire/Release 后缀。// 路线一LL/SC适合任意复杂的读改写 retry: ldxr w1, [x0] // 读并置独占监视器 add w1, w1, #1 stxr w2, w1, [x0] // 写回w2 0 表示成功 cbnz w2, retry // 失败重试 // 路线二LSE单条完成争用下更省 ldadd w1, w2, [x0] // *x0 w1旧值写回 w2ldadd的操作数顺序是新手最容易记反的第一个寄存器既是加数也是旧值的回写目标第二个才是内存地址的操作数宽度载体。写引用计数递增时ldadd w1, w2, [x0]之后 w2 里是递增前的值这个“顺带返回旧值”的特性正好匹配“先取旧值再判断是否归零”的模式。两条路线的取舍很清楚。LL/SC 能表达任意复杂的读改写逻辑比如“读到旧值后做三次不同修改再写回”但监视器是有限资源循环里嵌套 LL/SC、或者两条 LL/SC 之间隔着函数调用都可能让监视器被清掉导致无限重试。LSE 单指令没有重试循环争用激烈时性能更稳定但只能做固定几种运算。CLREX 的存在就是为了处理前一种情况——异常返回、上下文切换后监视器状态不确定主动清一次比猜测更安全。还有一条容易被忽略的中间态LDAPR。它是 RCpc 语义的 acquire 加载比 LDAR 弱但对“地址依赖”这种模式足够用在弱内存序机器上能省掉一部分屏障开销。什么时候该用 LDAR、什么时候 LDAPR 就够取决于你依赖的是数据依赖还是控制依赖这一点在规范的内存模型章节里有严格定义。3.3 DMB、DSB、ISB 各自拦截的内容三个屏障看着像一回事实际上拦截的是流水线不同阶段。DMB 只保证屏障前后的内存访问顺序对其他观察者可见不保证已经完成DSB 更强要求屏障前的所有访存真正完成才继续ISB 跟内存没关系它冲刷流水线让之前对系统寄存器的修改对后续指令可见。msr ttbr0_el1, x0 // 改页表基址 dsb ish // 等这次写入对内层共享域可见 isb // 冲刷流水线后续指令才用新页表 tlbi vmalle1 // 顺序反了就会用到旧 TLB dsb ish isb口诀是“改系统寄存器要 DSB ISB 配对”缺了 ISB 的典型症状是“代码逻辑没错但偶尔跑飞”因为后续指令可能已经预取并按照旧配置译码了。屏障的共享域参数ish、ishst、osh、nsh决定影响到哪一级缓存和哪些核写驱动时选错域的后果是多核之间偶发不一致这类问题用思路上没问题、就是偶发崩溃来形容最贴切。4. 条件执行IT 块消失后CSEL 家族承担了什么从 AArch32 迁过来的开发者最不习惯的一点是A64 没有 IT 块了。AArch32 能把最多四条指令挂在一个条件码下顺序执行A64 里这套机制彻底删除。原因不复杂——4 位条件码要占掉指令的高 4 位而定长 32 位编码里每一位都已经被操作码和寄存器号分完了更关键的是条件执行会给每条指令引入对 NZCV 的隐式依赖寄存器重命名和乱序执行都得为此让路。替代方案是把条件逻辑收拢到少数几条指令上剩下的交给分支预测。4.1 用条件选择替代条件执行条件执行的语义是“条件不成立就跳过”条件选择的语义是“两条路都算出来按条件挑一个”。前者省计算但阻碍并行后者多算一次但路径确定。// if (a b) r a; else r b; 编译器几乎总是生成这个 cmp w0, w1 csel w2, w0, w1, gt // 有符号比较用 gt无符号要用 hi用gt还是hi是另一个高频错误点CMP 之后 NZCV 的组合对有符号和无符号含义不同选错条件码在正数范围内测试完全正常一遇到负数或超大无符号值就出问题。这类 bug 的隐蔽性极高因为日常测试数据通常落在两种解释一致的区间里。分支和选择的边界在哪经验法则是看两条路径的指令数。一两条指令、且没有副作用不会触发异常、不会写内存用 CSEL 更快避免预测失败带来的十几拍惩罚路径里包含函数调用或访存分支更划算因为 CSEL 要求两边都执行可能白白多做一次访存。编译器在这个判断上做得比人稳但手写汇编时得自己拿捏。4.2 四种条件选择原语和它们的别名CSEL、CSINC、CSINV、CSNEG 构成一组最小完备集条件选择、条件选择加一、条件选择取反、条件选择取负。其他所有“条件 XX”指令都是它们的别名这是理解 A64 条件指令的关键。cmp w0, #0 cset w1, eq // 等价于 csinc w1, wzr, wzr, ne cinc w2, w2, ne // 条件自增等价于 csinc w2, w2, w2, eq cneg w3, w3, mi // 条件取负等价于 csneg w3, w3, w3, plcset w1, eq为什么编成csinc ..., ne因为 CSINC 的语义是“cond 成立取 Rn否则取 Rm1”。令 Rn Rm wzr条件取反为 ne则 eq 成立时走 Rn 得 0eq 不成立时走 Rm1 得 1——刚好是布尔值的两个结果。这种“用取反条件配合加减一”的技巧支撑起了整个条件运算体系四个原语加两种变形就覆盖了几乎所有常见模式编码开销被压到最小。反汇编器的默认行为在这里会制造困惑它优先显示别名。所以你在反汇编里看到cset翻指令表却找不到这个条目只能搜到 CSINC 并看到一行“an alias of”。这也解释了为什么查表时要先确认指令是不是别名否则会误以为手册漏了内容。4.3 CCMP 链式比较与 BTI、PAC 的控制流保护多条件判断如果逐条 CMP 再分支会产生一串分支指令每个都可能预测失败。CCMP 和 CCMN 就是为此设计的它们在条件成立时才真正做比较条件不成立时直接把 NZCV 设成指令里编码的 4 位立即数。// if (x0 0 x1 0) goto all_zero; cmp x0, #0 ccmp x1, #0, #0, eq // eq 成立才比较 x1 与 0否则 NZCV 直接置为 #0Z0即不等 b.eq all_zero第三个操作数的含义需要拆开看它是一个 4 位立即数直接对应 N、Z、C、V 四个标志位。写#0意味着“条件不成立时把 Z 清掉”从而让后面的b.eq必然失败等价于短路的“与”。#4则对应 Z1可以做“或”的短路。控制流保护还有两块内容绕不开。BTI 要求间接分支br、blr的目标必须落在带bti c或bti j标记的指令上否则触发异常用来缓解跳转导向编程攻击。PAC 则是对返回地址和函数指针做签名pacia用密钥和修饰符算出指针认证码写进指针高位autia在使用前校验。两者经常一起出现但作用对象不同——BTI 管“能不能跳到这里”PAC 管“这个地址有没有被改过”。// 典型的 PAC BTI 函数框架 func: bti c // 允许作为间接分支目标 paciasp // 用 SP 作为修饰符签名 LR stp x29, x30, [sp, #-16]! // ... ldp x29, x30, [sp], #16 autiasp // 校验并还原 LR retpaciasp与autiasp必须用同一个修饰符这里是 SP。如果在序言里用了pacia x30, x29尾声却写了autiasp认证一定失败——失败时高位被改成非法值下一条ret直接跳到不可执行地址。这类问题在启用 PAC 的构建里表现为“必现崩溃”反而好查难查的是修饰符一致但栈帧中途被改动的情况。5. 避坑与排查那些看起来对不上的指令5.1 汇编器报 immediate out of range现象很直接一段结构体访问代码在加了个字段之后突然编不过报错指向某条ldr。原因在于 LDR (immediate) 的 12 位无符号偏移会按访问宽度缩放8 字节访问上限是 32760第 4096 个 8 字节字段刚好越界。解决方式有两种让编译器帮你写成add ldr两段式或者改用非缩放的 LDUR但 LDUR 范围只有 -256 到 255通常是往回走时更合适。同类问题还有 ADD (immediate) 的 12 位立即数可选左移 12 位以及 MOV 常量必须拆成 MOVZ/MOVK 序列。排查时看反汇编里是不是多了一条来历不明的movz/movk组合一般就能对上号。5.2 指令表里查不到的助记符反汇编里出现cset、cinc、mov、cmp、tst、neg翻字母序指令表却找不到对应条目只在某条指令下看到“an alias of”。原因是这些全是别名编码层面只存在 CSINC、ORR、SUBS、ANDS、SUB 等原语。排查手段是用反汇编器的禁用别名选项输出原始助记符并与手册编码表对照能立刻看出真实指令。这个问题的影响不止于查资料。别名意味着编码字段可能做了重映射比如cmp x0, #0实际是 SUBS 丢弃目标寄存器的形式目标寄存器字段被填成了一个不写回的值。如果手工按别名语义去推算编码几乎必错。5.3 加了原子指令却仍然丢更新现象是引用计数在多核压力下偶尔少加一次反汇编一看确实用了 LDADD指令没写错。原因通常落在内存序上没有 A/L 后缀的 LDADD 是 relaxed 语义只保证原子性不保证顺序如果后面还依赖其他内存访问的顺序就必须换成 LDADDAL 或在两侧补 DMB。另一个常见原因是用 LL/SC 写循环时中间插了函数调用或异常可能发生的指令监视器被清掉后 STXR 一直返回失败看起来像死循环实际是重试条件没设计好。解法是把 LL/SC 的区间压到最短区间内不调用函数、不跨越可能触发异常的访存必要时用 CLREX 显式清理状态。5.4 特性缩写对不上号A64 相关文档里缩写密集混淆成本很高。一个高频误读是把 SME 当成内存加密——FEAT_SME 是可伸缩矩阵扩展做的是矩阵运算加速内存标记那一套是 MTE靠 IRG 生成标记、GMI 合并标记、LDG/STG 读写带标记的指针而数据加密相关的则是另外的密码学扩展指令逻辑完全不同。另一个混淆点是 BTI 和 PAC前者限制间接分支的落点后者校验指针完整性两者常一起开启但缺一个都不会报同一个错。排查这类问题时先确认崩溃是“跳到了非法目标”BTI还是“指针校验失败”PAC再去对特性开关能省掉大量试错。6. 从指令表到实现细节怎么验证你读到的是对的这份按字母序排布的 A64 指令集最值得称道的地方不是收录了多少条指令而是它把“指令—编码—别名—特性门控”四层信息放在同一个结构里。每条指令都附带编码位域图、操作数约束、以及它属于哪个架构版本扩展。这个组织方式本身就是一种设计取舍以 XML 为单一源生成文档好处是编码表和伪代码永远不会与实现脱节代价是阅读体验偏机械同一个助记符的多个形态要跳好几次。验证自己理解是否正确我一般走三步。第一步拿一条指令去查它的别名关系确认反汇编里看到的是原语还是别名第二步看编码位域表里 31 号槽位被解释成 SP 还是 ZR这一步能揪出大半寄存器相关误解第三步确认指令的特性门控比如 CAS 系列属于 LSE 扩展在只支持基础 Armv8.0 的核上根本不存在交叉编译时如果不加对应的-march选项汇编器可能直接报未知指令。取舍的脉络其实很统一。定长 32 位编码换来了译码器简单和流水线友好代价是立即数表达力被削平、条件执行被取消、大偏移寻址要拆成两条。原子操作给两套方案LL/SC 和 LSE是因为前者灵活后者稳定没法用一套覆盖所有场景。向量部分同时保留固定 128 位的 NEON 和长度可变的 SVE是因为前者代码密度高、后者面向未来硬件。这些选择单看每一条都像妥协放在一起才看得出是在“编码空间、执行效率、向后兼容”三角里反复挪位置。踩过的坑里印象最深的一次是手写一段上下文切换代码改完系统寄存器忘了跟 ISB测试跑了三天才复现一次跑飞。从那以后我每次碰系统寄存器、每次写屏障都强制把 DSB 和 ISB 成对写出来哪怕看上去多余——宁可多一条指令也不要那种只在压力下出现的偶发崩溃。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。