【10 函数栈帧的创建和销毁】

目录

  1. 什么是函数栈帧
  2. 理解函数栈帧能解决什么问题?
  3. 函数栈帧的创建和销毁解析
  4. 第2点问题解析

1. 什么是函数栈帧

函数栈帧(stack frame): 就是函数调用过程中的调用栈(call stack)所开辟的空间,这些空间是用来存放

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

2. 理解函数栈帧能解决什么问题

可以理解以下内容:

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

3. 函数栈帧的创建和销毁解析

3.1 什么是栈

栈(stack)是现在计算机最重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,也没有局部变量
栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),栈必须遵守一个规则,先入栈的数据后出栈
在经典的操作系统中,栈总是向下增长的(由高地址向低地址)的

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

相关寄存器

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

汇编指令

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

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

  1. 每一次函数调用,都要为本次调用开辟空间,就是函数栈帧的空间
  2. 这块空间维护用的是两个寄存器,esp和ebp,ebp记录的是栈底的地址,esp记录的是栈顶的地址

在这里插入图片描述
调用堆栈
演示代码:

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

int main()
{
	int a = 3;
	int b = 5;
	int ret = 0;
	ret = Add(a, b);
	printf("%d\n", ret);
	return 0;
}

在函数内加一个断点,打开调用堆栈窗口,可以反应函数的调用逻辑。执行到main函数右括号返回调用main函数处观察

在这里插入图片描述在这里插入图片描述

main函数也是被invoke_main函数调用的,它最初是被mainCRTStartup函数调用的,而main函数内部调用了add函数

准备
为了使干扰不要太多,可以右键项目打开配置属性,关闭以下选项,去除编译器附加的代码。同时,将程序改为32位,也就是x86
在这里插入图片描述
反汇编
运行到main函数第一行,鼠标右键转到反汇编
在这里插入图片描述在这里插入图片描述
栈帧的创建
接下来一步步拆解代码:

00151840 push ebp
//把ebp寄存器的值压入栈中,此时ebp存放的是调用main函数函数的ebp
00151841 mov ebp,esp
//move指令将esp存到ebp中,相当于产生main函数的ebp,就是invokemain的esp
00151843 sub esp,0E4h
//esp减去0xe4h,产生新的esp,这块空间就是维护main函数准备的
00151849 push ebx
0015184A push esi
0015184B push edi
//保存3个寄存器的值,以便后面恢复

打开监视和内存窗口,输入esp和ebp的地址观察
在这里插入图片描述在这里插入图片描述

  • 首先执行push ebp,esp的地址会变小4

在这里插入图片描述在这里插入图片描述

  • 执行move ebp,esp, ebp和esp的值相同
    在这里插入图片描述在这里插入图片描述* sub esp,0E4h ,esp往上移,产生一段mian函数的空间
    在这里插入图片描述在这里插入图片描述
  • push ebx,esi,edi, esp减去3个4
    在这里插入图片描述在这里插入图片描述

//下面的代码是在初始化main函数的栈帧空间。
//1. 先把ebp-24h的地址,放在edi中
//2. 把9放在ecx中
//3. 把0xCCCCCCCC放在eax中
//4. 将从edp-0x2h到ebp这一段的内存的每个字节都初始化为0xCC
0015184C lea edi,[ebp-24h]
0015184F mov ecx,9
00151854 mov eax,0CCCCCCCCh
00151859 rep stos dword ptr es:[edi]

  • lea全称加载有效地址,将ebp-24h的地址,ecx赋值为9,eax值为00CCCCCCCCh
    在这里插入图片描述
  • rep stos的作用,将edi往下ecx个内容的值都修改为eax的值,打开内存窗口观察效果
    在这里插入图片描述在这里插入图片描述
    关于0xcccc,它的汉字编码是烫,于是被初始化的内容都默认显示为文本烫

