APM_寄存器

前言

下面讨论的是 Apple 平台目前常见的 ARM64 寄存器,不同 CPU 架构的寄存器名称和调用规则不同。

寄存器是什么

寄存器 是 CPU 内部用来临时保存数据和执行状态的高速存储单元。

程序中的数据通常保存在内存里,但 ARM64 的大多数计算指令不能直接拿两块普通内存做运算,而是先把数据放入寄存器,再让 CPU 对寄存器进行计算。例如:

swift 复制代码
  ldr w0, [x1]       ; 从 x1 指向的内存读取数据,放进 w0
  add w0, w0, #1     ; 让 w0 加 1
  str w0, [x1]       ; 把结果写回内存

过程:

内存 -> 寄存器 -> CPU 计算 -> 寄存器 -> 内存

因此,寄存器主要承担三类工作:

  • 保存正在参与计算的数据
  • 保存内存地址
  • 保存程序当前执行到哪里、函数返回到哪里等控制信息

寄存器数量很少,不能替代内存。编译器会决定哪些值暂时放在寄存器里,哪些值放在栈中。如果寄存器不够,或者某个值必须长期保存,编译器可能把它临时写入栈内存,这叫做寄存器溢出(register spill)。

保存的内容

寄存器保存的是二进制。

寄存器本身并不知道里面保存的是整数、对象还是地址,它只保存一串二进制位。

假设 x0 中保存:

0x0000000100123456

这个值可能是:

  • 一个整数
  • 某个对象的内存地址
  • 某条机器指令的地址
  • 一组标志位
  • 计算过程中的中间结果

它的含义由当前指令和程序上下文决定。

所以在调试器中看到一个寄存器值时,不能仅仅因为它看起来像地址,就认为它一定是指针。

ARM64 中的通用寄存器

ARM64 提供了 31 个 64 位通用寄存器:

swift 复制代码
x0、x1、x2......x30

它们还可以使用 32 位名称:

swift 复制代码
w0、w1、w2......w30

x0w0 不是两个独立寄存器,而是同一个寄存器的不同宽度:

swift 复制代码
x0:完整的 64 位
w0:x0 的低 32 位

比如:

swift 复制代码
  add x0, x1, x2     ; 进行 64 位加法
  add w0, w1, w2     ; 进行 32 位加法

不同的寄存器有不同的作用

从硬件能力来说,大多数通用寄存器都可以保存普通整数或地址。

但是如果不同编译器、不同函数随意使用寄存器,它们就无法互相调用,因为如果规则不同,那么同样的寄存器会被解释成不同的东西。因此 ARM64 定义了一套函数调用约定,也就是 ABI。

ABI 可以理解成机器代码之间共同遵守的规则,它规定:

  • 参数放在哪里
  • 返回值放在哪里
  • 哪些寄存器允许函数随意修改
  • 哪些寄存器修改前必须保存
  • 栈应该怎样组织

在一次函数调用中:

  • 发起调用的函数叫 caller,调用者
  • 被调用的函数叫 callee,被调用者

在理解寄存器分类前,还需要理解两个概念:

  • Caller-saved:被调用函数可以修改。调用者如果还想保留原值,就要在调用前自己保存
  • Callee-saved:被调用函数如果要修改,就必须先保存旧值,并在返回前恢复

这不是说某一类寄存器更重要,而是在规定由谁负责保护原值。

常见寄存器

寄存器 常见作用
x0~x7 传递前 8 个整数或指针参数,也用于返回结果
x8 某些间接返回结果使用的地址,也可以作为临时寄存器
x9~x15 Caller-saved 临时寄存器
x16x17 链接器、跳板代码和函数内部使用的临时寄存器
x18 平台保留寄存器;Apple 不允许普通代码使用
x19~x28 Callee-saved,需要跨函数调用保留的值
x29 / fp Frame Pointer,栈帧指针
x30 / lr Link Register,链接寄存器,通常保存函数返回地址
sp Stack Pointer,栈指针
pc Program Counter,程序计数器
xzr / wzr Zero Register,零寄存器
NZCV 条件标志寄存器
v0~v31 浮点和 SIMD 向量寄存器

这些名字描述的是调用约定中的常见职责,不代表寄存器在函数内部只能做这一件事。

x0~x7:参数和返回值寄存器

对于整数、指针等普通参数,函数调用时通常使用 x0~x7 传递前 8 个参数。

比如:

swift 复制代码
  int add(int a, int b) {
      return a + b;
  }

  int result = add(10, 20);

可以用下面的简化 ARM64 汇编表示:

swift 复制代码
  mov w0, #10        ; 第一个参数 a
  mov w1, #20        ; 第二个参数 b
  bl  _add           ; 调用 add
                     ; 返回后,w0 保存结果 30

  _add:
      add w0, w0, w1
      ret

