函数栈帧的创建与销毁


在这里插入图片描述

今天写一个两数相加函数,由此来简单探讨下函数栈帧的创建与销毁。

栈帧

首先来说明一下什么是栈?

在数据结构中,栈是一个线性表,限定在表尾进行插入和删除的操作,
他按照后进先出的原则来存储和释放数据。
最先进入栈的数据压入栈底,称为压栈或进栈,
最后进来的数据为栈顶,需要读取数据时从栈顶读取,称为弹栈或出栈。
栈数据结构如同手枪弹夹,最先放入弹夹的子弹,最后才被射出去。

在这里插入图片描述

在计算机系统中,栈可以称为栈内存,是一个动态内存区域,
存储函数内部的局部变量和所调用函数的参数值。
栈用于维护函数调用的上下文。

而栈帧是什么呢?
我们将由下面的代码来说明:

int Add(int x, int y)
{
	int z=0;
	z = x + y;
	return z;
}

int main()
{
	int a = 10;
	int b = 20;
	int c=0;
	c = Add(a, b);
	return 0;
}

首先,在程序跑起来时,会有一个入口函数mainCRTStartup,然后该函数会调用main函数;因此,首先会在栈上为函数开辟mainCRTStartup开辟一段空间。
1.在系统中程序执行时,栈都是从高地址往低地址增长的;
2.一个函数的栈帧用ebp和esp这两个寄存器来划定范围,ebp指向当前栈帧的底部,esp始终指向当前栈帧的顶部;

在这里插入图片描述
下面我们直接进入main函数查看反汇编代码:
在这里插入图片描述

进入main函数的栈帧


 int main()
{
//重新调整esp和ebp的指向,对齐当前的栈帧的栈顶与栈底。
push        ebp  
mov         ebp,esp  
sub         esp,0E4h  
//
push        ebx  
push        esi  
push        edi  
//
lea         edi,[ebp-0E4h]  
mov         ecx,39h  
mov         eax,0CCCCCCCCh  
rep stos    dword ptr es:[edi] 

接下来我们逐行分析:

push        ebp 

push为压栈操作,将ebp的值压入栈顶,此时esp也会实时追随指向栈顶,如图:
在这里插入图片描述

mov         ebp,esp  

mov为移动指令,将esp移入ebp中,即让ebp指向esp的位置,
在这里插入图片描述

sub         esp,0E4h

sub为减指令,让esp减去E4(16进制数)个字节数,让esp指向了esp-0E4h的位置处。此时由ebp和esp所维护的内存空间即是main函数的栈帧大小:
在这里插入图片描述

push        ebx  
push        esi  
push        edi  

压入三个寄存器
ebx 是"基地址"(base)寄存器, 在内存寻址时存放基地址。
esi/edi分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
在这里插入图片描述

lea         edi,[ebp-0E4h]  

lea(load effective address):加载有效地址,将ebp-0E4h的地址存放到edi处


mov         ecx,39h  
mov         eax,0CCCCCCCCh  
rep stos    dword ptr es:[edi]  

eax 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
ecx 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
该三行汇编代码的意思是:将39h(=E4/4)给ecx,将0cccccccch给eax;
从edi开始的往下的ecx行的所有内存填充eax:
在这里插入图片描述
接后续反汇编代码

int a = 10;
mov         dword ptr [ebp-8],0Ah  
	int b = 20;
mov         dword ptr [ebp-14h],14h  
	int ret = 0;
mov         dword ptr [ebp-20h],0  
	ret = Add(a,b);
mov         eax,dword ptr [ebp-14h]  
push        eax  
mov         ecx,dword ptr [ebp-8]  
push        ecx  
call        010F10E6  
add         esp,8  
mov         dword ptr [ebp-20h],eax  	
	return 0;
xor         eax,eax
}

继续逐过程分析:

int a = 10;
mov         dword ptr [ebp-8],0Ah  

