C语言深入逐汇编详解函数栈帧的创建和销毁过程

Posted 林先生-1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言深入逐汇编详解函数栈帧的创建和销毁过程相关的知识,希望对你有一定的参考价值。

【C语言深入】逐汇编详解函数栈帧的创建和销毁过程

一、图解大概过程

一个清晰的流程图解能对我们的理解起到事半功倍的效果,所以我们先不管寄存器和汇编代码,先来大致的理一下函数栈帧的创建和销毁过程。
我所理解的函数栈帧的形成其实是从一个函数被另一个函数调用开始的,我把整个过程分成了6部分:
1、压入临时拷贝及返回信息
我们最开始先假设main函数的栈帧已经形成,并且在main函数内调用了函数。那么在正式进入被调用函数之前,就要先形成传入参数的临时拷贝,以及一些辅助返回的信息:

而在这个过程中一有两个指针来维护我们的栈,一个是栈顶指针top一个是栈底指针base,刚开始时,top和base分别指向main函数栈帧的栈顶和战底:

而随着我们元素的压入,栈顶指针top需要一直往上移动,直到信息压完:

2、形成栈帧
前面的准备工作做完后,就可以形成被调用函数的栈帧了,形成栈帧其实就是使base和top指向一块新的空间:

3、初始化局部变量
当我们形成了func函数的栈帧后,就需要对func内的局部变量进行初始化,初始化包括分配空间并赋值:

4、计算并返回
在func函数内初始化了一些局部变量后,我们就可以计算返回值了。
计算返回值时,出了用到func函数内的局部变量之外,还需要用用到我们传进来的参数。但当我们在func内计算的时候,并不会再次产生参数的拷贝,而是直接提取我们已压入栈的临时拷贝:

5、销毁栈帧
销毁栈帧的过程其实就是将top指向base,当top和base的指向相同时,也就说明base上面的空间被回收了:

6、弹栈
在弹栈之前要做的是,让base指向会原来的main函数的栈底,这个操作是通过之前压入的返回信息完成的,再返回信息中其实就包含的原本main函数栈底地址的信息:

然后就可以通过弹栈操作,将之前压入栈中的临时拷贝和返回信息都弹出栈去:

这样我就恢复到了main函数的栈帧。

至此,就大致地介绍完了函数栈帧的创建和销毁的主要过程。
接下来,就给大家逐语句的分析整个过程。

二、函数栈帧的创建过程

1、简介一些需要用到的汇编指令和寄存器

在正式开始之前,必须先要对我们将要用到的一些汇编指令和寄存器做一些了解。不需要深入理解,只需要了解一下它们的作用和含义即可。
相关汇编指令

mov: 数据转移指令,开辟空间并将数据写入空间
push: 数据入栈。同时esp栈顶寄存器也要发生改变
pop: 数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub: 减法命令
add : 加法命令
call: 函数调用,1.压入返回地址2.转入目标函数
jump: 通过修改eip,转入目标函数,进行调用
ret: 恢复返回地址,压入eip,类似pop eip命令

相关寄存器

eax: 通用寄存器,保留临时数据。常用于返回值ebx;通用寄存器,保留临时数据
ebp: 栈底寄存器
esp: 栈顶寄存器
eip: 指令尚存器,保存当前指令的下一条指令的地址

当然啦,大家如果是初次见的话,肯定还会有很多地方不理解的,但是一回生二回熟,在后面具体遇到的时候在对它们的功能做详细的介绍也不迟。

2、调用main函数的函数

我们先写上一个简单的测试代码:

#include <stdio.h>
int Add(int x, int y) 
	int z = 0;
	z = x + y;
	return z;

int main() 
	int a = 3;
	int b = 2;
	int c = 0;
	c = Add(a, b);
	printf("%d", c);
	return 0;

一个函数要想起作用,那就必须时被调用。
那当然我们的main函数也是被别的函数调用的,为了看到调用main函数的函数,我们可以在编译器vs2013的调试中看到:

当我们进入main函数内时,就会发现main函数也是被一个名为__tmainCRTStartup的函数调用的。
而至于__tmainCRTStartup这个函数又是被那个函数调用的我们不用管,我们这里只需要直到main函数也是被别的函数所调用的即可。

3、局部变量的初始化

知道了main函数也是被别的函数所调用的,我们就可以理解为什么可以假设main函数的栈帧已被创建好的了。
当我们的main函数的栈帧创建好之后,上面所说到的寄存器esp和ebp就会默认指向main函数的栈顶和栈底:


当创建好了main函数的栈帧后,紧接着就需要对main函数内定义的局部变量进行初始化了,我们可以看看这部分的汇编代码:

我们可以看到,三个局部变量的初始化对应的就是三条mov指令:

所以这三条语句所做的工作就是在地址为ebp - 8、ebp - 14和ebp - 20的地址处放上a、b、c三个变量:

4、形成临时拷贝

当我们完成局部变量的初始化工作之后,程序就直接来到了函数调用语句:

而我们知道进行函数调用的汇编语句是call,但上面的结果显示并没有直接执行call指令,而是执行了其他的4条语句。
其实在call指令之前的这4条语句完成的就是形成参数的临时拷贝。
这四条语句所执行的就是将ebp - 14的内容放入到寄存器eax中并把eax的值压入栈中,然后把ebp - 8的内容放入到寄存器ecx中并把ecx的值压入栈中。
而由上文我们知道,ebp - 14和ebp - 8中放的不就是变量b和变量a的内容吗?
所以成这个过程为形成临时拷贝:

而且通过以上过程,我们也可以得到两个结论:

1、参数的临时拷贝(形参)是在函数调用之前就完成的。
2、函数形参实例化的顺序是从右向左的。

5、函数调用

完成了临时拷贝我们就来到了,调用函数的call语句,而这一条语句所做的工作就不一般了,我们需要特别来看看:

这条语句一共做了两件事,我们想来看第一件事:压入返回地址;
压入返回地址的目的其实是为了在函数调用完后,返回到call命令的下一条指令:

所以我们要压入栈的就是这里的add这条命令的地址:

因为call指令完成了两个工作,所以我们应该按F11来观察更细节,当我们按下F11后就会发现esp的值变成了002E18F7:

而跳转目标函数其实是通过修改eip寄存器的内容达成的,修改的地址其实就是call指令后面跟的那个地址:


而通过上图我们也可以观察到此时的eip已经被修改成了对应内容:

而进入到call指令内部我们看到其实002E10B4其实对应的是一条jmp指令:

而jmp指令的功能是,通过修改eip,转入目标函数,进行调用:

所以我们是通过jmp指令转入被调用函数内的。

至此,我们完成了返回地址的压入和函数的调用:

6、形成栈帧

在进入函数后,其实就可以形成栈帧了,我们先看汇编:

第一条指令为push ebp,意思是将ebp的内容入栈,而我们知道ebp此时孩纸想的就是main函数的栈底:

所以这个指令所做的工作就是将main函数的栈底地址压入栈中:

下一条指令mov ebp,esp所做的工作就是将esp中的内容移入ebp,也就是使ebp与esp指向同一位置:

接下来的一条语句是sub esp,0CCh,其意思就是将esp里的内容减去CCh(十六进制),结果相当于让esp往上移动:

而此时,由esp和ebp所制定的这一段空间其实就是Add函数的栈帧:

至此,一个函数栈帧从无到有的创建过程也就演示完了。
而接下来的这一部分汇编指令其实就是在对我们新创建好的这块空间进行初始化,就像将一个数组元素全都初始化为0一样:

而这对于我们理解栈帧的创建和销毁的过程并没有什么实际的意义,所以这一部分我们可以忽略。

7、提取临时拷贝

而接下来的这条语句就是对局部变量进行初始化操作,这和前面是一样的,所以就不用多说:


现在我们执行到下一条语句:

