栈帧的创建与销毁
什么是栈帧
C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。
首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
注意:EBP指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念;ESP所指的栈帧顶部和系统栈的顶部是同一个位置。
常用的寄存器有 eax,ebx,ecx,edx,esp,ebp;
上代码
接下来我们用一段简单的程序,来观察栈帧的创建与销毁。
#include<stdio.h>
int add(int x, int y)
{
int c = 0;
c = x + y;
return c;
}
int main()
{
int a = 10;
int b = 20;
int c = 10;
c = add(a, b);
printf("%d", c);
return 0;
}
首先,当我调试起来时,查看调用堆栈,当程序走到最后一行时,可以发现,main函数是被一个名为__tmainCRTStartup的函数调用的,而函数__tmainCRTStartup 又是被函数mainCRTStartup调用的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-
前面讲过ebp指向的是当前栈帧的底,esp指向当前栈帧的顶部,由于main是被函数__tmainCRTStartup调用的,所以在这里我们先画出__tmainCRTStartup函数的栈帧。
接下来,重新调试代码,转到反编译我们可以看到
00241410 push ebp
00241411 mov ebp,esp
00241413 sub esp,0E4h
00241419 push ebx
0024141A push esi
0024141B push edi
0024141C lea edi,[ebp-0E4h]
00241422 mov ecx,39h
00241427 mov eax,0CCCCCCCCh
0024142C rep stos dword ptr es:[edi]
00241410 push ebp //将ebp的值压入栈中
00241411 mov ebp,esp //把esp的值赋值给ebp
00241413 sub esp,0E4h //将esp的值减去0E4h
//此时也就完成了对main函数空间的开辟(0E4h为开辟空间的大小)
空间开辟好了接下来就该对空间进行初始化了
00241419 push ebx //将ebx esi esi中的值压入栈中
0024141A push esi
0024141B push edi
0024141C lea edi,[ebp-0E4h] //下面的意思是,将为main函数开辟的空间初始化为0CCCCCCCCh
00241422 mov ecx,39h
00241427 mov eax,0CCCCCCCCh
0024142C rep stos dword ptr es:[edi]
通过查看内存我们可以看到,此时已经初始化完成。
为main函数中的变量开辟空间
int a = 10;
0024142E mov dword ptr [ebp-8],0Ah //将0Ah(a--10)放到ebp-8的地址处
int b = 20;
00241435 mov dword ptr [ebp-14h],14h //将14h(b--20)放到ebp-14h的地址处
int c = 0;
0024143C mov dword ptr [ebp-20h],0 //将0(c--0)放到ebp-20的地址处
在内存中可以看到
c = add(a, b);
00241443 mov eax,dword ptr [ebp-14h] //将ebp-14h(b)处的值放到eax寄存器中
00241446 push eax //将eax的值压入栈中
00241447 mov ecx,dword ptr [ebp-8] //将ebp-8(a)处的值放到寄存器ecx中
0024144A push ecx //将ecx的值压入栈中
0024144B call 002410E6//调用此地址处的函数,并将下一条指令的地址入栈00241450
00241450 add esp,8
通过这几行代码我们可以看到,在调用函数之前参数已经传了过去,在函数传参时,是从右向左开始传的,这里还充分证明了形参是实参的一份临时拷贝。此时我们看下现在的栈。
进入函数
0024144B call 002410E6//调用函数
当调试箭头指向这一行时,我们可以按f11进入到函数中。
//和前面一样,为add函数开辟空间初始化
002413C0 push ebp //将ebp中的值压入栈中
002413C1 mov ebp,esp //将esp的值赋值给ebp
002413C3 sub esp,0CCh //将esp减去occh
002413C9 push ebx //ebx的值压入栈中
002413CA push esi //esi的值压入栈中
002413CB push edi //edi的值压入栈中
002413CC lea edi,[ebp-0CCh] //初始化edp~(edp-occh)
002413D2 mov ecx,33h
002413D7 mov eax,0CCCCCCCCh
002413DC rep stos dword ptr es:[edi]
此时栈帧如下
进行函数运算
int c = 0;
002413DE mov dword ptr [ebp-8],0 //将edp-8(c)出赋值为0
c = x + y;
002413E5 mov eax,dword ptr [ebp+8] //将edp+8(x)处的值放到寄存器eax中
002413E8 add eax,dword ptr [ebp+0Ch] //将edp+0ch(y)处的值与寄存器eax中的值相加
002413EB mov dword ptr [ebp-8],eax //将eax中的值赋值给ebp-8(c)
return c;
002413EE mov eax,dword ptr [ebp-8]//将ebp-8位置处的数据放到寄存器eax中
栈帧的销毁
002413F1 pop edi //弹出栈顶放到寄存器edi
002413F2 pop esi //弹出栈顶放到寄存器esi
002413F3 pop ebx //弹出栈顶放到寄存器ebx
002413F4 mov esp,ebp //拷贝ebp的值到esp
002413F6 pop ebp //弹出栈顶放到寄存器ebp
002413F7 ret //相当于pop,会出栈一次,程序将跳转到栈中地址的位置
函数add函数的栈帧被销毁,程序跳转到了函数调用前的下行处到此,函数栈帧的创建于销毁就结束了。
后面的调用printf函数可以参考上面add函数的过程,在这将不再描述。
00241450 add esp,8 //寄存器esp+8
00241453 mov dword ptr [ebp-20h],eax //将eax中的值赋值到ebp-20h地址处(变量c的地址处)
printf("%d", c);
00241456 mov esi,esp
00241458 mov eax,dword ptr [ebp-20h]
0024145B push eax
0024145C push 245858h
00241461 call dword ptr ds:[00249114h]
00241467 add esp,8
0024146A cmp esi,esp
0024146C call 0024113B
return 0;
00241471 xor eax,eax
}
以上全部代码是在vs2013中进行的,不同的编译器还是有些差异,但是思想都是一致的,建议使用vc6++,或者使用vs2013级以前的版本进行。
8
0024146A cmp esi,esp
0024146C call 0024113B
return 0;
00241471 xor eax,eax
}
以上全部代码是在vs2013中进行的,不同的编译器还是有些差异,但是思想都是一致的,建议使用vc6++,或者使用vs2013级以前的版本进行。