【C语言】函数栈帧的创建和销毁

这篇具有很好参考价值的文章主要介绍了【C语言】函数栈帧的创建和销毁。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

👦个人主页:Weraphael
✍🏻作者简介:目前正在回炉重造C语言(2023暑假)
✈️专栏:【C语言航路】
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍


前言

在学习C语言的时候,我们可能有很多困惑。比如:

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

这些都和函数栈帧的创建和销毁有关

一、什么是函数栈帧

我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。

函数栈帧stack frame就是函数调用过程中在程序的 调用栈call stack所开辟的空间,这些空间是用来存放:

  • 函数参数和函数返回值
  • 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
  • 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)

二、什么是栈?

  • stack是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
  • 在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(也就是入栈push),也可以将已经压入栈中的数据弹出(出栈pop),但是栈这个容器必须遵守一条规则:先进的数据后出
  • 在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。
  • 在经典的操作系统中,栈总是向下增长(由高地址向低地址)的

三、认识相关寄存器和汇编指令

3.1 相关寄存器

eax:通用寄存器,保留临时数据,常用于返回值
ebx:通过寄存器,保留临时数据
ebp:栈底寄存器(本章重点)
esp:栈顶寄存器(本章重点)
eip:指令寄存器,保留当前指令的下一条指令的地址

3.2 相关汇编指令

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

四、解析函数栈帧的创建和销毁

2.1 预备知识

首先我们达成一些预备知识才能有效的帮助我们理解函数栈帧的创建和销毁:

  1. 每一次调用函数,都会在栈区上创建空间,就是函数栈帧的空间
  2. 这块空间的维护使用了两个寄存器:espebpebp记录的是栈底的地址,esp记录的是栈顶的地址。

如图所示:

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

2.2 函数栈帧的创建

2.2.1 main函数也是被其他函数调用

【演示代码】

#include <stdio.h>
int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int a = 20;
	int b = 30;
	int c = 0;
	c = Add(a, b);
	printf("%d\n", c);
	return 0;
}

在此之前,我们需要理解:main函数其实也是被其他函数调用的。我们可以通过函数调用堆栈来观察,因为它是反馈函数调用逻辑的,通过下图观察到,main函数调用之前,其实是由invoke_main函数来调用的

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

那我们可以确定,invoke_main函数也会有自己的栈帧,main函数和Add函数也会维护自己的栈帧,每个函数栈帧都有自己的ebpesp来维护栈帧空间。

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

2.2.2 分析反汇编(主函数的创建)

int main()
{
001C18B0  push        ebp  
001C18B1  mov         ebp,esp  
001C18B3  sub         esp,0E4h  
001C18B9  push        ebx  
001C18BA  push        esi  
001C18BB  push        edi  
001C18BC  lea         edi,[ebp-24h]  
001C18BF  mov         ecx,9  
001C18C4  mov         eax,0CCCCCCCCh  
001C18C9  rep stos    dword ptr es:[edi]
	int a = 20;
001C18D5  mov         dword ptr [ebp-8],14h  
	int b = 30;
001C18DC  mov         dword ptr [ebp-14h],1Eh  
	int c = 0;
001C18E3  mov         dword ptr [ebp-20h],0  
	c = Add(a, b);
001C18EA  mov         eax,dword ptr [ebp-14h]  
001C18ED  push        eax  
001C18EE  mov         ecx,dword ptr [ebp-8]  
001C18F1  push        ecx  
001C18F2  call        001C10B4  
001C18F7  add         esp,8  
001C18FA  mov         dword ptr [ebp-20h],eax  
	printf("%d\n", c);
001C18FD  mov         eax,dword ptr [ebp-20h]  
001C1900  push        eax  
001C1901  push        1C7B30h  
001C1906  call        001C10D2  
001C190B  add         esp,8  
	return 0;
001C190E  xor         eax,eax  
}
001C1910  pop         edi  
001C1911  pop         esi  
001C1912  pop         ebx  
001C1913  add         esp,0E4h  
001C1919  cmp         ebp,esp  
001C191B  call        001C1244  
001C1920  mov         esp,ebp  
001C1922  pop         ebp  
001C1923  ret 
  1. 创建main函数的栈帧