其意思就是将ebp + 8中的内容移入eax中,那么ebp+8中又是哪里的内容呢?
因为我们现在所在的平台是32位的,所以每个地址是4个字节,所以ebp + 8就相当于跳过了两个指针类型变量的空间,所以此时的ebp+8指向的就应该是a`,也就是实参a的临时拷贝:

接下来的一条语句是:

其实就是将ebp - 12(0C为十六进制,转为十进制就是12)中的值与eax中的只进行相加,其结果依然保存到eax当中。而这里的ebp+12指向的就是实参b的临时拷贝:

所以我们此时就完成了两个临时变量的相加,其结果保存在eax当中。
而接下来的这条指令所做的就是将eax中的值写入到ebp - 8中:

而此时我们的ebp - 8不就是我们变量z的地址吗?
所以我们就将计算结果赋值给了变量z。
而此时我们Add的逻辑也就完成了。

8、return返回

此时,我们就来到了我们的return。

return对应各这条汇编指令所做的就是将ebp - 8的值移动到eax当中,也就是将计算得到的结果移动到eax当中。

三、函数栈帧的销毁过程

1、释放栈帧

而下面的指令中其实我们也只需要关心最后三条:

因为其他的都是别的一些设置(相当于把这块空间再次初始化成默认值),我们这里可以不管。
而我们这里的前两条其实和前面是类似的,我们先要做的就是再次让esp与ebp指向同一个位置:

而到此时,原本Add的栈帧空间也就被释放了。

2、弹栈

接下来的这条指令就是弹栈了:


所以我们这里的这条指令所做的就是将栈顶的数据pop到ebp当中,而我们当前的栈顶数据其实就是之前压入栈中的main函数的栈底地址:

所以,当我们这条指令执行结束后,ebp也就恢复到了main函数的栈底,并且esp也往回走了:

最后我们就到一条很重要的汇编指令ret:


这条语句其实做的就是将我们之前压入栈中的返回地址移入eip中,这样我们下一条指令就是从返回值地址处的指令开始执行了,也就是跳转回了我们用于调用Add函数的call命令的下一条指令add:

并且执行完后esp也相应的要往会走:

3、回到main函数

接下来我们就回到了main函数:

我们发现回到main函数后首先执行的就是将esp加8,其所对应的效果就是将两个临时拷贝的空间也给释放了:

至此我们也就完全回到了main函数当中。也就是Add函数栈帧的创建和销毁也都全完成了。至于后面的指令也就和之前的一样了,毕竟main函数也是函数,只要是函数那它们的栈帧的创建和销毁也都是一样的啦。

图解C/C++语言底层:函数调用过程之函数栈帧的创建和销毁(上)

**

文章目录


​ —— POWERED BY CAIXYPROMISE


函数栈帧的创建和销毁

通过前面的学习,我们了解到最基础的C语言程序的语法与使用,但你是否有疑问?

比如:

  • 函数的作用域是怎么形成的呢?

  • 局部变量是如何创建的?

  • 为什么未初始化的局部变量的值是随机值或是乱码呢?

  • 函数是如何传参的?传参的顺序又是怎么样的呢?

  • 形参和实参的关系是什么?

  • 函数的调用是怎么实现的呢?

  • 函数调用结束后是怎么返回的呢?

  • 为什么会存在函数递归的最大深度呢?到达最大深度所提出的堆栈溢出错误是什么意思呢?

当你了解了函数的栈帧创建与销毁的时候,这些疑惑将会一一解开!带着这些问题,我们来进入函数栈帧!

由于篇幅较长,本系列文章共分为上、下两篇。本篇为上篇,将主要介绍:

有疑问欢迎在评论区回复,点击立即查看下一篇第一时间学习下篇新知识。

  • 什么是寄存器?
  • 什么是栈?
  • 函数栈帧的形成过程
  • 函数变量的形成过程

了解函数栈帧需要涉及到反汇编操作,笔者会根据相关的汇编指令来做介绍。

圆规正转,进入正题!


什么是寄存器?

首先需要了解的:什么是寄存器?

计算机硬件中,具有存储功能的硬件有什么?它们分别是 硬盘 --> 内存 --> 高速缓存(cache) --> 寄存器,它们4个中从左到右访问速度和存储速度不断递增;同时,它们的大小也是从左到右依次递减的,到最后的寄存器,它的存储空间可能只有4byte位的存储单元大小,与此同时它的访问速度是最快的,因为寄存器一般是集成在CPU上,与内存是不同的独立的存储空间,常言道,网速飞快是坐在服务器上打游戏,而读取速度越快就是坐在CPU上读取,寄存器读取快就是这个道理。

寄存器分类

计算机的寄存器还分多种

  • 一般寄存器:EAX、EBX、ECX、EDX

    ax:累积暂存器,bx:基底暂存器,cx:计数暂存器,ed:资料暂存器

  • 索引暂存器:ESI、EDI

    si:来源索引暂存器,di:目的索引暂存器

  • 堆叠、基底暂存器:ESP、EBP

    sp:堆叠指标暂存器,bp:基底指标暂存器;这两个寄存器,也是函数栈帧中最重要的两个寄存器

其中:

  • EAX、ECX、EDX、EBX:为ax,bx,cx,dx的延伸,各为32位
  • ESI、EDI、ESP、EBP:为si,di,sp,bp的延伸,各32位
  • EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP等都是X86 汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。

寄存器用途

那么,它们在程序中的用途是怎么样的呢?

这些32位的寄存器但每一个都有“专长”,有各自的特别之处。

  • EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。

  • EBX 是"基地址"(base)寄存器, 在内存寻址时存放基地址。

  • ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。

  • EDX 则总是被用来放整数除法产生的余数。

  • ESI/EDI分别叫做"源/目标索引寄存器,因为在很多字符串操作指令中,其中DS:ESI指向源串,而ES:EDI指向的是目标串。

  • EBP是"基址指针", 它最经常被用作高级语言函数调用的"框架指针"。 在破解软件时,经常可以看见一个标准的函数起始汇编代码:

    push ebp ;保存当前ebp
    mov ebp,esp ;EBP设为当前堆栈指针
    sub esp, xxx ;预留xxx字节给函数临时变量.
    ...
    这样一来,EBP 构成了该函数的一个框架, 在EBP上方分别是原来的EBP, 返回地址和参数. EBP下方则是临时变量. 函数返回时作 mov esp,ebp/pop ebp/ret 即可.
    
  • ESP 专门用作堆栈指针,被形象地称为栈顶指针。堆栈的顶部是地址小的区域,压入堆栈的数据越多,ESP也就越来越小。在32位操作平台上,ESP每次会减少4个字节。

关于寄存器的概念就说到这。实际运用起来是将内容存到寄存器内而使用其地址。真正与形成函数栈帧有密切关系的是:EBP和ESP这两个寄存器地址。

什么是"栈"?

在开始讲解前,需要再注意一个关键词:什么是“栈”?栈是一类数据结构,本篇不会对其实现方法做太多解释,只需要了解它的一个特性:数据依次放入栈内后,取出元素时顺序是最先进入的元素最后出;例如在一个木桶内放入一堆书籍,在你需要取出底部的书时,你需要先把上部分内容取出才能取出最底部的内容。而本篇说的栈区,主要是运行在系统内存之上的

函数栈帧的概念

在寄存器内,EBP、ESP这2个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。

每一次函数调用时,都需要在栈区内创建一个空间;调用了哪个函数,EBP、ESP两个地址就会去维护这个函数的内存空间,这就是函数的栈帧;例如main函数在运行过程的当中,esp和ebp两个指针地址会同时指向它的栈顶和栈底。

这么说,你可能会不理解。那就画图吧!

函数压栈的过程

为了方便演示和理解,我会使用VS2013版本演示函数压栈的过程,带着你一块一步步的读程序的汇编指令并讲解每一个步骤会做出什么样的操作,最后会对我会对整个指令进行一个总结。因为不同的编译器对于程序汇编封装的方法可能是不同的,而更高阶的编译器对于程序的封装会更加细致,不利于观察。同时,以下的汇编指令的地址会随着每次程序编译而变化(因为内容都是随机分配的),如果你在本地也在进行调试时,请保持在同一个编译情景下。但原理上都是相通的。

需要说明的是,在VS2013之前的版本中,运行程序调试时查看调用堆栈时会发现main函数也是被其他函数调用的

分别是__tmainCRTStartup和mainCRTStartup函数,其中,mainCRTStartup压在最底部。

调用逻辑是mainCRTStartup – > __tmainCRTStartup --> main 函数

__tmainCRTStartup函数压栈

mainCRTStartup函数压栈

由此,我们可以理解此时的内存栈表示为

通过在VS2013编译环境下观察可以发现,函数在运行过程当中,会用esp栈顶指针和ebp栈底指针形成一块内存空间而形成函数的栈帧。那么,程序具体是怎么做的呢?我们可以通过查看程序的反汇编代码来研究它的压栈过程。

以下是主函数的部分反汇编代码,现在我们来看看它的具体原理是如何走的。

示例代码和主函数汇编指令 (部分)

程序代码,本篇将以本段代码进行举例,以此来介绍函数的栈帧、局部变量和函数调用的生成与销毁的过程。

以下的汇编指令是下面将会讲到的部分内容。

本篇中,我讲结合C语言X86代码生成细节的汇编指令文档来讲解本篇的汇编指令,以下是将会常用到的汇编语句。

当程序进入主函数时,我们刚刚提到主函数也是被其他函数调用的,那么这个调用主函数的原函数,是不是已经创建起它的函数栈帧了呢?答案是肯定的。此时原函数__tmainCRTStartup 是被esp和ebp两个栈顶/底指针维护的。

最初开始时的栈区应该如图所示

汇编指令:构建函数栈帧准备 (一)

接下来我们来看main函数进来的第一句汇编指令

进来的第一句话就是push ebp,汇编指令中,它的意思是把ebp的值放到栈顶

那么我们可不可以假设:因为esp维护的是程序的栈顶,此时的esp已经跑到了栈的最顶部,esp的地址会指向ebp的值?如图所示

该如何论证这个假设呢?

当你去开启监视器去监控esp就可以发现它的值会变动。

当前是esp栈顶指针的初始值

push ebp完成后,esp地址是不是由高到低,所以地址应该是减小吧?

监视器进入逐过程时就可以证明这个道理:a8 到 a4 减少了4个字节

那么esp的值会不会是ebp的值呢?打开内存块,搜索新的esp的地址会是ebp的值,答案一目了然!~

刚刚ebp的值时多少?008ffbf4, 现在搜索esp的地址的值就是008ffbf4,假设成立。

esp维护的是程序的栈顶,此时的esp已经跑到了栈的最顶部,新的esp的地址会指向ebp的值

而压入的这个ebp是什么的ebp,我们到下面会进行讲解。

汇编指令:构建函数栈帧准备 (二)

现在我们再来看第二句汇编指令:mov 把esp的值给到ebp。

事实果真如此吗?我们运行调试下一步,监控器反馈如下

此时它的栈区示意图应该是

汇编指令:构建函数栈帧的范围

再来看第三句会汇编指令:sub esp的地址,减去0E4h。(sub是英文中减少的意思,add同理为加上)

通常来说,ebp减去的值都是0E4h,而这里的0E4h实际上是一个八进制的数字。当你想查看0E4h是一个什么数字时,你可以将它放入监控区后可以显示其十六进制的值,再查看十进制数字

走到这里,不就是相当于esp减去了0E4h的值吧?那此时的esp会不会已经发生变化了呢?监视器逐过程查看结果

此时的esp的值已经变成了0x008ffac0,相当于esp的地址值变小上移不再指向原来的地方,而是指向原地址上方某一块区域内。

这个时候你有没有发现,新的esp栈顶和ebp栈顶指针在进入main函数后已经形成的一块新的维度空间,并且esp和ebp不再是维护原来的函数空间了呢?没错,这一块新的区域就是为main函数预开辟的函数栈帧区。而sub就是提出为main函数开辟的多少字节空间。

栈区示意图可以理解成下面这张图

汇编指令:放入三个非易失寄存器

这里的ebx、esi、edi是我们前面所说的寄存器中基底、来源索引、目标索引暂存器,它们三个在这里统称为非易失寄存器。这是一个C语言中的调用约定,这里将三个寄存器压栈的原因就是实现跨平台使用。在X86平台下的调用约定下这3个寄存器用途在于,调用函数时要求压入这3个寄存器以此用来保存调用前的数据,应在调用期间长期存储。

它们此处是在入栈操作,别忘了入栈的同时,esp栈顶指针也在不断的变化。

入栈的过程详情可以如下:

观察监视器内esp和ebx的值

ebx开始压入栈时,esp会如何变化呢?答案是肯定的,esp的值会递减向上挪动

打开内存器时,会发现对应esp的地址是ebx的值0x007e5000

同理继续往里压入esi时,esp的变化如下

压入edi时,esp的变化如下

综上,原来的esp栈顶指针的值已经由最初的008ffac0变成现在的008ffab4,地址在不断的减小,栈顶在不断的上移。

现在的栈区的示意图可以理解为

汇编指令:加载栈帧有效空间

到了这里为了方便直观感受与理解,我们会显示汇编符号名。

到了第七句这里的lea语句,它的全名应该是load effective address(加载有效地址);顾名思义,从此处开始,程序会正式加载当前函数的有效栈帧区域。我们来看看它该如何走吧。

lea edi, [ebp-0E4h],这里的0E4h是不是很眼熟?没错,就是刚刚在预申请main函数的函数栈帧中所预申请的大小。在这里的意思就是将ebp - 0E4h大小的空间存到edi当中去,而这个edi不就是栈底指针吗?由栈区图,我们可以观察到 ebp - 0E4h 实际表达的空间就是当前main函数的栈区空间。此时,它们已经被存到edi寄存器当中。

如何论证呢?翻到刚刚前面的三个非易失寄存器未压入栈时esp的地址

现在,我们打开监视器查看ebp-0E4h和edi的地址,答案显而易见!~ebp-0E4h的地址就是当前esp所指的第3个栈——edi的位置,也是三个非易失寄存器未压入栈时esp的地址。

mov ecx,39h和mov eax,0CCCCCCCCh意思分别是,对应的39h和0CCCCCCCCh分别放在ecx, eax寄存器内。

再来看下一句:rep stos dword ptr es:[edi] ,这里就非常有意思,此处会最终形成函数的栈帧有效空间。来看看指令语句的表述:从edi内所标记处开始重复拷贝ecx次eax的内容,直到栈底指针ebp处。需要注意的是,dword表达的是double word双字节的意思,一个word是2个字节,double word就是双字节等同于4字节。

它们的具体流程是什么样的呢?从edi所标记的ebp-0E4h处开始,向高地址的部分进行字节拷贝,每一次拷贝4个字节。 拷贝的内容就是eax的内容(0CCCCCCCCh),拷贝次数为39h次,到栈底指针ebp处停止。

结合当前按理,程序会从008ffac0处开始往高地址进行字节拷贝,直到ebp栈底指针处;当你打开内存图查看此时的内存情况时,就可以论证这一个观点~

从008ffac0出开始向高地址拷贝

到008ffba4栈底指针处结束

可能你会有疑问,这个cccccccc是什么意思呢?他们在各个编译器可能都有些许不同,而当我们平时在编写程序时,变量未定义初始值时,打印输出来的是“烫烫烫”乱码字符,不是计算机对自己自身温度的自我表达:),实际上这就是内存中放的0CCCCCCCCh字符。

综上,栈区的示意图可以如下

程序运行到现在,程序历经五步,为main函数开辟的函数栈帧正式完成,这一块由esp和ebp形成的区域就是一个函数的作用域,而由0CCCCCCCCh形成的一块空间则是用来存放局部变量的空间。接下来,程序会正式执行有效代码。

汇编指令:生成函数局部变量

由上面的诸多操作下来,一个函数的有效栈帧区已经形成,此时程序才会真正执行它的有效代码。根据之前写的代码要求,程序一进来会创建局部变量,在栈帧区内,局部变量又是如何被创建的呢?

首先我们来看汇编指令:这个语法是不是很熟悉呢?语句的的意思是:依次的将0Ah、14h、0 放入到ebp - 8、ebp - 14h、ebp - 20h 位置处。

ebp - 8、ebp - 14h、ebp - 20h是以栈底指针为基准向低地址减小的一串地址,在这里就是开辟一块空间分配给0Ah、14h、0 ,而这个0Ah、14h、0 就是计算机的十六进制的10、20、0的表达形式。所

现在我们来论证刚刚所说的。首先来继续观察ebp栈底指针的值,看它是不是往低地址存放变量;

逐过程进入语句,答案很明显,从栈底指针008ffba4处往低地址 - 8处,存放的值就是0ah。此时局部变量a = 10已创建

我们继续看下一步,创建局部变量 b = 20,后面的c同理。

根据观察在栈帧中局部变量的创建过程,我们可以发现局部变量是在形成一片有效的栈帧空间后,由高地址向低地址存放。如果变量未设置初始值时,程序会划定好一块区域规定为该变量的地址。

此时的栈区示意图可以如下

本篇小结

本篇我们基于main主函数简单介绍了一个函数的栈帧建立的基本过程;我们了解到:

  • 一个函数的栈帧实际上是由esp和ebp两个栈顶和栈底指针共同维护的一片内存空间;
  • 当一个函数在开始生成栈帧以后,会首先压入上一个函数的栈底指针ebp地址。
  • 在生成栈帧过程中,不断扩大的栈帧、压入新的内容或寄存器都会使得esp栈顶指针向上偏移;
  • 在进行确定方位相关操作时,都是以栈底指针ebp的位置作为偏移量向低地址开始偏移的。
  • 在压入3个非易失寄存器后,程序会基于ebp栈顶指针向低地址的填充一块区域,而这块区域就是一个函数的作用域。在这块作用域中,程序会根据ebp指针向上(低地址)作为方位,生成对应的变量。

下一篇,我们将会介绍函数的调用与返回过程,以及对我们开篇提出的问题做出一个总结。

有疑问欢迎在评论区提问。

本篇结语

函数栈帧销毁与过程(1)介绍完了,还有相关主题的第二篇,内容较多,干货满满。如果你觉得这篇文章对你有用的话,别忘了点赞在看+关注噢!

创作不易,你的关注与赞赏是对笔者最大的激励!笔者会继续分享有关于C/C++学习、Python实战运用相关知识。你的支持会使得后续笔者更努力推出更多高质量的文章,与你一起学习升级进阶打怪!

查看下一篇

下一篇预计在本周内发表,本系列文章已在微信公众号:01编程小屋 率先发布,欢迎大家关注第一时间学习新知识;关注小屋 学习编程不迷路

以上是关于C语言深入逐汇编详解函数栈帧的创建和销毁过程的主要内容,如果未能解决你的问题,请参考以下文章

图解C/C++语言底层:函数调用过程之函数栈帧的创建和销毁(上)

C语言进阶 顶级神功! 函数栈帧的创建和销毁

C语言学习 -- 函数栈帧的创建和销毁

C语言绝命七连问,你能坚持到第几问?欢迎挑战

C语言函数栈帧的创建和销毁,以简单函数的调用来进行详细刨析

学好C语言,还需要掌握这个内功——函数栈帧的创建与销毁