当前位置: 首页 > news >正文

汇编学习——iOS开发对arm64汇编的初步了解

汇编学习——iOS开发对arm64汇编的初步了解

文章目录

  • 汇编学习——iOS开发对arm64汇编的初步了解
    • 前言
    • 栈 指令 寄存器
      • 寄存器
      • 指令
        • 运算指令
        • 寻址指令
        • 前变基 与 后变基
        • 堆(Heap)内存机制
        • 三、栈(Stack)内存机制
      • 3. 多级调用示例
    • 例子
    • 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),栈顶恢复高地址。

这部分可能有点乱,我们在后面用一个例子的展示一下栈具体的存储结构

运算指令
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)

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

寻址指令的格式

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,起始地址是较小的那个地址,结束地址是较大的那个地址。

img

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

img

三、栈(Stack)内存机制
  1. 核心特征
  • 分配方式:函数调用自动创建帧(Frame)
  • 地址增长:从高位地址向低位地址扩展
  • 生命周期:函数结束时自动释放

简单来说,栈是由于函数运行而临时占用的内存区域

img

  1. 函数帧结构
int main() {int a = 2;  // ↘ 主函数帧int b = 3;  // │ 变量存储区
}               // ↖ 栈顶地址 0x8000

上面的代码中,系统开始执行 main 函数的时,会为它在内存里面建立一个帧(frame),所有 main 的内部变量(比如a和b)都保存在这个帧里面。main 函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。

img

3. 多级调用示例

int test(int x, int y) {   // ↘ 子帧return x + y;          // │ 参数/变量存储区
}                          // ↖ 新栈顶 0x7FF0int main() {               // ↘ 主帧test(2, 3);            // │ 返回地址保存区
}                          // ↖ 初始栈顶 0x8000

当我们在main函数之中调用了test函数时,当我们执行这一步,系统就会为了这个test函数创建一个帧,现在栈区就有了两个函数帧,一般调用了多少层的函数就有多少个帧

img

等到test函数运行结束,它的帧就会被系统自动回收,实现了函数的层层调用。

前面我们说到,arm64架构下的栈时从高位(地址)向低位(地址)分配的,,内存区域的结束地址是 0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0。

操作栈地址范围剩余空间
主帧分配(16字节)0x8000 - 0x7FF00x7FF0
子帧分配(64字节)0x7FF0 - 0x7FB00x7FB0

例子

// 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代码

.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 的倍数,则按照最小倍数补全

.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函数

 //源代码int test(int a, int b) {int res = a + b;return res;}//汇编sub	sp, sp, #16             ; =16.cfi_def_cfa_offset 16str	w0, [sp, #12]str	w1, [sp, #8]ldr	w0, [sp, #12]ldr	w1, [sp, #8]add	w0, w0, w1str	w0, [sp, #4]ldr	w0, [sp, #4]add	sp, sp, #16             ; =16ret

在编译器生成汇编时,会首先计算需要的栈空间大小,并利用 sp (stack pointer)指针指向低地址开辟相应的空间。从 test 函数可以看到这里涉及了3个变量,分别是 a、b、res,int变量占据4个字节,因此需要12个字节,但 ARM64 汇编为了提高访问效率要求按照16字节进行对齐,因此需要16 byte 的空间,也就是需要在栈上开辟16字节的空间

代码的大致意思如下

sub sp, sp, #16             ; =16

将栈顶指针下移,即为函数的栈帧扩充空间

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 进行相加,需要注意的是,只有寄存器才能参与运算,因此接下来的汇编代码又将变量的值从内存中读出来,再进行相加运算。

ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1

为什么需要先存取后取出再操作,这个操作确实多余,是因为汇编没有进行优化的结果,把这个操作体现出来更有利于

ARM64 栈结构示意图(从高地址向低地址生长)

