想要弄清楚函数的栈帧结构,我们就要先搞明白什么叫做栈帧。
在《深入理解计算机系统》这本书中,对栈帧的概念定义如下:
为单个过程分配的那部分栈就叫做栈帧。栈帧的最顶端以两个指针界定——帧指针(寄存器ebp)和栈指针(esp)。
其实我觉得对于我们初学者来说,把它理解为函数就可以了。简单来说,栈帧就是帧指针ebp和栈指针esp之间的内容,即函数主体。其中,ebp指向函数头,位置固定不动,esp指向函数尾,随函数内部变量的增加或减少而移动。
每一个被调用的函数都有一个自己的栈帧结构,并且栈帧结构是由函数自己形成的。需要注意的是:CPU中的寄存器ebp和esp都只有一个。
那么问题来了,前面我说过每一个被调用的函数都有一个自己的栈帧结构,那大家都知道一段代码至少有一个函数,至多没有界限,但是栈帧的寄存器ebp和esp都只有一个,那么他是如何来协调使用完成函数调用的呢?
想要搞清楚这个问题,就要深入理解从代码到地址空间这个过程中空间的分配问题。
这里,我们先来说一说地址空间分配的问题,如下图:
由低地址到高地址依次是代码段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指令的第一个作用我调用内存给大家看一下它的存储位置:
00041514 call _fun (0411F4h) //调用子程序即调用fun函数
00041519 add esp,8 //寄存器esp上移8字节,释放为y和x分配的空间
根据上面地址空间分配的规则我们可以知道,这段代码产生的数据存在于栈区。下面根据反汇编代码,我来画一下它的地址空间分配图。
上图是main函数跳到fun函数形成栈帧结构的过程,其中esp寄存器的指向顺序按照红橙黄绿青蓝紫的顺序依次排列。从fun函数回到main函数的过程,按照倒序依次pop出栈就可以了。如下图,按照粗线箭头红黄绿的顺序释放。
这里需要补充的是,这段代码存储在代码段,决定CPU执行那一条指令的是PC程序计数器,PC程序计数器指向当前执行指令的下一条指令。当执行call指令时,先要保存PC指针当前指向的指令,即call的下一条指令add。同时pc指针转指向fun函数里将要执行的第一条指令。当fun函数调用完毕后,add指令的地址出栈,pc指针重新指向这条指令。至此,fun函数的栈帧结构的形成与释放结束。
如有错误,欢迎指出!