函数的栈帧结构

      想要弄清楚函数的栈帧结构,我们就要先搞明白什么叫做栈帧。

      在《深入理解计算机系统》这本书中,对栈帧的概念定义如下:

      为单个过程分配的那部分栈就叫做栈帧。栈帧的最顶端以两个指针界定——帧指针(寄存器ebp)和栈指针(esp)。

       其实我觉得对于我们初学者来说,把它理解为函数就可以了。简单来说,栈帧就是帧指针ebp和栈指针esp之间的内容,即函数主体。其中,ebp指向函数头,位置固定不动,esp指向函数尾,随函数内部变量的增加或减少而移动。

       每一个被调用的函数都有一个自己的栈帧结构,并且栈帧结构是由函数自己形成的。需要注意的是:CPU中的寄存器ebp和esp都只有一个。
       那么问题来了,前面我说过每一个被调用的函数都有一个自己的栈帧结构,那大家都知道一段代码至少有一个函数,至多没有界限,但是栈帧的寄存器ebp和esp都只有一个,那么他是如何来协调使用完成函数调用的呢?
      想要搞清楚这个问题,就要深入理解从代码到地址空间这个过程中空间的分配问题。
      这里,我们先来说一说地址空间分配的问题,如下图:

        函数的栈帧结构_第1张图片


       由低地址到高地址依次是代码段code,静态全局区,堆区,栈区。其中堆区和栈区之间有一部分空间被称为共享区,这是因为栈区是由高地址向低地址生长的,而堆区以及其他区都是由低地址到高地址生长,因而堆区和栈区之间的空间可以共用,称为共享区。静态全局区里主要分为代码段,静态只读区,未初始化变量区和已初始化变量区。这里对静态全局区不做过多讨论。我们编写代码时定义的局部变量和函数参数等变量都是存储在栈区里的,我们将要讨论的栈帧就是在栈区里。
      下面来看看这个代码:


#include
#include
int fun(int x, int y)
{
	int c = 0xcccccccc;
	return c;
}
int main()
{
	int a = 0xaaaaaaaa;
	int b = 0xbbbbbbbb;
	int ret = fun(a, b);
	printf("congratulations,you run here!\n");
	system("pause");
	return 0;
}


       这个代码很简单,他就是说main函数调用了一个fun函数,这个函数接收变量a和变量b的一份临时拷贝,然后返回fun函数中变量c的值。OK,我们来看看这段代码的反汇编是如何的。



#include
#include
int fun(int x, int y)
{
00043D60  push        ebp  //ebp压栈
00043D61  mov         ebp,esp  //形成栈帧结构
00043D63  sub         esp,0CCh  //esp下移,为栈帧分配空间
00043D69  push        ebx  //寄存器ebx入栈
00043D6A  push        esi  //寄存器esi入栈
00043D6B  push        edi  //寄存器edi入栈
00043D6C  lea         edi,[ebp-0CCh]  
00043D72  mov         ecx,33h  
00043D77  mov         eax,0CCCCCCCCh  
00043D7C  rep stos    dword ptr es:[edi]  
	int c = 0xcccccccc;
00043D7E  mov         dword ptr [c],0CCCCCCCCh  //定义变量c并初始化
	return c;
00043D85  mov         eax,dword ptr [c]  //把c的值放入寄存器eax
}
00043D88  pop         edi  
00043D89  pop         esi  
00043D8A  pop         ebx  
00043D8B  mov         esp,ebp  //释放空间
00043D8D  pop         ebp  //出栈
00043D8E  ret  //返回(return)



