iOS逆向学习-002函数本质

2,158 阅读5分钟

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

15193998892055.jpg

SP和FP寄存器

  • sp寄存器在任意时刻会保存我们栈顶的地址.
  • fp寄存器也称为x29寄存器属于通用寄存器,但是在某些时刻(比如说嵌套调用)我们利用它保存栈底的地址!

注意:ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stp ARM64里面 对栈的操作是16字节对齐的!!

函数调用栈

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

sub    sp, sp, #0x40             ; 拉伸0x4064字节)空间
stp    x29, x30, [sp, #0x30]	 ;x29\x30 寄存器入栈保护
add    x29, sp, #0x30            ; x29指向栈帧的底部
... 
ldp    x29, x30, [sp, #0x30]	 ;恢复x29/x30 寄存器的值
add    sp, sp, #0x40             ; 栈平衡
ret

关于内存读写指令

注意:读/写 数据是都是往高地址读/写

str(store register)指令

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

ldr(load register)指令

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

此ldr 和 str 的变种ldp 和 stp 还可以操作2个寄存器.

###堆栈操作练习 使用32个字节空间作为这段程序的栈空间,然后利用栈将x0和x1的值进行交换.

sub    sp, sp, #0x20	;拉伸栈空间32个字节
stp    x0, x1, [sp, #0x10] ;sp往上加16个字节,存放x0 和 x1
ldp    x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来,放入x1 和 x0
add    sp, sp, #0x20    ;栈平衡

[]是寻址,中间逗号可以加上后面的偏移量,可以拿到该地址的值或者放值到该地址,有点像swift中,UnsafePointerpointee

我们可以通过View Memory查看内存的数据状况(个人更喜欢x/8g) image.png

View Memory刷新数据需要Page来回切 image.png

bl和ret指令

bl标号

  • 将下一条指令的地址放入lr(x30)寄存器
  • 转到标号处执行指令

ret

  • 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!

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

x30寄存器

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

注意:在函数嵌套调用的时候.需要讲x30入栈!

.text
.global _A,_B

_A:
    str x30, [sp, #-0x10]! ;x30入栈,并且sp指针开辟栈空间
    mov x0, #0xaaaa
    bl _B
    mov x0, #0xaaaa
    ldr x30, [sp], #0x10 ;x30恢复,栈恢复,平衡
    ret

_B:
    mov x0, #0xbbbb
    ret

这里有两条简写语句:

str x30, [sp, #-0x10]!相当于

sub sp, sp, #0x10
str x30, [sp]

ldr x30, [sp], #0x10相当于

ldr x30, [sp]
add sp, sp, #0x10

这里有点a++++a的意思

函数的参数和返回值

ARM64下,函数的参数是存放在X0到X7(W0到W7)这8个寄存器里面的.如果超过8个参数,就会入栈。OC的方法最好不要超过6个,因为默认会有selfcmd这两个参数

函数的返回值是放在X0寄存器里面的.

实现一个sum函数

// 调用前声明
int sum(int a, int b)

// 汇编代码
.text
.global  _sum

_sum:
    add x0, x0, x1
    ret

函数的局部变量

函数的局部变量放在栈里面!

函数参数超过8个示例演示

我们可以看下系统是怎么做的,先写一个超过8个参数的函数:

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;
}
test(1, 2, 3, 4, 5, 6, 7, 8, 9);

然后断点看汇编代码:

image.png

我们可以很明显看到,1-8这8个立即数依次放在了寄存器w0-w7中。而9放在了w10中,w10又放在了x8所在的地址,而x8前面取值了sp,所以9放在了sp所在的地址,也就是当前函数的栈顶的位置。

我们在看下,跳进test函数里又是如何写汇编的:

image.png

test函数最开始进行压栈,sub了0x30,sp值向低地址走了0x30,但是第二步w8取值时,从sp向高地址偏移0x30取的值,所以也就是上个栈的栈顶,也就是刚才存放在栈里的9,此时此刻,w8中存放了9.

接下来依次把w0-w8存放到栈里,最后分别从栈中取值相加,返回最终的结果。相加的过程有点繁琐,但是这个是Debug模式下的流程,编译器并没有优化。

总结下,如果函数的参数多于8个,那么前8个参数还是老样子,通过寄存器x0-x7传递给下个栈,后面多余的参数则放在栈里,等着下个栈区间来取。

函数返回值大于8个字节

我们定义一个大于8个字节的结构体,然后定义一个函数返回这个结构体:

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 = c;
    str1.d = d;
    str1.f = f;
    str1.g = g;
    return str1;
}

getStr(1, 2, 3, 4, 5, 6);

我们看下getStr的汇编实现:

image.png

我们可以清楚看到,参数的赋值最后都给了以x8存放的地址为基地址,一次偏移存放,那么x8所存放的地址,就是返回值结构体的地址了。在这个调用栈中,并不能看到x8里存放的是什么,所以看上一个调用栈:

image.png

在我们函数调用之前,x8最终的值是sp偏移0x8的地址,所以函数调用的返回值也是存放在上个栈里