【函数栈帧的创建和销毁】(超详细图解)

article/2025/10/18 19:53:14

   想必大家在学完C语言函数章节之后,是否有这样的困惑:

  • 局部变量是怎么创建的 ?

  • 为什么局部变量的值是随机值 ?

  • 函数是怎么传参的?传参的顺序又是什么样的 ?

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

  • 函数调用是怎么做的 ?

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


    今天我们来学习函数栈帧的创建与销毁,让我们一起了解更多的底层原理,看完之后这些问题都迎刃而解了!!!

    注:在不同编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节诎诘语编译器的实现

    演示环境:Win10+x86+Vs2013

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

在介绍函数栈帧的创建之前,我们首先要了解一个东西--------------------寄存器

寄存器的种类有很多种,今天主要介绍两种:

ebp和esp  这两个寄存器存放的是地址,用来维护函数栈帧的,简单来说就是维护函数开辟的那一块空间

每一个函数的调用,都要在栈区开辟一块空间,而ebp和esp就是维护这块空间的,如下图:

 

为了方便演示,我们编写一个加法程序:

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", c);return 0;
}

接下来就是函数栈帧的创建和销毁,这里我们需要打开反汇编代码,逐条分析:

 操作步骤:F10----->光标停留在代码块处右击鼠标----->转到反汇编

 main函数汇编代码:

int main() {
002718A0  push        ebp  
002718A1  mov         ebp,esp  
002718A3  sub         esp,0E4h  
002718A9  push        ebx  
002718AA  push        esi  
002718AB  push        edi  
002718AC  lea         edi,[ebp-24h]  
002718AF  mov         ecx,9  
002718B4  mov         eax,0CCCCCCCCh  
002718B9  rep stos    dword ptr es:[edi]  
002718BB  mov         ecx,27C003h  
002718C0  call        0027131B  int a = 10;
002718C5  mov         dword ptr [ebp-8],0Ah  int b = 20;
002718CC  mov         dword ptr [ebp-14h],14h  int c = 0;
002718D3  mov         dword ptr [ebp-20h],0  c = Add(a, b);
002718DA  mov         eax,dword ptr [ebp-14h]  
002718DD  push        eax  
002718DE  mov         ecx,dword ptr [ebp-8]  
002718E1  push        ecx  
002718E2  call        002710B4  
002718E7  add         esp,8  
002718EA  mov         dword ptr [ebp-20h],eax  printf("%d", c);
002718ED  mov         eax,dword ptr [ebp-20h]  
002718F0  push        eax  
002718F1  push        277B30h  
002718F6  call        002710D2  
002718FB  add         esp,8  return 0;
002718FE  xor         eax,eax  
}
00271900  pop         edi  
00271901  pop         esi  
00271902  pop         ebx  
00271903  add         esp,0E4h  
00271909  cmp         ebp,esp  
0027190B  call        00271244  
00271910  mov         esp,ebp  
00271912  pop         ebp  
00271913  ret  

 首先我们要知道,main函数也是被其他函数所调用的,它是被_tmainCRTStartup这个函数所调用,我们这里主要说明函数栈帧的创建和销毁,所以这里就不带大家介绍这个函数的由来了,如果感兴趣可以自己去翻阅一下资料------

在此之前,ebp和esp两个寄存器都在维护_tmainCRTStartup所分配的空间,接下来我们来分析反汇编代码:

1.

002718A0  push        ebp 

 push ----------------------压栈的意思,这一步我们将ebp压栈

 


2.

002718A1  mov         ebp,esp  
002718A3  sub         esp,0E4h  

 mov:移动,将esp的值赋给edp

 sub:减,将esp的值减去0E4h大小的空间

通过监视我们可以发现,此时esp和ebp的值已经一模一样了,说明ebp已经移到 _tmainCRTSstartup函数的栈顶了,并且esp的值也发生了变化,如图所示:

 由此我们可以发现,减去的0E4h的大小原来是为main函数开辟的空间大小,而edp和esp也由维护原来的_tmainCRTSstartup函数的栈帧转变为维护main函数的栈帧了


3.

​​​​002718A9  push        ebx  
002718AA  push        esi  
002718AB  push        edi 

 这里又是压栈了,将ebx,esi,edi从main函数栈顶依次压入:

 


