【内功】函数栈帧的创建与销毁

问题引入

  • 局部变量是如何创建的?
  • 为什么局部变量不初始化内容是随机的?
  • 函数调用时参数如何传递?传参的顺序是怎样的?
  • 函数的形参和实参分别是怎样实例化的?
  • 函数的返回值是如何带回的?

我们带着这几个问题来进入今天的正题。

一、什么是函数栈帧

函数栈帧(stack frame):函数调用过程中在程序的调用栈(call stack)所开辟的空间。

这些空间是用来存放:

  • 函数参数和函数返回值
  • 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
  • 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)

下图是程序执行的简单步骤:


二、函数栈帧的创建和销毁解析

1.何为栈?

被定义为一种特殊的容器。

用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop)

但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)大家可以想象弹夹,先装进去的子弹在最内层,后装进去的子弹在外层,后装的子弹先被设计出膛。

在计算机系统中,栈则是一个具有以上属性的动态内存区域。可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。

在经典的操作系统中,栈总是向下增长(由高地址向低地址)的

2.认识相关寄存器和汇编指令

3.具体实现

3.1.具体实现前的认知

首先,我们需要明确:

  • 每个函数调用,都要在栈上开辟空间,这个空间就是函数栈帧
  • 这个空间由ebp和esp来维护

  • 不同编译器对于函数栈帧的销毁与创建略有差异
  • 此篇文章是在vs2022 x86环境下实现的

3.2.函数调用堆栈

int Mul(int x, int y) {
	int z = 0;
	z = x * y;
	return z;
}

int main() {
	int a = 10;
	int b = 11;
	int c = 0;

	c = Mul(a, b);

	printf("%d\n", c);
	return 0;
}

我们可以看到:

main()函数调用之前,是由invoke_main()函数来调用main()函数。

invoke_main()函数之前的函数调用我们就暂时不考虑了。

那么,我们就可以有如下认知:

invoke_main() main() Mul()都有自己的函数栈帧

3.3.函数栈帧的创建

我们将main()转到返汇编,如下图:

接下来我们一行一行分析

3.3.1.进入main()前的准备
//压入ebp 现在ebp存的是invoke_main()的函数栈帧的ebp esp-4
008E18D0  push        ebp  
//移动esp到压入ebp的所在位置 将esp的值存到ebp中 产生了main()的ebp 而这个值就是invoke_main()的esp
008E18D1  mov         ebp,esp
//esp-0E4h esp向上移动0E4h的字节,这是main()的esp,产生了main()的函数栈帧
008E18D3  sub         esp,0E4h  
//将ebx压入栈 esp-4
008E18D9  push        ebx  
//将esi压入栈 esp-4
008E18DA  push        esi  
//将edi压入栈 esp-4
008E18DB  push        edi  
//上面3条指令保存了3个寄存器的值在栈区 这3个寄存器的在函数随后执行中可能会被修改 
//所以先保存寄存器原来的值 以便在退出函数时恢复

//把[ebp-24h]的地址放在edi中
008E18DC  lea         edi,[ebp-24h]  
//把9放在ecx中
008E18DF  mov         ecx,9  
//把0CCCCCCCCh放在eax中
008E18E4  mov         eax,0CCCCCCCCh  
//将栈帧空间初始化为0xCC
008E18E9  rep stos    dword ptr es:[edi]  
//8EC00Eh赋给ecx
008E18EB  mov         ecx,8EC00Eh  
//调用地址为 008E132F 的函数
008E18F0  call        008E132F  

 ​​

小tips:为什么我们未初始化字符数组,会打印出烫烫烫?

0xCCCC(两 个连续排列的0xCC)的汉字编码就是“烫”

3.3.2.main()核心代码
//局部的变量的创建和初始化 在函数栈中中进行

//将10储存到[ebp-8]的位置
	int a = 10;
000E18F5  mov         dword ptr [ebp-8],0Ah  
//将11储存到[ebp-14h]的位置
	int b = 11;
000E18FC  mov         dword ptr [ebp-14h],0Bh  
//将0储存到[ebp-20h]
	int c = 0;
000E1903  mov         dword ptr [ebp-20h],0  

//调用Mul()传参 
	c = Mul(a, b);