内存地址存储内容(示例值)用途说明备注
0xFFFFFFC0(未分配,原栈顶)父函数栈帧栈初始位置,SP 初始指向此处(高地址)
0xFFFFFFBCw0(参数 a = 0x12345678)参数存储区(sp + 12)32 位参数,按 小端序 存储:0x78 0x56 0x34 `0x12
0xFFFFFFB8w1(参数 b)参数存储区(sp + 8)第二个参数,通过 w1 传递
0xFFFFFFB4w0(计算结果 c)临时结果存储(sp + 4)计算后的 32 位结果,通过 w0 返回
0xFFFFFFB0(填充区,未使用)16 字节对齐填充因栈分配需按 16 字节对齐(48 字节或 16 的倍数)
↓ SP 新位置SP = 0xFFFFFFB0当前函数栈顶(低地址)通过 sub sp, sp, #16 分配空间,栈向低地址生长
关键说明:
  1. 栈生长方向

    • 栈从 高地址(0xFFFFFFC0)向低地址(0xFFFFFFB0) 扩展,符合 ARM64 的 满减栈(FD)
    • 每次函数调用会通过 sub sp, sp, #N 分配栈空间,N 必须是 16 的倍数。
  2. 数据读写规则

    • 存储指令(str:写入时从 低地址向高地址 填充字节(如 0xFFFFFFB40xFFFFFFB7)。
    • 加载指令(ldr:读取时从起始地址(如 sp + 4)连续读取 4 字节,按小端序组合为 32 位值。
  3. 对齐与填充

    • 若存储 64 位数据(如 x0),需占用 8 字节且起始地址按 8 字节对齐(如 0xFFFFFFB4 对齐到 0xFFFFFFB0)。
    • 未使用的填充区域(如 0xFFFFFFB0)确保栈帧按 16 字节对齐,避免内存访问错误。
示例验证(小端序存储):

假设 w0 = 0x12345678,存储到 0xFFFFFFB4 后的内存布局:

地址字节值说明
0xFFFFFFB40x78最低有效字节 (LSB)
0xFFFFFFB50x56
0xFFFFFFB60x34
0xFFFFFFB70x12最高有效字节 (MSB)

参考文章

iOS 汇编入门 - arm64基础

相关文章:

  • go 通过汇编学习atomic原子操作原理
  • AI面经总结-试读
  • C# 方法(方法重载)
  • 【MySQL】表空间结构 - 从何为表空间到段页详解
  • 服务器mysql连接我碰到的错误
  • Git的核心作用详解
  • 智能语音助手的未来:从交互到融合
  • HTTP 错误状态码以及常用解决方案
  • 基于OpenCV的人脸识别:LBPH算法
  • FastAPI+MongoDB+React实现查询博客详情功能
  • 【Android】cmd命令
  • 使用 FastAPI 和 MongoDB 实现分页查询功能,并在 React 中进行分页展示
  • 《Hadoop 权威指南》笔记
  • LabVIEW车牌自动识别系统
  • 比亚迪全栈自研生态的底层逻辑
  • C语言_函数调用栈的汇编分析
  • 每日c/c++题 备战蓝桥杯(P1002 [NOIP 2002 普及组] 过河卒)
  • 【Mac 从 0 到 1 保姆级配置教程 12】- 安装配置万能的编辑器 VSCode 以及常用插件
  • 外网访问内网海康威视监控视频的方案:WebRTC + Coturn 搭建
  • 微服务架构中如何保证服务间通讯的安全
  • 兰州大学教授安成邦加盟复旦大学中国历史地理研究所
  • 欧元区财长会讨论国际形势及应对美国关税政策
  • 吉林:消纳绿电,“氢”装上阵
  • 新造古镇丨乌镇的水太包容了,可以托举住任何一种艺术
  • 视觉周刊|纪念苏联伟大卫国战争胜利80周年
  • 河北邯郸一酒店婚宴发生火灾:众人惊险逃生,酒店未买保险