4.

 这里4条汇编指令的意思是将edi向下的39h这么大的空间里全部赋值为cccccccc,如图:

 到这里main函数的栈帧就已经创建好了


5.

	int a = 10;
002718C5  mov         dword ptr [ebp-8],0Ah  int b = 20;
002718CC  mov         dword ptr [ebp-14h],14h  int c = 0;
002718D3  mov         dword ptr [ebp-20h],0  c = Add(a, b);

 这里是创建a,b,c三个变量,假设我们用一个格子代表4个字节,那么创建的a,b,c三个变量如下图所示:

 

通过查看内存可发现,刚好和我们在main函数栈帧里创建的吻合


6.

002718DA  mov         eax,dword ptr [ebp-14h]  
002718DD  push        eax  
002718DE  mov         ecx,dword ptr [ebp-8]  
002718E1  push        ecx  

将[ebp-14h]的值传给eax,再进行压栈,将[ebp-8]的值传给ecx,再进行压栈,如图:

 大家有没有发现,这一步操作正是我们函数的传参!


7.

002718E2  call        002710B4  
002718E7  add         esp,8  

call指令是调用的意思,这里我们需要将call指令的下一条指令的地址进行压栈,这里因为函数调用会返回,而返回的地址正是call指令的下一条地址

 接下来就正是进入我们的Add函数了

Add函数汇编代码:


int Add(int x, int y) {
00271770  push        ebp  
00271771  mov         ebp,esp  
00271773  sub         esp,0CCh  
00271779  push        ebx  
0027177A  push        esi  
0027177B  push        edi  
0027177C  lea         edi,[ebp-0Ch]  
0027177F  mov         ecx,3  
00271784  mov         eax,0CCCCCCCCh  
00271789  rep stos    dword ptr es:[edi]  
0027178B  mov         ecx,27C003h  
00271790  call        0027131B  int z = 0;
00271795  mov         dword ptr [ebp-8],0  z = x + y;
0027179C  mov         eax,dword ptr [ebp+8]  
0027179F  add         eax,dword ptr [ebp+0Ch]  
002717A2  mov         dword ptr [ebp-8],eax  return z;
002717A5  mov         eax,dword ptr [ebp-8]  
}
002717A8  pop         edi  
002717A9  pop         esi  
002717AA  pop         ebx  
002717AB  add         esp,0CCh  
002717B1  cmp         ebp,esp  
002717B3  call        00271244  
002717B8  mov         esp,ebp  
002717BA  pop         ebp  
002717BB  ret  

8.

00271770  push        ebp  
00271771  mov         ebp,esp  
00271773  sub         esp,0CCh  
00271779  push        ebx  
0027177A  push        esi  
0027177B  push        edi  
0027177C  lea         edi,[ebp-0Ch]  
0027177F  mov         ecx,3  
00271784  mov         eax,0CCCCCCCCh  
00271789  rep stos    dword ptr es:[edi]  

这里就进入了我们Add函数的汇编指令了,大家有没有发现这串代码和前面main函数开辟函数栈帧的代码很相似:

 push:首先是ebp压栈,

 mov:移动,将esp的值赋给edp

 sub:将esp的值减去0Ch大小的空间

将edi向下的39这么大的空间里全部赋值为cccccccc

 


9.

int z = 0;
00271795  mov         dword ptr [ebp-8],0  z = x + y;
0027179C  mov         eax,dword ptr [ebp+8]  
0027179F  add         eax,dword ptr [ebp+0Ch]  
002717A2  mov         dword ptr [ebp-8],eax  return z;
002717A5  mov         eax,dword ptr [ebp-8]  
00271795  mov         dword ptr [ebp-8],0  

 首先将[ebp-8]位置赋值为0给变量z

0027179C  mov         eax,dword ptr [ebp+8]  
0027179F  add         eax,dword ptr [ebp+0Ch]  
002717A2  mov         dword ptr [ebp-8],eax  

 接下来将[ebp+8]位置的值赋给eax,而此时[ebp+8]正是我们在上面创建好的a变量10,即:eax=10,接着执行add,将[ebp+0ch]的值加给eax,而[ebp+0ch]的值正是我们在上面创建好的b变量20,此时eax=30,接着继续mov,将eax的值赋给[ebp-8],而[ebp-8]是我们上面创建好的z,一切都是那么的吻合!太美妙了!