001C18B0 push ebp — 将ebp的值压入栈中。

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

因为ebp记录的是栈底的地址,esp记录的是栈顶的地址,所以当压栈的时候,esp应该更新。

001C18B1 mov ebp,espmove指令会把esp的值存放到ebp

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

001C18B3 sub esp,0E4hesp减去0E4h(228)这个值

sub会让esp中的地址减去一个八进制数字,产生新的esp,此时的espmain函数栈帧的esp,此时结合上一条指令的ebp和当前的espebpesp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数中的局部变量。

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

001C18B9 push ebx
001C18BA push esi — 分别将寄存器ebx、esi、edi的值压栈
001C18BB push edi

上面3条指令保存了3个寄存器的值在栈区,这3个寄存器的在函数随后执行中可能会被修改,所以先保存寄存器原来的值,以便在退出函数时恢复。

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

001C18BC  lea         edi,[ebp-24h]  
001C18BF  mov         ecx,9  
001C18C4  mov         eax,0CCCCCCCCh  
001C18C9  rep stos    dword ptr es:[edi] 

上面的这段代码最后4句,等价于下面的伪代码:

edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx = 0; --ecx,edi+=4)
{
	*(int*)edi = eax;
}
  1. lea其实是加载有效地址(load effective address),把ebp-24h的地址放在edi
  2. 9放在ecx
  3. 0xCCCCCCCC放在eax
  4. 将从edp-0x2hebp这一段的内存的每个字节都初始化为0xCC

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

接下来我们再来分析main函数中的核心代码:

	int a = 20;
001C18D5  mov         dword ptr [ebp-8],14h  
	int b = 30;
001C18DC  mov         dword ptr [ebp-14h],1Eh  
	int c = 0;
001C18E3  mov         dword ptr [ebp-20h],0  

dword ptr [ebp-8],14h它对应的汇编指令就是把14h20)这个值放到ebp-8a)这个地址里。以下也同理,其实就是在初始化变量。

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

补充知识:烫烫烫

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

假设我们没有为变量初始化,并且会看到程序输出这么一个奇怪的字,是因为main函数调用时,在栈区开辟的空间的其中每一个字节都被初始化为0xCC,而arr数组是一个未初始化的数组,恰好在这块空间上创建的,0xCCCC(两个连续排列的0xCC)的汉字编码就是,所以0xCCCC被当作文本就是

main函数调用函数Add

	c = Add(a, b);
// 传参的过程
001C18EA  mov         eax,dword ptr [ebp-14h]  
001C18ED  push        eax  
001C18EE  mov         ecx,dword ptr [ebp-8]  
001C18F1  push        ecx  
  1. 001C18EA mov eax,dword ptr [ebp-14h] — 将ebp-14h的值也就是b存入寄存器eax
  2. eax的值压栈
  3. 001C18EE mov ecx,dword ptr [ebp-8] — 将ebp-8的值也就是a存入寄存器ecx
  4. ecx的值压栈
  • 因此我们发现,函数传参的顺序应该先传b,最后再传a

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

001C18F2  call        001C10B4  
001C18F7  add         esp,8   (调完函数讲解)
001C18FA  mov         dword ptr [ebp-20h],eax  (调完函数讲解) 

call指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是:为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

接下来就来到Add函数定义的内部:

int Add(int x, int y)
{
004A1830  push        ebp  
004A1831  mov         ebp,esp  
004A1833  sub         esp,0CCh  
004A1839  push        ebx  
004A183A  push        esi  
004A183B  push        edi  
004A183C  lea         edi,[ebp-0Ch]  
004A183F  mov         ecx,3  
004A1844  mov         eax,0CCCCCCCCh  
004A1849  rep stos    dword ptr es:[edi]  
004A184B  mov         ecx,4AC003h  
004A1850  call        004A131B  
	int z = 0;
004A1855  mov         dword ptr [ebp-8],0  
	z = x + y;
004A185C  mov         eax,dword ptr [ebp+8]  
004A185F  add         eax,dword ptr [ebp+0Ch]  
004A1862  mov         dword ptr [ebp-8],eax  
	return z;
004A1865  mov         eax,dword ptr [ebp-8]  
}
004A1910  pop         edi  
004A1911  pop         esi  
004A1912  pop         ebx  
004A1920  mov         esp,ebp  
004A1922  pop         ebp  
004A1923  ret  

