首先,给出一段简单的代码
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a=42;
int b=35;
int c=0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
代码实现了一个简单的函数,加法函数。
函数栈帧简单概念
每一个函数调用,都要在栈区指定一块空间,而这块栈空间就被称为函数栈帧。每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量 函数栈帧由两个寄存器中存放的指针所确定。
寄存器
在此简单提一下寄存器。寄存器是有限存储容量的高速存储部件,它们可用来暂存指令、数据和地址。此处函数栈帧由esp和ebp寄存器确定空间范围。 下文还会用到的寄存器还有eax,ebx,ecx,edx这4个寄存器。 esp中存放的指针指向栈顶,ebp中存放的指针指向栈底。两个寄存器之间的内存空间就是函数栈帧。以最开始给出的加法函数为例,看图演示: 
函数栈帧的创建与销毁
接下来通过分析上述代码的汇编代码来讲述函数栈帧的主要创建过程。
push ebp
mov ebp,esp
sub esp,0E4h
push意为压栈,即将某元素进入栈顶。第一行指令意为,将ebp的值放到栈顶,此时因为有新元素入栈,栈顶发生改变,且esp始终指向栈顶元素,故esp指向ebp值对应的位置。图示如下:  因为main函数也是被调用的,此处是被invoke_main的函数调用,因此,在调用main之前,esp和ebp在维护invoke_main的空间。
mov ebp,esp意为,将esp中的数据放入ebp中,因为esp中存放着指针,mov完成后,ebp就和esp一起指向同一元素。图示: 
sub esp,0E4h ,sub是减法指令,指令意为esp=esp-0E4h 栈区空间使用特点为,先使用高地址,再使用低地址。此处esp值减小,则esp向上移动,指向新位置。而ebp留在原来的位置,esp和ebp之间出现了间隔,这个间隔就是两者维护的空间,即main函数的函数栈帧。 
push ebx
push esi
push edi
三个元素依次压栈,esp也依次指向栈顶元素。 
lea edi,[ebp-24h]
mov ecx,9
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
lea全称load effective address,加载有效地址 ecx中存入9h,eax中存入CCCCCCCC 这4条语句意为,将从edi开始向下ecx个dword(double word双字,一字为2个字节,dword为4个字节)初始化为eax中的内容。 即从edi开始,将4个字节的数据初始化为CCCCCCCC,进行9次这样的操作 
int a=42;
mov dword ptr [ebp-8],2Ah
int b=35;
mov dword ptr [ebp-14h],23h
int c=0;
mov dword ptr [ebp-20h],0
将abc要赋的值,存入对应分配给abc的指定空间。 
c = Add(a, b);
mov eax,dword ptr [ebp-14h]
push eax
mov ecx,dword ptr [ebp-8]
push ecx
对照上图知,ebp-14h为b,ebp-8为a。以上指令,就是在传参。先传b的值,再传a的值,b,a分别放入寄存器eax和ecx并分别入栈,需要说明的是,传参过程中,参数存储在寄存器中,而并非在Add函数内部,图示如下:  然后执行call指令 
push ebp
mov ebp,esp
sub esp,0CCh
push ebx
push esi
push edi
lea edi,[ebp-0Ch]
mov ecx,3
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
以上指令是Add函数的部分。同main函数类似,通过几步完成Add函数栈帧的创建。如图 
int z = 0;
mov dword ptr [ebp-8],0
z = x + y;
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
mov dword ptr [ebp-8],eax
return z;
mov eax,dword ptr [ebp-8]
ebp+8就是刚才传参时的a,即42,ebp+0Ch就是b,35,执行完的效果如图 将a+b的结果存入ebp-8。也就是说传参并没有传到Add函数内部,而是传到外面,需要时直接找到刚才传参的位置调用。  最后return z的时候,z的值存在ebp-8中,mov将z的值放入寄存器eax中,寄存器集成在CPU上,所以Add函数调用完成后,销毁局部变量,释放内存空间。z的值也正常存储,因为在函数结束前,已经拷贝到寄存器中了,寄存器是独立于内存的。
函数栈帧的销毁
pop edi
pop esi
pop ebx
三个元素依次出栈,esp随之下移 
mov esp,ebp
pop ebp
ret
mov esp,ebp 函数完成,esp与ebp间隔消失,空间回收,函数栈帧随之销毁  pop ebp pop意为出栈,ebp出栈,通过ebp–main中存储的数据,回到原来的位置  ret 栈顶元素出栈,通过栈顶存放的地址,转到对应地址的指令。即执行call指令下一行的指令。
007E18F1 push ecx
007E18F2 call 007E10B4
007E18F7 add esp,8
007E18FA mov dword ptr [ebp-20h],eax
add esp,8 ,刚才ret执行完,栈顶元素出栈,此时esp指向值为42的形参,esp+8,将两个形参所占空间释放。 mov dword ptr [ebp-20h],eax ,刚才将Add函数返回值存入了eax,eax再将值传入ebp-20h,即c。  最后依然是函数栈帧的销毁,上文已经讲过,此处不再赘述。
局部变量的创建
通过分析与观察,不难发现,main函数中的局部变量是在main函数栈帧创建之后,在函数栈帧内部规律地被分配空间。且因为在函数栈帧创建之初内部大量空间会被赋一个随机值,故在给局部变量赋值之前,局部变量存放的是一个随机值。局部变量在最终使用完之后,空间才会被释放掉。
笔者能力有限,如文章有问题,还请及时指正。
|