0%

函数调用堆栈图-c语言

我们就使用一个简单的c语言程序来对描述一下在函数调用的时候都发生了什么。

中间的一小段没有意义的汇编语言是为了方便设置断点,为后面的调试做好铺垫,因为有时会碰到找不到断点位置的情况,使用这个方法,可以在找不到断点的时候向后执行一次,而不破坏我们想调试的程序当前的堆栈状态,这里对main函数和sum函数的效果是类似的,这里直接跟着断点来执行分析sum函数的堆栈操作。

我们先假设初始状态下的堆栈图如下,esp与ebp的真实距离我们省略。

接下来我们来看一下后面的操作。

在程序的执行当中,我们一般都是按照从右向左的方式去处理的,这里也不例外,我们可以发现当我们调用sum函数对数字1和数字2进行处理的时候,将数字2和1依次压入栈中,这个时候堆栈的情况是这个样子的,esp的值已经减8。

接下来调用了call,这时进行了两步操作,先将call后面的地址push进堆栈,然后再jmp到call所调用的地址。

因为jmp是不会影响堆栈的,所以现在的堆栈情况是这样的

然后因为编译器的原因在call的时候还会有一个jmp来中转到后面的处理函数,因为jmp不影响堆栈,我们可以忽略掉它,这里是跳转到了sum函数的处理位置。

此时的堆栈是没有发生变化的,现在开始到了函数调用的关键阶段了。

首先先将ebp的值push到堆栈中,因为用到了ebp寻址的方式,所以这里用这种方式来保存ebp中原本的值,然后将esp的值赋给ebp,用ebp寻址来代替esp寻址,因为esp的值一直在不断的发生变化,使用esp寻址会带来很大的计算负担,此时esp与ebp都指向了同一块地址,其中的内容是原来的ebp的值。

然后让esp减去了0c0h位,开始提升堆栈了,为程序的运行开辟一个存储空间,这个区域也就是平时所说的缓冲区,因为一个单元是四个字节,c0也就是往上提了48个格,由于位置有限中间依旧省略,此时堆栈就变成了如下的样子。

后面又进行了一系列的push操作,也是为了方便在后续使用这些寄存器的时候保证它们初始的值不丢失,与前面保存ebp的值是一样的方式。

然后接下来的四步操作只有一个目的,那就是将中间的48格全部值为CC,CC在调试的时候相当于断点,也就是如果你程序跑过的话,就会触发断点不会再继续执行了。

lea是交换地址中的值,给eax和ecx赋值是为rep的执行做准备的,stos是将eax中的值赋给edi,rep是执行后面的指令ecx次。

接下来的两步指令我们忽略,它们是vs编译器添加的调试指令

因为eax一般是来用作返回值的,所以这里的计算都是跟eax进行计算的,因为我们这里直接使用的是return返回,没有涉及到临时变量,所以不会用到缓冲区来存储。

接下来的三步pop,是将之前存储在栈中的元素都恢复到它原来的位置。

此时的堆栈情况就变成了,上面的值还是没有清除的,它们现在已经是垃圾数据的,下一次填充的时候会把它们覆盖掉,这也就造成了可以在其中获取到某些程序不想让人知道的临时变量值。

接下来让esp增加0c0,也就恢复到了提升堆栈之前的位置,此时esp与ebp到了一个位置。

接下来的三步操作依旧可以忽略,它们是vs编译器生成的,用来检测堆栈是否平衡,如果不平衡的话在这里就会产生报错。

最后就是使用pop,将ebp恢复到之前的位置。

最后使用ret回到堆栈中存储的地址,也就是call调用的下一个地址。

但是此时还有个问题,esp并没有回到调用前的位置,所以堆栈还是没有平衡的,如果堆栈不平衡,那在不断的执行的过程中,就会发生堆栈溢出,这里编译器是使用外平栈的方式来使堆栈恢复平衡的,它在esp的基础上增加了8。

此时堆栈也就恢复到了平衡状态

还有另一种方式是使用内平栈的方式,即在函数内部就将堆栈恢复平衡,使用ret 8的方式。

再往后面的操作就是main函数的堆栈平衡的处理了,与上面的函数调用类似,就不提了。