进入 add() 时:

swift 复制代码
w0 = 10
w1 = 20

执行:

add w0, w0, w1

意思是:

w0 = w0 + w1

所以执行后:

w0 = 30

函数返回时,调用者从 w0 中取得返回值。

x0 ~ x7 是 caller-saved,函数一旦继续执行,编译器可以重新使用它们。因此在函数中看到 x0,不能保证里面仍然是最初的第一个参数。

x19~x28:需要保留的寄存器

x19 ~ x28 属于 callee-saved。

假设 main() 把一个需要长期保存的值放进 x19,然后调用 foo()

swift 复制代码
main
	x19 = 某个重要值
	调用 foo
	返回后仍需使用 x19

如果 foo() 也想使用 x19,它必须:

swift 复制代码
进入 foo
	把旧 x19 保存到栈中
	使用 x19
	从栈中恢复旧 x19
返回 main

这样 main() 可以相信调用结束后,x19 仍然保持原值。

这类寄存器适合保存需要跨越多个函数调用继续使用的数据。

sp:栈指针

sp 是 Stack Pointer,指向当前线程栈正在使用的位置。

函数需要保存返回地址、旧寄存器或者局部数据时,可以先调整 sp,在栈中开辟空间:

swift 复制代码
sub sp, sp, #32

表示让 sp 向低地址移动 32 字节,为当前函数分配栈空间。

函数返回前再恢复:

swift 复制代码
add sp, sp, #32

需要注意,sp 指向的是当前栈边界,不是固定指向某个局部变量。随着函数进入、退出以及临时空间分配,sp 会不断变化。

Apple ARM64 ABI 要求 sp 保持 16 字节对齐。

fp / x29:栈帧指针

fp 是 Frame Pointer,也是 x29 的别名,是同一个寄存器。

sp 会随着函数内部的空间分配不断变化,因此函数有时需要一个相对稳定的位置作为当前栈帧的参考,这就是 fp

典型的函数入口可能执行:

