函数栈帧的创建和销毁(图文详细)

预备知识:

相关汇编命令

mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令


相关寄存器

eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器
esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址


什么是栈?

栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。

在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。
在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小

栈总是向下增长(由高地址向低地址)的。

相关知识

  • 1.每一次函数调用,都要为本次函数调在栈区上用开辟空间,就是函数栈帧的空间。
  • 2.这块空间的维护是使用了2个寄存器: espebp ,ebp记录的是栈底的地址,esp记录的是栈顶的地址。
  • 3.正在调用哪个函数,ebp,esp就维护哪个函数的栈帧在这里插入图片描述

函数栈帧

什么是函数栈帧

我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。
那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。

函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
1.函数参数和函数返回值
2.临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
3.保存上下文信息(包括在函数调用前后需要保持不变的寄存器)

下面用VS2013去演示,每个编译器产生的过程会有差异
演示代码:

#include <stdio.h>

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;
}

函数的调用堆栈

  • 首先按F10,进行编译,然后点击调试–>窗口–>调用堆栈请添加图片描述
  • 进入堆栈后可以看到main被调用请添加图片描述

这时,我们会困惑main函数在被谁调用

  • 继续按F10,让代码运行,当代码执行完时,会发现main函数被一个叫__tmainCRTStartup的函数调用请添加图片描述

这里要知道一点:main函数也是被其他函数调用的

  • __tmainCRTStartup函数也是被一个叫mainCRTStartup的函数调用请添加图片描述
  • 此时,我们可以获知一个大概的轮廓:请添加图片描述

转到反汇编

  • F10编译->单击右键->转到反汇编请添加图片描述

  • 然后就可以看见代码对应的汇编代码请添加图片描述

  • 在进入main之前,main就被__tmainCRTStartup调用,所以此时,走到了main函数中,所以__tmainCRTStartup的函数栈帧已经创建请添加图片描述

  • 1.执行00341410 push ebp 语句,把ebp的值压栈,同时esp指针向上移动,指向栈顶请添加图片描述

这里也可以通过监视窗口看到,执行push语句之后,esp的地址减小了,就说明esp向上移动了请添加图片描述
同时,在内存窗口中,也可以看到ebp的值被存到了esp指向的空间里·请添加图片描述

  • 2.执行00341411 mov ebp,esp ,这句的作用是:把esp的值赋给ebp请添加图片描述

此时esp,ebp的值相等请添加图片描述

  • 3.执行00341413 sub esp,0E4h 语句,给esp的值减去0E4h请添加图片描述

,这就意味着esp指向上面的某一块区域,此时espebp之间的空间,就是给main预开辟的空间请添加图片描述

  • 4.执行这段语句,在栈顶压入三个元素请添加图片描述

在这里插入图片描述

  • 5.0034141C lea edi,[ebp-0E4h] ,edi
  • 6.00341422 mov ecx,39h 把39h这个值赋给ecx
  • 7.00341427 mov eax,0CCCCCCCCh 0CCCCCCCCh赋给eax
  • 8.0034142C rep stos dword ptr es:[edi] 这句的意思是把从edi向下ecx(39h)大小的空间,改为eax的值,也就是0CCCCCCCCh请添加图片描述

此时main的栈帧已经真正意义上开辟好了在这里插入图片描述

  • 9.0034142E mov dword ptr [ebp-8],0Ah ,把0Ah的值放到[ebp-8]中去在这里插入图片描述

请添加图片描述
在程序中定义变量时进行了初始化,所以在相应位置把值存入栈中,如果没有初始化,栈中对应位置中的值就是cccccccccc,这也就是有时未初始化变量而导致打印烫烫烫烫烫烫烫烫烫烫烫烫的原因

  • 10.00341435 mov dword ptr [ebp-14h],14h ,把14h的值放到[ebp-14h]中去在这里插入图片描述

  • 11.0034143C mov dword ptr [ebp-20h],0 把0的值放到[ebp-20h]中去在这里插入图片描述

  • 12.00341443 mov eax,dword ptr [ebp-14h] [ebp-14h]的值放到eax里,00341446 push eax 再把eax压入栈中在这里插入图片描述

  • 13.00341447 mov ecx,dword ptr [ebp-8] [ebp-14h]的值放到eax里,0034144A push ecx 再把ecx压入栈中在这里插入图片描述

