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)
 
Note:
 
x0 - x7 :用于子程序调用时的参数传递,超过八个会放到栈上传递
 
x0 和 w0 是同一个寄存器的不同尺寸的区别,x0 为 8 字节,w0 为 4 字节(x0 寄存器的低4字节), x0/w0 还用于返回值的传递
 
 

指令:

 
运算指令
 
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 。
常见的条件码有以下这些:
 
iOS 汇编入门 - arm64基础_第1张图片
数据来源: here
 
 

内存模型:

 
堆:
 
在了解栈之前先来了解一下堆(Heap)。
 
由于寄存器只能存放少量数据,在大多数时候,CPU 跟指挥寄存器跟内存交换数据。所以除了寄存器,还必须了解内存是怎么存储数据的。
 
程序运行的时候,操作系统会给它分配一段内存,用来存储程序和运行产生的数据。这段内存有起始地址和结束地址,比如从 0x1000 到 0x8000,起始地址是较小的那个地址,结束地址是较大的那个地址。
iOS 汇编入门 - arm64基础_第2张图片
程序运行过程中,对于动态占用请求(比如新建对象,或者使用 malloc ),系统就会从预先分配好的那段内存之中,划出一部分给用户,具体规则是 从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户申请10个字节的内存,那么从起始地址0x1000开始给他分配,一直分配到0x100A,如果再申请22个字节,那么就分配到0x1020。
 
iOS 汇编入门 - arm64基础_第3张图片
这种因为用户主动请求而划分出来的内存区域,叫做堆(Heap)。 它由起始地址开始,从低位(地址)向高位(地址)增长。Heap 的一个重要特点是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。
 
 
 
 
除了堆(Heap)以外,其他的内存占用叫做栈(Stack)。简单来说,栈是由于函数运行而临时占用的内存区域, 是一种往下(低地址)生长的数据结构
iOS 汇编入门 - arm64基础_第4张图片
 
int main() {
   int a = 2;
   int b = 3;
}

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

iOS 汇编入门 - arm64基础_第5张图片
如果 main 函数内部又调用了其他函数,情况又会是怎样呢?
int main() {
   int a = 2;
   int b = 3;
   return test(a, b);
}

上面的代码中,main 函数内部调动了 test 函数。当执行到这一步的时候,系统也会为 test 新建一个帧,用来存储它的内部变量。也就是说,此时同时存在两个帧:main 和 test。一般来说,调用栈有多少层,就有多少帧。

iOS 汇编入门 - arm64基础_第6张图片
等到 test 运行结束,它的帧就会被回收,系统会回到函数 main 刚才中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。
 
栈(Stack)是有内存区域的结束地址开始,从高位(地址)向低位(地址)分配的。比如,内存区域的结束地址是 0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0。
iOS 汇编入门 - arm64基础_第7张图片
 
 
 
 
实战
 
了解完以上的基础知识之后,下面就用一个简单的例子了解汇编的栈操作。
// hello.c
#include 
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  用于指定程序的对齐方式,这类似于结构体的字节对齐,为的是加速程序的执行速度,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
iOS 汇编入门 - arm64基础_第8张图片
 
 
接着看下面这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个字节的空间,栈结构图变为如下所示:
 
iOS 汇编入门 - arm64基础_第9张图片
 
接下来 test 函数内部将 a 和 b 进行相加,需要注意的是,只有寄存器才能参与运算,因此接下来的汇编代码又将变量的值从内存中读出来,再进行相加运算。
        
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
到这里可能会纳闷,先存储在读取后运算,感觉这一步很多余,确实是这样的,因为这是没有进行编译优化的结果,为了是能够更好的学习和了解汇编的工作机制。
 
计算完成之后将结果存储到了w0寄存器,地址是 sp+4         
str w0, [sp, #4]
 
iOS 汇编入门 - arm64基础_第10张图片
 
接下来就要进行返回操作了,上文中我们提到,函数的返回值一般存储在 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》这本书的一节,给截了个图过来了,如下:
 
iOS 汇编入门 - arm64基础_第11张图片
 
 
 
参考资料:
 
阮一峰 - 汇编语言入门教程
[C in ASM(ARM64)]第一章 一些实例
iOS汇编入门教程(一)ARM64汇编基础
The A64 Instruction set《Advanced Apple Debugging & Reverse Engineer》- Chapter 6: Thread, Frame & Stepping Around

 

你可能感兴趣的:(iOS进阶)