在了解栈帧之前,首先了解一下函数的调用,每一次函数的调用,内存都会为函数来开辟一块空间,那么这块空间从何而来,又是怎样维护的呢?
先回答第一个问题,这块空间是从内存中的栈空间拿出来使用的,而栈空间的使用是从高地址向低地址使用的。
然后我们在来看一个叫做寄存器的东西。
在介绍寄存器之前,我们都知道,现代计算机的组成是由运算器、 存储器,控制器、输入和输出这五大部分组成,而我们平时所知道的内存、硬盘、缓存和今天要稍微介绍一下的寄存器都是可以用来存储数据的,之所以有如此多类型的存储器,主要是为了解决计算机内部各大部件速度不匹配的问题。
而今天我们谈到的函数调用,会涉及到EIP、ESP和EBP这三个寄存器。
首先,EIP被称之为程序计数器,CPU只负责解析,执行和返回执行结果,而下一条指令的执行顺序则由EIP中存储的地址来决定的,EIP中存储的地址就是CPU下一条执行的指令。ESP被称之为栈顶寄存器,在形成栈帧的时候会指向栈顶,EBP被称之为栈底寄存器,在形成栈帧的时候会指向栈底。
接下来,我们来看一段代码:
#include<stdio.h>
#include<stdlib.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 9;
int b = 10;
int c = 0;
c = Add(a, b);
printf("%d ", c);
system("pause");
return 0;
}
代码写的非常简单,但足够我们对栈帧进行一次深入的探究。
首先,将程序运行至调试过程,转到反汇编,逐过程调试,观看调用堆栈:可以观察到,最先调用的函数是mainCRTStartup,接下来调用是函数是__mainCRTStartup,然后才轮到我们的main函数。
执行的第一条汇编语句是push ebp,将ebp中的数据压栈,然后移动esp中的数据到ebp,这意味着esp和ebp指向同一位置,接下来,压入ebx,esi,edi,然后给esp减去0E4h(这是16进制数),上文说过,esp是用来指向栈顶的,而栈空间的使用是由高地址向低地址使用的,当减去0E4h的时候,esp向低地址移动0E4h个字节,那么栈顶和栈底之间的空间就有0E4h个字节。接下来的指令是将新开辟的空间初始化为0xcccccccch。然后将a,b和c分别赋值。然后创建形参,可以从我们的汇编代码中看到,第一个创建的形参是b,压栈,然后是a,压栈。
接下来执行call(这在汇编中是调用函数的指令),我们可以看到call命令的下一条指令的地址被压入到了栈空间。
接下来就跳转到了Add函数之中,我们可以对照main函数栈帧的形成过程,发现调用函数的命令是完全相同的,这也就是我们所说的调用函数所需要的开销。
最后依次是pop(弹栈)命令的执行,ret会返回到我们刚才call命令执行的下一条命令,其实我们也可以知道,CPU执行命令的顺序就是EIP寄存器所存的地址,我们想要改变它的执行顺序,只需要改变EIP上的地址。