函数调用的本质
从反汇编角度窥探平时开发调用的函数或者方法的本质。平时咱们编写的高级语言最终经过编译器、连接生成机CPU执行的机器指令。 不一样的CPU对应着不一样着机器指令,而且每一条机器指令对应着一条汇编。linux
先看一个最简单的C语言函数,这里主要经过C++来反编译分析汇编指令。windows
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207104946022-295278539.png" width=600/>函数
能够经过反汇编看到调用func函数的汇编指令,当前环境是8086汇编。spa
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207105149822-1871992655.png" width=600/>指针
经过最终的汇编指令能够看出,在执行调用一个函数:本质就是经过call指令调用函数在代码段的地址进行直接调用。code
注意:在上面的汇编指令能够看到当函数执行完毕,执行ret汇编指令退出函数。其实一个完整的函数调用一定包含call和ret指令。blog
那么只有了解了call和ret才能完全从最根本了解函数的调用过程。内存
call 标号 1.将下一条指令的偏移地址入栈 2.转到标号出执行指令 ret 将栈顶的值出栈,赋值给IP
下面经过汇编代码调用printf函数标号打印HelloWorld执行验证上面的结论。作用域
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207105343691-1758314410.png" width=600/>开发
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207111011221-68861063.png" width=600/>
在即将执行执行printf函数以前栈顶指针SP指向内存单元的数据。
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207105447268-321117954.png" width=600/>
上面说到执行函数前会将下一条指令的偏移地址入栈,上图能够看出的下一条CPU执行的指令偏移地址IP为:000D。开始执行,看下栈顶指针SP的指向和指向内存单元的数据。
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207105522887-757981176.png" width=600/>
函数printf执行完毕后,执行ret指令,栈顶偏移地址出栈赋值给IP中,栈顶指针向上移动两个字节。
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207105637159-2025675130.png" width=600/>
无论什么开发语言最终都会转成二进制汇编指令,对应着相应的汇编指令,本质都是一致的。这里是经过C++反汇编窥探函数调用本质。
上述介绍只是最简单函数调用,一说到函数首先就会想到函数的三要素,函数的返回值、函数的参数、局部变量。窥探下函数返回值的实现。
若是调用函数想拿到函数返回值,就得有容器来存放返回值,咱们能够想到用栈、数据区、寄存器来保存。
首先栈段不能够的,以下图,函数内部push返回值,栈顶存储的是CPU函数执行完毕后的IP的偏移地址。
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207105750295-560968837.png" width=600/>
能够考虑将返回值放入数据段,这个须要与调用者约好协议,好比越好将返回值放在ds:[0]
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110133049-1764170239.png" width=600/>
这样侧面证实了数据段里的数据是全局,全局区的数据是做用域是全局的。上面的实例代码比如下面的C++代码。
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110214345-1433373661.png" width=600/>
在实际中,大多数平台,windows、linux、Android等一般的作法是将方法返回值放在寄存器ax。其实这样的效率比上面返回值放在全局区效率高,CPU从寄存器中读取数据要快,放在全局区须要从内存先读取到寄存器。
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110307278-1974910310.png" width=600/>
下面在X86环境下写一段代码看下汇编指令
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110343526-394389412.png" width=600/>
对于函数的返回值本质清楚以后,接下来看函数的第二个要素-函数的形参。
一样咱们先考虑将参数放入数据段来实现一个求和的函数。
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110427514-209968254.png" width=600/>
放在数据段是能够的,在咱们概念中形参的做用因而数据函数内部,函数执行完毕形参所占用的内存空间会被回收。这样就很明显了,一般,形参是放在栈中的。
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110509159-902437971.png" width=600/>
注意:在函数调用完毕后,必定要保证栈平衡,否者会致使栈的空间会被用完,一般保持栈平衡有两种方式:内平栈和外平栈。
上面的案例是使用了外平栈方式,也就是在函数调用完毕后,对栈顶指针进行回复到函数调用前的位置。
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110541461-2041452995.png" width=600/>
对于函数的封装性跟人觉的栈内平衡的方式会好一些,让函数调用者不用关心内部细节。函数的形参本质了解后,接下来窥探最后一个函数的局部变量本质,这个相对复杂一些。
函数的内部须要定义局部变量,C语言特别简单,那么在汇编中怎么分配内存空间给局部变量呢,局部变量的做用域只是当前函数,函数执行完毕后局部所栈中的空间被回收,所以局部变量空间分配仍是经过栈来实现。
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110620935-537749251.png" width=600/>
上面开始没有问题,惟一缺陷是在函数内部调用函数时,因为咱们没有对bp进行恢复,一旦对函数内部在调用函数就会存存在问题, 所以须要对bp进行记录和恢复。
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110719153-1623126868.png" width=600/>
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110744056-955025758.png" width=600/>
<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110807313-176724709.png" width=600/>