接下来的内容开始进入main函数的代码,初始化局部变量
int a = 3;
0015185B mov dword ptr [ebp-8],3
int b = 5;
00151862 mov dword ptr [ebp-14h],5
int ret = 0;
00151869 mov dword ptr [ebp-20h],0

  • 将ebp-8的值改为3,ebp-14赋值5,ebp-20为0
    在这里插入图片描述
    变量中间空两个元素,这是根据不同的编译器来的

//开始准备调用add函数
ret = Add(a, b);
00151870 mov eax,dword ptr [ebp-14h]
00151873 push eax
00151874 mov ecx,dword ptr [ebp-8]
00151877 push ecx
00151878 call 001510B9
0015187D add esp,8
00151880 mov dword ptr [ebp-20h],eax

  • 第一步,传参,将ebp-14h里存的值给eax,也就是上一步赋值的5,然后压入eax,ebp-8的值3给ecx,然后压入ecx
    在这里插入图片描述在这里插入图片描述

  • 按F11进入add函数,这时候观察栈顶,压入保存了add函数下一条代码的地址,因为add函数执行完后返回要执行add函数之后下一条代码

在这里插入图片描述在这里插入图片描述

小端存储,0015187d,刚好就是上面call之后的一条代码,进入跳转代码,继续F11

00151780 push ebp
00151781 mov ebp,esp
00151783 sub esp,0CCh
00151789 push ebx
0015178A push esi
0015178B push edi

这部分和main函数开辟空间的处理一致,直接做出图示:

在这里插入图片描述在这里插入图片描述

开始add函数的计算过程并返回
int z = 0;
0015178C mov dword ptr [ebp-8],0
z = x + y;
00151793 mov eax,dword ptr [ebp+8]
00151796 add eax,dword ptr [ebp+0Ch]
00151799 mov dword ptr [ebp-8],eax
return z;
0015179C mov eax,dword ptr [ebp-8]

  • z=0,ebp-8的位置赋为0
    在这里插入图片描述* z=x+y,先吧ebp+8的值,下图蓝色标注,也就是3给eax,然后eax加上ebp+c,也就是传进来的5的位置,然后将计算结果存到ebp-8

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

  • return z, 将ebp-8计算结果的值保存到eax寄存器中
    在这里插入图片描述

函数栈帧的销毁

0015179F pop edi
001517A0 pop esi
001517A1 pop ebx
001517A2 mov esp,ebp
001517A4 pop ebp
001517A5 ret

  • 三次pop,esp增加,弹出edi到edi寄存器中…
    在这里插入图片描述

  • esp等于ebp
    在这里插入图片描述

  • 弹出ebp到ebp寄存器,ebp中保存的正是main函数的ebp地址,同时esp+4
    在这里插入图片描述

  • 执行ret指令,此时esp栈顶保存的是add函数下一条指令的地址,ret的作用就是弹出并跳转到此地址处
    回到了add函数下一条指令处在这里插入图片描述

  • add esp,8 , 销毁拷贝的形式参数
    在这里插入图片描述在这里插入图片描述

  • mov ebp-20,eax, eax的值给ebp-20地址,eax的值保存的是add函数的计算结果,刚好ebp-20就是main函数里申请的ret的地址
    在这里插入图片描述在这里插入图片描述

接下来的原理相似,不过多赘述

  • 函数返回对象是内置类型时,一般是通过寄存器带回的,返回类型如果较大时,会在主调函数开辟空间,把地址隐式传递给被调函数,被调函数通过地址找到主调函数预留空间,将返回值保存到主调函数

这就是函数栈帧的销毁与创建的过程

4. 第2点解析

  • 局部变量是在函数栈空间开辟后,先初始化空间为cccc,然后把cccc赋值为局部变量的值。如果不初始化局部变量的值,很有可能存的是cccc或者函数调用过的随机值
  • 参数是对实参的一份拷贝,计算时直接使用这份新内容,所以修改形参并不会改变实参的值,使用完之后会销毁
  • 函数的返回值保存到寄存器中,然后通过寄存器保存到局部变量中
  • 11
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值