C语言函数栈帧详解(下)


前言

继上文对于函数栈帧的基础知识的讲解,本文我们将继续探究对于从汇编的角度详解函数栈帧的创建和销毁


一、详解函数栈帧的创建和销毁

1.入栈和出栈

入栈(压栈):先将栈顶指针向上移动四字节的大小空间,再将寄存器的数据放入那四字节空间。这里的向上移动是指向低地址处移动。

入栈指令:push a。

出栈:将栈顶指针向下移动四字节,这里的向下是往低地址处移动四个字节的空间。并将这四个字节的数据放入某个寄存器中。

出栈指令:pop a。

我们可以理解就像子弹的弹夹一样,怎么压子弹和怎么把子弹退出来,如下图所示

在这里插入图片描述

在这其中,对于这块空间使用了两个寄存器,ebp和esp,在上面的预备知识中我们讲到,ebp 记录的是栈底的地址, esp 记录的是栈顶的地址,并且栈中的地址都是由高地址向低地址延申的。

2.main()函数调用

在这里插入图片描述

在调试中,打开调用堆栈,我们可以发现,add()函数被main()函数调用,而main 函数是由invoke_main 函数来调用的。在 invoke_main 函数之前的函数调用我们就暂时不考虑了

3.细说汇编

以下这个程序运行时的汇编代码,我将对列出来的相关汇编做一下详细的解释

int add(int x, int y)
{
 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]  
 mov         ecx,0BBC003h  
 call        00BB131B  
	int sum = 0;
 mov         dword ptr [ebp-8],0  
	sum = x + y;
 mov         eax,dword ptr [ebp+8]  
 add         eax,dword ptr [ebp+0Ch]  
 mov         dword ptr [ebp-8],eax  
	return sum;
 mov         eax,dword ptr [ebp-8]  
}
 pop         edi  
 pop         esi  
 pop         ebx  
 add         esp,0CCh  
 cmp         ebp,esp  
 call        00BB1244  
 mov         esp,ebp  
 pop         ebp  
 ret    
int main()
{
 push        ebp  
 mov         ebp,esp  
 sub         esp,0E4h  
 push        ebx  
 push        esi  
 push        edi  
 lea         edi,[ebp-24h]  
 mov         ecx,9  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 mov         ecx,0BBC003h  
 call        00BB131B  
	int a = 10;
 mov         dword ptr [ebp-8],0Ah  
	int b = 20;
 mov         dword ptr [ebp-14h],14h  
	int c = 0;
 mov         dword ptr [ebp-20h],0  
	add(a, b);
 mov         eax,dword ptr [ebp-14h]  
 push        eax  
 mov         ecx,dword ptr [ebp-8]  
 push        ecx  
 call        00BB1023  
 add         esp,8  
	printf("%d", c);
 mov         eax,dword ptr [ebp-20h]  
 push        eax  
 push        0BB7B30h  
 call        00BB10D2  
 add         esp,8  
	return 0;
 xor         eax,eax  
}
 pop         edi  
 pop         esi  
 pop         ebx  
 add         esp,0E4h  
 cmp         ebp,esp  
 call        00BB1244  
 mov         esp,ebp  
 pop         ebp  
 ret  

main函数的栈帧创建

    1. 将ebp压入栈,存放的是invoke_main函数栈帧的ebp
push        ebp 
    1. mov ebp,esp
mov         ebp,esp 
    1. sub会让esp中的地址减去一个16进制数字0xe4,产生新的esp,此时的esp是main函数栈帧的esp,此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数中的局部变量,临时数据,已经调试信息等。
sub         esp,0E4h 
    1. 将ebx esi edi 压入栈
 push        ebx  
 push        esi  
 push        edi  
    1. 这四句话主要是将main函数的栈帧的每个字节都初始化为0xCC
 lea         edi,[ebp-24h]  
 mov         ecx,9  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]

热知识:为什么出现这么多‘烫烫烫’,是因为在main函数调用的时候栈区开辟的空间每个字节都被初始化为0XCC,且arr数组没有被初始化,又恰好在此空间上开辟,所以0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”。
在这里插入图片描述

    1. 以下是对局部变量的创建与初始化
	int a = 10;//将10存储到ebp-8的地址处,ebp-8的位置存放的是a变量
 mov         dword ptr [ebp-8],0Ah  
	int b = 20; //将20存储到ebp-14h的地址处,ebp-14h的位置存放的是b变量
 mov         dword ptr [ebp-14h],14h  
	int c = 0;//将0存储到ebp-20h的地址处,ebp-20h的位置存放的是ret变量
 mov         dword ptr [ebp-20h],0 

add函数创建并初始化栈帧

这里的创建栈帧与上面的main函数大同小异,我们直接将add函数语句内的语句执行

    1. 将0放在ebp-8的地址处,其实就是创建并初始化sum
int sum = 0;
mov         dword ptr [ebp-8],0 
  • 2.将新的地址存到sum的地址中
sum = x + y;
mov         eax,dword ptr [ebp+8]//将ebp+8地址处的数字存储到eax中
add         eax,dword ptr [ebp+0Ch]//将ebp+12(即ebp+0Ch)地址处的数字加到eax寄存中
mov         dword ptr [ebp-8],eax//将eax的结果保存到ebp-8的地址处,其实就是放到sum中
    1. 将处理的结果返回
return sum;
mov         eax,dword ptr [ebp-8]  //将ebp-8地址处的值放在eax中

add函数栈帧的销毁

 pop         edi  
 pop         esi  
 pop         ebx  
 add         esp,0CCh  
 cmp         ebp,esp  
 call        00BB1244  
 mov         esp,ebp  
 pop         ebp  
 ret    

在这里插入图片描述
将edi、esi、ebx出栈
在这里插入图片描述
0CCh是Add函数栈帧的大小,所以esp向下移动到dbp的位置,之后pop ebp,由于栈顶指向的是main函数栈帧的栈底,因此出栈ebp指向main函数栈帧的栈底。
之后的指令就和上述讲的差不多了,这里就不细细讲解了

4. 总结

最后,通过本文的了解,我们就可以对很多问题有了一个答案

函数是如何调用的?

答:先传参,也就是把参数的值分别放在寄存器中,然后再push压入栈中;把主调函数ebp的值和下一条指令的地址push压入栈中,随后进入调用的函数中,创建函数栈帧并初始化,然后执行函数内的语句。

为什么局部变量若不初始化,内容是随机的?

答:函数栈帧创建后会自动将空间中存储的值全部初始化为一个特定值(如VS2019下为0xcccccccc),编译器不同值也不同。

函数调用时参数时如何传递的?传参的顺序是怎样的?

其实传参就是把参数push到栈帧空间中,传参时先压入的是后面参数的值,(参数,参数,…)从右往左压入。

函数的形参和实参分别是怎样实例化的?

形参通过寄存器的值压栈创建,而实参通过 ebp 存储的地址进行偏移,由编译器决定一块未使用空间创建。

形参和实参是什么关系?

形参是实参的临时拷贝,只是值相同却是不同的地址

局部变量是如何创建的?

函数栈帧创建后编译器分配由高到低地址创建变量


感谢观看,你的支持就是对我的最大鼓励,有什么问题欢迎在评论区指正,谢谢!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值