概念
栈帧:栈帧在函数中用于声明局部变量,保存函数参数,保存函数返回地址等。
EBP 寄存器又叫栈帧寄存器(作用通过EBP寄存器访问保存在栈中的局部变量,函数参数,函数返回地址等)
栈(stack)是一种数据结构,栈中每个指针(当运行到那个变量时)会指向堆中的某一内存区域或说是空间。它是以先进后出为原则的。
堆(heap)就直接是内存区域了,它是为了栈的引用而开发内存的。通常内置变量就是值类型是被保存在栈中的。其他由.NET框架(Framework)提供的,或者是我们自己定义的对象即引用类型,一般被创建在堆中并将由栈中变量引用。
那么它们运行时的释放是怎样呢?首先刚才说了,栈中会有指针指向堆,其实那叫栈帧(当一个函数被调用时,栈就去堆中借一块内存给它),当函数返回时,栈帧被释放,栈帧中的对象超出作用域而被销毁。这就是一个变量的生命周期了,一只工蜂的生命就是他完成了采蜜工作。而堆中的对象是在没有栈帧指向它的时候,也就是工蜂采完蜜了,蜂王产下卵之后就结束生命了。之后堆中的对象就通过CLR垃圾回收系统销毁。
当一个方法被调用,调用栈会分配一块空间成为栈页,用来保存被调用方法的下一条指令的返回地址。也就是说它会将调用方法之后会产生什么的返回地址发给被调用方法的参数以及被调用方法的所有局部变量。所以“展开调用栈”就是指找到被调用方法的返回地址并强制方法返回,在调用方法的方法中寻找catch语句或其他处理异常的语句来处理异常。在找到异常处理代码之前,栈要“展开”很多被调用的方法。如果最终栈展开到main方法儿还是没找到异常处理代码,默认的异常处理就会被调用,整个程序被终结,栈中的变量和栈帧被销毁,程序终止。
其实如果程序找到了异常处理的代码就会从那段代码开始继续,而不是从抛出异常的地方,或是从调用了抛出异常方法的方法(除非这个方法包含异常处理代码),一定拿栈页被展开,他就被释放。
栈帧对应的汇编代码如下:
push ebp
mov ebp,esp
...
mov esp,ebp
pop ebp
RETN
原理:
首先将EBP寄存器中的值进栈
将ESP的值赋值给EBP
接下来无论是访问局部变量,还是调用函数都是以EBP 寄存器为基准,这样无论ESP怎么变化都不会影响访问局部变量,函数返回地址,函数等。
最后:
将ebp的值赋值给esp 寄存器,pop 一个数据赋值到ebp寄存器(其实此时pop的就是最初push ebp的值)
即最后一步骤的操作就是为拉恢复寄存器中为调用函数前的状态
栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(地址地)。下图为典型的存取器安排,观察栈在其中的位置
入栈操作:push eax; 等价于 esp=esp-4,eax->[esp];如下图
出栈操作:pop eax; 等价于 [esp]->eax,esp=esp+4;如下图
我们来看下面这个C程序在执行过程中,栈的变化情况
int func(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 2;
int b = 3;
int ret = 0;
ret = func(a, b);
printf("%d", ret);
system("pause");
return 0;
}
这是一个两个数相加的程序,在VS中打开调试中的反汇编,查看汇编代码
int main()
{
002233C0 push ebp
002233C1 mov ebp,esp
002233C3 sub esp,0E4h
002233C9 push ebx
002233CA push esi
002233CB push edi
002233CC lea edi,[ebp-0E4h]
002233D2 mov ecx,39h
002233D7 mov eax,0CCCCCCCCh
002233DC rep stos dword ptr es:[edi]
int a = 2;
002233DE mov dword ptr [a],2
int b = 3;
002233E5 mov dword ptr [b],3
int ret = 0;
002233EC mov dword ptr [ret],0
ret = func(a, b);
002233F3 mov eax,dword ptr [b]
002233F6 push eax
002233F7 mov ecx,dword ptr [a]
002233FA push ecx
002233FB call _Add (02211EFh)
00223400 add esp,8
00223403 mov dword ptr [ret],eax
printf("%d", ret);
00223406 mov esi,esp
00223408 mov eax,dword ptr [ret]
printf("%d", ret);
0022340B push eax
0022340C push 225858h
00223411 call dword ptr ds:[22911Ch]
00223417 add esp,8
0022341A cmp esi,esp
0022341C call __RTC_CheckEsp (022114Ah)
system("pause");
00223421 push 22585Ch
00223426 call _system (02210AFh)
0022342B add esp,4
return 0;
0022342E xor eax,eax
}
00223430 pop edi
00223431 pop esi
00223432 pop ebx
00223433 add esp,0E4h
00223439 cmp ebp,esp
0022343B call __RTC_CheckEsp (022114Ah)
00223440 mov esp,ebp
00223442 pop ebp
00223443 ret
在main调用func函数前,栈的情况,也就是说main的栈帧:
从低地址esp到高地址ebp的这块区域,就是当前main函数的栈帧。当main中调用func时,写成汇编大致是:
push m
push n; 两个参数压入栈
call func; 调用func,将返回地址填入栈,并跳转到func
当跳转到了func,来看看func的汇编大致的样子:
int func(int x, int y) { 00221C00 push ebp 00221C01 mov ebp,esp 00221C03 sub esp,0CCh 00221C09 push ebx 00221C0A push esi 00221C0B push edi 00221C0C lea edi,[ebp-0CCh] 00221C12 mov ecx,33h 00221C17 mov eax,0CCCCCCCCh 00221C1C rep stos dword ptr es:[edi] int z = 0; 00221C1E mov dword ptr [z],0 z = x + y; 00221C25 mov eax,dword ptr [x] 00221C28 add eax,dword ptr [y] 00221C2B mov dword ptr [z],eax return z; 00221C2E mov eax,dword ptr [z] } 00221C31 pop edi 00221C32 pop esi 00221C33 pop ebx 00221C34 mov esp,ebp 00221C36 pop ebp 00221C37 ret
push ebp; 这个很重要,因为现在到了一个新的函数,也就是说要有自己的栈帧了,那么,必须把上面的函数main的栈帧底部保存起 ; 来,栈顶是不用保存的,因为上一个栈帧的顶部讲会是func的栈帧底部。(两栈帧相邻的)
mov ebp, esp; 上一栈帧的顶部,就是这个栈帧的底部
暂时先看现在的栈的情况
到这里,新的栈帧开始了
sub esp, 0CCh ; int a, b 这里声明了两个int,所以esp减小8个字节来为a,b分配空间
mov dword ptr [esp+4], [ebp+12]; a=m
mov dword ptr [esp], [ebp+8]; b=n
这样,栈的情况变为:
ret 8 ; 返回,然后8是什么意思呢,就是参数占用的字节数,当返回后,esp-8,释放参数m,n的空间
通过ebp,能够很容易定位到上面的参数。当从func函数返回时,首先esp移动到栈帧底部(即释放局部变量),然后把上一个函数的栈帧底部指针弹出到ebp,再弹出返回地址到cs:ip上,esp继续移动划过参数,这样,ebp,esp就回到了调用函数前的状态,即现在恢复了原来的main的栈帧。