iOS逆向学习之十(arm64汇编入门)

iOS汇编

iOS汇编语音有不少钟。常见的有8086汇编、arm汇编、x86汇编等等。xcode

arm汇编

iOS的架构从最初的armv6发展到后来的armv7和armv7s,最后发展到如今的arm64,不论是armv6仍是后来的armv7,以及arm64都是arm处理器的指令集。armv7和armv7s是真机32位处理器使用的架构,而arm64是真机64位处理器使用的架构。bash

iPhone 5C是最后一款arm32位版本的iPhone,在iPhone5s以后,全部的iPhone设备都采用arm64架构。arm64汇编在真机上使用,以下:markdown

TestFont`-[ViewController test]:
    0x10286e574 <+0>:  sub    sp, sp, #0x20 ; =0x20 
    0x10286e578 <+4>:  mov    w8, #0x14
    0x10286e57c <+8>:  mov    w9, #0xa
    0x10286e580 <+12>: str    x0, [sp, #0x18]
    0x10286e584 <+16>: str    x1, [sp, #0x10]
->  0x10286e588 <+20>: str    w9, [sp, #0xc]
    0x10286e58c <+24>: str    w8, [sp, #0x8]
    0x10286e590 <+28>: add    sp, sp, #0x20 ; =0x20 
    0x10286e594 <+32>: ret  
复制代码

x86汇编

x86汇编是模拟器使用的汇编语言,它的指令和arm64汇编的语法不一样,以下数据结构

TestFont`-[ViewController test]:
    0x10b089520 <+0>:  pushq  %rbp
    0x10b089521 <+1>:  movq   %rsp, %rbp
    0x10b089524 <+4>:  movq   %rdi, -0x8(%rbp)
    0x10b089528 <+8>:  movq   %rsi, -0x10(%rbp)
->  0x10b08952c <+12>: movl   $0xa, -0x14(%rbp)
    0x10b089533 <+19>: movl   $0x14, -0x18(%rbp)
    0x10b08953a <+26>: popq   %rbp
    0x10b08953b <+27>: retq   
复制代码

为何要学习arm64汇编?

代码调试

在日常开发中,在调试程序的时候,若是程序crash,一般会定位到具体的崩溃代码。可是有时候也会遇到一些比较诡异的crash,好比说崩溃在了系统库中,这个时候定位到具体的crash缘由会很是困难。若是利用汇编调试技巧来进行调试,可能会让咱们事半功倍。架构

逆向调试

在逆向别人App过程当中,咱们能够经过LLDB对内存地址进行断点操做,可是当执行到断点时,LLDB展示给咱们的是汇编代码,而不是OC代码,因此想要逆向而且动态调试别人的App,就须要学习汇编的知识。iphone

arm64汇编入门

想要学习arm64汇编,须要从如下三个方面入手,寄存器、指令和堆栈。函数

寄存器

arm64中有34个寄存器,以下学习

通用寄存器

  • 64 bit的通用寄存器哟28个,分别是x0 ~ x28
  • 32 bit的也有28个,分别是w0 ~ w28(属于x0 ~ x28的低32位)

  • 其中x0 ~ x7一般拿来存放函数的参数,若是参数更多,则采用堆栈来进行传递
  • x0中一般存放函数的返回值

也会有人将x0 ~ x30叫作通用寄存器,可是在实际使用中x29和x30并无对应的低32位的寄存器w2九、w30,并且x29和x30寄存器有着特殊的用途,因此在此我只讲x0 ~ x28记为通用寄存器测试

程序计数器

pc (Program Counter)寄存器,它记录着当前CPU正在执行的指令的地址,经过register read pc查看寄存器中存储的值spa