2.2.3 分析反汇编(Add函数的销毁)

首先我们逐步分析:

004A1830  push        ebp  
004A1831  mov         ebp,esp  
004A1833  sub         esp,0CCh  
004A1839  push        ebx  
004A183A  push        esi  
004A183B  push        edi  
004A183C  lea         edi,[ebp-0Ch]  
004A183F  mov         ecx,3  
004A1844  mov         eax,0CCCCCCCCh  
004A1849  rep stos    dword ptr es:[edi]  
004A184B  mov         ecx,4AC003h  
004A1850  call        004A131B  

以上这些操作和刚开始进入main函数其实是类似的,就是为Add函数建立栈帧。这里就不过多赘述了(如下图)

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

接下来开始执行Add函数内部的有效代码:

	int z = 0;
004A1855  mov         dword ptr [ebp-8],0  
	z = x + y;
004A185C  mov         eax,dword ptr [ebp+8]  
004A185F  add         eax,dword ptr [ebp+0Ch]  
004A1862  mov         dword ptr [ebp-8],eax  
	return z;
004A1865  mov         eax,dword ptr [ebp-8]  
  1. mov dword ptr [ebp-8],0 — 将0放入ebp-8地址中去
  2. mov eax,dword ptr [ebp+8] — 把ebp+8的值放入寄存器eax中。那ebp+8的值是多少呢?如上图,+8ebp向下走,也就指向了ecx - 20,其实就是a。同理,add eax,dword ptr [ebp+0Ch] — 就是把刚刚的eax的值加上ebp+0Ch(其实就是b)的结果再次存入寄存器eax中。从这可以看出形参只是实参的一份临时拷贝
  3. mov dword ptr [ebp-8],eax — 再把eax的值放入ebp-8的地址处
  4. 最后就是返回:mov eax,dword ptr [ebp-8] — 把ebp-8的值放入eax中,其实就是把值拷贝给了寄存器,eax是一个寄存器,它不会因为程序的销毁而销毁。

【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

那么寄存器eax是如何把值给返回的呢?接着往下看

// pop - 出栈
004A1910  pop         edi  
004A1911  pop         esi  
004A1912  pop         ebx  
004A1920  mov         esp,ebp  
004A1922  pop         ebp  
004A1923  ret  
  1. edi、esi、ebx出栈
    【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法
  2. mov esp,ebp — 把ebp赋值给esp ,这其实是在将函数Add的空间还给操作系统
    【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法
  3. pop ebp — 将ebp出栈。注意:此时ebp要维护main函数的ebp,其实此时此刻ebpesp维护的是main函数的栈顶和栈底。
    【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法
  4. 注意:刚刚上面在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是:为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。也就是ret这个指令其实是返回到call指令的下一条指令的地方,继续往后执行
    【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法
    然后执行add esp,8 — 就是将esp的地址加上8这一步就是将形参还给操作系统
    【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法
    最后 mov dword ptr [ebp-20h],eax — 就是将eax值给ebp-20h,也就是赋值给z
    【C语言】函数栈帧的创建和销毁,C语言航路,c语言,开发语言,c++,学习,visualstudio,算法

五、总结

因此通过函数栈帧的创建和销毁文章来源地址https://www.toymoban.com/news/detail-536216.html

  1. 局部变量是怎么创建? — 在某函数的栈帧空间创建的
  2. 为什么局部变量是随机值?— 在栈区开辟的空间的其中每一个字节都被初始化为0xCC
  3. 函数是怎么传参的?传参的顺序是怎样的?— 再开一个新的空间存储参数,其顺序是从右向左的
  4. 形参和实参的关系?— 地址空间不同,也就是说形参是实参的临时拷贝
  5. 函数调用是怎么做的? 函数调用结束后是怎么返回的? — 看文章过程即可