我们在学习函数的时候,有一句话叫形参是实参的一份临时拷贝,现在看过来,这句话完全正确,因为我们在传参的时候,并没有独立去开辟新的空间去接收形参,而是通过寄存器去找到我们之前在主函数里压栈进去的实参!


10.

	return z;
002717A5  mov         eax,dword ptr [ebp-8]  
}
002717A8  pop         edi  
002717A9  pop         esi  
002717AA  pop         ebx  
002717B8  mov         esp,ebp  
002717BA  pop         ebp  

mov  将[ebp-8]的值由eax保管

pop   意思是弹出,接下来就是函数栈帧的销毁,此时edi,esi,ebx就被销毁了

 mov   将ebp的值赋给esp

 pop   弹出ebp

 到这里,红线以上的Add函数的栈帧就被销毁了

 回过头看,我们为什么要将[ebp-8]的值先由eax保管,原因是[ebp-8] (也就是z的值)会销     毁,如果不由eax保管,那么返回值将带不出来!


10.

002717BB  ret  

ret    返回值

此时栈顶上存放的就是call指令的下一条指令的地址,此时按F10,就直接跳到main函数的Add指令了

 这就是我们为什么要存放call指令的下一条指令的地址,就是为了确保函数销毁时我还能回得来!这一套逻辑真的是太严密了!!!


11.

002718E7  add         esp,8  
002718EA  mov         dword ptr [ebp-20h],eax 

add    将esp的地址+8,就回到了我们的edi上面了

此时红线以上的部分又被销毁了,此时的形参x,y的空间就释放了 

mov     将eax的值赋给[ebp-20h],而此时的[ebp-20h]就是我们之前压栈的c的空间,eax使我们上面带回来的30,赋给了变量c,这一切又是那么的巧妙!!!

二 . 总结

当我们真正理通函数栈帧创建和销毁的过程,我们会产生一种敬畏之心(小编是有的),对前辈的敬畏,这么严密的底层逻辑思维,每一步汇编指令都是精心设计,回头来你会发现,原来当我们在写代码的时候,底层的一些东西原来是这样实现的,这个世界真的很奇妙!

如果对上文有意见或者有错误,还请大佬们斧正,觉得有帮助的童鞋们,蟹蟹三连!


http://chatgpt.dhexx.cn/article/54ywedxb.shtml

相关文章

C语言函数栈帧详解

系列文章目录 前言 最近正在学习栈帧方面的知识,由于本人对汇编不太熟悉,对其中频繁出现的ESP寄存器和EBP寄存器一直没搞清楚,在查找资料后,在此进行整理,方便以后温故知新。 一、预备知识 要清楚理解栈帧的概念&…

详解栈帧结构

https://www.1024do.com/?p367 栈帧结构 含义:C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。栈帧也叫过程活动记录,是编译器用来实现过程函数调用的一种数据结构。 从逻辑上讲,栈帧就是…

函数栈帧详解

目录 一.什么是函数栈帧 1.寄存器: 2.函数栈帧 3.栈帧的作用和维护 4.栈帧结构 二.函数栈帧的创建 1.汇编代码 2.main函数函数栈帧的创建 1.汇编语言讲解: 2.栈帧创建: 3.详细步骤 3.ADD函数栈帧的创建 栈帧创建: 3.函…

栈帧

首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(地址地)。下图为典型的存取器安排,观察栈在其中的位置 入栈操…

什么是函数栈帧

函数栈帧的创建与销毁 一、函数栈帧的创建1.寄存器2.函数栈帧3.函数中调用函数 二、函数栈帧的销毁总结 一、函数栈帧的创建 1.寄存器 一般来说,计算机中的寄存器有六种 分别是:eax, ebx, ecx,edx,ebp,esp 而ebp,esp这两个寄存器中存放的是地址&#…

栈帧 stack frame

栈帧 stack frame 每一次函数调用都会维护一个栈帧(stack frame),栈帧主要用于传递参数、保存返回地址、保存局部变量等。先直接上一个《深入理解计算机系统》上的原图。 其中,%rsp 指向栈顶位置,%rbp 指向栈底位置。…

C/函数栈帧

