iOS逆向实战--002:函数本质

栈:是一种具有特殊的访问方式的存储空间,具有后进先出的特性(Last In Out FirtLIFO

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

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

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

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

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

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

死循环和死递归的区别

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

堆栈溢出(Stack Overflow

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

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

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

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

strstore register)指令

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

ldrload register)指令

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

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

案例:

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

搭建Demo项目

创建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
  • sub sp, sp, #0x20:开辟栈空间,sp拉伸32字节栈空间
  • mov x0, #0xa0:将#0xa0写入x0寄存器
  • mov x1, #0xb0:将#0xb0写入x1寄存器
  • stp x0, x1, [sp,#0x10]:将x0x1寄存器的值,写入到sp向上偏移16字节的内存地址中
  • ldp x1, x0, [sp,#0x10]:读取sp向上偏移16字节后内存中的值,写入到x1x0寄存器,相当于交换
  • add sp, sp, #0x20:将拉伸后的sp32字节,恢复栈平衡
  • ret:返回

打开ViewController.m文件,写入以下代码:

#import "ViewController.h"

int A(void);

@implementation ViewController

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

@end

真机运行项目,使用断点单步调试,来到A函数

单步调试,向下执行1步。开辟栈空间,拉伸32字节

  • 拉伸后,sp指向地址0x000000016d250fd0

向下执行2步,将#0xa0写入x0寄存器,将#0xb0写入x1寄存器

向下执行1步,将x0x1寄存器的值,写入到sp向上偏移16字节的内存地址中

单步调试,向下执行1步。读取sp向上偏移16字节后内存中的值,写入到x1x0寄存器,相当于交换

  • x0x1寄存器的值交换,但内存的数据并没有发生任何变化
  • 内存充当临时变量的作用

向下执行1步,将拉伸后的sp32字节,恢复栈平衡

  • 恢复栈平衡,sp指向地址0x000000016d250ff0
  • 内存中的数据依然存在,它们并不需要被回收。当下一轮栈空间开辟后,新数据会将其覆盖
bl和ret指令
bl
  • bl 地址
  • 将下一条指令的地址放入lrx30)寄存器
  • 转到标号处执行指令
ret
  • 默认使用lrx30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址

ARM64平台的特色指令,它面向硬件做了优化处理

x30寄存器
  • x30寄存器存放的是函数的返回地址,当ret指令执行时刻,会寻找x30寄存器保存的地址值

在函数嵌套调用的时候,需要将x30入栈

案例1:

演示lr寄存器,在函数嵌套调用时的作用

延用上述Demo案例

打开asm.s文件,写入以下代码:

.text
.global _A,_B

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

_B:
   ret
  • A函数:拉伸栈空间
  • 跳转B函数
  • 恢复栈平衡
  • B函数:直接返回

真机运行项目,来到viewDidLoad方法

  • 即将执行的指令是bl 0x104bee9bc,也就是跳转到A函数
  • bl指令的特性,一旦执行,先将下一条指令地址0x104bee644放入lr寄存器,然后进行跳转

单步调试,向下执行1步。跳转到A函数

  • 打印lr寄存器的值,已经赋值为0x104bee644

向下执行1步,开辟栈空间

  • 即将执行的又是bl指令
  • 一旦执行,将下一条指令地址0x104bee9c4放入lr寄存器,然后进行跳转B函数

向下执行1步,跳转到B函数

  • 打印lr寄存器的值,已经赋值为0x104bee9c4
  • 即将执行B函数中的ret指令
  • ret指令的特性,使用lr寄存器的值作为下条指令地址。即:跳转至0x104bee9c4

向下执行1步,顺利回到了A函数的0x104bee9c4指令地址

向下执行1步,恢复栈平衡

  • 即将执行的是A函数的ret指令
  • 按照正常逻辑,应该跳转回viewDidLoad方法的0x104bee644指令地址
  • 但是,打印lr寄存器的值,保存的依然是A函数的0x104bee9c4指令地址

向下执行1步,产生死循环,又回到A函数的0x104bee9c4指令地址

此时取消断点,让程序继续运行。程序会在add sp, sp, #0x20ret两句指令上,循环往复的执行,从而形成一个死循环

上述问题的产生,牵扯到lr寄存器的现场保护

lr寄存器保存的相当于回家的路。函数嵌套调用过程中,在bl到另一个函数前,必须保护好当前的lr寄存器,否则函数返回势必出现问题

如果当前函数为叶子函数,里面没有其他函数的调用,例如案例中的B函数,则无需保护

bl指令执行,lr寄存器会被替换,如何对它进行现场保护?

能否将lr寄存器的值,存储在其他寄存器中?

  • 肯定是不行的。因为寄存器的数量有限,在后续的函数嵌套调用中,有可能被任意函数覆盖掉,这种做法是不安全的

正确的作法是什么?我们参考llvm编译器生成的汇编代码,看看它是如何对lr寄存器现场保护的

案例2:

打开ViewController.m文件,写入以下代码:

#import "ViewController.h"

void D(void){
}

void C(void){
   D();
}

@implementation ViewController

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

@end

真机运行项目,使用断点单步调试,来到C函数

  • stp x29, x30, [sp, #-0x10]!:该指令完成两个功能,先将sp16字节,开辟栈空间,等同于sub sp, sp, #0x10指令。然后将x29x30寄存器写入到内存
  • ldp x29, x30, [sp], #0x10:该指令同样完成两个功能,先读取内存中的值,写入x29x30寄存器。然后将sp16字节,恢复栈平衡,等同于add sp, sp, #0x10指令

单步调试,向下执行1步。开辟栈空间,同时x29x30寄存器的值写入内存

  • 当前lr寄存器的值为0x000000010081263c

向下执行2步,进入B函数,同时lr寄存器的值被覆盖

  • 当前lr寄存器的值被覆盖为0x00000001008125ec

向下执行2步,回到A函数。先读取内存中的值,写入x29x30寄存器,然后恢复栈平衡

  • 当前lr寄存器的值恢复为0x000000010081263c

向下执行1步,成功retviewDidLoad方法的0x10081263c指令地址

由此可见,编译器在开辟栈空间后,先将x29x30寄存器保存在当前函数栈中。然后在ret指令前,读取内存中的值,写入x29x30寄存器。最后恢复栈平衡,ret回到正确的指令地址

将数据入栈保护的行为,统称:现场保护

案例3:

按上述llvm编译器的作法,重新修改案例1的汇编代码

打开asm.s文件,写入以下代码:

.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

真机运行项目,来到viewDidLoad方法

  • 下一条指令地址0x10211a63c

单步调试,向下执行3步。跳转到A函数,开辟栈空间,将x29x30寄存器写入到内存

  • 下一条指令地址0x10211a9c0

向下执行1步,跳转到B函数

  • lr寄存器被覆盖为0x10211a9c0

向下执行2步,回到A函数。读取内存中的值,写入x29x30寄存器

向下执行2步,恢复栈平衡,成功retviewDidLoad方法的0x10211a63c指令地址

上述案例中,将简写指令拆解,以便理解:

stp x29, x30, [sp, #-0x10]!

  • sub sp, sp, #0x10
  • stp x29, x30, [sp]

ldp x29, x30, [sp], #0x10

  • ldp x29, x30, [sp]
  • add sp, sp, #0x10

案例4:

如果只有一个x30寄存器需要现场保护,开辟16字节栈空间过于浪费,只开辟8字节可以吗?

打开asm.s文件,将栈空间的开辟和恢复都改为8字节

  • 当前sp寄存器的地址为0x000000016d54cff0

单步调试,向下执行1步。开辟栈空间,将x30寄存器入栈保护

  • sp寄存器的地址为0x000000016d54cfe8

向下执行2步,先跳转到B函数,又返回到A函数

截止到此时此刻,一切都是正常

向下执行1步,从栈中取值写入x30寄存器,恢复栈平衡

  • 异常出现了。在ARM64中,对栈的操作是16字节对齐。所以栈空间的开辟,必须为16字节的倍数

此处还有一个细节,上述代码使用简写指令,x30寄存器成功写入内存。但如果将指令拆解,在写入时就会报出异常

函数的参数和返回值
  • ARM64中,函数的参数是存放在x0x78个寄存器里面。如果超过8个参数,就会入栈
  • 函数的返回值是放在x0寄存器里面

案例1:

查看编译器是如何传递参数并计算返回的

打开ViewController.m文件,写入以下代码:

#import "ViewController.h"

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

@implementation ViewController

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

@end

真机运行项目,来到viewDidLoad方法

  • 传递给sun函数的1020两个参数,分别写入w0w1两个寄存器

单步调试,向下执行4步。跳转到sun函数,开辟16字节栈空间,将w0w1寄存器入栈保护

  • 相当于函数内存储了两个局部变量

向下执行2步,从内存中取值,写入w8w9寄存器

向下执行1步,将w8w9两个寄存器的值相加,赋值给w0寄存器

  • 由于CPU无法直接对内存中的数据进行计算操作,故此先将内存数据写入寄存器,然后进行相加,赋值给x0寄存器
  • x0寄存器之前存储的是参数,此刻被结果覆盖为0x000000000000001e

案例2:

使用汇编代码实现sum函数

打开asm.s文件,写入以下代码:

.text
.global _sum

_sum:
   add x0, x0, x1
   ret

打开ViewController.m文件,写入以下代码:

#import "ViewController.h"

int sum(int a, int b);

@implementation ViewController

- (void)viewDidLoad {
   [super viewDidLoad];
   printf("sum:%d",sum(10, 20));
}

@end

真机运行项目,来到viewDidLoad方法

  • 传递给sun函数的1020两个参数,分别写入w0w1两个寄存器

单步调试,向下执行2步。跳转到sun函数,直接将x0x1寄存器进行相加,将结果赋值给x0

打印结果:sum:30

案例3:

当函数超过8个参数,编译器是如何传递的?

打开ViewController.m文件,写入以下代码:

#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
  • 注释[super viewDidLoad]方法,减少生成的汇编代码,避免干扰

真机运行项目,来到viewDidLoad方法

  • bl指令前的汇编代码进行分析
  • sub sp, sp, #0x30:开辟48字节栈空间
  • stp x29, x30, [sp, #0x20]:现场保护,将x29x30sp + #0x20位置入栈
  • add x29, sp, #0x20:将sp + #0x20位置设为栈底
  • stur x0, [x29, #-0x8]:将x0fp - #0x8位置入栈。stur指令:把寄存器的值(32位)写进内存
  • str x1, [sp, #0x10]:将x1sp + #0x10位置入栈
  • mov w0, #0x1~mov w7, #0x8:将前8个参数,分别写入到w0~w7
  • mov x8, sp:将sp所在位置,写入到x8
  • mov w9, #0x9:将第9个参数,写入到w9
  • str w9, [x8]:将w9入栈到x8所存储的sp位置

bl指令执行前View Memory中的内存数据

bl指令执行前的栈图

继续执行代码,跳转到test函数

  • add sp, sp, #0x30指令前的汇编代码进行分析
  • sub sp, sp, #0x30:开辟48字节栈空间
  • ldr w8, [sp, #0x30]:将sp + #0x30位置的值写入w8sp + #0x30viewDidLoad方法调用栈的位置,这里写入的是w9的值,也就是第9个参数的值
  • str w0, [sp, #0x2c]~str w8, [sp, #0xc]:现场保护,将w0w89个寄存器分别入栈
  • ldr w8, [sp, #0x2c]:将sp + #0x2c位置的值写入w8,也就是第1个参数的值
  • ldr w9, [sp, #0x28]:将sp + #0x28位置的值写入w9,也就是第2个参数的值
  • add w8, w8, w9w8w9相加,将结果赋值给w8
  • 依次从内存中将后续6个参数值写入w9,然后和w8相加,将结果赋值给w8
  • add w0, w8, w9:最后一个参数的相加,w8w9相加,将结果赋值给w0

add sp, sp, #0x30指令执行前View Memory中的内存数据

add sp, sp, #0x30指令执行前的栈图

打印结果:sum:45

上述案例中,函数参数超过8个,超出的参数不再使用寄存器传递,而是直接入栈。当下一个函数使用时,从上一个函数调用栈中读取

从栈中读取数据效率并不高,所以在开发中,应避免函数超过8个参数

  • C函数:最好不要超过8个参数
  • OC方法:最好不要超过6个参数。因为objc_msgSend(id self, SEL _cmd, ...)自身还有两个隐含参数

案例4:

上述案例,使用Release模式运行,编译器会如何优化?

选择Release模式运行

真机运行项目,来到viewDidLoad方法

  • viewDidLoad方法调用栈中,经过编译器优化只剩下少量代码,甚至连test函数的调用也被优化掉了。编译器直接使用mov w8, #0x2d指令,将计算结果45写入x8寄存器

案例5:

使用汇编代码实现带参数的函数嵌套调用

打开asm.s文件,写入以下代码:

.text
.global _funcA,_sum

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

_sum:
   add x0, x0, x1
   ret
  • _funcA函数嵌套调用_sum函数,需要现场保护
  • _sum函数直接使用add x0, x0, x1指令进行相加,结果写入x0

更简单的实现方式,将bl替换为b指令

.text
.global _funcA,_sum

_funcA:
   b _sum

_sum:
   add x0, x0, x1
   ret
  • b指令:用于不返回的跳转,仅跳转到标号处,不改变lr寄存器的值
  • b指令常用于破解的地方,可以绕过代码执行

案例6:

返回值一般是8字节指针。如果返回一个结构体,大小超过8字节,编译器会如何处理?

打开ViewController.m文件,写入以下代码:

#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

真机运行项目,来到viewDidLoad方法

  • sub sp, sp, #0x40:开辟64字节栈空间
  • stp x29, x30, [sp, #0x30]:现场保护,将x29x30sp + #0x30位置入栈
  • add x29, sp, #0x30:将sp + #0x30位置设为栈底
  • stur x0, [x29, #-0x8]:将x0fp - #0x8位置入栈
  • stur x1, [x29, #-0x10]:将x1fp - #0x10位置入栈
  • add x8, sp, #0x8:将x8指向sp + #0x8位置

bl指令执行前的栈图

继续执行代码,跳转到getStr函数

  • sub sp, sp, #0x20:开辟32字节栈空间
  • str w0, [sp, #0x1c]~str w5, [sp, #0x8]:现场保护,将w0w56个寄存器分别入栈
  • ldr w9, [sp, #0x1c]:将sp + #0x1c位置的值写入w9,也就是第1个参数的值
  • str w9, [x8]:将w9入栈到x8所存储的位置,x8存储的是viewDidLoad方法调用栈的位置
  • 后续逻辑同上,使用w9依次获取参数值,写入到viewDidLoad方法调用栈中
  • 最后add sp, sp, #0x20恢复栈平衡并ret

add sp, sp, #0x20指令执行前的栈图

上述案例,当返回值超过8字节,则不再使用x0寄存器作为返回值,而是将返回的数据写入到上一个函数调用栈中

所以在开发中,应避免返回的数据超过8字节。如果函数需要返回结构体,可以将返回类型定义为结构体指针,将大小控制在8字节,这样可以使用x0寄存器传递,效率会更高

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:

编译器如何存储函数的局部变量?

打开ViewController.m文件,写入以下代码:

#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

真机运行项目,来到funcB方法

  • mov w8, #0x6#0x6的值为6,相当于局部变量c,将其写入x8寄存器
  • str w8, [sp, #0x4]:将w8寄存器入栈
  • 相加时,先从栈中取值,写入w9寄存器,然后运算
  • 恢复栈平衡后,函数调用栈中的局部变量就不存在了

案例2:

编译器如何处理函数嵌套调用时的局部变量、参数和返回值?

打开ViewController.m文件,写入以下代码:

#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方法

  • 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指令执行前的栈图

总结

  • 栈:存储空间,具有后进先出的访问方式
  • 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寄存器作为参照

函数的局部变量

  • 使用栈保存局部变量

你可能感兴趣的:(iOS逆向实战--002:函数本质)