汇编学习------iOS开发对arm64汇编的初步了解
文章目录
- 汇编学习------iOS开发对arm64汇编的初步了解
前言
最近也是开始每天都在看源码,其中在阅读源码的过程之中,无论是属性关键字还是消息转发的内容,都多多少少涉及到了关于汇编的内容,看着真是令人头大,本着看了就要看全的精神,于是开始对arm64的汇编进行简单的学习。这篇文章就是对汇编浅尝则止的学习记录
栈 指令 寄存器
汇编最重要的就是三个部分就是栈 指令 寄存器,我们从较为复杂的寄存器说起
寄存器
寄存器是 CPU 中的高速存储单元,存取速度比内存快很多。
寄存器 | 职责 |
---|---|
X0 | 返回值、第一个参数(self ) |
X1-X7 | 第 2~8 个参数、临时变量(如 Objective-C 方法的选择器 _cmd 通常通过 X1 传递) |
X8 | 间接返回地址、全局变量偏移、系统调用号(在 Objective-C 中可能用于计算全局变量或类对象的偏移地址) |
X9-X15 | 调用者保存的临时变量 |
X19-X28 | 被调用者保存的长期变量 |
X29 (FP) | 栈帧管理、调试支持 |
X30 (LR) | 保存函数调用后的返回地址(如 bl 指令跳转时写入) |
XZR | 清零操作,用于快速清零其他寄存器(如 mov x0, xzr 等价于 x0 = 0 ) |
注:
x0 - x7 :用于子程序调用时的参数传递,超过八个会放到栈上传递
x0 和 w0 是同一个寄存器的不同尺寸的区别,x0 为 8 字节,w0 为 4 字节(x0 寄存器的低4字节), x0/w0 还用于返回值的传递
指令
需要提前知道的就是:在ARM64架构中,栈是向下生长的,换句话来说,栈内存从高地址向低地址方向扩展。具体来说,当数据被压入栈时,栈指针(SP)会减小(指向更低的内存地址);当数据弹出时,SP会增大(指向更高的内存地址)。这是ARM64与其他架构(如x86)共有的特性。
- 压栈操作会导致SP减小(如sp = sp - 16),栈顶向低地址方向移动
- 出栈操作则会使SP增大(如
sp = sp + 16
),栈顶恢复高地址。
这部分可能有点乱,我们在后面用一个例子的展示一下栈具体的存储结构
运算指令
asm
mov x1,x0 ;将寄存器x0值 赋值 给x1
add x0,x1,x2 ;x0 = x1 + x2
sub x0,x1,x2 ;x0 = x1 - x2
mul x0,x1,x2 ;x0 = x1 * x2
sdiv x0,x1,x2 ;x0 = x1 / x2;
and x0,x0,#0xF ;x0 = x0 & #0xF (与操作)
orr x0,x0,#9 ;x0 = x0 | #9 (或操作)
eor x0,x0,#0xF ;x0 = x0 ^ #0xF (异或操作)
寻址指令
寻址指令简单的可以分为 存和取
L开头的就是取: LDR(Load Register)、LDP(Load Pair)
S开头的就是存: STR(Store Register)、STP(Store Pair)
asm
ldr x0,[x1] ;从 x1 指向的地址里面取出一个64位大小的数存入x0
ldp x1,x2,[x10, #0x10] ;从 x10+0x10 指向的地址里面取出2个64位的数,分别存入x1、x2
str x5,[sp, #24] ;往内存中写数据(偏移值为正), 把 x5 的值(64位的数值)存到 sp+24 指向的地址内存上
stur w0,[x29, #0x8] ;往内存中写数据(偏移值为负),将 w0 的值存储到 x29 - 0x8 这个地址里
stp x29,x30,[sp, #-16]! ;把 x29、x30 的值存到 sp-16 的地址上,并且把sp-=16 后面有个感叹号表示前变基模式
ldp x29,x30,[sp],#16 ;从 sp 地址取出16 byte数据,分别存入x29、x30,然后 sp+=16
寻址指令的格式
asm
mov x0
[x10, #0x10] ;从 x10+0x10 的地址取值
[sp, #-16]! ;从 sp-16 地址取值,取完后再把 sp-16 writeback 回 sp
[sp], #16 v从 sp 地址取值,取完后把 sp+16 writeback 回 sp
前变基 与 后变基
模式 | 操作顺序 | 语法示例 | 应用场景 |
---|---|---|---|
前变基 | 1. 先更新基址寄存器 2. 再访问内存 | LDR X0, [X1, #8]! |
函数调用时预留栈空间 |
后变基 | 1. 先访问内存 2. 再更新基址寄存器 | LDR X0, [X1], #8 |
函数返回时恢复栈指针 |
栈
关于栈的内容,需要我们复习一下之前学习过的相关内容,就是计算机的内存结构
程序运行的时候,操作系统会给它分配一段内存,用来存储程序和运行产生的数据。这段内存有起始地址和结束地址,比如从 0x1000 到 0x8000,起始地址是较小的那个地址,结束地址是较大的那个地址。

堆(Heap)内存机制
- 核心特征
- 分配方式 :动态申请(
malloc
/new
等) - 地址增长:从低位地址向高位地址扩展
- 生命周期:需手动释放或依赖垃圾回收
- 分配实例
c
// 内存起始地址 0x1000
void* p1 = malloc(10); // 分配 0x1000-0x100A
void* p2 = malloc(22); // 分配 0x100B-0x1020

三、栈(Stack)内存机制
- 核心特征
- 分配方式:函数调用自动创建帧(Frame)
- 地址增长:从高位地址向低位地址扩展
- 生命周期:函数结束时自动释放
简单来说,栈是由于函数运行而临时占用的内存区域

- 函数帧结构
c
int main() {
int a = 2; // ↘ 主函数帧
int b = 3; // │ 变量存储区
} // ↖ 栈顶地址 0x8000
上面的代码中,系统开始执行 main 函数的时,会为它在内存里面建立一个帧(frame),所有 main 的内部变量(比如a和b)都保存在这个帧里面。main 函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。

3. 多级调用示例
c
int test(int x, int y) { // ↘ 子帧
return x + y; // │ 参数/变量存储区
} // ↖ 新栈顶 0x7FF0
int main() { // ↘ 主帧
test(2, 3); // │ 返回地址保存区
} // ↖ 初始栈顶 0x8000
当我们在main函数之中调用了test函数时,当我们执行这一步,系统就会为了这个test函数创建一个帧,现在栈区就有了两个函数帧,一般调用了多少层的函数就有多少个帧

等到test函数运行结束,它的帧就会被系统自动回收,实现了函数的层层调用。
前面我们说到,arm64架构下的栈时从高位(地址)向低位(地址)分配的,,内存区域的结束地址是 0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0。
操作 | 栈地址范围 | 剩余空间 |
---|---|---|
主帧分配(16字节) | 0x8000 - 0x7FF0 | 0x7FF0 |
子帧分配(64字节) | 0x7FF0 - 0x7FB0 | 0x7FB0 |
例子
objc
// hello.c
#include <stdio.h>
int test(int a, int b) {
int res = a + b;
return res;
}
int main() {
int res = test(1, 2);
return 0;
}
使用clang指令把以上内容编译为arn64代码
asm
.section __TEXT,__text,regular,pure_instructions
.build_version ios, 13, 2 sdk_version 13, 2
.globl _test ; -- Begin function test
.p2align 2
_test: ; @test
.cfi_startproc
; %bb.0:
sub sp, sp, #16 ; =16
.cfi_def_cfa_offset 16
str w0, [sp, #12]
str w1, [sp, #8]
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
str w0, [sp, #4]
ldr w0, [sp, #4]
add sp, sp, #16 ; =16
ret
.cfi_endproc
; -- End function
.globl _main ; -- Begin function main
.p2align 2
_main: ; @main
.cfi_startproc
; %bb.0:
sub sp, sp, #32 ; =32
stp x29, x30, [sp, #16] ; 16-byte Folded Spill
add x29, sp, #16 ; =16
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
stur wzr, [x29, #-4]
orr w0, wzr, #0x1
orr w1, wzr, #0x2
bl _test
str w0, [sp, #8]
mov w0, #0
ldp x29, x30, [sp, #16] ; 16-byte Folded Reload
add sp, sp, #32 ; =32
ret
.cfi_endproc
; -- End function
.subsections_via_symbols
完整如上
.p2align 2 用于指定程序的对齐方式,这类似于结构体的字节对齐,为的是加速程序的执行速度,p2align 的单位是指数,即按照 2 的 n 次方对齐,这里的 .p2align 2 表示按照 2^2 = 4 字节对齐,如果单行指令数据长度不足4字节,将用 0 补全,超过 4 但不是 4 的倍数,则按照最小倍数补全
asm
.cfi_startproc ;定义函数开始
.cfi_endproc ;定义函数结束
汇编中的如下部分被称为方法头(prologue),用于保存上一个方法调用栈帧的帧头,以及预留部分用于局部变量的栈空间。
sub sp, sp, #32 ; =32
stp x29, x30, [sp, #16] ; 16-byte Folded Spill
add x29, sp, #16 ; =16
汇编中的如下部分被称为方法尾(epilogue),用于取出方法头中栈帧信息及方法的返回地址,并将栈恢复到调用前的位置
ldp x29, x30, [sp, #16] ; 16-byte Folded Reload
add sp, sp, #32 ; =32
ret
我们先来看看Test函数
asm
//源代码
int test(int a, int b) {
int res = a + b;
return res;
}
//汇编
sub sp, sp, #16 ; =16
.cfi_def_cfa_offset 16
str w0, [sp, #12]
str w1, [sp, #8]
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
str w0, [sp, #4]
ldr w0, [sp, #4]
add sp, sp, #16 ; =16
ret
在编译器生成汇编时,会首先计算需要的栈空间大小,并利用 sp (stack pointer)指针指向低地址开辟相应的空间。从 test 函数可以看到这里涉及了3个变量,分别是 a、b、res,int变量占据4个字节,因此需要12个字节,但 ARM64 汇编为了提高访问效率要求按照16字节进行对齐,因此需要16 byte 的空间,也就是需要在栈上开辟16字节的空间
代码的大致意思如下
asm
sub sp, sp, #16 ; =16
将栈顶指针下移,即为函数的栈帧扩充空间
asm
str w0, [sp, #12]
str w1, [sp, #8]
这2句的意思是,将 w0 存储在 sp+12 的地址指向的空间,w1 存储在 sp+8 存储的空间里,寄存器x0~x7用于子程序调用时的参数传递,按顺序入参。 x0 和 w0 是同一个寄存器的不同尺寸形式,x0为8字节,w0为x0的前4个字节,因此w0是函数的第一个入参a,w1是函数的第二个入参b
接下来 test 函数内部将 a 和 b 进行相加,需要注意的是,只有寄存器才能参与运算,因此接下来的汇编代码又将变量的值从内存中读出来,再进行相加运算。
asm
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
为什么需要先存取后取出再操作,这个操作确实多余,是因为汇编没有进行优化的结果,把这个操作体现出来更有利于
ARM64 栈结构示意图(从高地址向低地址生长)
内存地址 | 存储内容(示例值) | 用途说明 | 备注 |
---|---|---|---|
0xFFFFFFC0 | (未分配,原栈顶) | 父函数栈帧 | 栈初始位置,SP 初始指向此处(高地址) |
0xFFFFFFBC | w0 (参数 a = 0x12345678) |
参数存储区(sp + 12) | 32 位参数,按 小端序 存储:0x78 0x56 0x34 `0x12 |
0xFFFFFFB8 | w1 (参数 b) |
参数存储区(sp + 8) | 第二个参数,通过 w1 传递 |
0xFFFFFFB4 | w0 (计算结果 c) |
临时结果存储(sp + 4) | 计算后的 32 位结果,通过 w0 返回 |
0xFFFFFFB0 | (填充区,未使用) | 16 字节对齐填充 | 因栈分配需按 16 字节对齐(48 字节或 16 的倍数) |
↓ SP 新位置 | SP = 0xFFFFFFB0 |
当前函数栈顶(低地址) | 通过 sub sp, sp, #16 分配空间,栈向低地址生长 |
关键说明:
-
栈生长方向:
- 栈从 高地址(0xFFFFFFC0)向低地址(0xFFFFFFB0) 扩展,符合 ARM64 的 满减栈(FD)。
- 每次函数调用会通过
sub sp, sp, #N
分配栈空间,N
必须是 16 的倍数。
-
数据读写规则:
- 存储指令(
str
) :写入时从 低地址向高地址 填充字节(如0xFFFFFFB4
→0xFFFFFFB7
)。 - 加载指令(
ldr
) :读取时从起始地址(如sp + 4
)连续读取 4 字节,按小端序组合为 32 位值。
- 存储指令(
-
对齐与填充:
- 若存储 64 位数据(如
x0
),需占用 8 字节且起始地址按 8 字节对齐(如0xFFFFFFB4
对齐到0xFFFFFFB0
)。 - 未使用的填充区域(如
0xFFFFFFB0
)确保栈帧按 16 字节对齐,避免内存访问错误。
- 若存储 64 位数据(如
示例验证(小端序存储):
假设 w0 = 0x12345678
,存储到 0xFFFFFFB4
后的内存布局:
地址 | 字节值 | 说明 |
---|---|---|
0xFFFFFFB4 | 0x78 | 最低有效字节 (LSB) |
0xFFFFFFB5 | 0x56 | |
0xFFFFFFB6 | 0x34 | |
0xFFFFFFB7 | 0x12 | 最高有效字节 (MSB) |