美文网首页iOS逆向
iOS逆向-从汇编代码理解函数调用栈

iOS逆向-从汇编代码理解函数调用栈

作者: 傻傻木 | 来源:发表于2018-06-08 22:50 被阅读360次

    程序的栈空间有什么特点呢?首先会想到的就是,栈空间是往低地址增长的,当调用一个函数时,先开辟栈空间,用来存放当前函数的参数和局部变量;执行函数之前还需要先保护现场,当函数执行完之后会恢复现场。那么函数调用的过程中内存和CPU的寄存器到底发生了什么变化呢?这是本篇要探讨的问题。

    先看一段C代码,定义了三个函数,调用关系为:funcA调用funcB, funcB调用funcC

    int funcA(int a, int b) {
        int ret = funcB(a, b);
        return ret;
    }
    
    int funcB(int a, int b) {
        return funcC(a, b);
    }
    
    int funcC(int a, int b) {
        int c = a + b;
        return c;
    }
    

    从函数是否还调用了其他函数的角度看,函数可分为叶子函数非叶子函数
    叶子函数:函数内部没有调用其他函数了,例如上面的funcC
    非叶子函数:函数内部还调用了其他的函数,例如上面的funcA、funcB

    为什么要这么划分呢?因为叶子函数非叶子函数生成的汇编代码有所不同,这也是本篇要分析的一个点。

    获取汇编代码


    得到对应的汇编代码有两种方式

    方式一:
    用clang编译器把C代码编译为汇编代码,编译函数所在的.c文件即可

    xcrun -sdk iphoneos clang -S -arch arm64 Ctest.c
    

    指定架构为arm64,执行命令后会得到Ctest.s文件,就是对应的汇编代码了。

    方式二:
    新建一个iOS项目,并且运行在真机设备上,在真机上才是arm64架构的汇编哦。在三个函数分别打断点,把Xcodedebug模式设置为显示汇编模式,在Debug -> Debug Workflow -> Aways Show Disassembly设置。当函数断住时,会显示当前函数的汇编代码。

    这里采用方式二,得到的汇编代码如下:

    funcA

     Assembly`funcA:
     0x100086ab8 <+0>:  sub    sp, sp, #0x20             ; =0x20
     0x100086abc <+4>:  stp    x29, x30, [sp, #0x10]
     0x100086ac0 <+8>:  add    x29, sp, #0x10            ; =0x10
     0x100086ac4 <+12>: stur   w0, [x29, #-0x4]
     0x100086ac8 <+16>: str    w1, [sp, #0x8]
     0x100086acc <+20>: ldur   w0, [x29, #-0x4]
     0x100086ad0 <+24>: ldr    w1, [sp, #0x8]
     0x100086ad4 <+28>: bl     0x100086aec               ; funcB at Ctest.c:57
     0x100086ad8 <+32>: str    w0, [sp, #0x4]
     0x100086adc <+36>: ldr    w0, [sp, #0x4]
     0x100086ae0 <+40>: ldp    x29, x30, [sp, #0x10]
     0x100086ae4 <+44>: add    sp, sp, #0x20             ; =0x20
     0x100086ae8 <+48>: ret
    

    funcB

     Assembly`funcB:
     0x100086aec <+0>:  sub    sp, sp, #0x20             ; =0x20
     0x100086af0 <+4>:  stp    x29, x30, [sp, #0x10]
     0x100086af4 <+8>:  add    x29, sp, #0x10            ; =0x10
     0x100086af8 <+12>: stur   w0, [x29, #-0x4]
     0x100086afc <+16>: str    w1, [sp, #0x8]
     0x100086b00 <+20>: ldur   w0, [x29, #-0x4]
     0x100086b04 <+24>: ldr    w1, [sp, #0x8]
     0x100086b08 <+28>: bl     0x100086b18               ; funcC at Ctest.c:75
     0x100086b0c <+32>: ldp    x29, x30, [sp, #0x10]
     0x100086b10 <+36>: add    sp, sp, #0x20             ; =0x20
     0x100086b14 <+40>: ret
    

    funcC

     Assembly`funcC:
     0x100086b18 <+0>:  sub    sp, sp, #0x10             ; =0x10
     0x100086b1c <+4>:  str    w0, [sp, #0xc]
     0x100086b20 <+8>:  str    w1, [sp, #0x8]
     0x100086b24 <+12>: ldr    w0, [sp, #0xc]
     0x100086b28 <+16>: ldr    w1, [sp, #0x8]
     0x100086b2c <+20>: add    w0, w0, w1
     0x100086b30 <+24>: str    w0, [sp, #0x4]
     0x100086b34 <+28>: ldr    w0, [sp, #0x4]
     0x100086b38 <+32>: add    sp, sp, #0x10             ; =0x10
     0x100086b3c <+36>: ret
    

    汇编分析


    先回忆一下几个关键寄存器和指令,
    sp (Stack Point) :寄存器r31,指向函数调用栈的栈顶
    fp (Frame Point):寄存器r29,指向当前正在执行函数栈帧的栈底
    lr (Link Register) :寄存器r30,存储的是函数返回地址,用于当函数结束时,返回函数调用方继续往下执行。
    bl:跳转指令,它做了两件事情,
    1. 把下一条指令的地址存储到lr寄存器中
    2. 跳转到标记处执行指令
    ret:函数返回,返回到lr保存的地址继续执行指令。
    更多的寄存器和指令学习可参考iOS逆向-arm64汇编学习

    由于funcAfuncB都是非叶子函数,生成的主要汇编代码大致相同,所以下面只分析funcBfuncC对应的汇编代码。

    Assembly funcB:

     Assembly`funcB:
    // 第一部分:开辟栈空间,保护现场
     0x100086aec <+0>:  sub    sp, sp, #0x20           ;// sp指针往下移动0x20个字节,开辟栈空间。
     0x100086af0 <+4>:  stp    x29, x30, [sp, #0x10]   ;// x29(fp)、x30(lr)寄存器中的内容存放内存中
     0x100086af4 <+8>:  add    x29, sp, #0x10          ;// x29(fp) 栈底指针往下移动0x10个字节。
    
    // 第二部分:函数逻辑
     0x100086af8 <+12>: stur   w0, [x29, #-0x4]        ;// 把第一个参数存入内存中
     0x100086afc <+16>: str    w1, [sp, #0x8]          ;// 把第二个参数存入内存中
     0x100086b00 <+20>: ldur   w0, [x29, #-0x4]        ;// 从内存中读取到w0中,也就是第一个参数
     0x100086b04 <+24>: ldr    w1, [sp, #0x8]          ;// 从内存中读取到w1中,也就是第二个参数
     0x100086b08 <+28>: bl     0x100086b18             ;// 调用 funC 函数
    
    // 第三部分:恢复现场,回收栈空间
     0x100086b0c <+32>: ldp    x29, x30, [sp, #0x10]   ;// 从内存中读取数据到x29、x30,恢复 x29、x30的值。
     0x100086b10 <+36>: add    sp, sp, #0x20           ;// 函数执行完毕,回收栈空间
     0x100086b14 <+40>: ret                            ;// 返回,跳转回上一个函数(funcA)执行
    

    Assembly funcC

     Assembly`funcC:
    // 第一部分:开辟栈空间
     0x100086b18 <+0>:  sub    sp, sp, #0x10             ;// sp指针往下移动0x20个字节,开辟栈空间。
    
    // 第二部分:函数逻辑
     0x100086b1c <+4>:  str    w0, [sp, #0xc]
     0x100086b20 <+8>:  str    w1, [sp, #0x8]
     0x100086b24 <+12>: ldr    w0, [sp, #0xc]
     0x100086b28 <+16>: ldr    w1, [sp, #0x8]
     0x100086b2c <+20>: add    w0, w0, w1
     0x100086b30 <+24>: str    w0, [sp, #0x4]
     0x100086b34 <+28>: ldr    w0, [sp, #0x4]
    
    // 第三部分:回收栈空间
     0x100086b38 <+32>: add    sp, sp, #0x10             ;// 函数执行完毕,回收栈空间
     0x100086b3c <+36>: ret                              ;// 返回,跳转回上一个函数(funcB)执行
    

    经过上面的分析,Assembly funcB:Assembly funcC:最大的却别是在Assembly funcB:中有对x29 (fp)、x30 (lr)的内容存储到内存,进行保护,并在结束时恢复其原来的值;而在Assembly funcC:中没有这样的操作。

    原因分析:

    1. Assembly funcC:bl指令,bl指令会修改x30( lr )寄存器的值,所以需要在真正执行函数之前保存。
    2. 那为什么要保存x29的值呢?从汇编代码看,x29是被修改了,但是不修改也可以的,其他指令并没有修改x29的值;单独通过sp也可以完成所有的寻址(访问具体的内存空间)工作。说说我个人的理解
      2.1)通过x29 (fp)x30(sp)可以确定当前函数的栈帧的地址范围,寻址时不可以超出这个范围。访问超出这个范围地址的数据可能是脏数据,至少对当前函数来说是脏数据。
      2.2)有了x29寄存器,寻址的时候可以用x29的值做一个偏移定位到要访问的地址,也可以用sp的值做一个偏移找到要访问的地址。靠近栈底的用x29做偏移,靠近栈顶的用sp做偏移,这样可以提高寻址的速度。那CPU怎么知道要访问的地址是靠近x29还是靠近sp呢?CPU是不知道的,但编译器知道,要访问什么地址,在程序编译的时候就确定了。
      2.3)那Assembly funcC:为什么没有栈底指针x29,有的话也可以提高寻址速度啊。那为什么没有自己的栈底指针呢?目前还没找到答案,也是自己的一个疑惑。

    好了,上面啰嗦了那么多,下面通过图来看函数调用时栈空间寄存器的变化情况,

    stack.png
    对图稍微说明一下,栈是高地址往低地址增长的,图中每一行表示4个字节。左边是函数调用时spfp的变化过程,右边是函数调用结束后spfp的变化过程。通过观察,开辟栈空间时,不是需要多大的内存就开闭多少的内存空间,而是16的倍数的字节数。这要做有助于提高CPU访问内存的速度。

    总结


    通过上面的分析,理解了函数调用时栈空间寄存器的变化情况,以及为什么需要那样变化。但还留下了一个疑问,叶子函数为什么没有自己fp,知道的大神麻烦告知。

    相关文章

      网友评论

        本文标题:iOS逆向-从汇编代码理解函数调用栈

        本文链接:https://www.haomeiwen.com/subject/sjpssftx.html