寄存器

通用寄存器

寄存器 位宽 别名 用途
X0-X30 64 位 - 通用数据存储、地址计算、函数参数/返回值
W0-W30 32 位 X0-X30 的低 32 位 32 位操作时使用
  • 特殊用途
    • **X0-X7**:函数参数传递(第 1-8 个参数)和返回值
    • **X8**(XR):间接返回值寄存器(如返回大结构体时存放地址)
    • **X9-X15**:临时寄存器(调用者保存)
    • **X16-X17**(IP0, IP1):临时寄存器,供链接器在函数调用间隙使用
    • **X18**:平台保留(某些操作系统如 Android 用作 TLS 寄存器,Thread-Local Storage)
    • X19-X28:通用寄存器(调用者保存)
    • **X29**(FP):帧指针(Frame Pointer)
    • **X30**(LR):链接寄存器(Link Register),保存函数返回地址

浮点寄存器

寄存器类型 位宽 数量 用途
S0-S31 32 位 32 个 单精度浮点(float
D0-D31 64 位 32 个 双精度浮点(double
Q0-Q31 128 位 32 个 SIMD/向量运算(如 NEON
  • S/D/Q 是同一组寄存器的不同视图
    • S0D0 的低 32 位,D0Q0 的低 64 位

特殊寄存器

  • XZR / WZR零寄存器(Zero Register):读取总是 0,写入无效果非常有用!

  • sp寄存器(Stack Pointer):保存栈顶地址

  • fp寄存器(x29):Frame Pointer,保存栈底地址

  • lr寄存器(x30):Link Register,存放函数的返回地址

  • pc寄存器: Program Counter,保存当前正在执行的指令的地址(注:在 AArch64 中,你不能像 ARM32 那样直接读写 PC 寄存器)

cpsr寄存器(状态寄存器)

CPSR (Current Program Status Register) 记录程序运行时的状态

最常见的是NZCV标志位,们通常由 CMP 指令或数据处理指令(如 ADDS/SUBS)设置,用于决定后续条件跳转(如 B.EQ)或条件执行(如 CSEL)的行为

  • NNegative Condition Flag,代表运算结果是负数(即最高位为 1)
  • ZZero Condition Flag,代表运算结果为 0
  • CCarry Condition Flag, 进位标志
    • ADD:无符号运算产生上溢出时 C 为 1
    • SUB:无符号运算产生下溢出(即“借位”)时 C 为 0
  • VOverflow Condition Flag, 溢出标志有符号运算(补码)产生溢出时 V 为 1

xzr(零寄存器)

xzr/wzr分别表示64/32位,存储的值为0,一般用于清零操作(如 mov x0, xzr)或作为比较/计算的基准

指令

ARM64 指令集(AArch64)采用 RISC(精简指令集) 设计,所有指令长度统一为 32 位(4 字节),这与 x86 等 CISC 架构(指令长度可变)完全不同

大多数 ARM64 指令遵循 操作码 目标寄存器, 源操作数1, 源操作数2 的格式

加载(Load)和存储(Store)

加载/存储指令⽤于在寄存器和存储器之间传送数据,加载指令⽤于将存储器中的数据传送到寄存器,存储指令则完成相反的操作

这是 ARM 架构(Load/Store 架构)的核心:计算必须在寄存器中进行,CPU 不能直接操作内存中的数据你必须先 LDR 进来,计算完,再 STR 回去

加载指令(Load)

  • 作用:从内存读取数据到寄存器

  • 格式LDR{后缀} 目标寄存器, [基址寄存器, 偏移]

  • ldr指令

1
2
3
ldr x8, [sp]         ; 将存储地址为sp的数据(64位)读取到x8寄存器中
ldr x9, [sp, #0x8] ; 将存储地址为sp+0x8的数据(64位)读取到x9寄存器中
ldr w10, [sp, #0x10] ; 将存储地址为sp+0x10的数据(32位)读取到w10寄存器中
  • ldrb指令(只操作一个字节)
1
ldrb w8, [sp, #0x8]    ; 将存储器地址为sp+0x8的1个字节数据读⼊寄存器w8,并将w8的⾼24位清零
  • ldrh指令(只操作两个字节)
1
ldrh w8, [sp, #0x8]    ; 将存储器地址为sp+0x8的2个字节数据读⼊寄存器w8,并将w8的⾼16位清零
  • ldur指令 (u和负地址运算相关)
1
ldurb w0, [x29, #-0x8]   ; 将存储地址为x29-0x8的数据(1字节)读取到w0寄存器中

存储指令(Store)

  • 作用:将寄存器数据写入内存

  • 格式STR{后缀} 源寄存器, [基址寄存器, 偏移]

  • str指令

1
2
3
str x0, [sp]         ; 将x0寄存器中的值(64位)存储到sp的存储地址
str x0, [sp, #0x10] ; 将x0寄存器中的值(64位)存储到sp+0x10的存储地址
str w1, [sp, #0x18] ; 将w1寄存器中的值(32位)存储到sp+0x18的存储地址
  • strb指令(只操作一个字节)
1
strb w0, [sp, #0x10]  ; 将w0寄存器中的低8位的字节的数据存储到sp+0x10的存储地址
  • strh指令(只操作两个字节)
1
strh w0, [sp, #0x10]  ; 将w0寄存器中的低16位的字节的数据存储到sp+0x10的存储地址
  • stur指令 (u和负地址运算相关)
1
stur w0, [x29, #-0x8]  ; 将w0寄存器中的数据(32位)存储到x29-0x8的存储地址

入栈/出栈 (STP / LDP)

在 ARM64 架构中,入栈(Push)和出栈(Pop) 操作通过 STP(Store Pair)和 LDP(Load Pair)指令实现,结合栈指针寄存器 SP 的调整来完成STP/LDP 可以一次操作两个寄存器,效率很高

注意事项

  • 栈增长方向:ARM64 中栈通常向低地址增长(满递减栈 Full-descending)
  • 栈指针(SP):始终指向栈顶(最新压入的数据地址)
  • 对齐要求SP 必须保持 16 字节对齐(硬件强制要求)
  • STP/LDP 操作两个 64 位寄存器(如 X0, X1)时,总共 16 字节,正好符合对齐要求

入栈(Push)操作

通用寄存器入栈

1
2
3
4
5
6
; 这是最标准的函数序言 (prologue)
; 1. 将 FP(x29) 和 LR(x30) 压入栈中
; 2. SP 指针先减 16 字节,再存数据(因为是满递减栈)
; 3. [SP, #-16]! 中的 '!' 表示 "pre-index",即先计算新地址(sp-16),
; 再存值,并且 SP 寄存器的值被 *更新* 为新地址
stp x29, x30, [sp, #-16]!
  • 效果
    • SP 的新值 = 原 SP - 16
    • 内存 [SP] (新) = X29
    • 内存 [SP+8] = X30

浮点寄存器入栈

1
stp d8, d9, [sp, #-16]!  ; 双精度浮点 d8/d9 压栈

出栈(Pop)操作

通用寄存器出栈

1
2
3
4
5
; 这是最标准的函数尾声 (epilogue)
; 1. 从当前 SP 指向的地址加载数据到 X29, X30
; 2. [sp], #16 表示 "post-index",即先从 sp 加载数据,
; 然后 SP 寄存器的值被 *更新* 为 sp + 16
ldp x29, x30, [sp], #16
  • 效果
    • X29 = [SP]
    • X30 = [SP+8]
    • SP 的新值 = 原 SP + 16

浮点寄存器出栈

1
ldp d8, d9, [sp], #16    ; 弹出双精度浮点 d8/d9

栈空间分配与释放

分配栈空间(如局部变量)

1
sub sp, sp, #32  ; 分配 32 字节栈空间 (必须是 16 的倍数)

释放栈空间

1
add sp, sp, #32  ; 释放 32 字节

地址计算指令 (ADR, ADRP)

问题LDR/STR 需要一个内存地址如果我想访问一个全局变量或一个字符串常量,的地址是固定的,但我写的代码(尤其是 .so 动态库)可能被加载到内存的任意基地址(这称为位置无关代码, PIC)我该如何获取这个变量的运行时地址?

答案就是使用 PC 相对寻址:ADRADRP

  • adr指令 (Address)

    • 作用:获取一个近距离标签(label)的确切地址
    • 原理ADR 计算目标地址与当前指令(PC)的字节偏移量,并将结果(PC + offset)存入目标寄存器
    • 范围:有限,只能寻址 PC 周围 ±1MB 的范围
    • 使用:常用于加载函数内或模块内较近的字符串、常量
1
2
3
4
5
6
7
8
; 假设 my_string 标签在 0x10080
; 且 adr 指令在 0x10000
adr x0, my_string ; 汇编器计算偏移量 0x80
; 运行时: X0 = (PC值 0x10000) + 0x80 = 0x10080
bl printf ; 调用 printf(my_string)

my_string:
.string "Hello, World!"
  • adrp指令 (Address of Page)

    • 作用:获取一个远距离标签所在的内存页 (Page) 的基地址
    • 原理:现代操作系统以 4KB(0x1000 字节)为一页ADRP 计算目标地址与当前指令(PC)所在的页页偏移量(即相差多少个 4KB)
    • 关键:只计算并加载高 52 位地址(页基址),并将低 12 位(页内偏移)清零
    • 范围:极大,可以寻址 PC 周围 ±4GB 的范围
  • ADRP + ADD:黄金组合

    ADRP 几乎从不单独使用帮你拿到页基址(高位),你还需要 ADD 来加上页内偏移(低位),才能得到确切地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
; 目标:获取 my_global_var 的确切地址到 X0
; 假设 my_global_var 在 0x123456789ABC
; (页基址 0x123456789000, 页内偏移 0xABC)

adrp x0, my_global_var
; 此时 X0 = 0x123456789000 (变量所在页的基地址)

add x0, x0, #:lo12:my_global_var
; #:lo12: 是一个链接器魔法,会提取 my_global_var 的
; 页内偏移量 (0xABC)
; 最终 X0 = 0x123456789000 + 0xABC = 0x123456789ABC

; 现在 X0 里是确切地址了
ldr w1, [x0] ; 从 my_global_var 加载一个 32 位值

这个 ADRP + ADD 的组合是 AArch64 访问全局变量的标准方式

数据处理指令

  • mov指令(移动数据)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
; --- 基本 MOV ---
mov w8, #0x1 ; 将立即数 1 (32位) 赋值给 w8 寄存器
mov x0, x8 ; 将 x8 寄存器的值 (64位) 复制到 x0 寄存器
; 注意: mov x0, w8 会将 w8 零扩展 (Zero-Extend) 为 64 位再复制
mov w0, w8 ; 将 w8 寄存器的值 (32位) 复制到 w0 寄存器
mov x0, sp ; 将栈指针 SP (64位地址) 复制到 x0

; --- MOV 别名 ---
; 汇编器可能会将某些简单的立即数加载转换为其他指令
mov w0, #0 ; 通常汇编为 movz w0, #0 或 orr w0, wzr, wzr
mov x0, #0 ; 通常汇编为 movz x0, #0 或 orr x0, xzr, xzr
mov w0, #-1 ; 通常汇编为 movn w0, #0 (Move Wide with NOT)
mov x0, #-1 ; 通常汇编为 movn x0, #0

; --- MVN (Bitwise NOT Move) ---
mvn w0, w1 ; 将 w1 寄存器的值按位取反后存入 w0 (w0 = ~w1)
mvn x0, x1 ; 将 x1 寄存器的值按位取反后存入 x0 (x0 = ~x1)

; --- MOVZ (Move Wide with Zero) ---
; 用于加载较大的立即数 (最多64位),分16位块加载,其余位清零
movz w0, #0x1234 ; w0 = 0x00001234 (加载低16位,高位清零)
movz w0, #0x1234, lsl #16 ; w0 = 0x12340000 (加载到高16位,低位清零)
movz x0, #0xABCD ; x0 = 0x000000000000ABCD (加载低16位,高位清零)
movz x0, #0xABCD, lsl #48 ; x0 = 0xABCD000000000000 (加载到最高16位,其余清零)

; --- MOVK (Move Wide with Keep) ---
; 与 MOVZ 配合使用,加载立即数的某16位块,*保持*寄存器中其他位不变
movz x0, #0xAAAA ; x0 = 0x000000000000AAAA
movk x0, #0xBBBB, lsl #16 ; x0 = 0x00000000BBBBAAAA (加载中低16位,保持低16位)
movk x0, #0xCCCC, lsl #32 ; x0 = 0x0000CCCCBBBBAAAA (加载中高16位,保持低32位)
movk x0, #0xDDDD, lsl #48 ; x0 = 0xDDDDCCCCBBBBAAAA (加载最高16位,保持低48位)

; --- MOVN (Move Wide with NOT) ---
; 加载16位立即数的按位取反值,其余位清零 (常用于加载 -1 或其他类似掩码)
movn w0, #0 ; w0 = ~0 = 0xFFFFFFFF (-1)
movn x0, #0 ; x0 = ~0 = 0xFFFFFFFFFFFFFFFF (-1)
movn w0, #0xFFFE, lsl #16 ; w0 = ~(0xFFFE << 16) = ~0xFFFE0000 = 0x0001FFFF

; --- CSEL (Conditional Select) ---
; 类似于三元运算符 ?: ,根据条件标志位选择两个源寄存器之一赋值给目标寄存器
cmp w1, w2 ; 比较 w1 和 w2,设置标志位
csel w0, w1, w2, EQ ; 如果 EQ (相等) 标志位为1, 则 w0 = w1; 否则 w0 = w2
csel x0, x1, xzr, NE ; 如果 NE (不相等) 标志位为1, 则 x0 = x1; 否则 x0 = xzr (零寄存器, 即0)
  • add / adds 指令 (加法)
1
2
3
4
5
6
7
add sp, sp, #0x20      ; sp = sp + 0x20
add x0, x1, x2 ; x0 = x1 + x2
; add x0, x1, [x2] ; !!! 错误:AArch64 不支持这种内存操作
; 必须先 LDR: ldr x3, [x2] 然后 add x0, x1, x3

; adds 指令: add and set flags
adds x0, x1, x2 ; x0 = x1 + x2, 并且根据结果设置 NZCV 状态位
  • sub / subs 指令 (减法)
    subs 常用于 cmpcmp x0, x1 等价于 subs xzr, x0, x1
1
2
3
sub sp, sp, #0x20      ; sp = sp - 0x20 (分配栈空间)
sub x0, x1, x2 ; x0 = x1 - x2
subs x0, x1, #1 ; x0 = x1 - 1, 并设置 NZCV 标志 (常用于循环倒计数)
  • mul指令 (乘)
1
mul x0, x1, x2       ; x0 = x1 * x2
  • madd指令 (乘加)
1
2
; Multiply-Add
madd x0, x1, x2, x3 ; x0 = (x1 * x2) + x3
  • msub指令 (乘减)
1
2
; Multiply-Subtract
msub x0, x1, x2, x3 ; x0 = (x1 * x2) - x3
  • sdiv / udiv 指令 (有符号除 / 无符号除)
1
2
sdiv x0, x1, x2      ; x0 = x1 / x2 (有符号)
udiv x0, x1, x2 ; x0 = x1 / x2 (无符号)
  • and / ands 指令 (位与)
    ands 常用于测试某位是否为 1(如 ands x0, x0, #1,然后用 b.eq 判断奇偶)
1
2
and x0, x1, x2       ; x0 = x1 & x2
and x0, x1, #0xFF ; x0 = x1 & 0xFF (取最低 8 位)
  • orr指令 (位或)
1
2
orr x0, x1, x2       ; x0 = x1 | x2
orr x0, x1, #0x1 ; 将 x1 的 bit 0 置 1
  • eor指令 (位异或)
1
eor x0, x1, x2       ; x0 = x1 ^ x2 (常用于不通过立即数反转某些位)
  • lsl指令 (逻辑左移)
    LSL (Logical Shift Left) 等价于乘以 2 的 N 次方
1
2
lsl x0, x1, #3       ; x0 = x1 << 3 (即 x1 * 8)
lsl x0, x1, x2 ; x0 = x1 << x2 (x2 寄存器中的值作为移位数)
  • lsr指令 (逻辑右移)
    LSR (Logical Shift Right) 等价于无符号除以 2 的 N 次方(高位补 0)
1
lsr x0, x1, #3       ; x0 = x1 >> 3 (无符号)
  • asr指令 (算术右移)
    ASR (Arithmetic Shift Right) 等价于有符号除以 2 的 N 次方(高位补充符号位
1
2
3
4
; 假设 x1 = -16 (0xFF...F0)
asr x0, x1, #1 ; x0 = -8 (0xFF...F8) (保持了负号)
; 假设 x1 = -16 (0xFF...F0)
lsr x0, x1, #1 ; x0 = 0x7F...F8 (一个很大的正数)
  • cbnz指令 (和⾮ 0 ⽐较跳转)
    Compare and Branch if Non-Zero
1
2
cbnz x0, 0x100002f70   ; 如果 x0 不为 0,跳转到 0x100002f70 指令执行
; 优点: 一条指令完成比较和跳转,不修改 NZCV 标志
  • cbz指令 (和0 ⽐较跳转)
    Compare and Branch if Zero
1
cbz x0, 0x100002f70    ; 如果 x0 为 0,跳转到 0x100002f70 指令执行

条件执行指令

AArch32 中几乎所有指令都可以条件执行AArch64 取消了这个设计,改为使用专门的条件指令,这让 CPU 流水线更简单高效

这些指令依赖 CMPSUBS 等设置的 NZCV 标志位

  • cset指令 (Conditional Set)

    • 作用:如果条件满足,将寄存器设为 1,否则设为 0
1
2
3
cmp w0, #10
cset w1, ge ; w1 = (w0 >= 10) ? 1 : 0
; (GE = Greater or Equal)
  • csel指令 (Conditional Select)

    • 作用:如果条件满足,从一个寄存器中选择值,否则从另一个中选择这是 AArch64 中实现 if-then-else 和三目运算符的核心
    • 格式csel Xd, Xn, Xm, <cond> (如果 <cond> 满足, Xd = Xn, 否则 Xd = Xm)
1
2
3
4
5
6
7
8
; C 语言: int x = (a > b) ? 100 : 200;
; 假设 w0 = a, w1 = b

cmp w0, w1 ; 比较 a 和 b
mov w2, #100 ; 准备 "then" 的值
mov w3, #200 ; 准备 "else" 的值
csel w0, w2, w3, gt ; 关键: w0 = (w0 > w1) ? w2 : w3
; (GT = Greater Than)

跳转指令

  • cmp指令
    Compare将两个寄存器相减(cmp x0, x1 等价于 subs xzr, x0, x1),不保存结果,但会更新cpsr寄存器的NZCV标志位,为 B.condCSEL 做准备
1
2
cmp x0, x1           ; 比较 x0 和 x1
cmp x0, #10 ; 比较 x0 和 10
  • b指令 (直接跳转, Branch)
    b指令可以接上后缀,用来和cmp比较后待条件的跳转

    EQ:equal 相等 (Z=1)
    NE:not equal,不相等 (Z=0)
    GT:great than,大于 (有符号, Z=0 and N=V)
    GE:greate equal,大于等于 (有符号, N=V)
    LT:less than,小于 (有符号, N!=V)
    LE:less equal,.小于等于 (有符号, Z=1 or N!=V)
    (还有用于无符号的 HI, HS, LO, LS)

1
2
3
4
b 0x100002fd0    ; 无条件跳转到 0x100002fd0

cmp x0, x1 ; 比较 x0 和 x1
b.lt label ; 如果 x0 < x1 (有符号) 则跳转到 label
  • bl指令 (Branch with Link)
    带链接的跳转
    1. 跳转到目标地址(通常是一个函数)
    2. 在跳转前,将下一条指令的地址(即返回地址)保存到 LR (X30) 寄存器
1
2
bl 0x100002f54   ; 调用 0x100002f54 处的函数
; 此时 LR = (这条 bl 指令的地址 + 4)
  • br指令(Branch to Register)
    寄存器跳转
    直接跳转到某寄存器中的值所指向的地址不会改变 LR (x30) 寄存器的值
    常用于函数指针调用或 switch-case 跳转表
1
2
; 假设 X10 中存着地址 0x123456
br x10 ; 跳转到 0x123456
  • blr指令 (Branch with Link to Register)
    带链接的寄存器跳转
    1. 跳转到某寄存器中的值所指向的地址
    2. 在跳转前,将返回地址保存到 LR (X30)
      常用于虚函数调用高阶函数(函数指针)调用
1
2
; 假设 X10 中存着一个函数A的地址
blr x10 ; 调用 A, 并将返回地址存入 LR
  • ret指令 (函数返回)
    Return from subroutine
    ret 是一条伪指令,实际上是 br lr 的别名
    直接跳转到 LR (X30) 寄存器中保存的地址,实现函数返回
1
2
; 在函数末尾, 配合 ldp x29, x30, [sp], #16 使用
ret ; 等价于 br lr

相对跳转(Relative Jump)

特点

  • 目标地址基于当前 PC(程序计数器)计算,与代码位置无关(地址无关代码)
  • 适用于:函数内分支、短距离跳转

核心指令

指令 作用 跳转范围 示例
B label 无条件相对跳转 ±128MB(26 位偏移) B loop_start
B.cond 条件相对跳转(如 B.EQ ±1MB(19 位偏移) B.NE exit
BL label 跳转并保存返回地址到 LR ±128MB BL subroutine

底层原理

1
B label  // 实际编码为:PC = PC + (sign_extend(offset) << 2)
  • 偏移量是 有符号整数,左移 2 位(因指令为 4 字节对齐)

优势

  • 位置无关代码(PIC):动态库、共享代码无需重定位
  • 代码紧凑:指令长度固定(4 字节)

绝对跳转(Absolute Jump)

特点

  • 目标地址为固定值,直接指定内存地址
  • 适用于:动态调用(函数指针)、长距离跳转

核心指令

指令 作用 示例
BR Xn 跳转到寄存器 Xn 中的地址 BR X0(函数指针调用)
BLR Xn 跳转并保存返回地址到 LR BLR X1(虚函数调用)
RET 返回(等价于 BR LR RET

地址加载方式

绝对地址需先加载到寄存器:

1
2
3
4
5
6
7
8
// 方法1:PC相对地址加载(位置无关,推荐)
ADRP X0, target_page // 加载目标页基址(4KB对齐)
ADD X0, X0, #target_offset // 加上页内偏移
BR X0 // 跳转

// 方法2:使用 LDR 伪指令加载(见下一节)
LDR X0, =0x400000 // 加载绝对地址(由汇编器转换)
BR X0

伪指令 (Pseudo-instructions)

这是一个非常重要的概念你写的汇编代码中,有一些指令并不是真实的 CPU 硬件指令,而是汇编器 (Assembler) 提供给开发者的“语法糖”汇编器在编译时,会自动将这些“伪指令”转换成一条或多条真实的硬件指令

  • ret

    • 伪指令ret
    • 真实指令br lr
    • 作用:汇编器帮你简化了 “return” 这个常用操作
  • mov (加载大立即数)

    • 伪指令mov x0, #0x123456789ABC
    • 真实指令:ARM64 指令长度固定 32 位,无法在一条指令里塞下一个 64 位的立即数汇编器会将其转换为多条指令:
      1
      2
      3
      4
      ; 汇编器可能会这么做:
      movz x0, #0x5678, lsl 16 ; M(OV) with Z(ero): X0 = 0x...000056780000
      movk x0, #0x1234, lsl 32 ; M(OV) with K(eep): X0 = 0x...123456780000
      ; (这只是一个例子,实际组合更复杂)
    • 作用:让你感觉可以 mov 任意立即数,而不用关心底层的 MOVZ/MOVK 组合
  • ldr (加载地址或常量)

    • 伪指令ldr x0, =0x123456789ABCDEF0ldr x0, =my_label

    • 真实指令:这是最强大的伪指令汇编器会“智能”选择最高效的方式去加载:

      1. 情况1 (加载小立即数): ldr w0, =123 -> 汇编器转为 mov w0, #123
      2. 情况2 (加载近地址): ldr x0, =my_nearby_label -> 汇编器转为 adr x0, my_nearby_label
      3. 情况3 (加载远地址): ldr x0, =my_far_away_label -> 汇编器转为 adrp x0, ... + add x0, ... (黄金组合)
      4. 情况4 (加载绝对常量): ldr x0, =0x123456789ABCDEF0
        • 汇编器无法用 MOVZ/MOVK (因为太长) 或 ADRP (因为不是 PC 相对地址) 来生成
        • 汇编器会在代码段附近自动生成一个“**字面量池 (Literal Pool)**”(一块数据区)
        • 0x123456789ABCDEF0 这个 8 字节的值存入池中
        • 然后将 ldr 伪指令转为一个PC 相对的真实 LDR 指令,从那个池子里把数据加载进来
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      ; 你写的代码:
      ldr x0, =0x123456789ABCDEF0
      b somewhere_else

      ; 汇编器实际生成的(示意):
      .L_actual_code:
      ldr x0, .L_literal_pool_1 ; 真实 LDR, PC 相对加载
      b somewhere_else

      .L_literal_pool_1:
      .quad 0x123456789ABCDEF0 ; 汇编器自动插入的数据

理解伪指令,能帮你弄清为什么有些看似简单的操作(如 mov 一个大数)在反汇编时会变成好几条指令

函数调用

三层函数调用反汇编示例

C语言源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int level3(int a, int b) {
return a * b;
}

int level2(int x) {
return level3(x, 5) + 3;
}

int level1(int y) {
int z = level2(y * 2);
return z + level2(y);
}

int main() {
return level1(10);
}

反汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
; ===== level3 函数 (最底层) =====
0000000000000000 <level3>:
; 函数序言
0: d10043ff sub sp, sp, #0x10 ; 分配16字节栈空间
4: b9000fe0 str w0, [sp, #12] ; 保存参数a
8: b9000be1 str w1, [sp, #8] ; 保存参数b

; 计算 a * b
c: b9400fe0 ldr w0, [sp, #12] ; 加载a
10: b9400be1 ldr w1, [sp, #8] ; 加载b
14: 1b017c00 mul w0, w0, w1 ; w0 = a * b

; 函数尾声
18: 910043ff add sp, sp, #0x10 ; 释放栈空间
1c: d65f03c0 ret ; 返回

; ===== level2 函数 (中间层) =====
0000000000000020 <level2>:
; 函数序言
20: a9be7bfd stp x29, x30, [sp, #-32]! ; 保存FP/LR,分配32字节
24: 910003fd mov x29, sp ; 设置新帧指针
28: b9001fe0 str w0, [sp, #28] ; 保存参数x

; 调用 level3(x, 5)
2c: b9401fe0 ldr w0, [sp, #28] ; 加载x到w0
30: 528000a1 mov w1, #5 ; 设置第二个参数=5
34: 97fffff3 bl 0 <level3> ; 调用level3

; 计算返回值 (result + 3)
38: 11000c00 add w0, w0, #3 ; w0 = result + 3

; 函数尾声
3c: a8c27bfd ldp x29, x30, [sp], #32 ; 恢复FP/LR
40: d65f03c0 ret ; 返回

; ===== level1 函数 (调用层) =====
0000000000000044 <level1>:
; 函数序言
44: a9bd7bfd stp x29, x30, [sp, #-48]! ; 保存FP/LR,分配48字节
48: 910003fd mov x29, sp ; 设置新帧指针
4c: b9002fe0 str w0, [sp, #44] ; 保存参数y

; 计算 y*2 并调用 level2
50: b9402fe0 ldr w0, [sp, #44] ; 加载y
54: 531f7800 lsl w0, w0, #1 ; w0 = y*2 (左移1位)
58: 94000000 bl 20 <level2> ; 调用level2(y*2)
5c: b9001be0 str w0, [sp, #24] ; 存储结果到z

; 再次调用 level2(y)
60: b9402fe0 ldr w0, [sp, #44] ; 加载y
64: 94000000 bl 20 <level2> ; 调用level2(y)
68: b90017e0 str w0, [sp, #20] ; 存储结果

; 计算最终返回值 (z + level2(y))
6c: b9401be1 ldr w1, [sp, #24] ; 加载z
70: b94017e0 ldr w0, [sp, #20] ; 加载level2(y)结果
74: 0b000020 add w0, w1, w0 ; w0 = z + level2(y)

; 函数尾声
78: a8c37bfd ldp x29, x30, [sp], #48 ; 恢复FP/LR
7c: d65f03c0 ret ; 返回

; ===== main 函数 (入口) =====
0000000000000080 <main>:
; 函数序言
80: a9bf7bfd stp x29, x30, [sp, #-16]! ; 保存FP/LR
84: 910003fd mov x29, sp ; 设置帧指针

; 调用 level1(10)
88: 52800140 mov w0, #10 ; 设置参数=10
8c: 97ffffe8 bl 44 <level1> ; 调用level1

; 函数尾声
90: a8c17bfd ldp x29, x30, [sp], #16 ; 恢复FP/LR
94: d65f03c0 ret ; 返回

ARM64 多层调用关键机制

1. 调用约定 (Calling Convention)

  • 参数传递
    • 前8个整型参数:寄存器 w0-w7
    • 前8个浮点参数:寄存器 d0-d7
    • 额外参数通过栈传递
  • 返回值
    • 整型:w0 寄存器
    • 浮点:d0 寄存器
  • 保留寄存器
    • 调用者保存寄存器 (Caller-Saved Registers): 包括 X0 - X18。如果一个函数(调用者)在调用另一个函数之前,希望 X0-X18 中的某个值在子函数返回后仍然保持不变,那么调用者自己有责任在调用前将这个值保存起来(比如压入栈中),并在调用返回后再恢复。被调用的子函数可以随意使用这些寄存器,无需担心破坏调用者的值。

    • 被调用者保存寄存器 (Callee-Saved Registers): 包括 X19 - X28。这个约定规定,如果一个函数(被调用者,也就是我们正在分析的这个函数)想要在自己的代码中使用 X19-X28 中的任何一个寄存器来存储自己的局部变量或中间计算结果,那么必须承担起责任:在函数开头(序言)将这些寄存器的原始值保存到栈上,并在函数结尾(尾声)必须将这些原始值从栈上恢复回对应的寄存器。

    • 保证调用者状态不变: 这样做的目的是为了给调用者提供保障。调用者可以将一些重要的、需要在多次函数调用之间保持不变的值(比如循环计数器、基址指针等)放心地存放在 X19-X28 中,因为知道,无论调用哪个遵循约定的子函数,当子函数返回时,X19-X28 里的值都会和调用前一模一样。

2. 栈帧管理

  • **帧指针 (FP)**:
    • x29 寄存器指向当前栈帧基址,与栈指针 SP 共同界定函数的局部变量区保存的寄存器区;在函数执行期间,SP 可能变化(如压栈/出栈),而 X29 保持固定,便于通过 固定偏移 访问局部变量和参数
    • 每个函数通过 mov x29, sp 设置自己的帧指针
  • 栈操作
    • 使用 stp/ldp 批量保存/恢复寄存器
    • 栈分配/释放:sub sp, sp, #size / add sp, sp, #size
    • 栈指针必须保持 16 字节对齐

3. 调用链管理

  • **返回地址 (LR)**:
    • x30 寄存器存储返回地址
    • 非叶子函数必须保存 LR (str x30, [sp])
    • bl 指令自动更新 LR 为下条指令地址
  • 调用深度限制
    • 硬件无直接限制,但受栈空间约束
    • 每次调用平均消耗 16-64 字节栈空间

4. 优化技术

  • **尾调用优化 (Tail Call)**:
    • 如果函数最后操作是调用其他函数,可优化为跳转
    • 示例:b level2 替代 bl level2; ret
  • 叶子函数优化
    • 不调用其他函数的叶子函数可省略栈帧
    • 如 level3 未保存 FP/LR
  • 寄存器重用
    • 编译器尽量在寄存器间传递中间结果
    • 减少内存访问次数

常见问题分析

Q1: 为什么 level3 没有保存 FP/LR?

A: level3 是叶子函数(不调用其他函数),不需要保存返回地址(LR),因此可以省略栈帧设置,减少开销

Q2: 栈对齐如何保证?

A: ARM64 要求 sp 必须 16 字节对齐所有栈操作都遵守:

  • stp/ldp 使用 16 字节倍数偏移
  • 栈分配大小通常是 16 的倍数
  • 函数序言/尾声保证对齐

Q3: 参数如何跨层传递?

A: 参数通过寄存器链式传递:

  1. main 设置 w0=10 调用 level1
  2. level1 计算 y*2 存入 w0 调用 level2
  3. level2 设置 w1=5 调用 level3
    寄存器传递比内存访问高效得多

Q4: 如何调试调用栈?

A: 使用帧指针回溯:

  1. 当前 FP 指向保存的前一 FP
  2. FP-8 位置是返回地址 (LR)
  3. 递归解析直到 FP=0