【函数栈帧】想让面试官刮目相看—想进一步了解程序执行内存发生的变化—走过路过千万不要错过

前言

在这一次的文章中,我们会解决以下几个问题

  • 局部变量是怎么创建的?
  • 为什么局部变量不初始化时的值是随机的?
  • 函数是怎么传参的?传参的顺序是怎么样的?
  • 形参和实参是什么关系?
  • 函数调用结束后是怎么返回的?

在这里插入图片描述

预备工作

欲解决以上问题,施主还得沉下心来慢慢领会
首先介绍一下
寄存器

eax
ebx
ecx
edx

ebp
esp

这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。

由于vs2013以上的IDE封装的太好,看不到,所以这一次我们使用vs2013来调试观察

正式开始

首先我们先编写这样一段简单的代码

#define _CRT_SECURE_NO_WARNINGS 1
#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);
	printf("%d\n", c);
	return 0;
}

然后F10启动调试,右键反汇编我们可以看到

在vs2013中,main函数也是被其他函数()调用的。
在这里插入图片描述
现在我们来理解一下汇编代码
在这里插入图片描述

可以得出结论:经历了push操作之后在内存块里面压进了ebp

现在我们来看一下内存里的表现,到底有没有把ebp压进内存中
在这里插入图片描述
通过上图可以看到esp里存放了ebp的地址,所以上述结论是成立的

接下来我们继续下一步–mov ebp,esp
也就是把ebp移到esp的位置。
在这里插入图片描述
通过观察上图,可以看到确实如此
在这里插入图片描述
接下来我们继续下一步,sub esp,0E4h
就是将esp的值减去0E4h
在这里插入图片描述
esp的值减小,如下图
ebp和esp就为main函数预开辟了一块空间
在这里插入图片描述
现在我们继续后面的操作
在这里插入图片描述
可以看到函数栈帧里压进去了ebx,esi,edi三个寄存器
如下图:

在这里插入图片描述
我们接着后面的操作
在这里插入图片描述
可以理解为从edi开始将eax中的值(CCCCCCCC)每次拷贝dword(double word,一个word两个字节)重复执行ecx次.,拷贝到es:[edi]指向的地址(ebp)。简而言之,就是把edi到ebp这部分空间初始化为(CCCCCCCC)。
如下图:
在这里插入图片描述
我们继续接下来的操作,
在这里插入图片描述
mov eax,dword ptr [ebp-8],0Ah将0Ah(10)移到ebp-8的位置
同理,后面两句执行后的效果如下图:
在这里插入图片描述

现在来执行下面这个句子
在这里插入图片描述

我们可以这样理解,mov 将ebp-14h中的值(也就是a的值20)存到eax里,然后将eax 压栈进去,同时esp会往上移动维护栈
这即是在实现int a = 20;
在这里插入图片描述
同理可得,还会压进去ecx(里面存储了b的值10)
这即是在实现int b = 10;
在这里插入图片描述

执行完这四个句子之后
在这里插入图片描述

同时我们也可以观察到,在调用函数的时候我们是先写的a,然后是b,但是在传参的时候是先传的b,然后是a
在这里插入图片描述

再点击F11之后再次来到了add的位置,
在这里插入图片描述
执行push ebp,往栈顶压入ebp
在这里插入图片描述

执行mov ebp,esp
在这里插入图片描述
可以看到ebp和esp指向同一个位置
现在来执行sub esp,0CCh
在这里插入图片描述
可以看到esp又往低地址的地方移动,所以和为main
函数开辟函数栈帧一样,为add函数也开辟了函数栈帧
如下图:
在这里插入图片描述

现在往栈帧里面压进3个寄存器ebx,esi,edi
在这里插入图片描述

接下来我们继续看后面的指令
001813CC lea edi,[ebp+FFFFFF34h]
001813D2 mov ecx,33h
001813D7 mov eax,0CCCCCCCCh
001813DC rep stos dword ptr es:[edi]
在这里插入图片描述
与main函数里面的那一段代码的用处相同,就是把地址从edi到esp的值都用cccccccc来初始化

在这里插入图片描述
下面接着执行mov dword ptr [ebp-8],0
即是将0放到ebp-8的位置上,就是在实现我们写的int z = 0 这一句c语言代码
如下图:
在这里插入图片描述
可以通过监视和观察地址来验证一下
会发现我们确实将0放到ebp-8的位置上了
在这里插入图片描述
下面执行黄色框中的内容,我们可以看到此时ebp+8与ebp+12就是x与y的值,但是这两个值都是通过前面将ab的值直接拷贝到寄存器eax,ecx中得到的,我们就算改变了形参xy值所在的寄存器里面的值,a与b都不会改变。所以形参是实参的临时拷贝,形参的改变不会引起实参的变化
同时可以看到我们的形参不是在add函数内部创建的,而是回到了main函数中压栈压进去的空间去找。
在这里插入图片描述

首先我们待会儿要解决的问题是,临时变量出了函数作用域就会被销毁,那么是如何返回的呢?
现在让我们来观察返回的过程

在这里插入图片描述
可以看到是先把z中的保存到一个相当于全局变量的一个寄存器eax里面
然后我们接着执行mov esp,ebp
在这里插入图片描述
执行后ebp和esp指向同一个地方,就实现了对add函数开辟栈帧的回收
我们都知道程序从main函数开始,结束于main函数
那么add函数完了之后,我们怎么样回到main函数呢?

在这里插入图片描述

现在执行add函数里的最后一条指令 001813F7 ret
按F10之后我们发现程序来到了call指令的下一条指令的位置
在这里插入图片描述
在这里插入图片描述

好的现在程序是已经回到了main函数了
接着后面的操作 add esp,8
(就是把esp+8)
在这里插入图片描述
可以看到此时把形参xy分分配的空间也还给了操作系统
继续下一步操作 mov dword ptr [ebp-20h],eax
在这里插入图片描述
可以看到通过这条指令和前面将最后的结果存到eax的操作,我们就成功的带回了add函数的返回值

总结

现在来回答一下最开始的那几个问题

  • 局部变量是怎么创建的?
    首先为函数预开辟一块空间,然后局部变量在这一块栈帧里面分配一点空间

  • 为什么局部变量不初始化时的值是随机的?
    随机值是系统为函数开辟空间时,随机放到函数开辟空间里的值(CCCCCCCC),只要初始化,就会覆盖掉原来的值

  • 函数是怎么传参的?传参的顺序是怎么样的?
    当要调用函数时,已经将形式参数从右到左push到了函数栈帧里面

  • 形参和实参是什么关系?
    形参是实参的临时拷贝,形参的改变不会引起实参的改

  • 函数调用结束后是怎么返回的?
    在调用函数之前,就先保存call指令的下一条指令的地址和main函数中的ebp的地址,当函数调用结束时通过pop,ebp回到main函数里。然后通过ret指令我们回到了call指令的下一条指令,就回到了main函数里。

最后我是Maria,一个来自重庆的女孩,现在在读大二,希望和大家一起学习,一起进步!!!
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Mr Maria

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

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

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

打赏作者

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

抵扣说明:

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

余额充值