(lldb) register read pc
      pc = 0x000000010286e588  TestFont`-[ViewController test] + 20 at ViewController.m:28
(lldb) 
复制代码

堆栈指针

  • sp (Stack Pointer)
  • fp (Frame Pointer),也就是以前所说的x29

连接寄存器

lr (Link Register)寄存器,也就是以前所说的x30寄存器,它存储着函数的返回地址

程序状态寄存器

arm体系中包含一个当前程序状态寄存器cpsr (Current Program Status Register)和五个备份的程序状态寄存器spsr (Saved Program Status Registe),备份的程序状态寄存器用来进行异常处理。

  • 程序状态寄存器的每一位都有特定的用途,此处只介绍几种经常使用的标志位

  • 其中N、Z、C、V均为条件码标志位,他们的内容可被算数或者逻辑运算的结果所改变,而且能够决定某条指令是否被执行。条件码标志各位的具体含义以下

指令

ARM指令列表

ARM指令以下:

助记符 ARM指令及功能描述
ADC 带进位加法指令
ADD 加法指令
AND 逻辑与指令
B 跳转指令
BIC 位清除指令
BL 带返回的跳转指令
BLX 带返回和状态切换的跳转指令
BX 带状态切换的跳转指令
CDP 协处理器数据操做指令
CMN 比较反值指令
CMP 比较指令
EOR 异或指令
LDC 存储器带协处理器的数据传输指令
LDM 加载多个寄存器指令
LDR 存储器到寄存器的数据传输指令
MCR 从ARM寄存器到协处理器寄存器的数据传输指令
MLA 乘加运算指令
MOV 数据传送指令
MRC 从协处理器寄存器到ARM寄存器的数据传输指令
MRS 传送CPSR或SPSR的内容到通用寄存器指令
MSR 传送通用寄存器到CPSR或SPSR指令
MUL 32位乘法指令
MLA 32位乘加指令
MVN 数据反传送指令
ORR 逻辑或指令
RSB 逆向减法指令
RSC 带借位的逆向减法指令
SBC 带借位减法指令
STC 协处理器寄存器写入存储器指令
STM 批量内存字写入指令
STR 寄存器到寄存器的数据传输指令
SUB 减法指令
SWI 软件中断指令
SWP 交换指令
TEQ 相等测试指令
TST 位测试指令

经常使用指令介绍

mov指令

指令介绍

mov指令能够将另外一个寄存器、被移位的寄存器或者将一个当即数加载到目的寄存器

mov指令在arm64汇编中的实际使用
  • 在xcode中新建test.s文件,在test.s文件中添加如下代码
; 此处.text表示此代码放在text段中
.text
; .global表示将后面跟随的方法给暴露出去,否则外部没法调用,方法名以_开头
.global _test

; 此处为_test方法
_test:
; mov指令,将当即数4加载到x0寄存器中
mov x0, #0x4
mov x1, x0
; 汇编指令中,ret表示函数的终止
ret
复制代码
  • 在xcode中新建test.h头文件,将test.s中的_test方法暴露出来
#ifndef test_h
#define test_h

void test(void);

#endif /* test_h */
复制代码
  • 在viewDidLoad中调用test()函数,而后在LLDB中使用register read x0 读取寄存器中存放的值
(lldb) register read x0
      x0 = 0x000000010320c980
(lldb) si
(lldb) register read x0
      x0 = 0x0000000000000004
(lldb) register read x1
      x1 = 0x00000001e60f3bc7  "viewDidLoad"
(lldb) si
(lldb) register read x1
      x1 = 0x0000000000000004
复制代码

经过对汇编指令增长断点,一步一步调试能够看出,在执行完mov指令后,x0和x1寄存器的值都被修改了

ret指令

ret指令表示函数的返回,并且它还有一个很是重要的做用,就是将lr(x30)寄存器的值赋值给pc寄存器

  • 在viewDidLoad中调用test()函数,在test()函数上打上断点,执行程序以下

  • 使用register read 查看lr和pc寄存器的值
(lldb) register read lr
      lr = 0x00000001021965a4  TestFont`-[ViewController viewDidLoad] + 68 at ViewController.m:23
