本节摘要:AArch64 的编程模型可以压缩成一张名册:三十一个通用寄存器
x0至x30(各可按 64 位x或 32 位w访问)、恒零寄存器xzr、按异常级别换影子的sp、几乎不可直接寻址的pc、携带条件标志的pstate,以及一大片以msr与mrs访问的系统寄存器。本节按"通用、专用、系统"三层把名册过一遍,并做一个寄存器现场核对实验。
上一节我们从时间轴看完了楼层怎么加盖,本节回到空间轴:走进今天的楼,把房间里每一张牌认一遍。认牌是后面一切章节的前提——第 3 章的编码字段全是寄存器编号,第 5 章的栈帧手推全程围着 sp、x29、x30 转。先说清一个阅读口径:教材与手册里"通用寄存器"与"特殊寄存器"的分界略有出入,本节采用"通用、专用、系统"三分法,把旧教材里分开讲的两组合并到一张图上看,因为它们本来就长在同一套编号体系里。
三十一个通用寄存器按调用约定划成四段。参数与返回值段 x0 至 x7:函数调用时前八个参数从这里进,返回值从 x0 出,这是第 5 章 AAPCS 的主角。临时寄存器段 x8 至 x15:可以随手用,但跨函数调用不保证存活——准确说 x8 有个间接结果寄存器的副业,x16、x17 被链接器与虚拟平台动用保留。被调方保存段 x19 至 x28:谁想用谁负责保存,函数序言里成排的 stp x19, x20, [sp, -16]! 就是在安顿它们。帧指针与链接寄存器 x29、x30:一个给栈帧当锚,一个存返回地址。
每个寄存器有两个马甲:x 名按 64 位访问,w 名按 32 位访问。写 w0 只改 x0 的低 32 位,高 32 位清零而不是保留——这是 AArch64 与许多旧架构不同的地方,也是初学者高频踩坑点:想在循环里对 64 位计数器用 w 后缀做加法,等于每次把高位清掉。另有第 32 号"编外"寄存器 xzr:读它得零,写它等于丢进下水道,mov x0, xzr 因此是清零的惯用法。

sp、pc、pstate 这三位是"专用寄存器"。sp 值得单独说:它按异常级别各有一份影子(sp_el0 至 sp_el3),且 AArch64 允许 EL0 用户态选用 x30 之外的独立栈指针,切换时用 mov sp, x29 这类指令搬运。pc 则最特殊——它不能作为普通操作数参与运算,读它的间接办法是 adr x0, .(取当前地址)之类,改它的唯一正路是跳转与 ret、eret。这与 x86 可以把 rip 当数据用截然不同,是从"你能拿到 PC"到"你只能按规矩走"的收紧。
pstate 是处理器状态的总包:NZCV 四个条件标志(负、零、进位、溢出)、中断屏蔽位、当前异常级别等都在里面。日常打交道最多的是 NZCV,第 5 章条件码一节会逐个标志做实验;访问它的指令不是 mov 而是 mrs x0, nzcv(读)与 msr daifset, #2(改中断屏蔽)这类专用通道。
系统寄存器是最大的一圈:页表基址、异常向量基址、计时器、性能计数器、缓存配置……每一个都有编号如 sctlr_el1、ttbr0_el1、vbar_el1。它们只能用 mrs 与 msr 访问,且多数要求 EL1 以上权限——这正是 2.3 节异常级别的用武之地。
纸上认牌不如现场点名。用上一章的调试环境,在调试器里把关键寄存器逐个核对:
(gdb) info registers x0 0x7ffffff000 x1 0x5 x2 0x0 x29 0x7ffffff0a0 x30 0x4007c4 sp 0x7ffffff0a0 pc 0x400780 cpsr 0x60000000 // 此行即 pstate 的传统视角 (gdb) p/x $x0 & 0xffffffff // 演示 w0 口径:低 32 位 $1 = 0xfffff000 (gdb) set $x2 = $xzr (gdb) p/d $x2 $2 = 0 // xzr 读零验证
三处值得盯住:x29 与 sp 恰好相等——函数序言刚把帧指针设成栈顶,这是标准 prologue 的指纹;cpsr 显示 0x60000000,二进制下正是 Z 标志与 C 标志同时为 1 的形态,说明此前某条比较指令比较出了"相等";pc 停在 sum 入口而 x30 指向调用方,返回路径已经铺好。变式实验:在函数中途打断点再看 sp,数一数它与入口值的差,再对照反汇编里的 stp 指令条数——你会亲手推出栈帧大小,这正是第 5 章 5.2 节手推栈帧的预热。
把"w 写入清零高位"这条规矩再钉一次,用一段真实事故。某驱动里有个 64 位地址累加循环,原作者为省事写了 add w2, w2, #16——他心想"增量只有 16,32 位够了"。单步调试时一切正常,跑到第 4 GiB 之后地址突然折返:w2 的写入把 x2 高 32 位清零,而 x2 里存的是完整地址。症状是"跑一段时间后设备读写错位",日志里看不到任何越权,因为根本没有越权——地址被合法地算错了。
排查这套路数值得记住:在调试器里对地址寄存器设 watchpoint(watch x2),捕获到清零发生的那一条指令,反汇编对准 w 后缀,事故定性只用了十分钟。变式:反过来利用这个特性也是合法技巧——mov w0, #1 正是"构造低 32 位为 1、高 32 位归零"的最短写法,很多库代码靠它省一条 movk。规矩本身无所谓好坏,关键是写的人要清楚它同时在干两件事。
问:xzr 既然恒零,为什么寄存器编号还要占用一个编号位? 答:它不占——31 个通用寄存器用满 1 到 31 的五位编号,编号 31 在指令里按上下文解释成 xzr 或 sp:多数数据处理指令里是 xzr,访存与部分系统指令里是 sp。同一个编号两副面孔,这正是把两个"特殊值"塞进有限编码空间的巧办法,第 3 章拆编码时会再碰到它。
问:调试器里怎么一次看清所有寄存器的两种口径? 答:info registers 默认按 x 口径显示;要看 w 口径,逐个打印 p/x $w3 即可——理解了"w 只是低 32 位的视图",两种显示就都成了同一份数据的不同切面。
x0 至 x7 传参返回、x8 至 x15 随手用、x19 至 x28 用完要还、x29 与 x30 管栈帧与返回。w 寄存器把高 32 位清零,不保留——与直觉相反的硬规定。sp 按异常级别有影子副本。mrs 与 msr 是唯一通道,多数门禁在 EL1 及以上。名册认完,还剩最后一问:这么大的权限差,处理器靠什么分桌管理?下一节讲异常级别与运行模式,把 EL0 到 EL3 的权限台阶搭起来。