//把地址为[ebp-14h]的值传给eax 即把b传给eax
000E190A  mov         eax,dword ptr [ebp-14h]  
//将eax的值压栈 esp-4
000E190D  push        eax  
//把地址为[ebp-8]的值传给ecx 即把a传给ecx
000E190E  mov         ecx,dword ptr [ebp-8]  
//将ecx的值压栈 esp-4
000E1911  push        ecx  
//跳转调用函数 函数地址为 000E104B 
000E1912  call        000E104B  
//添加保存call下一条指令的地址
000E1917  add         esp,8  
//把eax存储到[ebp-20h]的位置
000E191A  mov         dword ptr [ebp-20h],eax  

3.4.函数调用具体实现

int Mul(int x, int y) {
//将main()函数栈帧的ebp保存 esp-4
000E1790  push        ebp  
//将main()函数栈帧的esp赋给ebp 现在ebp就是Mul()的ebp
000E1791  mov         ebp,esp  
//将esp移动到0CCh的位置 现在esp就是Mul()的esp
000E1793  sub         esp,0CCh  
//将ebx压入栈 esp-4
000E1799  push        ebx
//将esi压入栈 esp-4  
000E179A  push        esi  
//将edi压入栈 esp-4
000E179B  push        edi  
//把[ebp-0Ch]存入edi中
000E179C  lea         edi,[ebp-0Ch]  
//把3放在ecx中
000E179F  mov         ecx,3  
//把0CCCCCCCCh放入eax中 
000E17A4  mov         eax,0CCCCCCCCh  
//将栈帧空间初始化为0xCC
000E17A9  rep stos    dword ptr es:[edi]  
//将0EC00Eh赋给ecx
000E17AB  mov         ecx,0EC00Eh  
//调用000E132F这个地址的函数
000E17B0  call        000E132F  
	int c = 0;
//把[ebp-8]这个位置初始化成0 创建了z变量
000E17B5  mov         dword ptr [ebp-8],0  
	c = x * y;
//把[ebp+8]地址的值存入eax
000E17BC  mov         eax,dword ptr [ebp+8]  
//把[ebp+0Ch]地址的值与eax的值相乘 并存入eax
000E17BF  imul        eax,dword ptr [ebp+0Ch]  
//将eax的结果存入[ebp-8]这个地址 即放到z中
000E17C3  mov         dword ptr [ebp-8],eax  
	return c;
//将ebp-8地址的值放在eax中 即把z的值存储到eax中
//这里是想通过eax寄存器带回计算的结果 做函数的返回值
000E17C6  mov         eax,dword ptr [ebp-8]  
}

3.5.函数栈帧的销毁

//将之前存入edi的值弹出栈 esp+4
009D17C9  pop         edi  
//将之前存入esi的值弹出栈 esp+4
009D17CA  pop         esi  
//将之前存入ebx的值弹出栈 esp+4
009D17CB  pop         ebx  
//将esp地址加0CCh 移动esp 清理栈帧空间
009D17CC  add         esp,0CCh  
//比较ebp和esp 检查栈的完整性 确保在函数执行过程中栈的使用没有出现异常
009D17D2  cmp         ebp,esp  
//调用009D1253这个地址的函数 
009D17D4  call        009D1253  
//将ebp的值赋给esp 相当于回收了Mul()的栈帧空间
009D17D9  mov         esp,ebp  
//弹出栈顶的值存放到ebp 栈顶此时的值恰好就是main()的ebp
//esp+4 此时恢复了main()的栈帧维护 esp指向main()函数栈帧的栈顶
//ebp指向了main()函数栈帧的栈底
009D17DB  pop         ebp  
//返回 从栈顶弹出一个值 此时栈顶的值就是call指令下一条指令的地址 此时esp+4
//然后直接跳转到call指令下一条指令的地址处 继续往下执行
009D17DC  ret  

回到这里

三、回答引入的问题

  • 局部变量是如何创建的?

  给函数分配好栈帧空间后,通过控制esp指针移动,将局部变量的值压入栈,保存在相应的寄存器中

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

在创建函数栈帧时,给空间中随机值0xCCCCCCCC

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

将参数值压入栈,存入寄存器,由寄存器移动传递参数,顺序从右至左传递

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

通过寄存器,将实参临时拷贝一份,开辟出独立空间存放形参数值,也就是说形参只有在被调用的时候存放实参传过来的值时才在栈区开辟函数栈帧空间

  • 函数的返回值是如何带回的?

寄存器保存着函数调用的下一条指令的地址,函数生命周期结束后,寄存器会通过保存的地址带回返回值

注意:本文只针对于栈空间来讨论

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vect.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值