(lldb) register read pc
      pc = 0x00000001021965a4  TestFont`-[ViewController viewDidLoad] + 68 at ViewController.m:23
(lldb) 
复制代码

此时,lr寄存器和pc寄存器的值都是test()函数起始地址

  • 使用si指令跳转到test()函数中

  • 再次查看lr和pc寄存器的值,发现lr的值变成了test()函数的下一条指令的地址,也就是test()函数执行完成以后,主程序须要执行的下一条指令。pc寄存器保存了当前即将执行的指令的地址,以下
(lldb) register read lr
      lr = 0x00000001021965a8  TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24
(lldb) register read pc
      pc = 0x0000000102196abc  TestFont`test
复制代码
  • 执行完test()函数,发现程序跳转到了lr寄存器所保存的指令地址,也就是0x00000001021965a8,此时再次查看lr和pc寄存器的值,发现pc寄存器存放的地址已经变成了lr寄存器存放的地址
(lldb) register read lr
      lr = 0x00000001021965a8  TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24
(lldb) register read pc
      pc = 0x00000001021965a8  TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24
(lldb) 
复制代码

add指令

add指令是将两个操做数相加,并将结果存放到目标寄存器中。具体说明以下

在arm64汇编中,相应的就是操做x0~x28,执行以下汇编代码

.text
.global _test

_test:

mov x0, #0x4
mov x1, #0x3

add x0, x1, x0

ret
复制代码

执行完test()函数,经过register read查询x0的值,最后能够看到x0存放的值为7,以下

(lldb) register read x0
      x0 = 0x0000000000000004
(lldb) si
(lldb) register read x1
      x1 = 0x0000000000000003
(lldb) si
(lldb) register read x0
      x0 = 0x0000000000000007
复制代码

sub指令

sub指令是将操做数1减去操做数2,再减去cpsr中的C条件标志位的反码,并将结果存放到目标寄存器中

cmp指令

cmp指令是把一个寄存器的内容和另外一个寄存器的内容或者当即数作比较,同时会更新CPSR寄存器中条件标志位的值

  • 执行以下汇编代码
.text
.global _test

_test:

mov x0, #0x4
mov x1, #0x3

cmp x0, x1

ret
复制代码
  • 在执行cmp代码以前和以后各打印一次CPSR寄存器的值以下
(lldb) register read cpsr
    cpsr = 0x60000000
(lldb) si
(lldb) si
(lldb) si
(lldb) register read cpsr
    cpsr = 0x20000000
(lldb) 
复制代码

能够发现,在执行cmp操做以后,cpsr寄存器的值变成了0x20000000,转换成16进制后,获得32位标志位以下

能够发现第31位,也就是N位的值为0,同时第30位,也就是Z位的值也为0,这就表示,x0和x1寄存器相比较以后的值为非零非负,而使用x0 - x1获得的结果是1,符合非零非负的条件。

  • 修改汇编代码,调换x0和x1寄存器的位置,以下
_test:

mov x0, #0x4
mov x1, #0x3

cmp x1, x0

ret
复制代码
  • 再次在cmp代码执行先后读取CPSR寄存器的值
(lldb) register read cpsr
    cpsr = 0x60000000
(lldb) s
(lldb) register read cpsr
    cpsr = 0x80000000
(lldb) 
复制代码

这个时候,cpsr寄存器的值变成了0x80000000,转换成16进制后,以下

能够看出,第31位N位的值变成了1,第30位Z位的值为0,这表示,x0和x1寄存器相比较以后的值为非零负数,使用x1-x0获得的结果是-1,符合非零负数的条件

跳转指令

B指令

B指令是最简单的跳转指令,一旦遇到B指令,程序会无条件跳转到B以后所指定的目标地址处执行。

BL指令

BL指令是另外一个跳转指令,可是在跳转以前,它会先将当前标记位的下一条指令存储在寄存器lr(x30)中,而后跳转到标记处开始执行代码,当遇到ret时,会将lr(x30)中存储的地址从新加载到PC寄存器中,使得程序能返回标记位的下一条指令继续执行。

  • 首先执行如下汇编代码
.text
.global _test

label:
mov x0, #0x1
mov x1, #0x8
ret

