精简函数栈帧:优化创建和销毁过程的完全解析(建议收藏,提升内功)

请添加图片描述
请添加图片描述
Alt

🌈个人主页:是店小二呀
🌈C语言笔记专栏:C语言笔记
🌈C++笔记专栏: C++笔记

🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅
请添加图片描述


【前文】

一、函数栈帧的概念(stack frame)

写代码的时候,常会把一个独立实现的功能抽象为函数,所以C语言是以函数为基础单位的。

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

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

二、函数栈帧的作用

主要解决下面问题:

  1. 局部变量如何创建的?

  2. 为什么局部变量不初始化内容是随机的?

  3. 函数调用时参数时如何传递的?

  4. 传参的顺序是怎么样的?

  5. 函数的形参和实参是分别怎么实例化的?

  6. 函数的返回值如何带会的?

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

3.1 铺垫知识:

栈(stack):是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。

这里的栈,不是数据结构中栈,而是动态内存区域中的栈。在计算机系统中,栈是一个具有先进后出(First In Last Out,FIFO)的属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。(压栈操作使得栈增大,而弹出操作使得栈减小)

在经典的操作系统中,栈总是向下增长(由高地址向低地址)

在这里插入图片描述

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

相关寄存器:

  • eax:通用寄存器,保留临时数据,常用于返回值
  • ebx:通用寄存器,保留临时数据
  • ebp:栈底寄存器
  • esp:栈顶寄存器
  • eip:指令寄存器,保存当前指令的下一条指令的地址(函数调用可以看到)
  • exc: 计数寄存器

相关汇编命令:

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

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

预备知识:

  • 每一次函数调用,都需要在栈中开辟空间(函数栈帧)

  • 这块空间需要使用两个寄存器维护:esp(栈顶)和ebp(栈底),ebp记录栈底的地址,esp记录栈顶的地址

  • 函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异(本次在vs2019)

在这里插入图片描述

函数调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到, main 函数调用之前,是由invoke_main 函数来调用main函数,那么内存中以invoke_main 函数开辟好了函数栈帧。

准备环境(便于函数栈帧的过程足够清晰)

在这里插入图片描述

转到反汇编(调试到main函数开始执行的第一行,右击鼠标转到反汇编 )

注意】:VS编译器每次调试都会为程序重新分配内存,对于每次反汇编代码是一次调试代码过程中数据,每次调试略有差异。


【正文】(逐步分析)

一、创建main函数的函数栈帧

00E718B0  push        ebp  //ebp寄存器中的值压栈,存放invoke_main函数栈帧的ebp,esp-4
00E718B1  mov         ebp,esp  //mov指令会把move指令会把esp的值存放到ebp中,相当于产生了main函数的ebp,这个值就是invoke_main函数栈帧的esp
00E718B3  sub         esp,0E4h  ///sub会让esp中的地址减去一个16进制数字0xe4,产生新的esp,此时的esp是main函数栈帧的esp,此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数中的局部变量,临时数据已经调试信息等。
00E718B9  push        ebx  //将寄存器ebx的值压栈,esp-4
00E718BA  push        esi  //将寄存器esi的值压栈,esp-4
00E718BB  push        edi  //将寄存器edi的值压栈,esp-4
 //上面3条指令保存了3个寄存器的值在栈区,这3个寄存器的在函数随后执行中可能会被修改,所以先保存寄存器原来的值,以便在退出函数时恢复。
00E718BC  lea         edi,[ebp-24h]//ebp-24h的地址,放在edi
00E718BF  mov         ecx,9  //次数
00E718C4  mov         eax,0CCCCCCCCh//0CCCCCCCCh数值存放在eax寄存器
00E718C9  rep stos    dword ptr es:[edi] // 将从edp-0x2h到ebp这一段的内存的每个字节都初始化为0xCCCCCCCh(0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”)

注:最后四条代码等价下面的伪代码
edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx = 0; --ecx,edi+=4)
{
*(int*)edi = eax;
}

请添加图片描述

二、main函数变量的创建

int a = 3;
00BE183B mov dword ptr [ebp-8],3 //将3存储到ebp-8的地址处,ebp-8的位置其实就是a变量
int b = 5;
00BE1842 mov dword ptr [ebp-14h],5 //将5存储到ebp-14h的地址处,ebp-14h的位置其实是b变量
int ret = 0;
00BE1849 mov dword ptr [ebp-20h],0 //将0存储到ebp-20h的地址处,ebp-20h的位置其实是ret变量

说明】:

以上汇编代码表示的变量a、b、ret的创建和初始化,这就是局部的变量的创建和初始化。其实是局部变量的创建时在局部变量所在函数的栈帧空间中创建的

请添加图片描述

三、传参操作(在函数调用前)

//调用Add函数
ret = Add(a, b);
//调用Add函数时的传参
//其实传参就是把参数push到栈帧空间中
00BE1850 mov eax,dword ptr [ebp-14h] //传递b,将ebp-14h处放的5放在eax寄存器00BE1853 push eax //将eax的值压栈,esp-4
00BE1854 mov ecx,dword ptr [ebp-8] //传递a,将ebp-8处放的3放在ecx寄存器中
00BE1857 push ecx //将ecx的值压栈,esp-4
00BE1858 call 00BE10B4//(函数的目标地址)    
 

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

请添加图片描述

代码执行到Add函数的时候,就要开始创建Add函数的栈帧空间了。
在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。

  1. 将main函数的 ebp 压栈

  2. 计算新的 ebp 和 esp

  3. 将 ebx , esi , edi 寄存器的值保存

  4. 计算求和,在计算求和的时候,我们是通过 ebp 中的地址进行偏移访问到了函数调用前压栈进去的
    参数,这就是形参访问。

  5. 将求出的和放在 eax 寄存器尊准备带回

