栈:是一种具有特殊的访问方式的存储空间,具有后进先出的特性(Last In Out FirtLIFO
iOS逆向实战--002:函数本质 - 图1

SP和FP寄存器
  • sp寄存器:在任意时刻会保存栈顶的地址(栈的开口方向)
  • fp寄存器:也称为x29寄存器,属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址(有局部变量且嵌套调用的时候)

注意:ARM64开始,取消32位LDMSTMPUSH(入栈)、POP(出栈)指令。 取而代之的是ldr\ldpstr\stp

32位模式下,空栈时栈顶和栈底指向同一个地方。当数据入栈,栈顶指针跟随向上移动。数据出栈,栈顶指针跟随向下移动

但是在ARM64中,栈的开口方向是向下的,由高地址到低地址。对栈的操作是16字节对齐

存储数据前先拉伸栈,也就是开辟栈空间,然后再往里面存储数据。使用完毕后恢复栈平衡,里面存储的数据不需要回收,因为下次开辟栈空间后,新数据会直接覆盖

栈空间的拉伸,在代码编译过程中,由编译器决定,将局部变量、参数等放入栈区

死循环和死递归的区别

  • 死循环:如果死循环内没有开辟任何空间,不会造成程序崩溃
  • 死递归:死递归将不断开辟栈空间,最终因为堆栈溢出导致程序崩溃

堆栈溢出(Stack Overflow
iOS逆向实战--002:函数本质 - 图2

  • 栈的开口方向是向下的,而堆区是向上的。当栈区与堆区边界碰撞,就会造成堆栈溢出
函数调用栈

常见的函数调用开辟和恢复的栈空间

  1. sub sp, sp, #0x40 ; 拉伸0x40(64字节)空间
  2. stp x29, x30, [sp, #0x30] ;x29\x30 寄存器入栈保护
  3. add x29, sp, #0x30 ; x29指向栈帧的底部
  4. ...
  5. ldp x29, x30, [sp, #0x30] ;恢复x29/x30 寄存器的值
  6. add sp, sp, #0x40 ; 栈平衡
  7. ret
  • 使用sub指令,减一个地址,相当于开辟栈空间
  • 使用add指令,加一个地址,相当于恢复栈平衡
  • 写数据时,必须先拉伸栈,有了栈空间后,才能存放
关于内存读写指令

读/写数据是都是往高地址读/写

strstore register)指令

  • 将数据从寄存器中读出来,存到内存中

ldrload register)指令

  • 将数据从内存中读出来,存到寄存器中

ldrstr的变种指令,ldpstp,可以操作2个寄存器

案例:

开辟32字节作为这段程序的栈空间。利用栈将x0x1寄存器中的值进行交换

搭建Demo项目
iOS逆向实战--002:函数本质 - 图3

创建asm.s文件,写入以下代码:

``` .text .global _A

_A: sub sp, sp, #0x20 mov x0, #0xa0 mov x1, #0xb0 stp x0, x1, [sp,#0x10] ldp x1, x0, [sp,#0x10] add sp, sp, #0x20 ret

  1. > - `sub sp, sp, #0x20`:开辟栈空间,`sp`拉伸`32字节`栈空间
  2. > - `mov x0, #0xa0`:将`#0xa0`写入`x0`寄存器
  3. > - `mov x1, #0xb0`:将`#0xb0`写入`x1`寄存器
  4. > - `stp x0, x1, [sp,#0x10]`:将`x0``x1`寄存器的值,写入到`sp`向上偏移`16字节`的内存地址中
  5. > - `ldp x1, x0, [sp,#0x10]`:读取`sp`向上偏移`16字节`后内存中的值,写入到`x1``x0`寄存器,相当于交换
  6. > - `add sp, sp, #0x20`:将拉伸后的`sp``32字节`,恢复栈平衡
  7. > - `ret`:返回
  8. > 打开`ViewController.m`文件,写入以下代码:
  9. >

import “ViewController.h”

int A(void);

@implementation ViewController

  • (void)viewDidLoad { [super viewDidLoad]; A(); }

@end

  1. > 真机运行项目,使用断点单步调试,来到`A`函数<br />
  2. ![](https://upload-images.jianshu.io/upload_images/9297953-bdfcc19320e79643.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  3. > 单步调试,向下执行`1`步。开辟栈空间,拉伸`32字节`<br />
  4. ![](https://upload-images.jianshu.io/upload_images/9297953-7b00e9f44f0a783a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  5. > - 拉伸后,`sp`指向地址`0x000000016d250fd0`
  6. > 向下执行`2`步,将`#0xa0`写入`x0`寄存器,将`#0xb0`写入`x1`寄存器<br />
  7. ![](https://upload-images.jianshu.io/upload_images/9297953-2472c5c996f83e13.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  8. > 向下执行`1`步,将`x0``x1`寄存器的值,写入到`sp`向上偏移`16字节`的内存地址中<br />
  9. ![](https://upload-images.jianshu.io/upload_images/9297953-a571e8578d6d8f03.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  10. > 单步调试,向下执行`1`步。读取`sp`向上偏移`16字节`后内存中的值,写入到`x1``x0`寄存器,相当于交换<br />
  11. ![](https://upload-images.jianshu.io/upload_images/9297953-7a5e26814bd77041.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  12. > - `x0``x1`寄存器的值交换,但内存的数据并没有发生任何变化
  13. > - 内存充当临时变量的作用
  14. > 向下执行`1`步,将拉伸后的`sp``32字节`,恢复栈平衡<br />
  15. ![](https://upload-images.jianshu.io/upload_images/9297953-47781a74ef304130.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  16. > - 恢复栈平衡,`sp`指向地址`0x000000016d250ff0`
  17. > - 内存中的数据依然存在,它们并不需要被回收。当下一轮栈空间开辟后,新数据会将其覆盖
  18. #####bl和ret指令
  19. > #####bl
  20. > - `bl 地址`
  21. > - 将下一条指令的地址放入`lr``x30`)寄存器
  22. > - 转到标号处执行指令
  23. > #####ret
  24. > - 默认使用`lr``x30`)寄存器的值,通过底层指令提示`CPU`此处作为下条指令地址
  25. > `ARM64`平台的特色指令,它面向硬件做了优化处理
  26. > #####x30寄存器
  27. > - `x30`寄存器存放的是函数的返回地址,当`ret`指令执行时刻,会寻找`x30`寄存器保存的地址值
  28. > 在函数嵌套调用的时候,需要将`x30`入栈
  29. > 案例1
  30. > 演示`lr`寄存器,在函数嵌套调用时的作用
  31. > 延用上述`Demo`案例
  32. > 打开`asm.s`文件,写入以下代码:
  33. >

.text .global _A,_B

_A: sub sp, sp, #0x20 bl _B add sp, sp, #0x20 ret

_B: ret

  1. > - `A`函数:拉伸栈空间
  2. > - 跳转`B`函数
  3. > - 恢复栈平衡
  4. > - `B`函数:直接返回
  5. > 真机运行项目,来到`viewDidLoad`方法<br />
  6. ![](https://upload-images.jianshu.io/upload_images/9297953-1e0801b2193c9283.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  7. > - 即将执行的指令是`bl 0x104bee9bc`,也就是跳转到`A`函数
  8. > - `bl`指令的特性,一旦执行,先将下一条指令地址`0x104bee644`放入`lr`寄存器,然后进行跳转
  9. > 单步调试,向下执行`1`步。跳转到`A`函数<br />
  10. ![](https://upload-images.jianshu.io/upload_images/9297953-5f1dbc2b6a9568ea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  11. > - 打印`lr`寄存器的值,已经赋值为`0x104bee644`
  12. > 向下执行`1`步,开辟栈空间<br />
  13. ![](https://upload-images.jianshu.io/upload_images/9297953-37816337fe486693.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  14. > - 即将执行的又是`bl`指令
  15. > - 一旦执行,将下一条指令地址`0x104bee9c4`放入`lr`寄存器,然后进行跳转`B`函数
  16. > 向下执行`1`步,跳转到`B`函数<br />
  17. ![](https://upload-images.jianshu.io/upload_images/9297953-e52e7f0ed387d7b0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  18. > - 打印`lr`寄存器的值,已经赋值为`0x104bee9c4`
  19. > - 即将执行`B`函数中的`ret`指令
  20. > - `ret`指令的特性,使用`lr`寄存器的值作为下条指令地址。即:跳转至`0x104bee9c4`
  21. > 向下执行`1`步,顺利回到了`A`函数的`0x104bee9c4`指令地址<br />
  22. ![](https://upload-images.jianshu.io/upload_images/9297953-e1911c2419fbefec.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  23. > 向下执行`1`步,恢复栈平衡<br />
  24. ![](https://upload-images.jianshu.io/upload_images/9297953-a9557e1549963142.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  25. > - 即将执行的是`A`函数的`ret`指令
  26. > - 按照正常逻辑,应该跳转回`viewDidLoad`方法的`0x104bee644`指令地址
  27. > - 但是,打印`lr`寄存器的值,保存的依然是`A`函数的`0x104bee9c4`指令地址
  28. > 向下执行`1`步,产生死循环,又回到`A`函数的`0x104bee9c4`指令地址<br />
  29. ![](https://upload-images.jianshu.io/upload_images/9297953-fd0921faba6098f5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  30. > 此时取消断点,让程序继续运行。程序会在`add sp, sp, #0x20``ret`两句指令上,循环往复的执行,从而形成一个死循环
  31. > 上述问题的产生,牵扯到`lr`寄存器的现场保护
  32. > `lr`寄存器保存的相当于`回家的路`。函数嵌套调用过程中,在`bl`到另一个函数前,必须保护好当前的`lr`寄存器,否则函数返回势必出现问题
  33. > 如果当前函数为叶子函数,里面没有其他函数的调用,例如案例中的`B`函数,则无需保护
  34. > `bl`指令执行,`lr`寄存器会被替换,如何对它进行现场保护?
  35. > 能否将`lr`寄存器的值,存储在其他寄存器中?
  36. > - 肯定是`不行`的。因为寄存器的数量有限,在后续的函数嵌套调用中,有可能被任意函数覆盖掉,这种做法是不安全的
  37. > 正确的作法是什么?我们参考`llvm`编译器生成的汇编代码,看看它是如何对`lr`寄存器现场保护的
  38. > 案例2
  39. > 打开`ViewController.m`文件,写入以下代码:
  40. >

import “ViewController.h”

void D(void){ }

void C(void){ D(); }

@implementation ViewController

  • (void)viewDidLoad { [super viewDidLoad]; C(); }

@end

  1. > 真机运行项目,使用断点单步调试,来到`C`函数<br />
  2. ![](https://upload-images.jianshu.io/upload_images/9297953-8536ba23e69a7f4b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  3. > - `stp x29, x30, [sp, #-0x10]!`:该指令完成两个功能,先将`sp``16字节`,开辟栈空间,等同于`sub sp, sp, #0x10`指令。然后将`x29``x30`寄存器写入到内存
  4. > - `ldp x29, x30, [sp], #0x10`:该指令同样完成两个功能,先读取内存中的值,写入`x29``x30`寄存器。然后将`sp``16字节`,恢复栈平衡,等同于`add sp, sp, #0x10`指令
  5. > 单步调试,向下执行`1`步。开辟栈空间,同时`x29``x30`寄存器的值写入内存<br />
  6. ![](https://upload-images.jianshu.io/upload_images/9297953-683729d44d511716.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  7. > - 当前`lr`寄存器的值为`0x000000010081263c`
  8. > 向下执行`2`步,进入`B`函数,同时`lr`寄存器的值被覆盖<br />
  9. ![](https://upload-images.jianshu.io/upload_images/9297953-4d2d0ede32245893.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  10. > - 当前`lr`寄存器的值被覆盖为`0x00000001008125ec`
  11. > 向下执行`2`步,回到`A`函数。先读取内存中的值,写入`x29``x30`寄存器,然后恢复栈平衡<br />
  12. ![](https://upload-images.jianshu.io/upload_images/9297953-17528020673fe3d3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  13. > - 当前`lr`寄存器的值恢复为`0x000000010081263c`
  14. > 向下执行`1`步,成功`ret``viewDidLoad`方法的`0x10081263c`指令地址<br />
  15. ![](https://upload-images.jianshu.io/upload_images/9297953-7ee34d92d6bf368a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  16. > 由此可见,编译器在开辟栈空间后,先将`x29``x30`寄存器保存在当前函数栈中。然后在`ret`指令前,读取内存中的值,写入`x29``x30`寄存器。最后恢复栈平衡,`ret`回到正确的指令地址
  17. > 将数据入栈保护的行为,统称:`现场保护`
  18. > 案例3
  19. > 按上述`llvm`编译器的作法,重新修改`案例1`的汇编代码
  20. > 打开`asm.s`文件,写入以下代码:
  21. >

.text .global _A,_B

_A: sub sp, sp, #0x10 stp x29, x30, [sp] bl _B ldp x29, x30, [sp] add sp, sp, #0x10 ret

_B: ret

  1. > 真机运行项目,来到`viewDidLoad`方法<br />
  2. ![](https://upload-images.jianshu.io/upload_images/9297953-c0ba4167308248e9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  3. > - 下一条指令地址`0x10211a63c`
  4. > 单步调试,向下执行`3`步。跳转到`A`函数,开辟栈空间,将`x29``x30`寄存器写入到内存<br />
  5. ![](https://upload-images.jianshu.io/upload_images/9297953-0158c7443027590b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  6. > - 下一条指令地址`0x10211a9c0`
  7. > 向下执行`1`步,跳转到`B`函数<br />
  8. ![](https://upload-images.jianshu.io/upload_images/9297953-9e8d5dbcd593cd59.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  9. > - `lr`寄存器被覆盖为`0x10211a9c0`
  10. > 向下执行`2`步,回到`A`函数。读取内存中的值,写入`x29``x30`寄存器<br />
  11. ![](https://upload-images.jianshu.io/upload_images/9297953-da4f783bd3544bf9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  12. > 向下执行`2`步,恢复栈平衡,成功`ret``viewDidLoad`方法的`0x10211a63c`指令地址<br />
  13. ![](https://upload-images.jianshu.io/upload_images/9297953-a180f1e2e92e20c8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  14. > 上述案例中,将简写指令拆解,以便理解:
  15. > `stp x29, x30, [sp, #-0x10]!`
  16. > - `sub sp, sp, #0x10`
  17. > - `stp x29, x30, [sp]`
  18. > `ldp x29, x30, [sp], #0x10`
  19. > - `ldp x29, x30, [sp]`
  20. > - `add sp, sp, #0x10`
  21. > 案例4
  22. > 如果只有一个`x30`寄存器需要现场保护,开辟`16字节`栈空间过于浪费,只开辟`8字节`可以吗?
  23. > 打开`asm.s`文件,将栈空间的开辟和恢复都改为`8字节`<br />
  24. ![](https://upload-images.jianshu.io/upload_images/9297953-ae9a5772c3e13815.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  25. > - 当前`sp`寄存器的地址为`0x000000016d54cff0`
  26. > 单步调试,向下执行`1`步。开辟栈空间,将`x30`寄存器入栈保护<br />
  27. ![](https://upload-images.jianshu.io/upload_images/9297953-14b99b401083cbad.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  28. > - `sp`寄存器的地址为`0x000000016d54cfe8`
  29. > 向下执行`2`步,先跳转到`B`函数,又返回到`A`函数<br />
  30. ![](https://upload-images.jianshu.io/upload_images/9297953-7cd51144209088ce.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  31. > 截止到`此时此刻`,一切都是`正常`
  32. > 向下执行`1`步,从栈中取值写入`x30`寄存器,恢复栈平衡<br />
  33. ![](https://upload-images.jianshu.io/upload_images/9297953-767a970c98ec2009.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  34. > - 异常出现了。在`ARM64`中,对栈的操作是`16字节`对齐。所以栈空间的开辟,必须为`16字节`的倍数
  35. > 此处还有一个细节,上述代码使用简写指令,`x30`寄存器成功写入内存。但如果将指令拆解,在写入时就会报出异常<br />
  36. ![](https://upload-images.jianshu.io/upload_images/9297953-a0b4581aec67c716.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  37. #####函数的参数和返回值
  38. > - `ARM64`中,函数的参数是存放在`x0``x7``8`个寄存器里面。如果超过`8`个参数,就会入栈
  39. > - 函数的返回值是放在`x0`寄存器里面
  40. > 案例1
  41. > 查看编译器是如何传递参数并计算返回的
  42. > 打开`ViewController.m`文件,写入以下代码:
  43. >

import “ViewController.h”

int sum(int a, int b){ return a + b; }

@implementation ViewController

  • (void)viewDidLoad { [super viewDidLoad]; sum(10, 20); }

@end

  1. > 真机运行项目,来到`viewDidLoad`方法<br />
  2. ![](https://upload-images.jianshu.io/upload_images/9297953-fa5a0323d3275e07.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  3. > - 传递给`sun`函数的`10``20`两个参数,分别写入`w0``w1`两个寄存器
  4. > 单步调试,向下执行`4`步。跳转到`sun`函数,开辟`16字节`栈空间,将`w0``w1`寄存器入栈保护<br />
  5. ![](https://upload-images.jianshu.io/upload_images/9297953-cbad3bd9c0326940.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  6. > - 相当于函数内存储了两个局部变量
  7. > 向下执行`2`步,从内存中取值,写入`w8``w9`寄存器<br />
  8. ![](https://upload-images.jianshu.io/upload_images/9297953-c55490086e803213.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  9. > 向下执行`1`步,将`w8``w9`两个寄存器的值相加,赋值给`w0`寄存器<br />
  10. ![](https://upload-images.jianshu.io/upload_images/9297953-6c49c74eca0279fa.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  11. > - 由于`CPU`无法直接对内存中的数据进行计算操作,故此先将内存数据写入寄存器,然后进行相加,赋值给`x0`寄存器
  12. > - `x0`寄存器之前存储的是参数,此刻被结果覆盖为`0x000000000000001e`
  13. > 案例2
  14. > 使用汇编代码实现`sum`函数
  15. > 打开`asm.s`文件,写入以下代码:
  16. >

.text .global _sum

_sum: add x0, x0, x1 ret

  1. > 打开`ViewController.m`文件,写入以下代码:
  2. >

import “ViewController.h”

int sum(int a, int b);

@implementation ViewController

  • (void)viewDidLoad { [super viewDidLoad]; printf(“sum:%d”,sum(10, 20)); }

@end

  1. > 真机运行项目,来到`viewDidLoad`方法<br />
  2. ![](https://upload-images.jianshu.io/upload_images/9297953-fee49fd96afd9df4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  3. > - 传递给`sun`函数的`10``20`两个参数,分别写入`w0``w1`两个寄存器
  4. > 单步调试,向下执行`2`步。跳转到`sun`函数,直接将`x0``x1`寄存器进行相加,将结果赋值给`x0`<br />
  5. ![](https://upload-images.jianshu.io/upload_images/9297953-39a756ea70410c09.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  6. > 打印结果:`sum:30`<br />
  7. ![](https://upload-images.jianshu.io/upload_images/9297953-5d20a1841d69581d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  8. > 案例3
  9. > 当函数超过`8`个参数,编译器是如何传递的?
  10. > 打开`ViewController.m`文件,写入以下代码:
  11. >

import “ViewController.h”

@implementation ViewController

int test(int a, int b, int c, int d, int e, int f, int g, int h, int i){ return a + b + c + d + e + f + g + h + i; }

  • (void)viewDidLoad { // [super viewDidLoad]; printf(“sum:%d”,test(1, 2, 3, 4, 5, 6, 7, 8, 9)); }

@end

  1. > - 注释`[super viewDidLoad]`方法,减少生成的汇编代码,避免干扰
  2. > 真机运行项目,来到`viewDidLoad`方法<br />
  3. ![](https://upload-images.jianshu.io/upload_images/9297953-3918ee760c4f5fc0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  4. > - `bl`指令前的汇编代码进行分析
  5. > - `sub sp, sp, #0x30`:开辟`48字节`栈空间
  6. > - `stp x29, x30, [sp, #0x20]`:现场保护,将`x29``x30``sp + #0x20`位置入栈
  7. > - `add x29, sp, #0x20`:将`sp + #0x20`位置设为栈底
  8. > - `stur x0, [x29, #-0x8]`:将`x0``fp - #0x8`位置入栈。`stur`指令:把寄存器的值(`32位`)写进内存
  9. > - `str x1, [sp, #0x10]`:将`x1``sp + #0x10`位置入栈
  10. > - `mov w0, #0x1`~`mov w7, #0x8`:将前`8`个参数,分别写入到`w0`~`w7`
  11. > - `mov x8, sp`:将`sp`所在位置,写入到`x8`
  12. > - `mov w9, #0x9`:将第`9`个参数,写入到`w9`
  13. > - `str w9, [x8]`:将`w9`入栈到`x8`所存储的`sp`位置
  14. > `bl`指令执行前`View Memory`中的内存数据<br />
  15. ![](https://upload-images.jianshu.io/upload_images/9297953-bad7e93c292b08a4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  16. > `bl`指令执行前的栈图<br />
  17. ![](https://upload-images.jianshu.io/upload_images/9297953-0bf0410687f28f59.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  18. > 继续执行代码,跳转到`test`函数<br />
  19. ![](https://upload-images.jianshu.io/upload_images/9297953-181d3ee28e525807.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  20. > - `add sp, sp, #0x30`指令前的汇编代码进行分析
  21. > - `sub sp, sp, #0x30`:开辟`48字节`栈空间
  22. > - `ldr w8, [sp, #0x30]`:将`sp + #0x30`位置的值写入`w8``sp + #0x30``viewDidLoad`方法调用栈的位置,这里写入的是`w9`的值,也就是第`9`个参数的值
  23. > - `str w0, [sp, #0x2c]`~`str w8, [sp, #0xc]`:现场保护,将`w0``w8``9`个寄存器分别入栈
  24. > - `ldr w8, [sp, #0x2c]`:将`sp + #0x2c`位置的值写入`w8`,也就是第`1`个参数的值
  25. > - `ldr w9, [sp, #0x28]`:将`sp + #0x28`位置的值写入`w9`,也就是第`2`个参数的值
  26. > - `add w8, w8, w9``w8``w9`相加,将结果赋值给`w8`
  27. > - 依次从内存中将后续`6`个参数值写入`w9`,然后和`w8`相加,将结果赋值给`w8`
  28. > - `add w0, w8, w9`:最后一个参数的相加,`w8``w9`相加,将结果赋值给`w0`
  29. > `add sp, sp, #0x30`指令执行前`View Memory`中的内存数据<br />
  30. ![](https://upload-images.jianshu.io/upload_images/9297953-5846de26ee6462fb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  31. > `add sp, sp, #0x30`指令执行前的栈图<br />
  32. ![](https://upload-images.jianshu.io/upload_images/9297953-77428c014d09cfd9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  33. > 打印结果:`sum:45`<br />
  34. ![](https://upload-images.jianshu.io/upload_images/9297953-ac001624334fbab5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  35. > 上述案例中,函数参数超过`8`个,超出的参数不再使用寄存器传递,而是直接入栈。当下一个函数使用时,从上一个函数调用栈中读取
  36. > 从栈中读取数据效率并不高,所以在开发中,应避免函数超过`8`个参数
  37. > - `C`函数:最好不要超过`8`个参数
  38. > - `OC`方法:最好不要超过`6`个参数。因为`objc_msgSend(id self, SEL _cmd, ...)`自身还有两个隐含参数
  39. > 案例4
  40. > 上述案例,使用`Release`模式运行,编译器会如何优化?
  41. > 选择`Release`模式运行<br />
  42. ![](https://upload-images.jianshu.io/upload_images/9297953-73f7def878c801d2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  43. > 真机运行项目,来到`viewDidLoad`方法<br />
  44. ![](https://upload-images.jianshu.io/upload_images/9297953-dd8680a77343852b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  45. > - `viewDidLoad`方法调用栈中,经过编译器优化只剩下少量代码,甚至连`test`函数的调用也被优化掉了。编译器直接使用`mov w8, #0x2d`指令,将计算结果`45`写入`x8`寄存器
  46. > 案例5
  47. > 使用汇编代码实现带参数的函数嵌套调用
  48. > 打开`asm.s`文件,写入以下代码:
  49. >

.text .global _funcA,_sum

_funcA: stp x29, x30, [sp, #-0x10]! bl _sum ldp x29, x30, [sp], #0x10 ret

_sum: add x0, x0, x1 ret

  1. > - `_funcA`函数嵌套调用`_sum`函数,需要现场保护
  2. > - `_sum`函数直接使用`add x0, x0, x1`指令进行相加,结果写入`x0`
  3. > 更简单的实现方式,将`bl`替换为`b`指令
  4. >

.text .global _funcA,_sum

_funcA: b _sum

_sum: add x0, x0, x1 ret

  1. > - `b`指令:用于不返回的跳转,仅跳转到标号处,不改变`lr`寄存器的值
  2. > - `b`指令常用于破解的地方,可以绕过代码执行
  3. > 案例6
  4. > 返回值一般是`8字节`指针。如果返回一个结构体,大小超过`8字节`,编译器会如何处理?
  5. > 打开`ViewController.m`文件,写入以下代码:
  6. >

import “ViewController.h”

@implementation ViewController

struct str { int a; int b; int c; int d; int f; int g; };

struct str getStr(int a,int b,int c,int d,int f,int g){ struct str str1; str1.a = a; str1.b = b; str1.c = d; str1.d = d; str1.f = f; str1.g = g; return str1; }

  • (void)viewDidLoad { // [super viewDidLoad]; struct str str2 = getStr(1, 2, 3, 4, 5, 6); }

@end

  1. > 真机运行项目,来到`viewDidLoad`方法<br />
  2. ![](https://upload-images.jianshu.io/upload_images/9297953-00771669f19a4f51.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  3. > - `sub sp, sp, #0x40`:开辟`64字节`栈空间
  4. > - `stp x29, x30, [sp, #0x30]`:现场保护,将`x29``x30``sp + #0x30`位置入栈
  5. > - `add x29, sp, #0x30`:将`sp + #0x30`位置设为栈底
  6. > - `stur x0, [x29, #-0x8]`:将`x0``fp - #0x8`位置入栈
  7. > - `stur x1, [x29, #-0x10]`:将`x1``fp - #0x10`位置入栈
  8. > - `add x8, sp, #0x8`:将`x8`指向`sp + #0x8`位置
  9. > `bl`指令执行前的栈图<br />
  10. ![](https://upload-images.jianshu.io/upload_images/9297953-1c17b47a29e01be7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  11. > 继续执行代码,跳转到`getStr`函数<br />
  12. ![](https://upload-images.jianshu.io/upload_images/9297953-3d14d5aa1091453b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  13. > - `sub sp, sp, #0x20`:开辟`32字节`栈空间
  14. > - `str w0, [sp, #0x1c]`~`str w5, [sp, #0x8]`:现场保护,将`w0``w5``6`个寄存器分别入栈
  15. > - `ldr w9, [sp, #0x1c]`:将`sp + #0x1c`位置的值写入`w9`,也就是第`1`个参数的值
  16. > - `str w9, [x8]`:将`w9`入栈到`x8`所存储的位置,`x8`存储的是`viewDidLoad`方法调用栈的位置
  17. > - 后续逻辑同上,使用`w9`依次获取参数值,写入到`viewDidLoad`方法调用栈中
  18. > - 最后`add sp, sp, #0x20`恢复栈平衡并`ret`
  19. > `add sp, sp, #0x20`指令执行前的栈图<br />
  20. ![](https://upload-images.jianshu.io/upload_images/9297953-3f6929efdaff250b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  21. > 上述案例,当返回值超过`8字节`,则不再使用`x0`寄存器作为返回值,而是将返回的数据写入到上一个函数调用栈中
  22. > 所以在开发中,应避免返回的数据超过`8字节`。如果函数需要返回结构体,可以将返回类型定义为结构体指针,将大小控制在`8字节`,这样可以使用`x0`寄存器传递,效率会更高
  23. >

struct str getStr(int a,int b,int c,int d,int f,int g){ struct str str1 = malloc(24); str1 -> a = a; str1 -> b = b; str1 -> c = d; str1 -> d = d; str1 -> f = f; str1 -> g = g; return str1; }

  1. #####函数的局部变量
  2. > 函数的局部变量放在栈里面
  3. > 案例1
  4. > 编译器如何存储函数的局部变量?
  5. > 打开`ViewController.m`文件,写入以下代码:
  6. >

import “ViewController.h”

@implementation ViewController

int funcB(int a, int b){ int c = 6; return a + b + c; }

  • (void)viewDidLoad { // [super viewDidLoad]; printf(“sum:%d”,funcB(10, 20)); }

@end

  1. > 真机运行项目,来到`funcB`方法<br />
  2. ![](https://upload-images.jianshu.io/upload_images/9297953-b06c0e97cca1a9ce.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240#alt=)
  3. > - `mov w8, #0x6``#0x6`的值为`6`,相当于局部变量`c`,将其写入`x8`寄存器
  4. > - `str w8, [sp, #0x4]`:将`w8`寄存器入栈
  5. > - 相加时,先从栈中取值,写入`w9`寄存器,然后运算
  6. > - 恢复栈平衡后,函数调用栈中的局部变量就不存在了
  7. > 案例2
  8. > 编译器如何处理函数嵌套调用时的局部变量、参数和返回值?
  9. > 打开`ViewController.m`文件,写入以下代码:
  10. >

import “ViewController.h”

@implementation ViewController

int funcB(int a, int b){ int c = 6; int d = sumD(a, b, c); int e = sumD(a, b, c); return d + e; }

int sumD(int a, int b, int c){ int d = a + b + c; return d; }

  • (void)viewDidLoad { // [super viewDidLoad]; printf(“sum:%d”,funcB(10, 20)); }

@end ```

真机运行项目,来到funcB方法
iOS逆向实战--002:函数本质 - 图4

  • sub sp, sp, #0x30:开辟48字节栈空间
  • stp x29, x30, [sp, #0x20]:现场保护,将x29x30入栈
  • add x29, sp, #0x20:设置栈底
  • stur w0, [x29, #-0x4]:将w0入栈,即:第1个参数
  • stur w1, [x29, #-0x8]:将w1入栈,即:第2个参数
  • mov w8, #0x6:将#0x6写入w8,即:局部变量c
  • stur w8, [x29, #-0xc]:将w8入栈,即:局部变量c
  • ldur w0, [x29, #-0x4]~ldur w2, [x29, #-0xc]:从栈中读取两个参数和局部变量c的值,分别写入w0~w2
  • bl 0x102c925c4:调用sumD函数
  • str w0, [sp, #0x10]sumD函数使用w0作为返回值,将其入栈,即:局部变量d
  • ldur w0, [x29, #-0x4]~ldur w2, [x29, #-0xc]:再次从栈中读取两个参数和局部变量c的值,分别写入w0~w2
  • bl 0x102c925c4:再次调用sumD函数
  • str w0, [sp, #0xc]sumD函数依然使用w0作为返回值,将其入栈,即:局部变量e
  • ldr w8, [sp, #0x10]:从栈中读取局部变量d的值,写入w8
  • ldr w9, [sp, #0xc]:从栈中读取局部变量e的值,写入w9
  • add w0, w8, w9:将w8w9相加,结果赋值给w0
  • ldp x29, x30, [sp, #0x20]:恢复x29x30的值
  • add sp, sp, #0x30:恢复栈平衡
  • ret:返回

add sp, sp, #0x30指令执行前的栈图
iOS逆向实战--002:函数本质 - 图5

总结

  • 栈:存储空间,具有后进先出的访问方式
  • sp寄存器:在任意时刻会保存我们栈顶的地址
  • fp寄存器:也称x29寄存器,属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址
  • ARM64中,栈是递减栈,由高地址向低地址延伸。对栈的操作是16字节对齐的

栈的读写指令

  • 读:ldrload register)指令
  • 写:strstore register)指令
  • ldrstr的变种指令,ldpstp,可以操作2个寄存器

简写指令:stp x0, x1, [sp, #-0x10]!

  • 数据入栈,一般从栈底开始存入。所以使用简写的条件是,不需要额外的栈空间,存入的数据刚好放满
  • 简写的执行顺序,一定是先开辟空间,再入栈
  • 等同于:
    sub sp, sp, #0x10:拉伸16字节栈空间
    stp x0, x1, [sp]:在sp所在位置存放x0x1

bl指令

  • 跳转指令:bl 地址。表示程序执行到标号处。将下一条指令的地址保存到lr寄存器
  • b:代表跳转
  • l:代表lrx30)寄存器

ret指令

  • 类似函数中的return
  • CPU执行lr寄存器所指向的指令
  • 当函数嵌套调用时,需要现场保护。lr寄存器入栈

函数嵌套调用

  • 会将x29x30寄存器入栈保护

函数的参数

  • ARM64中,参数是放在x0x78个寄存器中
  • 如果是浮点数,使用浮点寄存器
  • 如果超过8个参数就会用栈传递

函数的返回值

  • 默认情况下,函数的返回值放在x0寄存器
  • 如果返回值大于8字节,就会利用内存,写入上一个调用栈的内部,用x8寄存器作为参照

函数的局部变量

  • 使用栈保存局部变量