函数栈帧的创建和销毁

本文详细介绍了函数栈帧的创建机制,包括栈顶指针esp和栈底指针ebp的作用,以及寄存器如eax、ebx等在栈帧中的使用。通过VS2019示例,展示了函数调用、参数传递和返回值处理的过程,以及栈帧的销毁方法。

       

目录

 函数栈帧的创建

函数内部的实现

函数调用结束后返回值的返回和栈帧的销毁


        "函数栈帧的创建和销毁"这个知识点使我知道我初学程序语言时打印烫烫烫乱码的原因,另外也让我搞懂了一些代码内部的工作方式,函数栈帧的创建和销毁无疑是一种修炼内功的方式。

        这篇内容看上去很复杂,一串连下来,其实很容易掌握,所以需要耐心,干货多多,细节饱满。

        函数栈帧大概分为创建、内部实现和销毁,ebp称为栈底指针,esp称为栈顶指针,共同维护一块函数栈帧,esp再次使用时向上移动且移动到栈顶,ebp的再次使用也是需要修改的。

        eax是用来存放需要修改的值,edi是用来存放需要修改(一块空间所有的值)的地址,ebx是用来存放从edi地址向下需要修改值的次数,连起来就是将edi地址向下ecx次全部初始化为eax的值,eax,ebx,ecx,edx都属于寄存器。

        压栈是在栈顶压,即push,出栈是在栈顶出,即pop,这些寄存器就是压栈来搞一块属于自己的地址。

        这里我用VS2019演示实际的函数栈帧的创建和销毁,VS2013更好,版本越高简化的越多,底部具体步骤相比更简单。

        调用堆栈可以看函数与函数之间的调用关系,比如main函数是由__mainCRTstartup调用(vs2013可以查看)

 

 函数栈帧的创建

        调试时右键代码区域转到反汇编,可以查看汇编代码

为了查看的更细致,右键将显示符号名关掉

从上往下,一步一步来。

push压栈ebp,在__mainCRTstartup栈顶压栈,mov是后者移动到前者,因为esp和ebp都是指针变量,所以将esp的值改为ebp的值,sub是前者减后者,esp-0E4h,逻辑上的一块栈帧,高地址在下,低地址在上,这里esp向上移动,栈顶压栈ebx,esi,edi,随后lea是指load effective address加载有效地址,rep stos的作用是edi加载地址向下ecx次全部初始化为0CCCCCCCCh,这就是打印出烫烫烫乱码的原因,其中dword的word是两个字节,dword是四个字节,其次发现寄存器并不是特定的需要存特定的值,call意思为调用

 内存窗口发现函数栈帧的创建已经成功了,一块很大的区域(这里只是展示了一部分)全部初始化为cccccccc,上面的call调用的不必关心。

继续往下走~

函数内部的实现

 ebp-8,就是栈底指针与我们要用的4个字节(dword)的指针之间隔了8个字节,相当于栈底指针是一块,向上隔一块,0Ah16进制,A*16^0=10,所以ebp-8的地址就是为a所用的,b和c也就迎刃而解了

 其次,压栈存放20的eax,压栈存放10的ecx,F11调用Add函数

 压栈ebp,把原来esp的值传给ebp,这里画图演示,esp自动向上移动

 return x+y上面的都是函数栈帧的创建,ebp+8就是ecx,20,ebp+0ch(12)就是上图eax,10,这里足以证明形参是实参的一份临时拷贝,不会影响实参的值,不会在函数内部新开辟一块空间,eax最后等于30,寄存器不会销毁,所以这就是为什么返回值不会销毁的原因。

函数调用结束后返回值的返回和栈帧的销毁

vs2013

 vs2019

一条指令一条指令走,pop出栈edi、esi、ebx,回过头看Add函数调用时的汇编代码第三行,再看0c0h,一目了然,这个需要思考一下,其实跟着代码一步一步走,就是把Add函数栈帧销毁了而已。

 函数栈帧用完后就销毁了,其次就是看返回值eax是怎么执行的

esp+8指到存放eax的地址处

 ebp-20就是存放c的地址,把c mov 成eax(30)就ok了。

C语言函数创建销毁与程序在内存中的执行过程紧密相关。以如下代码为例进行讲解: ```c #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; } ``` ### 函数创建 在程序执行时,每个函数在调用时都会在创建一个,用于保存该函数的局部变量、参数、返回地址等信息。当调用`main`函数时,会为`main`函数创建一个。在`main`函数中定义局部变量`a`、`b`、`c`,这些变量会被存储在`main`函数中。当调用`Add`函数时,会进行以下操作: - **参数传递**:函数的参数并未在函数创建,而是通过压操作放到`Add`函数底的下面位置。例如,`Add(a, b)`调用时,会将实参`a``b`的值进行压操作,传递给`Add`函数。这的形参其实是实参的一份拷贝,对形参的修改不会影响实参 [^2][^3]。 - **保存返回地址**:记录调用`Add`函数结束后要返回的`main`函数中的位置,以便`Add`函数执行完毕后能回到正确的位置继续执行。 - **创建**:为`Add`函数创建新的,在这个中会保存`Add`函数的局部变量,如`z`。 ### 函数销毁函数执行完毕后,会进行销毁操作。以`Add`函数为例,执行完`return z`语句后: - **恢复寄存器状态**:将寄存器的值恢复到调用`Add`函数之前的状态。 - **释放局部变量空间**:释放`Add`函数中为局部变量分配的空间,如`z`。 - **恢复返回地址**:从中取出之前保存的返回地址,程序跳转到该地址继续执行,也就是回到`main`函数中调用`Add`函数的下一条语句继续执行。 - **释放参数空间**:释放之前压的参数所占用的空间。 函数在递归调用时,就是把`Add`函数看成新的`main`函数,不断重复创建工作。如果递归调用太多层,会不断创建新的,最终导致空间耗尽,引发 stack overflow(溢出) [^2]。
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值