函数栈帧的创建和销毁

本文通过分析一段简单的C语言代码,详细解释了函数栈帧的创建过程,包括寄存器的作用、main函数和add函数的调用细节,以及参数传递和返回值处理。文章还展示了在VS2013X86环境下,如何通过寄存器ESP和EBP维护栈帧,并探讨了栈帧的销毁机制。
摘要由CSDN通过智能技术生成

前言

前面在使用函数时一直说到函数栈帧的创建与销毁,但也只是云里雾里的,今天就来讲讲关于函数栈帧的知识。实验环境:VS2013,系统环境X86

我们通过一段简单的代码来了解函数的栈帧:

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

寄存器

再了解函数栈帧前,我们需要先知道一些前景知识,首先来了解一下寄存器。再VS2013中,我们可以通过调试窗口中的寄存器窗口查看有哪些寄存器:

在这里插入图片描述

可以看到有下面几种寄存器:

通用寄存器(数据存放数据使用):
eax
ebx
ecx
edx

变址寄存器(偏移量):
esi
edi

指令寄存器(下一条指令的地址):
eip

指针寄存器(地址,维护函数栈帧):
esp(堆栈指针寄存器,用于存放栈顶指针的位置)
ebp(基址指针寄存器,用于寻找栈内的元素)

标志性寄存器(不知道什么东西):
efl

寄存器的详细知识可以点这里:汇编——寄存器的分类和功能

main函数的调用

每一个函数的调用都需要在栈区上开辟一块开空间,在栈上一块专门为函数开辟的空间就是函数的栈帧。这么一块栈帧其实是由两个寄存器维护的,根据上面寄存器的介绍,大概能猜到是esp, ebp两个寄存器维护的了:

在这里插入图片描述

调用main函数的函数

main函数也是函数,所以我们可以在调试中通过函数调用堆栈来看一下main函数是由谁调用的:

在这里插入图片描述

由此我们可以很明显得看到main函数的调用关系

在这里插入图片描述

调用main函数的函数__tmainCRTStartup也是需要栈帧的,同样的是由esp, ebp来维护

在这里插入图片描述

main函数的栈帧如何开辟的

然后我们可以通过反汇编来看一下main函数是怎么调用的:

在这里插入图片描述


push(保存调用方的ebp

当执行第一个反汇编指令时相当于将ebp的值放到__tmainCRTStartup的栈帧的顶部:

在这里插入图片描述

那么esp的值,就相当于减去了 4 ,我们可以通过监视来看一下

执行前:

在这里插入图片描述

执行完push指令后:

在这里插入图片描述

至于为什么是减4,是因为在32为系统下指针的大小是4字节

这样之后的函数返回后就可以快速找到调用方的栈底,从而继续维护调用方。


move(维护新开栈帧的栈底)

move指令相当于将 esp的值复制给ebp

在这里插入图片描述

通过监视窗口可以看到ebp值的变化:

执行前:

在这里插入图片描述

执行完move指令后:

在这里插入图片描述

这样ebp 就是新的函数栈帧的栈底,继续干着栈底指针的老本行

sub(维护新开栈帧的栈顶)

执行sub命令相当于esp向下走了 0E4h,那么esp ~ ebp中间的空间就是main函数的栈帧。

执行前:

在这里插入图片描述

执行完sub命令后:

在这里插入图片描述

相当于:

在这里插入图片描述

三连push(添加栈帧的信息的变量)

push前:

在这里插入图片描述

push 后:

在这里插入图片描述

注意每次压栈后都esp的值都会变化

相当于:

在这里插入图片描述

这三个新压栈的寄存器在后面将会起到大作用。

lea (存放栈顶地址)

在这里插入图片描述

lea ,全称 load effictive address,翻译一下就是加载有效地址。可以看到ebp - 0E4h 就是三连 push前main函数的栈顶,相当于将该地址放到 edi 中。

后面两个move等价于:ecx = 39h, eax = 0CCCCCCCCh

rep stos(初始化栈帧)

执行前:

在这里插入图片描述

dword

d: double
word: 一个word是2字节
dword: 4字节

整条指令就是相当于将edi (ebp - 0E4h)往下,每次操作4字节,操作 ecx (39h)次,全部初始化为 eax (0xcccccccc)

在这里插入图片描述

也就是:

在这里插入图片描述

至此,一个函数的栈帧的创建就完成了,这也是为什么局部变量没有初始化的时候,它的值会是随机数。

add函数的执行

创建变量

函数栈帧创建完成之后就是正常的执行代码了,后面三条语句就是普通的初始化

在这里插入图片描述

赋值后:

在这里插入图片描述

也就是相当于:

在这里插入图片描述

可以看到在VS2013 中,是隔了两个整型的大小来进行初始化的,但是在不同的编译器下可能实现的方式不同。

传参

接下来就是万众瞩目的传参的过程了:

在这里插入图片描述

将a和b的值依次传给eax和ecx压栈

也就是:

在这里插入图片描述

正好印证了形参是实参的一份临时拷贝

call (函数调用 )

执行完传参后,就是函数调用了,call 指令会将下一条指令压栈,让函数返回时,正常往下执行

在这里插入图片描述

然后跳到指定的地址

在这里插入图片描述

再通过jmp 命令进行跳到add函数内部

在这里插入图片描述

跳到add函数内部之后,就开始函数的创建和初始化等一系列操作,

在这里插入图片描述

参数的使用

参数使用时通过ebp找到对应的参数,然后将计算的结果返回回去

在这里插入图片描述

也就是:

在这里插入图片描述

可以发现参数在传递的时候是从右向左传,在使用形参时是从左向右取,刚好跟参数传递的顺序一致

函数的返回值和栈帧的销毁

返回时,先将返回值放到寄存器 eax

在这里插入图片描述

然后依次销毁函数栈帧,现将顶上的三个寄存器弹出

在这里插入图片描述

也就是:

在这里插入图片描述

然后通过move指令将栈顶指向栈底

在这里插入图片描述

也就是:

在这里插入图片描述

然后通过pop命令将ebp指向main函数的栈底,那么ebp, esp又重新开始维护main函数的栈帧。

在这里插入图片描述

也就是:

在这里插入图片描述

可以看到esp 指向的就是调用函数之后的下一条指令的地址,ret指令就是通过pop回到调用函数的下一条指令

在这里插入图片描述

执行ret指令后回到下一条指令的位置:

在这里插入图片描述

esp也向下走了一个位置:

在这里插入图片描述

形参的销毁

在这里插入图片描述

执行该命令后,形参的栈帧也就销毁了,意味着返回到了main函数中,add函数的栈帧彻底销毁了

在这里插入图片描述

之后的就是正常的进行计算等等,各种函数的调用,其栈帧的创建和销毁基本上都是一样的。

总结

自己总结的草图:栈帧的创建和销毁都是一一对应的,怎么创建,就怎么反着销毁。

在这里插入图片描述

评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

_featherbrain

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

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

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

打赏作者

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

抵扣说明:

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

余额充值