在[ebp-8]指针所指的4字节内存空间中放入a的值:0Ah(十进制=10);

	int b = 20;
mov         dword ptr [ebp-14h],14h  
	int ret = 0;
mov         dword ptr [ebp-20h],0  

与上代码同理
查看内存
在这里插入图片描述
在这里插入图片描述
这样,a,b,c的值都存入了main的栈帧中。
下面进入add函数

	c=Add(a, b);
 mov         eax,dword ptr [ebp-14h]  
 push        eax  
 mov         ecx,dword ptr [ebp-8]  
 push        ecx  

在进入函数前,先将存在[ebp-14h](b的值) 内存空间中的值(b的值)交给eax,然后压栈, [ebp-8]内存空间中的值(a的值)交给ecx,随后压栈。这个步骤便是函数的传参 注意,函数的传参是从右往左的,先拷贝b的值压栈,再拷贝a的值压栈
在这里插入图片描述

继续往下走:

 call        006910B4  

call为调用指令,他将调用处于006910B4处的指令。同时call指令将下一行指令的地址进行压栈
注意此时在反汇编界面右击打开“显示地址
在这里插入图片描述
在这里插入图片描述
注意call下一行指令的地址为006917F7,这一地址作为值进行压栈,esp自动跟随指向栈顶。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这样在函数调用结束后就能自动进入call之后的指令了!

调用Add函数

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,33h  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
	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]  
}
 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret  

逐步分析:
1)

 push        ebp        

和main函数开始时的汇编代码一致:
首先push(压栈)ebp,此时的ebp的值为之前指向main函数栈底的地址,此刻esp依旧自动指向栈顶;
在这里插入图片描述
在这里插入图片描述
esp中存放着main栈帧的ebp的值
在这里插入图片描述

2)

mov         ebp,esp

将esp的值给ebp,时ebp也指向同一位置:
在这里插入图片描述
3)

 sub         esp,0CCh  
 push        ebx  
 push        esi  
 push        edi  

esp减去0cch个字节数,再接连压入ebx,esi,edi

4)

 lea         edi,[ebp-0Ch]  
 mov         ecx,33h
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]

同样再Add的栈帧中放入0CCCCCCCCh,共放入33h行:
在这里插入图片描述
5)

	int z=0;
 mov         dword ptr [ebp-8],0  
	z = x + y;
 mov         eax,dword ptr [ebp+8]  
 add         eax,dword ptr [ebp+0Ch]  

将0存入ebp-8的位置中,即z的内存空间的位置,再将ebp+8的值(实参a的拷贝)给eax,然后将eax与ebp+0Ch的值(实参b的拷贝)相加:
在这里插入图片描述
6)

 mov         dword ptr [ebp-8],eax  

将得到eax的值放到z的内存空间中。

7)

	return z;
 mov         eax,dword ptr [ebp-8]  

返回z的值,将z位置中的值放入eax中。

8)

 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret  

将edi,esi,ebx相继弹栈,再把ebp的值给esp,使esp指向Add栈帧的栈底位置,实现add的回收。此时完成了Add函数的调用
在这里插入图片描述
pop ebp 表示将栈顶弹出的值放到ebp中,栈顶此时放的是main函数的栈底指针,于是ebp可顺利找回main函数栈底的位置去了。
最后的ret会使指令跳到栈顶esp所指的地址中去,也就是之前留下的call指令的下一行指令的地址
在这里插入图片描述

返回main函数

 call        006910B4  
 add         esp,8  
 mov         dword ptr [ebp-20h],eax  
	return 0;

在这里插入图片描述

这时来到了call指令的下一行:
在这里插入图片描述

 add         esp,8 

为esp加8个字节,意味着实参a,b的拷贝相继销毁:

在这里插入图片描述

mov         dword ptr [ebp-20h],eax

将之前函数的返回值eax放入main栈帧中的c开辟的空间中。
在这里插入图片描述
在这里插入图片描述

最后main函数调用结束。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值