C语言内功修炼之函数栈帧的创建与销毁(举例加图解)

大家可能会函数栈帧不了解,可能都没有听过这个,不用着急,在理解函数栈帧之前,我们先来了解一下程序对内存使用的分区大概情况:

 区域作用
栈区(stack)由编译器自动分配和释放,存放函数的参数值,局部变量的值等。操作方式类似与数据结构中的栈
堆区(heap)一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。与数据结构中的堆是两码事,分配方式类似于链表
静态区(static)全局变量和静态变量存放于此

那么函数栈帧又是什么呢?从逻辑上讲栈帧就是一个函数执行的环境。际上,栈帧可以简单理解为:栈帧就是存储在栈上的每一次函数调用涉及的相关信息的记录单元。

栈是从高地址向低地址分配内存的。每调用一个函数的时候,函数都有它自己的函数栈帧去维护,这个栈帧中维护着所需要的各种信息。

再了解函数栈帧的概念之后,我们还需要学习一些寄存器:

寄存器名称作用

eax

累加(Accumulator)寄存器,常用于函数返回值
ebx基址(Base)寄存器,以它为基址访问内存
ecx基址(Base)寄存器,以它为基址访问内存
edx数据(Data)寄存器,常用于乘除法和I/O指针
esi源变址寄存器
dsi目的变址寄存器
esp堆栈(Stack)指针寄存器,指向堆栈顶部
ebp基址指针寄存器,指向当前堆栈底部
eip指令寄存器,指向下一条指令的地址

下面用一个简单的函数调用去介绍函数栈帧:

#define _CRT_SECURE_NO_WARNINGS
#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 = add(a, b);
	printf("%d", c);
	return 0;
}

然后进去vs2019的函数调试中,进入反汇编看函数的具体过程,首先是函数的初始化(我的是从高地址指向低地址,是往上画的):

int main()
{
00B018B0  push        ebp  
00B018B1  mov         ebp,esp  
00B018B3  sub         esp,0E4h  
00B018B9  push        ebx  
00B018BA  push        esi  
00B018BB  push        edi  
00B018BC  lea         edi,[ebp-24h]  
00B018BF  mov         ecx,9  
00B018C4  mov         eax,0CCCCCCCCh  
00B018C9  rep stos    dword ptr es:[edi]  
00B018CB  mov         ecx,0B0C003h  
00B018D0  call        00B0131B  

 前面的布置都是main函数的栈帧

	int a = 10;
00B018D5  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
00B018DC  mov         dword ptr [ebp-14h],14h

	int c = add(a, b);
00B018E3  mov         eax,dword ptr [ebp-14h]  
00B018E6  push        eax  
00B018E7  mov         ecx,dword ptr [ebp-8]  
00B018EA  push        ecx  
00B018EB  call        00B01023  

int add(int x, int y)
{
00B01850  push        ebp  
00B01851  mov         ebp,esp  
00B01853  sub         esp,0CCh  
00B01859  push        ebx  
00B0185A  push        esi  
00B0185B  push        edi  
00B0185C  lea         edi,[ebp-0Ch]  
00B0185F  mov         ecx,3  
00B01864  mov         eax,0CCCCCCCCh  
00B01869  rep stos    dword ptr es:[edi]  
00B0186B  mov         ecx,0B0C003h  
00B01870  call        00B0131B 

这里可以看和和main函数初始化的时候基本上没有什么差别,这里就不做讲解,留给读者自己去思考,可参考前面main函数初始化的情况去看。

	int z = 0;
00B01875  mov         dword ptr [ebp-8],0  
	z = x + y;
00B0187C  mov         eax,dword ptr [ebp+8]  
00B0187F  add         eax,dword ptr [ebp+0Ch]  
00B01882  mov         dword ptr [ebp-8],eax  
	return z;
00B01885  mov         eax,dword ptr [ebp-8]  
}

00B01888  pop         edi  
00B01889  pop         esi  
00B0188A  pop         ebx  
00B0188B  add         esp,0CCh  
00B01891  cmp         ebp,esp  
00B01893  call        00B01244  
00B01898  mov         esp,ebp  
00B0189A  pop         ebp  
00B0189B  ret  

 

esp+0CCh为什么就指向了ebp的位置,大家可以直接动手去调试,看看内存那一块,就知道为什么了 ,然后cmp指令到pop ebp指令都是在销毁add函数的栈帧

 ret指令让我们回到了第一次调用add函数的地方(因为在调用的时候,我们已经记住了这个地址,所以可以返回到main函数的栈帧里面去):

00B018F0  add         esp,8  
00B018F3  mov         dword ptr [ebp-20h],eax  
	printf("%d", c);
00B018F6  mov         eax,dword ptr [ebp-20h]  
00B018F9  push        eax  
00B018FA  push        0B07B30h  
00B018FF  call        00B010D2  
00B01904  add         esp,8  
	return 0;

然后就是在ebp-20h的位置创建了局部变量c,并把eax的值赋给了它,后面在进行打印,最后把main函数的栈帧销毁(和add函数的栈帧的销毁类似)

总结一下:1.局部变量的创建是在为当前包含局部变量的函数初始化一片空间之后,在这个空间内给局部变量进行初始化;2.为什么局部变量不初始化的时候是随机值,我们可以看见前面在对函数栈帧进行初始化的时候,里面的被分配的空间值一部分被赋值成了0CCCCCCCCh这个值,不同的编译器初始化的值也可能不同,而局部变量的创建是在那些被初始化为随机值的地方去创建的,所以说局部变量不进行初始化的时候就是随机值;3.函数传参的顺序,我们也可以看到是从右往左传参的,并且形参是实参的临时拷贝,因为在调用函数之前我们已经把实参进行了拷贝并且压入栈中了;

由于本人对函数栈帧的了解还不够深入,只能给各位讲到现在这样,以后还会发布关于函数栈帧的文章,到时候会更加详细的给大家进行讲解,望大家理解,希望这篇文章能给大家带来一点帮助,上述的内容有什么不对的地方,欢迎大家在评论区留言指正。

 

 

 

  • 16
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值