函数栈帧的创建和销毁 - 局部变量|函数传参|函数调用|函数返回|图文详解

目录

1.寄存器EBP和ESP

2.函数栈帧的创建

3.函数的调用

4. 函数栈帧的销毁


函数栈帧(function stack frame)是在函数调用期间在上分配的内存区域,用于存储函数的局部变量、参数、以及用于函数调用和返回的相关信息。每当函数被调用时,都会创建一个新的栈帧,函数执行结束后,该栈帧会被销毁。

1.寄存器EBP和ESP

寄存器是位于CPU内部的一组用于存储和处理数据的小型临时存储器。它们被设计用于执行指令、进行算术和逻辑运算、控制程序流程等任务。寄存器通常比内存访问速度更快,因为它们直接集成在CPU内部,而不需要通过外部总线进行访问。

EBP和ESP是 x86 架构下的寄存器,用于在函数调用过程中维护被调用的函数的栈帧。

EBP是扩展基址寄存器(栈底指针),通常用来指向当前函数的栈帧的基址(高地址处)。ESP是栈指针寄存器(栈顶指针),指向当前栈顶的位置(低地址处)

每当函数调用时时,都要在栈区上创建一个空间,并且将栈区的地址分别交由寄存器EBP和ESP来来维护。正在调用的是哪个函数,这两个寄存器就维护哪个函数的栈帧。

2.函数栈帧的创建

接下来通过一个简单求和函数来了解函数栈帧的创建过程

#include<stdio.h>

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

int main()
{
	int a = 2;
	int b = 1;

	int c = Add(a, b);

	return 0;
}

再来看这段反汇编代码:

可以看到,在进入main函数的时候有一系列的反汇编指令,它们是什么意思?干了什么事情?

下面是一个内存演示图 :在进入main函数之前,ebp和esp分别指向调用main函数的函数的栈底和栈顶。


push ebp(压栈):执行这一条语句后,将ebp的值放在esp的位置,esp维护栈顶位置,所以esp向高地址走一步(值减小)


mov ebp,esp:将esp的值给ebp 


sub esp,0E4h:esp向上低地址处移动,现在ebp与esp之间的空间就是为main函数开辟的栈帧空间


push  ebx , push esi, push sdi:把ebx,esi,edi分别压入栈


lea  edi,[ebp-24h]:ebp的值减去24(十六进制)的偏移量,然后将结果储存在edi中。这样做是为了将edi指向要填充的内存区域的起始地址

mov ecx,9:存储到ecx中。这个值表示要填充的内存区域的大小,单位为dword(Double Word 即4字节).

mov eax,0cccccccch:将0xcccccccc 存储到eax中.

rep stos dword ptr es:[edi]  :一个重复(rep)操作,它会将eax中的值(0xcccccccc)存储到edi所指向的内存地址处,存储的长度为ecx中的值(9 dword)


3.函数的调用

到这里为止,调用main函数的准备工作才算结束,接下来才刚开始执行我们写的代码:

 mov  eax,dword ptr [a]:写入数据到a的位置。这里的b其实就是ebp - 8 的位置

后面的mov都是写入数据,只是写入的位置逐渐向低地址处移动。


终于来到调用函数Add的部分,首先我们进行函数的参数传递

mov eax,dword ptr [b]:将ptr[b]放到eax里去

push eaxeax入栈

mov eax,dword ptr [b]:将ptr[a]放到eax里去

push eaxeax入栈

注意,由上面的四条指令可以看出:虽然形参的顺序是先a后b,但是实际压入栈的顺序是先b后a

call  _Add (03410B9h) :调用函数Add,其中03410B9h是函数的地址。并且把call指令的下一条指令压入栈,使Add函数执行完后知道下一条该执行的指令。


现在来到Add函数里面:

可以发现前面部分跟在调用main函数的时候是相似的,即为Add函数创建栈并初始化。

要注意的时,现在的ebp,esp已经由维护main函数的栈帧变为维护Add函数,因为此时我们已经开始创建Add的栈帧了。

mov eax,dword ptr[x]:这里其实就是找到刚刚压入Add函数的值,即ptr[x]位置的eax,值为1

add eax,dword ptr[y]:将ptr[x]位置的eax,值为2,和prt[x]相加,得到3

mov dword ptr[z],eax把3放入eax,即得到z=3

从这里可以看出,当我们真正进入函数调用两个数相加时,形参根本不是在Add中创建的,而是在Add中找到刚刚调用函数时压入的空间所存放的数据,即图中所示的空间:

所以,这样就能很明确的知道:形参是实参的一份临时拷贝 

mov  eax,dword ptr [z]:将z的值放入eax中,因为z会随着函数的结束而被销毁,要想返回一个值需要用eax这样一个寄存器来保留,因为寄存器是不会随着函数的结束而被销毁的。


4. 函数栈帧的销毁

pop edi、pop esi、pop ebx:弹出edi,,esi,,ebx,同时每次弹出时esp也向高地址处移动

mov esp,ebp:将esp指向ebp的位置

pop ebp:弹出ebp,此时ebp回到main函数中ebp原本的位置,esp由于这一次pop也向高地址处移动1偏移量,指向刚刚保存call的下一条指令的位置,准备执行下一条指令。1

add esp,8:将esp向高地址处移动两个偏移量,此时用于保存之前压入Add函数的两个形参的eax就被释放了

mov dword ptr [c],eax:把刚刚保留c的那个eax复制给c


到这里整个过程就介绍的差不多了,从中我们可以的出许多结论比如:

1.释放栈帧所占用的内存空间,是通过移动栈帧指针,从而允许后续操作直接覆盖数据来实现的

2.函数调用后还能找到下一条执行的语句是因为在调用函数之前,当前函数的上下文需要被保存到栈中,以便在函数执行完毕后能够正确返回到调用函数。

3.函数传参时是将函数调用时传递的参数复制到栈帧中的相应位置,以便函数内部能够访问这些参数。

  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值