int main()
{
000414E0  push        ebp  //压栈
000414E1  mov         ebp,esp  //形成栈帧结构
000414E3  sub         esp,0E4h  //esp下移,为栈帧分配空间
000414E9  push        ebx  
000414EA  push        esi  
000414EB  push        edi  
000414EC  lea         edi,[ebp-0E4h]  
000414F2  mov         ecx,39h  
000414F7  mov         eax,0CCCCCCCCh  
000414FC  rep stos    dword ptr es:[edi]  
	int a = 0xaaaaaaaa;
000414FE  mov         dword ptr [a],0AAAAAAAAh  //定义变量a
	int b = 0xbbbbbbbb;
00041505  mov         dword ptr [b],0BBBBBBBBh  //定义变量b
	int ret = fun(a, b);
0004150C  mov         eax,dword ptr [b]  
0004150F  push        eax  //b放入寄存器eax,入栈
00041510  mov         ecx,dword ptr [a]  
00041513  push        ecx  //a放入寄存器ecx,入栈
00041514  call        _fun (0411F4h)  //调用子程序即调用fun函数
00041519  add         esp,8 //寄存器esp上移8字节,释放为b和a分配的空间
0004151C  mov         dword ptr [ret],eax  //fun的返回值给ret
	printf("congratulations,you run here!\n");
0004151F  mov         esi,esp  
00041521  push        45858h  
00041526  call        dword ptr ds:[49120h]  
0004152C  add         esp,4  
0004152F  cmp         esi,esp  
00041531  call        __RTC_CheckEsp (04114Ah)  
	system("pause");
00041536  mov         esi,esp  
00041538  push        458ACh  
0004153D  call        dword ptr ds:[49114h]  
00041543  add         esp,4  
00041546  cmp         esi,esp  
00041548  call        __RTC_CheckEsp (04114Ah)  
	return 0;
0004154D  xor         eax,eax  
}
0004154F  pop         edi  
00041550  pop         esi  
00041551  pop         ebx  
00041552  add         esp,0E4h  
00041558  cmp         ebp,esp  
0004155A  call        __RTC_CheckEsp (04114Ah)  
0004155F  mov         esp,ebp  
00041561  pop         ebp  
00041562  ret 


      在上面的代码中,我加了对应的注释帮助大家理解,需要注意的是反汇编代码中的call指令,它的作用有两个:第一,保存当前正在执行的指令的下一条指令的地址(压栈保存);第二,跳转到目标函数。对于call指令的第一个作用我调用内存给大家看一下它的存储位置:

                                          函数的栈帧结构_第2张图片


00041514  call        _fun (0411F4h)  //调用子程序即调用fun函数
00041519  add         esp,8 //寄存器esp上移8字节,释放为y和x分配的空间

       在调用fun函数的时候,传入了参数a,b。但是函数在形参x和y实体化的时候,是从右向左传值的,也就是说,先把b的值传给y,在把a的值传给x。在上面内存地址分配可以看到,存储b,a以及add指令地址的地址是依次减小的。这是因为执行call指令时,先在栈区里为y分配了一个空间存放b,然后再为x分配存储空间来存放a,然后再把add指令的地址放入下一个地址空间。( 对应反汇编代码中对应call指令的下一条指令即为add,他的地址是00041519。这里涉及大小端存储问题,我的笔记本是小端存储,读取地址时从高地址到低地址读。)

      根据上面地址空间分配的规则我们可以知道,这段代码产生的数据存在于栈区。下面根据反汇编代码,我来画一下它的地址空间分配图。


函数的栈帧结构_第3张图片


       上图是main函数跳到fun函数形成栈帧结构的过程,其中esp寄存器的指向顺序按照红橙黄绿青蓝紫的顺序依次排列。从fun函数回到main函数的过程,按照倒序依次pop出栈就可以了。如下图,按照粗线箭头红黄绿的顺序释放。

函数的栈帧结构_第4张图片


       这里需要补充的是,这段代码存储在代码段,决定CPU执行那一条指令的是PC程序计数器,PC程序计数器指向当前执行指令的下一条指令。当执行call指令时,先要保存PC指针当前指向的指令,即call的下一条指令add。同时pc指针转指向fun函数里将要执行的第一条指令。当fun函数调用完毕后,add指令的地址出栈,pc指针重新指向这条指令。至此,fun函数的栈帧结构的形成与释放结束。





       如有错误,欢迎指出!






你可能感兴趣的:(C)