🌱博客主页:大寄一场. 🌱系列专栏:C语言学习笔记 😘博客制作不易欢迎各位👍点赞⭐收藏➕关注 ​ 目录 前言 寄存器 1. 寄存器的种类与功能 C语言汇编指令介绍 函数栈帧的创建与销毁过程 1.函数栈帧的…

浅谈函数栈帧(Stack Frame)

💙作者:阿润菜菜 📖专栏:C 本文目录 什么是栈帧 在调试中观察 总结 什么是栈帧 那我们先来看看什么是栈: 栈(stack)是限定仅在表尾进行插入或者删除的线性表。栈是一种数据结构,它按照后进先出的原则存储…

栈和栈帧

栈 堆栈(stack)又称为栈或堆叠,是计算机科学里最重要且最基础的数据结构之一,它按照FILO(First In Last Out,后进先出)的原则存储数据。 栈的相关概念: 栈顶和栈底:允许元素插入与删除的一端…

函数栈帧的创建和销毁(图解)

目录 基础知识介绍1. 寄存器的种类与功能2. 常用汇编指令3. 内存模型 演示函数栈帧的创建销毁过程1. 为main()函数开辟栈帧2. 在main()函数中创建变量3. 调用Add()函数前的准备4. 为Add()函数开辟栈帧5. 在Add()函数中创建变量并运算6. Add()栈帧的销毁7. 返回main()函数栈帧 总…

运行时栈帧结构是怎样的?

写在前面 本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢! 本专栏目录结构和文献引用请见100个问题搞定Java虚拟机 解答 栈帧(Stack Frame)是J…

函数栈帧(详细图解)

目录 一、栈 二、常用寄存器及简单汇编指令 三、理解栈帧 3.1 main函数栈帧创建 3.1.1 main函数栈帧创建动态演示 3.2 局部变量创建 3.2.1 局部变量创建动态演示 3.3 函数传参与调用 3.3.1 函数传参 3.3.2 函数传参动态演示 3.3.3 函数调用 3.3.4 函数返回 四、END…

栈帧详解——C语言进阶

目录 传统艺能😎过渡区🤣正片开始👀寄存器👏main函数创建👏局部变量创建👏函数部分👏形参与实参👏 传统艺能😎 小编是大一菜鸟不赘述,欢迎大佬指点江山&…

栈帧结构详解

前言 Java虚拟机以方法作为基本的执行单位,“栈帧”是用于支持虚拟机进行方法调用和执行的数据结构,每一个方法从调用开始到执行结束,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程,栈帧也是虚拟机运行时数据区中虚拟机栈的栈…

浅谈栈帧

一、 什么是栈帧? 什么是栈帧,首先引用百度百科的经典解释:“栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。 实际上,可以简单理解为:栈帧就是存储在用户栈上的(当然内…

栈帧(Stack Frame)

0x01.栈在计算机中的应用 在计算机系统中,栈也可以称之为栈内存是一个具有动态内存区域,存储函数内部(包括main函数)的局部变量和方法调用和函数参数值,是由系统自动分配的,一般速度较快;存储地址是连续且…

什么是栈帧

栈帧浅析 什么是栈帧 引用百度百科中的解释: 栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。函数的每次调用,都有它自己独立的栈帧。栈帧中维持着函数调用所需要的各种信息,包括函数的入参、函数的局部变…

【详解】函数栈帧——多图(c语言)

目录 前言 一.函数栈帧是什么? 二、栈帧准备知识 1.内存分区 2.什么是栈? 3.esp,ebp,eax寄存器 三、详解栈帧创建与销毁全过程 调用函数之前: 将传入函数的值放入栈中 函数执行: 1.保护当前ebp 2.…

【mcuclub】模数转换ADC0832

一、实物图 二、原理图 编号名称功能1CS片选使能,低电平芯片使能。2CH0模拟输入通道0,或作为IN/-使用。3CH1模拟输入通道1,或作为IN/-使用。4GND电源地5DI数据信号输入,选择通道控制。6DO数据信号输出,转换数据输出。7…

[技术讨论] [DDS] AD9833原理介绍及chiliDDS驱动分享(上)

​ 其实本文还有另一标题:《AD9833调不通?看这篇**就够了》 总觉字里行间隐隐霸气外露,不符合作者低调的风格,于是换了个朴素标题。标题狂不狂暂且不评,作者水平有限却是个事实;看到这篇**是你我缘分&…