int Add(int x, int y)
{
00BE1760 push ebp //将main函数栈帧的ebp保存,esp-4
00BE1761 mov ebp,esp //将main函数的esp赋值给新的ebp,ebp现在是Add函数的ebp
00BE1763 sub esp,0CCh //给esp-0xCC,求出Add函数的esp
00BE1769 push ebx //将ebx的值压栈,esp-4
00BE176A push esi //将esi的值压栈,esp-4
00BE176B push edi //将edi的值压栈,esp-4
int z = 0;
00BE176C mov dword ptr [ebp-8],0 //将0放在ebp-8的地址处,其实就是创建z
z = x + y;
//接下来计算的是x+y,结果保存到z中
00BE1773 mov eax,dword ptr [ebp+8] //将ebp+8地址处的数字存储到eax中
00BE1776 add eax,dword ptr [ebp+0Ch] //将ebp+12地址处的数字加到eax寄存中
00BE1779 mov dword ptr [ebp-8],eax //将eax的结果保存到ebp-8的地址处,其实就是放到z中
return z;
00BE177C mov eax,dword ptr [ebp-8] //将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。
}

请添加图片描述

图片中的 a’ 和 b’ 其实就是 Add 函数的形参 x , y 。这里的分析很好的说明了函数的传参过程,以及函数
在进行值传递调用的时候,形参其实是实参的一份拷贝。对形参的修改不会影响实参。

四、函数栈帧的销毁

00BE177F pop edi //在栈顶弹出一个值,存放到edi中,esp+4
00BE1780 pop esi //在栈顶弹出一个值,存放到esi中,esp+4
00BE1781 pop ebx //在栈顶弹出一个值,存放到ebx中,esp+4
00BE1782 mov esp,ebp //再将Add函数的ebp的值赋值给esp,相当于回收了Add函数的栈
帧空间
00BE1784 pop ebp //弹出栈顶的值存放到ebp,栈顶此时的值恰好就是main函数的ebp,esp+4,此时恢复了main函数的栈帧维护,esp指向main函数栈帧的栈顶,ebp指向了main函数栈帧的栈
底。
00BE1785 ret //ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行

请添加图片描述

回到了call指令的下一条指令的地方:

00BE185D add esp,8 
 //esp直接+8,相当于跳过了main函数中压栈的a'和b'
00BE1860 mov dword ptr [ebp-20h],eax 
 //将eax中值,存档到ebp-0x20的地址处,其实就是存储到main函数中ret变量中,而此时eax中就是Add函数中计算的x和y的和,可以看出来,本次函数的返回值是由eax寄存器带回来的。程序是在函数调用返回之后,在eax中去读取返回值的.

注意】:其实返回对象时内置类型时,一般都是通过寄存器来带回返回值的,返回对象如果时较大的对象时,一般会在主调函数的栈帧中开辟一块空间,然后把这块空间的地址,隐式传递给被调函数,在被调函数中通过地址找到主调函数中预留的空间,将返回值直接保存到主调函数的。

具体可以参考《程序员的自我修养》一书的第10章。


请添加图片描述
以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二C语言笔记,希望对你在学习C语言中有所帮助!

  • 92
    点赞
  • 72
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 67
    评论
创建视图和触发器是关系型数据库中的两个重要的概念。视图是虚拟的表,它是基于 SQL 语句检索出来的结果集,可以像表一样使用。而触发器是数据库中的一种特殊对象,它可以在某些特定的事件发生时自动执行一些操作。 下面是一个简单的实验总结,介绍如何在 MySQL 数据库中创建视图和触发器。 1. 创建视图 在 MySQL 中,创建视图的语法如下: ``` CREATE VIEW view_name AS SELECT column1, column2, ... FROM table_name WHERE condition; ``` 其中,`view_name` 是视图的名称;`column1, column2, ...` 是选择要显示的列;`table_name` 是基础表的名称;`condition` 是可选的筛选条件。 例如,我们可以创建一个名为 `product_list` 的视图,它显示出所有价格在 1000 元以上的产品: ``` CREATE VIEW product_list AS SELECT product_name, price FROM products WHERE price > 1000; ``` 这样,我们就可以像使用表一样查询这个视图: ``` SELECT * FROM product_list; ``` 2. 创建触发器 在 MySQL 中,创建触发器的语法如下: ``` CREATE TRIGGER trigger_name {BEFORE | AFTER} {INSERT | UPDATE | DELETE} ON table_name FOR EACH ROW BEGIN -- 触发器执行的语句 END; ``` 其中,`trigger_name` 是触发器的名称;`BEFORE | AFTER` 指定触发器是在执行操作前还是后执行;`INSERT | UPDATE | DELETE` 指定触发器是在插入、更新还是删除时执行;`table_name` 是触发器所属的表名;`FOR EACH ROW` 表示对于每一行数据都会执行触发器中的代码;`BEGIN` 和 `END` 之间是触发器执行的语句。 例如,我们可以创建一个名为 `update_stock` 的触发器,在更新产品表时自动更新库存表中该产品的库存数量: ``` CREATE TRIGGER update_stock AFTER UPDATE ON products FOR EACH ROW BEGIN UPDATE stocks SET quantity = quantity - (NEW.quantity - OLD.quantity) WHERE product_id = NEW.product_id; END; ``` 这样,每当更新产品表中的某一行数据时,触发器中的代码就会自动执行,更新库存表中对应产品的库存数量。 总之,视图和触发器都是非常有用的数据库对象,可以帮助我们更方便地管理和操作数据库中的数据。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是店小二呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值