swift 复制代码
  stp x29, x30, [sp, #-16]!
  mov x29, sp

第一条指令把旧 x29 和返回地址 x30 保存到栈中,第二条指令让新的 x29 指向当前 Frame Record。

于是栈中形成:

swift 复制代码
  当前 x29
     │
     ▼
  ┌──────────────────┐
  │ 调用者的 x29      │
  ├──────────────────┤
  │ 返回地址          │
  └──────────────────┘

通过保存的旧 x29,可以继续找到上一层函数的 Frame Record。

不过,不是每个函数都会创建新的 Frame Record。叶子函数,也就是不再调用其他函数的函数,可能直接沿用调用者的 x29

Apple 要求 x29 始终指向一条有效的 Frame Record 链,但允许叶子函数、尾调用等不为自己增加新的记录。

lr / x30:返回地址

lr 是 Link Register,也是 x30 的别名。

ARM64 使用 bl 调用函数:

swift 复制代码
bl _add

这条指令同时做两件事:

  1. 把下一条指令的地址写入 lr
  2. 跳转到 _add

因此,lr 中保存的是:

被调用函数执行结束后,应该回到调用者的哪个位置。

函数执行 ret 时,默认跳转到 lr 中的地址。

但是如果当前函数又调用了另一个函数,新的 bl 会覆盖 lr。因此非叶子函数通常需要在调用其他函数前,把自己的返回地址保存到栈中。

这也是为什么 fplr 经常成对保存:

swift 复制代码
stp x29, x30, [sp, #-16]!

pc:当前执行位置

pc 是 Program Counter,程序计数器,用来表示 CPU 当前执行到了哪条机器指令。

可以把程序想象成一系列带地址的指令:

swift 复制代码
  0x1000  mov w0, #10
  0x1004  mov w1, #20
  0x1008  bl  _add
  0x100c  ...

当 CPU 执行到 0x1008 附近时,pc 就反映相应的指令位置。

不过在 ARM64 中,pc 不是像 x0 那样可以随意参与普通运算的通用寄存器。跳转、函数调用、函数返回等指令会改变程序的执行位置,调试器和系统保存的线程状态也会显示 pc

xzr / wzr:永远为零的寄存器

xzrwzr 是 Zero Register:

  • 读取它,永远得到 0
  • 向它写入数据,结果会被丢弃

例如:

swift 复制代码
mov x0, xzr

可以把 x0 清零。

NZCV:条件标志

  • N:Negative,结果为负
  • Z:Zero,结果为零
  • C:Carry,产生无符号进位或借位相关状态
  • V:Overflow,发生有符号溢出

比如:

swift 复制代码
cmp w0, w1
b.eq equal

cmp 比较 w0w1,更新 NZCVb.eq 检查其中的 Z 标志,如果结果为零,说明两个值相等,于是跳转到 equal

v0 ~ v31:浮点和向量寄存器

除了通用寄存器,ARM64 还有 32 个 128 位的浮点 / SIMD 寄存器:

v0 ~ v31

同一个寄存器可以按照不同宽度访问。例如 v0 可以使用:

  • b0:低 8 位
  • h0:低 16 位
  • s0:低 32 位,常用于 float
  • d0:低 64 位,常用于 double
  • q0:完整 128 位

比如:

swift 复制代码
fadd s0, s0, s1

表示两个 32 位单精度浮点数相加。

swift 复制代码
fadd d0, d0, d1

表示两个 64 位双精度浮点数相加。

SIMD 是 "单条指令处理多份数据"。一个 128 位向量寄存器可以同时保存多个较小的数据,一条指令便可以并行处理它们。

CPU 寄存器和线程寄存器

CPU 拥有真正用于执行指令的寄存器,线程拥有的是一套需要被保存和恢复的 "寄存器状态"。

真正的寄存器是 CPU 核心的那一套物理寄存器,线程的只是 "逻辑寄存器"。

假设一个进程里有两个线程:

swift 复制代码
  线程 A:
  pc = 0x100012340
  sp = 0x16fd00000
  x0 = 10
  x1 = 20

  线程 B:
  pc = 0x100056780
  sp = 0x16fc00000
  x0 = 某个对象地址
  x1 = 0

线程 A 和 线程 B 都有自己的:

  • pc:自己执行到了哪里
  • sp:自己的栈在哪里
  • x0~x30:自己当前使用的数据
  • 处理器状态和浮点 / 向量状态

否则线程 A 被暂停后,线程 B 一运行就会覆盖寄存器,线程 A 就无法继续执行。

也就是它们拥有自己的一套独立逻辑状态。

线程运行的时候,状态进入 CPU 寄存器,当线程 A 被调度到 CPU 核心运行时,可以简化理解为:

swift 复制代码
  线程 A 保存的寄存器状态
              ↓ 恢复
  CPU 核心的 x0、x1、sp、pc......
              ↓
  CPU 从线程 A 的 pc 位置继续执行

此时:

swift 复制代码
  CPU 的 x0 = 线程 A 的 x0
  CPU 的 sp = 线程 A 的 sp
  CPU 的 pc = 线程 A 的 pc

此时如果需要从 A 切到 B,操作系统需要进行上下文切换:

swift 复制代码
  1. 暂停线程 A

  2. 保存线程 A 恢复执行所需的寄存器状态
     x0~x30
     sp
     pc
     处理器状态
     必要的浮点/向量状态

  3. 恢复线程 B 之前保存的寄存器状态

  4. CPU 从线程 B 的 pc 继续执行

也就是:

swift 复制代码
  线程 A 的上下文                       线程 B 的上下文
  x0 = 10                              x0 = 100
  sp = 0x16fd00000                     sp = 0x16fc00000
  pc = functionA                       pc = functionB
         │                                    ▲
         │ 装入 CPU                            │ 保存/恢复
         ▼                                    │
  ┌──────────────────────────────────────────────┐
  │ CPU 核心当前的体系结构寄存器                     │
  │ x0、x1......x30、sp、pc、状态寄存器                 │
  └──────────────────────────────────────────────┘

多核 CPU

如果设备有多个 CPU 核心,多个线程可以同时运行,每个核心都有当前正在生效的寄存器状态。

线程也不一定永远运行在同一个核心上,线程 A 这次可能运行在核心 0,下次可能被调度到核心 2,只要它的体系结构状态被正确保存和恢复,线程就能继续执行。

相关推荐
2501_916008893 小时前
iOS App 请求地址查看,不用配置看到 App 调了哪些接口
网络协议·计算机网络·网络安全·ios·adb·https·udp
2501_915106326 小时前
Swift 开发环境搭建指南:三条路径按目标对号入座
开发语言·vscode·ios·objective-c·个人开发·swift·敏捷流程
_瑞7 小时前
APM_符号化
ios
ii_best8 小时前
ios群控鹰眼中控系统越狱版 PC 端 v3.1.9 版本更新|三大核心功能迭代,优化群控投屏作业流程
ios
二流小码农8 小时前
鸿蒙开发:了解Context
android·ios·harmonyos
时代分流9 小时前
云手机推荐2026:安卓全能、批量挂机、原生iOS全覆盖
android·ios·智能手机
catchadmin21 小时前
NativePHP v4 让 Blade 构建原生 iOS 与 Android 界面
android·ios·ai·php
weixin_403810131 天前
iOS免越狱自动化三种方式对比:代理模式、蓝牙HID与OTG HID怎么选
ios·自动化·跨境电商·ios脚本·苹果群控·苹果投屏·苹果脚本
_瑞1 天前
APM_函数调用栈展开
ios·编译原理