_test:
mov x0, #0x4
bl label
mov x1, #0x3
cmp x1, x0
ret
复制代码
  • 断点到bl label指令时,读取lr寄存器和PC寄存器的值

  • 执行bl label指令,跳转到label标记处,再次读取lr(x30)寄存器和PC寄存器的值,这个时候会发现lr(x30)寄存器存放的地址已经变成mov x1, #0x3这条指令的内存地址

  • 执行完label标记中的全部代码,发现程序再次回到lr寄存器所存储的地址,也就是mov x1, #0x3这句指令继续执行,而且此时pc寄存器所存储的地址也变成了mov x1, #0x3这句指令的地址。

条件域指令

当处理器工做在arm状态时,几乎全部的指令均根据CPSR寄存器中条件码的状态和指令的条件域有条件的执行,当指令的执行条件知足时,指令被执行,不然指令被忽略。
每一条ARM指令包含4位的条件码,位于指令的最高四位[31:28]。条件码共有16种,每种条件码可用两个字符表示,这两个字符可用添加在指令助记符的后面和指令同时使用。例如:跳转指令B后可用加上后缀EQ变为BEQ,表示相等则跳转,即当CPSR寄存器中的Z标志置位时发生跳转。

OC代码演示条件域指令的做用
  • 在ViewController中增长如下代码
- (void)test{
    int a = 1;
    int b = 2;
    if (a == b) {
        NSLog(@"a==b");
    }else{
        printf("a!=b");
    }
}
复制代码
  • 断点到test方法中,获得关键汇编代码以下

  • 其中w8,w9分别存放这0x2和0x1,cmp指令则对比w8和w9寄存器的值,而且修改CPSR寄存器对应的标志位
  • 执行cmp w8, w9指令后,查看CPSR寄存器的值以下
(lldb) register read cpsr
    cpsr = 0x80000000
复制代码

获得对应16进制的值为

  • b.ne 0x102522584这条指令表示若是CPSR中的Z标志位(即第30位)为0,则执行跳转操做,跳转到0x102522584地址处指令执行,如上图所示。一般也能够理解为w8和w9两个寄存器存放的当即数不相等时,则执行跳转操做。此处由于1!=2,因此跳转到0x102522584处执行。
条件标志码

在16种条件标志码中,只有15种可使用,以下图,第16种(1111)为系统保留,暂时不能使用

内存操做指令

内存操做指令分为内存读取和内存写入指令

内存读取指令LDR、LDUR、LDP
LDR指令格式为
LDR(条件) 目的寄存器, <存储器地址>
复制代码

LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令一般用于从存储器中读取32位字数据到通用寄存器中,而后对数据进行处理。当程序计数器PC做为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而实现程序流程的跳转。该指令在程序设计中比较经常使用,切寻址方式灵活多样。示例以下:

LDR x0, [x1]        ;将存储器地址为x1的字数据读入寄存器x0
LDR x0, [x1, x2]    ;将存储器地址为x1+x2的字数据读入寄存器x0
LDR x0, [x1, #8] ;将存储器地址为x1+8的字数据读入寄存器x0
LDR x0, [x1, x2]!   ;将存储器地址为x1+x2的字数据读入寄存器x0,并将新地址x1+x2写入x1
LDR x0, [x1, #8]! ;将存储器地址为x1+8的字数据读入寄存器x0,并将新地址x1+8写入x1
LDR x0, [x1], x2    ;将存储器地址为x1的字数据读入寄存器x0,并将新地址x1+x2写入x1
LDR x0, [x1, x2, LSL#2]! ;将存储地址为x1+x2*4的字数据写入寄存器x0,并将新地址x1+x2*4写入x1
LDR x0. [x1], x2, LSL#2 ;将存储地址为x1的字数据写入寄存器x0,并将新地址x1+x2*4写入x1
复制代码

经过一个简单的例子来了解LDR的做用:

  • 首先建立test.s文件,在文件中添加以下代码
.text
.global _test

_test:
; ldr指令,找到x1寄存器中存储的地址,从该地址开始读取8个字节的数据,存放到x0寄存器中
ldr x0, [x1]

ret
复制代码

为何此处是读取8个字节的数据呢?由于目标寄存器x0能够存放8个字节的数据,若是将x0换成w0,则读取4个字节的数据存放到w0中

  • 在viewDidLoad中调用test()函数,同时在test()函数以前声明一个局部变量,以下
- (void)viewDidLoad{
    [super viewDidLoad];

    int a = 5;
    test();

}
复制代码
  • 断点到test()函数处,运行程序,首先读取变量a的内存地址,将其内存地址存放到x1寄存器中,操做以下

能够发现,此时的x1寄存器存放着a变量的地址。

  • 输入si执行语句ldr x0, [x1],查看x0寄存器的值,此时发现x0寄存器的值变为0x31e09a5000000005,而不是5,这是由于变量a是int类型,而int类型为4个字节,可是LDR指令会将x1寄存器存放地址开始的8个字节的数据读取出来存放到x0寄存器中,因此x0寄存器中存放的值不是5,经过x 0x000000016f2c52ec也能够看出
(lldb) x 0x000000016f2c52ec
0x16f2c52ec: 05 00 00 00 50 9a e0 31 01 00 00 00 58 0f b4 00  ....P..1....X...
0x16f2c52fc: 01 00 00 00 c7 3b 0f e6 01 00 00 00 50 9a e0 31  .....;......P..1
复制代码

前4个字节存放的是5,也就是变量a的值

  • 将x0寄存器换成w0,从新执行上面的步骤,最后会发现w0中存放的是变量a的值,也就是5
LDUR指令

LDUR指令用法和LDR指令相同,区别在于LDUR后的当即数为负数,以下

LDR x0, [x1, #8]

LDUR x0, [x1, #-8]
复制代码
LDP指令

LDP中的P是pair的简称,能够看出LDP能够同时操做两个寄存器

; 如下命令表示,从sp寄存器的地址加上0x30后的地址开始,读取前8个字节的数据存放到寄存器x29中,读取后8个字节的数据放入x30寄存器中
ldp    x29, x30, [sp, #0x30]
复制代码
内存写入指令STR、STUR、STP
STR指令

STP指令的格式为:

STR{条件} 源寄存器, <存储器地址>
复制代码

STR指令用于从源寄存器中将一个32位的字数据传送到存储器中。示例以下

STR x0, [x1], #8 ;将x0中的字数据写入以x1为地址的存储器中,并将新地址x1+8写入x1
STR x0, [x1, #8] ;将x0中的字数据写入以x1+8为地址的存储器中
复制代码
STUR指令

STUR指令和STR指令用法相同,区别在于STUR后的当即数为负数

STR x0, [x1, #8]

STUR x0, [x1, #-8]
复制代码
STP指令

STP指令能够同时操做两个寄存器

; 如下指令表示,将x29+x30的字数据写入以sp+0x8为地址的存储器中,
stp    x29, x30, [sp, #0x8]
复制代码

零寄存器

零寄存器中存放的值为0,主要做用是进行寄存器的置0操做

  • wzr(32位零寄存器)
  • xzr(64位零寄存器)
  • 在OC代码中若是给变脸赋值为0,实际上是执行以下指令
#OC代码
int a = 0;

; 汇编代码
str    wzr, [sp, #0xc]
复制代码

具体效果是将wzr寄存器中的字数据,也就是0,写入sp+0xc为地址的存储器中

寻址方式

所谓寻址方式就是处理器根据指令中给出的地址信息来寻找物理地址的方法,目前ARM支持如下几种常见的寻址方式

当即寻址

当即寻址也叫作当即数寻址,是一种特殊的寻址方式,操做数自己就在指令中给出来,只要取出指令也就取到了操做数,这个操做数被称为当即数,对应的寻址方式也叫作当即寻址,例如如下指令:

ADD x0, x1, #1 ; x0 ← x1+1
ADD x0, x1, #0x3f ; x0 ← x1+0x3f
复制代码

在以上两条指令中,第二个操做数即为当即数,要求以“#”号为前缀,对于以16进制表示的当即数,还要求在“#”后加上“0x”或“&”。

寄存器寻址

寄存器寻址就是利用寄存器中的数值做为操做数,这种寻址方式是各种微处理器常常采用的一种方式,也是一种执行效率较高的寻址方式,指令以下

ADD x0, x1, x2          ; x0 ← x1+x2
复制代码

该指令的执行效果是将寄存器x1和x2的内容相加,其结果存放在寄存器x0中

寄存器间接寻址

寄存器间接寻址就是以寄存器中的值做为操做数的地址,而操做数自己存放在存储器中,例如以下指令

ADD x0, x1, [x2]        ; x0 ← x1+[x2]
LDR x0, [x1]            ; x0 ← [x1]
STR x0, [x1]            ; [x1] ← x0
复制代码
  • 第一条指令中,以寄存器x2的值做为操做数的地址,在寄存器中取得一个操做数后与x1相加,结果存储到寄存器x0中
  • 第二条指令是将以x1的值为地址的存储器中的数据传送到x0中
  • 第三条指令是将x0的值传送到以x1的值为地址的存储器中

基址变址寻址

基址变址寻址就是将寄存器(该寄存器通常称做基址寄存器)的内容与指令中给出的地址偏移量相加,从而获得一个操做数的有效地址。变址寻址方式经常使用于访问某基地址附近的地址单元。采用变址寻址方式的指令有如下常见的几种形式:

LDR x0, [x1, #4] ; x0 ← [x1+4]
LDR x0, [x1, #4]! ; x0 ← [x1+4]、x1 ← x1+4
LDR x0, [x1], #4 ; x0 ← [x1]、x1 ← x1+4
LDR x0, [x1, x2]        ; x0 ← [x1+x2]
复制代码
  • 第一条指令中,将寄存器x1的内容加上4造成操做数的有效地址,从而取得操做数存入寄存器x0中
  • 第二条指令中,将寄存器x1的内容加上4造成操做数的有效地址,从而取得操做数存入寄存器x0中,让x1寄存器的内容自增4个字节
  • 第三条指令中,以寄存器x1的内容做为操做数的有效地址,从而取得操做数存入寄存器x0中,而后寄存器x1的内容自增4个字节
  • 第四条指令中,将寄存器x1的内容加上寄存器x2的内容造成操做数的有效地址,从而取得操做数存入寄存器x0中

多寄存器寻址

采用多寄存器寻址方式,一条指令能够完成多个寄存器值的传送,这种寻址方式能够用一条指令完成传送最多16个通用寄存器的值,指令格式以下:

LDMIA x0, [x1, x2, x3, x4]      ; x1 ← [x0]
                                ; x2 ← [x0+4]
                                ; x3 ← [x0+8]
                                ; x4 ← [x0+12]
复制代码

该指令的后缀IA表示在每次执行完加载/存储操做后,x0按字长度增长,所以,指令能够将连续存储单元的值传送到x1~x4

相对寻址

与基址变址寻址方式相相似,相对寻址以程序计数器PC的当前值为基地址,指令中的地址标号为偏移量,将二者相加之和获得操做数的有效地址。如下程序段完成子程序的调用和返回,跳转指令BL就是采用了相对寻址方式:

BL  NEXT        ; 跳转到子程序NEXT处执行
    ......
NEXT
    ......
    MOV PC, LR      ; 从子程序返回
复制代码

堆栈寻址

堆栈是哟中数据结构,按先进后出(FILO)的方式工做,使用一个称做堆栈指针的专用寄存器指示当前的操做位置,堆栈指针老是指向栈顶位置。
当栈顶指针指向最后压入堆栈的数据时,称为满堆栈(Full Stack),而当堆栈指针指向下一个将要放入数据的空位置时,称为空堆栈(Empty Stack)
同时、根据堆栈的生成方式,又能够分为递增堆栈(Ascending Stack)和递减堆栈(Decending Stack),当堆栈由低地址向高地址生成时,称为递增堆栈,当堆栈由高地址向低地址生成时,称为递减堆栈。这样就有四种类型的堆栈工做方式,ARM微处理器支持这四种类型的堆栈工做方式。即:

  • 满递增堆栈:堆栈指针指向最后压入的数据,且由低地址向高地址生成。
  • 满递减堆栈:堆栈指针指向最后压入的数据,且由高地址向低地址生成
  • 空递增堆栈:堆栈指针指向下一个将要放入数据的空位置,且由低地址向高地址生成
  • 空递减堆栈:堆栈指针指向下一个将要放入数据的空位置,且由高地址向低地址生成

堆栈操做

函数的类型

在了解堆栈操做以前,首先得了解函数的类型,函数类型主要分为两种:叶子函数、非叶子函数

  • 叶子函数是指在此函数中,没有调用其它任何函数
  • 非叶子函数是值在此函数中,有调用其它函数

了解了什么是叶子函数和非叶子函数,那么咱们就要从汇编代码的层面来深刻理解叶子函数和非叶子函数的区别,以及堆栈指针在其中起到的做用。

叶子函数

上文介绍过叶子函数的具体定义,下面经过具体的汇编代码来深刻了解叶子函数

  • 首先在Xcode中建立MyTest.c文件,在文件中添加以下代码
void leafFuncion(){
    int a = 1;
    int b = 2;
}
复制代码
  • 进入MyTest.c文件所在目录,使用如下指令生成MyTest.s文件
xcrun --sdk iphoneos clang -S -arch arm64 MyTest.c -o MyTest.s
复制代码
  • 获得MyTest.s中的关键汇编代码以下
sub	sp, sp, #16 ; sp = sp - 16
    
	orr	w8, wzr, #0x2
	orr	w9, wzr, #0x1
	str	w9, [sp, #12]
	str	w8, [sp, #8]
	
	add	sp, sp, #16 ; sp = sp + 16
	ret
复制代码
  • 获得汇编代码以后,咱们就来一句一句分析汇编代码
    • sub sp, sp, #16指令表示将堆栈指针sp向前偏移#16

堆栈指针<font color=red>sp</font>开始指向<font color=red>0x10010</font>,偏移以后指向<font color=red>0x10000</font>,至关于开辟了从<font color=red>0x10000</font>到<font color=red>0x10010</font>这一段内存供函数使用。
复制代码
  • orr指令用于在两个操做数上进行逻辑或运算,并把结果放置到目的寄存器中。上文中orr w8, wzr, #0x2指令是将wzr寄存器的值与#0x2作逻辑或运算,获得的结果存放在w8寄存器中。通俗一点就是将#0x2赋值给了寄存器w8。指令orr w9, wzr, #0x1就是将#0x1赋值给了寄存器w9。
  • str w9, [sp, #12]指令表示将寄存器w9的值写入到以sp + 12的地址开始4个字节大小的内存中去,str w8, [sp, #8]指令同上。具体操做流程以下图:

  • 执行完内存的存储操做以后,当前栈空间的工做已经完成,为了保持堆栈平衡,须要将堆栈指针sp的位置还原成函数调用以前的初始位置。add sp, sp, #16的做用就是将sp指针的位置向后偏移16个字节,从新指向0x10010的位置。

为何要维持堆栈平衡?由于在函数调用以前,堆栈指针sp会偏移一段内存地址,为当前须要调用的函数分配一段内存空间,在函数调用完成以后将sp指针重置到开始位置,这样,刚刚分配的那段内存空间就是垃圾内存,下一次再有函数调用的时候,这段内存空间可重复利用。这就作到了堆栈平衡。若是函数调用完成以后不重置sp指针,那么,若是有足够多的函数一直调用,最后确定会出现栈溢出的问题。

非叶子函数

非叶子函数和叶子函数的区别在因而否有调用其它函数,下面一样经过具体的汇编代码来深刻了解非叶子函数

  • 首先在Xcode中建立MyTest.c文件,在文件中添加如下代码
void leafFuncion(){
    int a = 1;
    int b = 2;
}

void nonLeafFunction(){
    int a = 3;
    int b = 4;
    leafFuncion();
}
复制代码
  • 进入MyTest.c文件所在目录,使用如下指令生成MyTest.s文件
xcrun --sdk iphoneos clang -S -arch arm64 MyTest.c -o MyTest.s
复制代码
  • 获得MyTest.s中的关键汇编代码以下
sub	sp, sp, #32 ; sp=sp-32
	stp	x29, x30, [sp, #16] ; 8-byte Folded Spill
	add	x29, sp, #16 ; x29=sp+16

	orr	w8, wzr, #0x4
	orr	w9, wzr, #0x3
	stur	w9, [x29, #-4]
	str	w8, [sp, #8]
	bl	_leafFuncion

	ldp	x29, x30, [sp, #16] ; 8-byte Folded Reload
	add	sp, sp, #32 ; sp=sp+32
	ret
复制代码
  • 开始分析汇编代码

    • sub sp, sp, #32指令是执行内存分配的操做,将sp指针向前偏移32位,获得一片连续的内存空间

    • stp x29, x30, [sp, #16]指令是将x29(fp)和x30(lr)寄存器中存放的值写入以sp+16的地址为起始地址的一段内存空间中去,每一个寄存器占8个字节的空间。

    • add x29, sp, #16指令是将sp + 16的地址存放在x29(fp)寄存器中,由此,能够获得从sp到x29(fp),这两个地址之间的一段内存空间就是当前函数可使用的内存空间。

    如上图所示,橙色的那段内存就是咱们可使用的内存空间。

    • orr w8, wzr, #0x4和orr w9, wzr, #0x3其实就是将4赋值给寄存器w8,将3赋值给寄存器w9

    • stur w9, [x29, #-4]指令是将w9中存储的值,也就是3,写入到以x29(fp)- 4的地址为开始地址的4个字节的内存中去。str w8, [sp, #8]指令则是将w8中存储的值4,写入到以sp+8为起始地址的4个字节的内存中去,以下

    • bl _leafFuncion指令则表示跳转到_leadFunction函数的操做,前面提到过,执行bl指令以前,会将bl指令的下一条汇编指令ldp x29, x30, [sp, #16]的地址存放到lr寄存器中,以便执行完_leadFunction函数以后,能跳转回ldp x29, x30, [sp, #16]指令继续执行。这就能够明白为何以前须要先存储lr寄存器中的值,由于一旦执行完bl _leafFuncion指令以后,若是不将lr指令重置为初始值的话,一旦执行到后面的ret函数,会从新跳到ldp x29, x30, [sp, #16]指令的地址处从新执行,如此反复。

    • 执行完_leadFunction函数以后,会回到lr中存储的地址,也就是ldp x29, x30, [sp, #16]指令继续执行。ldp x29, x30, [sp, #16]指令的做用是以sp+16的地址为开始地址,依次读取16个字节的数据,前8个字节的数据存放到x29(fp)寄存器中去,后8个字节的数据存放到x30(lr)寄存器中去。其实就是将x29(fp)和x30(lr)寄存器的值恢复到调用函数以前所存放的值。

    • 最后add sp, sp, #32指令是将sp+32的地址值赋值给sp,其实就是还原sp指针的值,至此整个函数就调用完毕,给当前函数分配的内存空间就成了垃圾内存空间,能够给以后的函数重复使用。至此,咱们就能够明白叶子函数和非叶子函数的区别,以及堆栈指针在当前函数调用过程当中起到的做用。

    在非叶子函数调用过程当中,sp指针一直指向被分配栈空间的栈顶,因此又叫作栈顶指针,而fp指针指向可用栈空间的栈底,因此又叫作栈底指针。两个指针所指地址的中间一段内存就是函数可使用的内存空间。

    函数执行开始和结束的汇编指令就是用来分配内存以及维持堆栈平衡的操做。

相关文章
相关标签/搜索