iOS 汇编入门 - arm64基础
作者:互联网
前言
iOS 中常见的架构有 armv7、armv7s、arm64、i386、x86_64 这些。 其中, armv7 、armv7s 、arm 64 都是 ARM 处理器的指令集,i386 、x86_64 是 Mac 处理器的指令集 这些指令集对应的机型有以下这些:
arm64e: iphone XS | iphone XS Max | iPhoneXR
arm64: iPhone 8 | iPhone 8 Plus | iPhone X | iPhone 7/7 Plus | iPad (2018) | iPhone 6/6S | iPhone 6/6S Plus | iPhone 5s
armv7s: iPhone 5 | iPhone5C | iPad4(Retina屏) |
armv7: iPhone 4 | iPhone4S | iPad 2/3 | iPad mini | iPod Touch 3G | iPod Touch4
armv7/armv7s/i386 架构使用的是 32 位的处理器
arm64/x86_64 架构使用的是 64 位的处理器
查看 framework 包含的架构命令:lipo
基本概念
汇编里面涉及到最多的就是寄存器、栈和指令这 3 个。寄存器:
寄存器是 CPU 中的高速存储单元,存取速度比内存快很多。 常用的寄存器有以下这些:寄存器 | 描述 |
r0 - r30 | 通用整形寄存器,64 位,当使用 x0 - x30 访问时,代表的是 64 位的数;当使用 w0 - w30 访问的时候,访问的是这些寄存器的低 32 位 |
fp(x29) | 保存栈帧地址(栈底指针) |
lr(x30) | 通常称x30为程序链接寄存器,保存子程序结束后需要执行的下一条指令 |
sp | 保存栈指针,使用 sp/wsp 来进行对 sp 寄存器的访问 |
pc | pc 寄存器中存的是当前执行的指令的地址,在 arm64 中,软件是不能改写 pc 寄存器的 |
SPRs | 状态寄存器,存放状态标识,可分为 CPSR (The Current Program Status Register) 和 SPSRs(The Save Program Status Registers)。一般都是使用 CPSR,当发生异常时,CPSR 会存入 SPSR。当异常恢复,再拷贝回 CPSR |
zr | 零寄存器,里面存的是 0 (zero register)一般使用 wzr/xzr ,w 代表 32位,x 代表 64 位 |
v0 - v31 | 向量寄存器,也可以说是浮点型寄存器,每个寄存器大小是 128 位,可以用 Bn Hn Sn Dn Qn 来访问不同的位数(8 16 32 64 128) |
指令:
运算指令:
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 Note:后面有个感叹号的,然后没有stup这个指令哈
ldp x29,x30,[sp],#16 ;从 sp 地址取出16 byte数据,分别存入x29、x30,然后 sp+=16
「寻址」的格式分为下面3种类型:
mov x0
[x10, #0x10] ;从 x10+0x10 的地址取值
[sp, #-16]! ;从 sp-16 地址取值,取完后再把 sp-16 writeback 回 sp
[sp], #16 v从 sp 地址取值,取完后把 sp+16 writeback 回 sp
跳转指令
bl/b bl 是有返回的跳转;b 是无返回的跳转
1.有返回的意思就是会存 lr ,存了 lr 也就意味着可以返回到本方法继续执行,一般用于不同方法直接的调用; 2.无返回的一般是方法内的跳转,如 while 循环,if else 等。跳转指令一般还伴随着条件,以实心点.开头的都是表示条件,如 b.ne ,一般用于 if else 。 常见的条件码有以下这些: 数据来源:here
内存模型:
堆: 在了解栈之前先来了解一下堆(Heap)。 由于寄存器只能存放少量数据,在大多数时候,CPU 跟指挥寄存器跟内存交换数据。所以除了寄存器,还必须了解内存是怎么存储数据的。 程序运行的时候,操作系统会给它分配一段内存,用来存储程序和运行产生的数据。这段内存有起始地址和结束地址,比如从 0x1000 到 0x8000,起始地址是较小的那个地址,结束地址是较大的那个地址。 程序运行过程中,对于动态占用请求(比如新建对象,或者使用 malloc ),系统就会从预先分配好的那段内存之中,划出一部分给用户,具体规则是从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户申请10个字节的内存,那么从起始地址0x1000开始给他分配,一直分配到0x100A,如果再申请22个字节,那么就分配到0x1020。 这种因为用户主动请求而划分出来的内存区域,叫做堆(Heap)。它由起始地址开始,从低位(地址)向高位(地址)增长。Heap 的一个重要特点是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。 栈 除了堆(Heap)以外,其他的内存占用叫做栈(Stack)。简单来说,栈是由于函数运行而临时占用的内存区域,是一种往下(低地址)生长的数据结构。
int main() {
int a = 2;
int b = 3;
}
上面的代码中,系统开始执行 main 函数的时,会为它在内存里面建立一个帧(frame),所有 main 的内部变量(比如a和b)都保存在这个帧里面。main 函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。
如果 main 函数内部又调用了其他函数,情况又会是怎样呢?
int main() {
int a = 2;
int b = 3;
return test(a, b);
}
上面的代码中,main 函数内部调动了 test 函数。当执行到这一步的时候,系统也会为 test 新建一个帧,用来存储它的内部变量。也就是说,此时同时存在两个帧:main 和 test。一般来说,调用栈有多少层,就有多少帧。
等到 test 运行结束,它的帧就会被回收,系统会回到函数 main 刚才中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。 栈(Stack)是有内存区域的结束地址开始,从高位(地址)向低位(地址)分配的。比如,内存区域的结束地址是 0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0。 实战 了解完以上的基础知识之后,下面就用一个简单的例子了解汇编的栈操作。
// 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命令将其编译成arm64指令集汇编代码
clang -S -arch arm64 -isysroot `xcrun --sdk iphoneos --show-sdk-path` hello.c
可以看到完整的汇编如下:
.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
先看第一部分:
.section __TEXT,__text,regular,pure_instructions
.build_version ios, 13, 2 sdk_version 13, 2
.globl _test ; -- Begin function test
.p2align 2
代码中类似 .section 或者 .globl 等以 ‘.' 开头的被称之为编译指令,用于告知编译器相关的信息或者进行特定操作。
.section 里面的 __TEXT,__text 字段用来存放代码指令 .build_version 是编译版本信息 .globl _test 声明了全局变量(函数) ; -- Begin function test 分号后面是注释的意思 .p2align 2 用于指定程序的对齐方式,这类似于结构体的字节对齐,为的是加速程序的执行速度,p2align 的单位是指数,即按照 2 的 n 次方对齐,这里的 .p2align 2 表示按照 2^2 = 4 字节对齐,如果单行指令数据长度不足4字节,将用 0 补全,超过 4 但不是 4 的倍数,则按照最小倍数补全 _test、_main 称之为标签(label),用于辅助定位代码或者资源地址,也方便开发者理解和记忆再接着往下看
.cfi_startproc ;定义函数开始
.cfi_endproc ;定义函数结束
.cfi_xxx ;call frame information xxx, cfi 是 DWARF 2.0 定义的函数栈信息,用来告诉编译器生成响应的 DWARF 调试信息,主要是和函数有关。
汇编中的如下部分被称为方法头(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 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字节的空间,可以看汇编的第一句,正是将 sp 指针下移16字节。
sp (stack pointer)是栈指针,永远指向栈顶!
sub sp, sp, #16 ; =16
接着看下面这2句:
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,正如上文栈一节提到的,由于栈的存储是从高地址向低地址分配的,所以a将占据 sp+12 ~ sp+16 这4个字节的空间,b将占据 sp+8 ~sp+12 这4个字节的空间,栈结构图变为如下所示:
接下来 test 函数内部将 a 和 b 进行相加,需要注意的是,只有寄存器才能参与运算,因此接下来的汇编代码又将变量的值从内存中读出来,再进行相加运算。
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
到这里可能会纳闷,先存储在读取后运算,感觉这一步很多余,确实是这样的,因为这是没有进行编译优化的结果,为了是能够更好的学习和了解汇编的工作机制。
计算完成之后将结果存储到了w0寄存器,地址是 sp+4
str w0, [sp, #4]
接下来就要进行返回操作了,上文中我们提到,函数的返回值一般存储在 x0/w0 寄存器中返回的,这里也可以看到它将返回值res载入到了x0/w0 寄存器了
ldr w0, [sp, #4]
最后就是将栈还原,并返回到函数调用处继续向下执行。
add sp, sp, #16 ; =16
ret
显然,经过这样的操作,栈被完全还原到了函数调用以前的样子,需要注意的细节是,栈空间中的内存单元并未被清空,这就导致下一次使用栈时,未初始化单元的值是不确定的,这也就是局部变量不初始化会出现随机值的根本原因。
接着,再看看 main 函数的汇编代码就变得很好理解了:
sub sp, sp, #32 ; =32
stp x29, x30, [sp, #16] ; 16-byte Folded Spill
add x29, sp, #16 ; =16
----------------------------------------------------prologue-----------------------------------------------------------------
.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
----------------------------------------------------epilogue-----------------------------------------------------------------
ldp x29, x30, [sp, #16] ; 16-byte Folded Reload
add sp, sp, #32 ; =32
ret
附录: 对于 sp 指针和栈哪边是header哪边是tail还有疑问的可以看下《Advanced Apple Debugging & Reverse Engineering》这本书的一节,给截了个图过来了,如下: 参考资料: 阮一峰 - 汇编语言入门教程 [C in ASM(ARM64)]第一章 一些实例 iOS汇编入门教程(一)ARM64汇编基础 The A64 Instruction set《Advanced Apple Debugging & Reverse Engineer》- Chapter 6: Thread, Frame & Stepping Around
转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消转存失败重新上传取消
Hi_Aaron 发布了171 篇原创文章 · 获赞 333 · 访问量 141万+ 关注标签:入门,16,寄存器,sp,iOS,地址,arm64,w0,x0 来源: https://blog.csdn.net/chaoyuan899/article/details/104535224