12.13.步实际上是在传参,先压b,再压a,参数是从右往左传的

  • 14.按F11执行0034144B call 00341096 语句,会把它下一条语句的地址压到栈顶

请添加图片描述

在这里插入图片描述

call的下一条语句压到栈顶的目的是记住当前位置,等调用完函数时,方便回来

15.再按F11,就进入到了add函数里
请添加图片描述


add函数内

  • 1.执行003413C0 push ebp ,把ebp压入栈顶,此时ebp正在维护主函数,所以压入栈顶的ebp是主函数的ebp请添加图片描述

  • 2.003413C1 mov ebp,esp 与主函数中的类似,这里不多介绍请添加图片描述

  • 3.003413C3 sub esp,0CCh esp地址减小0CCh大小,是在为add函数开辟函数栈帧请添加图片描述

  • 4.接下来连续push三个寄存器,与main函数中的相同,这里不多介绍

  • 5.紧接着四句话与main函数中的相同,从edi开始到ebpecx次赋值为eax也就是0CCCCCCCCh请添加图片描述

  • 6.003413DE mov dword ptr [ebp-8],0 把0放到ebp-8位置上请添加图片描述

  • 7.执行下三句:请添加图片描述

ebp+8的值赋给eax,再把ebp+0Ch加到eax上,最后把eax值赋给ebp-8,这就是执行z = x+y语句
请添加图片描述
这里值得思考的是,参数是怎么传的?
形参根本不是再add函数中创建的,而是回来找在调用add前压入栈顶的参数请添加图片描述
而说形参就是实参的临时拷贝是十分正确的请添加图片描述

  • 8.执行return语句,003413EE mov eax,dword ptr [ebp-8] ebp-8的值赋给eax,即把30存到eax

add函数栈帧的销毁

  • 1.执行三句pop语句请添加图片描述

请添加图片描述

  • 2.003413F4 mov esp,ebp ebp的值赋给esp请添加图片描述
  • 3.003413F6 pop ebp 在栈顶弹出一个元素,并将其值赋给ebp,因为此时栈顶元素是main的ebp,所以弹出后,ebp就回到原先在main中的位置请添加图片描述
  • 4.003413F7 ret ,这句的本质是:弹出栈顶元素,也就是call的下一条语句的地址,并且返回main函数中,call语句的下一条语句请添加图片描述
  • 5.00341450 add esp,8 esp地址值减8,即向下移动两个地址,把两个形参弹出栈请添加图片描述
  • 6.00341453 mov dword ptr [ebp-20h],eax ,将eax的值赋给ebp-20h中,ebp-20h就是c的位置,eax是在add中,将结果寄存到其中的那个寄存器

main函数栈帧的销毁

请添加图片描述

思考

现在,来思考几个问题:
1.局部变量是如何创建的?
2.为什么局部变量不初始化内容是随机的?
3.函数调用时参数时如何传递的?传参的顺序是怎样的?
4.函数的形参和实参分别是怎样实例化的?
5.函数的返回值是如何带会的?

1.局部变量就是在其作用域的栈内某一块空间被赋予这个变量的值
2.如果没有初始化,变量在栈的值是CCCCCCCC
3.传参是在调用函数的前一个语句时,创建新的形参压在栈顶,传参的顺序是从右到左在这里插入图片描述
4.函数的形参是赋值一个实参的值,将它压入栈顶,而实参就是主函数的栈帧中,将其值赋给某一地址指向的值
5.先把要返回的值放在寄存器中,因为寄存器不会随着栈帧而被销毁,然后在主函数中,会把寄存器中的值赋给接受变量的地址

  • 13
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

疯癫了的狗

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

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

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

打赏作者

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

抵扣说明:

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

余额充值