到了这里,关于【C语言】函数栈帧的创建和销毁的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C语言-------函数栈帧的创建和销毁------剖析描骨

    🎂        ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂    🎂      作者介绍:                              🎂🎂        🎂 🎉🎉🎉🎉🎉🎉🎉              🎂           🎂作者id:老秦包你会,         🎂 简单介绍:🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂

    2024年02月14日
    浏览(25)
  • C语言之反汇编查看函数栈帧的创建与销毁

    函数栈帧是用于在计算机程序中实现函数调用的一种数据结构。在函数调用过程中,每个函数都需要在内存中创建一个栈帧,用于存储局部变量、返回地址和参数等。 具体来说,函数栈帧通常包含以下部分: 局部变量表:存储函数的局部变量,包括基本数据类型(如整数、

    2024年01月23日
    浏览(30)
  • 【C语言__函数栈帧的创建和销毁__复习篇9】

    目录 前言 一、知识补充 二、分析创建和销毁的过程 三、前言问题回答 本篇主要讨论以下问题: 1. 编译器什么时候为局部变量分配的空间 2. 为什么局部变量的值是随机的 3. 函数是怎么传参的,传参的顺序是怎样的 4. 形参和实参是什么关系 5. 函数调用是怎么做的 6. 函数调

    2024年04月25日
    浏览(28)
  • 函数栈帧的创建和销毁

    前言 观察函数栈帧的创建和销毁,不要使用太高级别的的编译器,越高级的编译器越不容易学习和观察。同时在不同编译器下,函数调用的过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现 我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以

    2023年04月17日
    浏览(35)
  • 函数栈帧的创建与销毁

    魔王的介绍:😶‍🌫️一名双非本科大一小白。 魔王的目标:🤯努力赶上周围卷王的脚步。 魔王的主页:🔥🔥🔥大魔王.🔥🔥🔥 ❤️‍🔥大魔王与你分享:莫泊桑说过,生活可能不像你想象的那么好,但是也不会像你想象的那么糟。人的脆弱和坚强都超乎了自己的想

    2024年02月15日
    浏览(29)
  • 从汇编代码探究函数栈帧的创建和销毁的底层原理

    人,只有在放弃战斗的时候才算输,只要坚持战斗,就还没输  本文收录于青花雾气-计算机基础 往期回顾 从0到1搞定在线OJ 数据在内存中的存储 计算机存储的大小端模式 目录 一、先导知识 二、函数调用堆栈 三、函数栈帧的创建 1.创建函数栈帧 2.创建变量 3.函数传参 4.函数

    2024年02月08日
    浏览(27)
  • 【C语言】函数栈帧的创建和毁销

    大家好,我是深鱼~ 目录 一、寄存器 二、栈区  三、函数栈帧的创建 1.为main函数开辟栈帧  2.在main函数中创建变量 3.调用Add函数前的准备  4.为Add函数开辟栈帧  5.在Add函数中创建变量并运算 四、函数栈帧的销毁 6.Add函数栈帧的销毁 7.返回main函数栈帧 【前言】 前期学习的时

    2024年02月14日
    浏览(26)
  • 函数栈帧的创建和毁销【C语言版】

    大家好,我是深鱼~ 目录 一、寄存器 二、栈区  三、函数栈帧的创建 1.为main函数开辟栈帧  2.在main函数中创建变量 3.调用Add函数前的准备  4.为Add函数开辟栈帧  5.在Add函数中创建变量并运算 四、函数栈帧的销毁 6.Add函数栈帧的销毁 7.返回main函数栈帧 【前言】 前期学习的时

    2024年02月15日
    浏览(30)
  • 探秘函数栈帧:『 揭开函数栈帧创建与销毁的神秘面纱 』

    .. 目录 知识点回顾 一、什么是栈帧(堆栈帧)? 1.内存布局 2.常用寄存器 3.汇编指令 👇👇对于栈的详细介绍 : 👇👇函数栈帧的介绍: 二、函数调用中的栈帧 1.探究main函数栈帧的创建 2.对main函数中的代码进行分析 3.探究Add函数栈帧的创建  三、函数栈帧的销毁过程 博客引

    2024年02月06日
    浏览(36)
  • 学C的第十一天【查看汇编代码一步步了解 函数栈帧(栈区局部变量)的创建和销毁】

    ========================================================================= 相关代码gitee自取: C语言学习日记: 加油努力 (gitee.com) ========================================================================= 接上期: 学C的第十天(继续深入学习函数、函数递归、练习)-CSDN博客 ============================================

    2024年02月04日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包