当前位置:   article > 正文

【C语言】函数栈帧的创建和销毁(启航——迎接崭新的自己)

【C语言】函数栈帧的创建和销毁(启航——迎接崭新的自己)

前言

大家平时学习C语言粗粮可能吃的有点多了,今天带着各位读者吃一些细糠。

想必大家在学习C语言之初,内心中一定有不少的困惑,如:

  1. 局部变量是如何创建的?
  2. 为什么局部变量的值是随机值?
  3. 函数是如何传参的?传参的顺序什么?
  4. 形参和实参之间的关系
  5. 函数调用是怎么做的?
  6. 函数调用结束后是如何返回的?

我会针对以上的问题,带着大家踏上寻求知识的旅途——讲解函数栈帧的创建和销毁。

哈哈哈

温馨提示:本次讲解所采用的编程环境是VS2013 。如果大家想自行测试的话,建议不要使用过高版本的编译器,因为高版本编译器会对函数栈帧的创建和销毁的过程封装得比较复杂,不易于观察和学习。另外,不同的编译器对于函数栈帧创建和销毁有所不同,但是都大同小异,希望大家在自行学习时牢记这一点。

1. 寄存器

这里我只会笼统的给大家聊一下寄存器的作用,而不会深入的探讨,毕竟有这些知识就足够用了。

现在的寄存器大多出现在CPU中,其作用,就是用来暂存ALU(算数逻辑单元)、CU(控制单元)等的数据。等到CPU接受到某一条相关的指令时,就会从寄存器提出数据出来使用。

在本文中,你可以将寄存器粗略地看成C语言中的指针和能够存储值的硬件。

下面,我列出几个大家在编程中常常接触到的寄存器:

eax
ebx
ecx

下面两个寄存器时本文的主角:
ebp(栈底指针)
esp(栈顶指针)

这两个寄存器就是专门用来维护函数栈帧的。

2. main其实也是被别的函数给调用的

我们平时写C语言代码时,在写完头文件时,紧接着就是写main函数。我们知道main函数是程序的入口,有且仅有一个。

可是对于main函数也是被其他函数调用的,这件事情我们好像没有感知。但是,它确实是存在的。

VS2013中,main函数是被__tmainCRTStartup函数调用,而__tmainCRTStartup函数是被mainStartup函数所调用,总结一幅图就是:
main
为什么要提这件事?
原因是要我们更深刻的理解代码在底层中的运行,以及说明一件事:
每当我们调用一个函数,编译器都会为这次函数调用创建一个函数栈帧。

请大家务必记住这个知识点!!!
哈哈哈

3. 函数栈帧创建与销毁的全过程

为了使过程观察得更加明显,我会把代码尽可能的详细写出:

#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

碍于篇幅原因,我将会采取部分讲解的方式。

下面是这个部分代码所对应的汇编代码:
汇编指令

在未执行上面汇编指令时,栈区的状况:
__tmainStartup的函数栈帧

  1. push ebp

这条指令的被称为“压栈操作”,意思讲ebp寄存器的值压入栈中,之后让esp(栈顶指针)往地址处挪动一个格子(4个字节)。做了这两件事。

值得牢记的一点是:栈区空间的使用规则一般先使用高地址的空间,再使用低地址的空间。

画成图就是这样的:
push ebp
2. mov ebp,esp

将esp的值给寄存器ebp。也就是说,经过这条指令过后,ebp(栈底指针)和esp(栈顶指针)都指向了同一块区域。
图
3. sub esp,0E4h

将esp寄存器里的值减去十六进制的0E4h。就是将esp指针向上挪动一定的距离。

从这里你可能会猜到,这会不会是正在给main函数的主体创建空间。没错!

图
4. push ebx / push esi / push edi

这三条指令放在一起讲,跟第一条指令一样的效果。

图

  1. lea edi,[ebp - 0E4h]

lea:load effective address,意思:加载有效地址

就是将ebp-0E4h的值存放在寄存器edi中。

图片

  1. move ecx ,39h
    move eax 0CCCCCCCCh
    rep stos dword ptr es:[edi]

这三条指令你可以看作是一体的,它们都在做着一件事:
将eax寄存器里面的内容,也就是0CCCCCCCCh。以4字节的形式,从edi寄存器所记录的地址开始,一直拷贝到ebp寄存器所记录的地址,拷贝的次数就刚好是ecx寄存器里面的值(39h)。

这里值得注意的一点是:为什么是4个字节?
其实是这样的,一个word(字)为2个字节,而dword的意思是double word也就是双字,计算一下就为4个字节了。
图片
直到这里我们发现,关于main函数的函数栈帧就创建完毕了!

太好了,我们终于要开始执行main函数里面的语句了。

图
8. 从这里开始,我们就要调用我们自定义Add函数了。

那肯定又要给我们的Add函数创建属于它自己的函数栈帧,具体是怎样的?我们往下看:
图
下面我就加快速度给大家讲解了,套路跟main函数差不太多!

先执行这些指令:

mov eax,dword ptr[ebp - 14h]
push eax
mov ecx dword ptr[ebp-8]
push ecx

图
看到这幅图,有了main函数带来的灵感,你会感觉到这是不是在给Add函数创建函数栈帧。我只能说,你的感觉没有一点毛病!!!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小惠珠哦/article/detail/1017490
推荐阅读
相关标签