美文网首页初见
函数调用本质

函数调用本质

作者: petyou | 来源:发表于2019-10-30 16:50 被阅读0次

    文章来源:CoderHong 的博客

    从反汇编角度窥探平时开发调用的函数或者方法的本质。平时我们编写的高级语言最终通过编译器、链接生成机CPU执行的机器指令。 不同的CPU对应着不同着机器指令,并且每一条机器指令对应着一条汇编。

    先看一个最简单的C语言函数,这里主要通过C++来反编译分析汇编指令。

    1.png

    可以通过反汇编看到调用func函数的汇编指令,当前环境是8086汇编。

    2.png

    通过最终的汇编指令可以看出,在执行调用一个函数:本质就是通过call指令调用函数在代码段的地址进行直接调用。

    注意:在上面的汇编指令可以看到当函数执行完毕,执行ret汇编指令退出函数。其实一个完整的函数调用必定包含callret指令。

    那么只有了解了callret才能彻底从最根本了解函数的调用过程。

    call 标号
    1.将下一条指令的偏移地址入栈
    2.转到标号出执行指令
    ret
    将栈顶的值出栈,赋值给IP
    

    下面通过汇编代码调用 printf 函数标号打印 HelloWorld 执行验证上面的结论。

    3.png

    在即将执行执行 printf 函数之前栈顶指针SP指向内存单元的数据。

    4.png

    上面说到执行函数前会将下一条指令的偏移地址入栈,上图可以看出的下一条CPU执行的指令偏移地址IP为:000D。开始执行,看下栈顶指针SP的指向和指向内存单元的数据

    5.png

    函数 printf 执行完毕后,执行 ret 指令,栈顶偏移地址出栈赋值给 IP 中,栈顶指针向上移动两个字节。

    6.png

    不管什么开发语言最终都会转成二进制汇编指令,对应着相应的汇编指令,本质都是一致的。这里是通过C++反汇编窥探函数调用本质。

    上述介绍只是最简单函数调用,一说到函数首先就会想到函数的三要素,函数的返回值函数的参数、局部变量**。

    返回值

    如果调用函数想拿到函数返回值,就得有容器来存放返回值,我们可以想到用栈、数据区、寄存器来保存。

    首先栈段不可以的,如下图,函数内部push返回值,栈顶存储的是CPU函数执行完毕后的IP的偏移地址。

    7.png

    可以考虑将返回值放入数据段,这个需要与调用者约好协议,比如约定好将返回值放在ds:[0]

    8.png

    这样侧面证明了数据段里的数据是全局,全局区的数据是作用域是全局的。上面的实例代码好比下面的C++代码。

    9.png

    在实际中,大多数平台,windows、linux、Android等通常的做法是将方法返回值放在寄存器ax。其实这样的效率比上面返回值放在全局区效率高,CPU从寄存器中读取数据要快,放在全局区需要从内存先读取到寄存器。

    10.png

    下面在X86环境下写一段代码看下汇编指令

    11.png

    参数

    同样我们先考虑将参数放入数据段来实现一个求和的函数。

    12.png

    放在数据段是可以的,在我们概念中形参的作用于是数据函数内部,函数执行完毕形参所占用的内存空间会被回收。这样就很明显了,通常,形参是放在栈中的。

    13.png

    注意:在函数调用完毕后,一定要保证栈平衡,否者会导致栈的空间会被用完,通常保持栈平衡有两种方式:内平栈和外平栈。

    上面的案例是使用了外平栈方式,也就是在函数调用完毕后,对栈顶指针进行回复到函数调用前的位置。

    14.png

    对于函数的封装性跟人觉的栈内平衡的方式会好一些,让函数调用者不用关心内部细节。函数的形参本质了解后,接下来窥探最后一个函数的局部变量本质,这个相对复杂一些。

    局部变量

    函数的内部需要定义局部变量,C语言特别简单,那么在汇编中怎么分配内存空间给局部变量呢,局部变量的作用域只是当前函数,函数执行完毕后局部所栈中的空间被回收,因此局部变量空间分配还是通过栈来实现。

    15.png

    上面开始没有问题,唯一缺陷是在函数内部调用函数时,由于我们没有对bp进行恢复,一旦对函数内部在调用函数就会存存在问题, 因此需要对bp进行记录和恢复。

    16.png 17.png 18.png

    函数的调用流程总结

    1  push参数,参数入栈
    2  将函数的返回地址(下一条指令的地址)入栈
    3  保护sp,将sp赋值给bp
    4  分配一定的空间给函数的局部变量使用(让sp减去该空间大小),为了安全,用CC填充(int 3h)
    5  保护寄存器, 因为在函数执行过程中会修改寄存器的值,所以在修改之前保存一下之前的值,后面再还原
    6  具体的业务代码
    7  恢复寄存器的值,跟第5步相反
    8  将bp赋值给sp,恢复bp
    9  返回(ret)
    

    相关文章

      网友评论

        本文标题:函数调用本质

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