函数栈帧的创建与销毁

Posted 敲代码的小王

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了函数栈帧的创建与销毁相关的知识,希望对你有一定的参考价值。

栈帧的创建与销毁

什么是栈帧

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级以前的版本进行。

以上是关于函数栈帧的创建与销毁的主要内容,如果未能解决你的问题,请参考以下文章

函数栈帧的创建与销毁

函数栈帧的创建与销毁,带你了解代码底层原理

图解函数栈帧 - 函数的创建与销毁

函数栈帧的创建与销毁

C语言进阶 顶级神功! 函数栈帧的创建和销毁

函数栈帧的创建和销毁