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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

对于函数的返回值本质清楚之后,接下来看函数的第二个要素-函数的形参。

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

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

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

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

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

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

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