C语言——详解函数栈帧的创建和销毁

news2024/12/25 9:13:46

函数栈帧

  • 前言:
  • 一、认识相关寄存器和汇编指令
    • 1.寄存器(寄存器是集成在cpu上的)
    • 2.汇编指令
  • 二、函数栈帧创建和销毁的过程
    • 1.main函数的调用
    • 2.函数栈帧的创建
    • 3.函数栈帧的销毁

前言:

为了深入学习C语言,也为了方便理解,我学习了函数栈帧。函数栈帧的创建和销毁能够让我更加深刻的了解编程逻辑和语法。我们学习语法和编程逻辑都是基于封装好的知识上得。因此,我们有必要对函数栈帧的创建和销毁进行学习。本篇博客将用来介绍函数栈帧的创建和销毁的过程,希望大家一起学习。如有不足之处,请大家多多指出,谢谢!
注意:
这里我使用的是vs2022和大家展示。不同编译器上展示的结果会有差异,但大体逻辑一样(也能起到参考的作用)。版本越高的编译器越不好观察,不容易观看函数栈帧创建和销毁的过程,封装过程也会复杂一下。

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

1.寄存器(寄存器是集成在cpu上的)

eax:累加寄存器,相对于其他寄存器,在运算方面比较常用
ebx:基地址寄存器,在内存寻址时存放基地址。
ecx:计数寄存器,用于循环操作,如重复的字符存储操作或者数字统计。
edx:作为EAX的溢出寄存器,总是被用来放整数除法产生的余数。
esi:源变址寄存器,主要用于存放存储单元在段内的偏移量。通常在内存操作指令中作为“源地址指针”使用。
edi:目的变址寄存器,主要用于存放存储单元在段内的偏移量。
ebp:栈底指针
esp:栈顶指针
esp和ebp这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧得;esp和ebp用来维护函数栈帧时,正在调用什么函数,就会维护那个函数。
rbp,rsp(64位编译,对于32位编译是ebp,esp寄存器)这2个寄存器中存放的是地址,这2个地址是用来维护函数栈帧的。

2.汇编指令

push:
压栈,给栈顶放一个元素。(数据入栈,同时esp栈顶寄存器也要发生改变)
pop:
出栈,给栈顶删除一个元素。(数据弹出至指定位置,同时esp栈顶寄存器也要发生改变)
mov:数据转移指令。(后面的指针指向前面)
sub:减法命令。(前面的值减后面的值)
add:加法命令。
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用。
lea:加载,把后面的有效地址加载到前面。

补充:
栈区的使用是从高地址到低地址
栈区的使用遵循先进后出,后进先出
栈区的放置是从高地址往低地址放置:push 是压栈
删除是从低往高删除:pop 是出栈
如图:
在这里插入图片描述

二、函数栈帧创建和销毁的过程

本次演示以vs2022为例
演示代码:

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

    return 0;
}

准备工作:
1)按F10进入函数调用模式:
在这里插入图片描述
2)打开调用堆栈,出现调用堆栈窗口:
在这里插入图片描述
在这里插入图片描述
3)在调用模式下右击鼠标后,单击转到反汇编,进入反汇编界面:
在这里插入图片描述
在这里插入图片描述

1.main函数的调用

main函数也可以被其他函数调用:
1)为了阅读方便,我们把“显示符号名”取消勾选。
在这里插入图片描述

2)按F10,从调用堆栈,我们可以看到main函数被别的函数调用:
在这里插入图片描述

main()函数被invoke_main()函数调用;
invoke_main()函数被__scrt_common_main_seh() 函数调用;
__scrt_common_main_seh()函数被__scrt_common_main() 函数调用;
__scrt_common_main() 函数被mainCRTStartup(void * __formal) 函数调用。
注意:
编译器版本越高,反汇编越不容易观察,编译器版本过高,会优化。

2.函数栈帧的创建

1)汇编代码如下:

int main()
{
00CD18B0  push        ebp  
00CD18B1  mov         ebp,esp  
00CD18B3  sub         esp,0E4h  
00CD18B9  push        ebx  
00CD18BA  push        esi  
00CD18BB  push        edi  
00CD18BC  lea         edi,[ebp-24h]  
00CD18BF  mov         ecx,9  
00CD18C4  mov         eax,0CCCCCCCCh  
00CD18C9  rep stos    dword ptr es:[edi]  
00CD18CB  mov         ecx,0CDC008h  
00CD18D0  call        00CD131B  
    int a = 3, b = 6,c = 0;
00CD18D5  mov         dword ptr [ebp-8],3  
00CD18DC  mov         dword ptr [ebp-14h],6  
00CD18E3  mov         dword ptr [ebp-20h],0  
    c = ADD(a,b);
00CD18EA  mov         eax,dword ptr [ebp-14h]  
00CD18ED  push        eax  
00CD18EE  mov         ecx,dword ptr [ebp-8]  
00CD18F1  push        ecx  
00CD18F2  call        00CD1217  
00CD18F7  add         esp,8  
00CD18FA  mov         dword ptr [ebp-20h],eax  
    printf("%d\n", c);
00CD18FD  mov         eax,dword ptr [ebp-20h]  
00CD1900  push        eax  
00CD1901  push        0CD7B30h  
00CD1906  call        00CD10CD  
00CD190B  add         esp,8  
    return 0;
00CD190E  xor         eax,eax  
}
00CD1910  pop         edi  
00CD1911  pop         esi  
00CD1912  pop         ebx  
00CD1913  add         esp,0E4h  
00CD1919  cmp         ebp,esp  
00CD191B  call        00CD1244  
00CD1920  mov         esp,ebp  
00CD1922  pop         ebp  
00CD1923  ret  

2)给main函数开辟空间

00CD18B0  push        ebp  /*压栈,栈顶放一个元素,把ebp寄存器中的值进行压栈,此时的ebp中存放的是
invoke_main函数栈帧的ebp,esp-4*/
00CD18B1  mov         ebp,esp  /*把esp的值存放到ebp中,相当于产生了main函数的
ebp,这个值就是invoke_main函数栈帧的esp*/
00CD18B3  sub         esp,0E4h  /*sub会让esp中的地址减去一个16进制数字0xe4,产生新的
esp,此时的esp是main函数栈帧的esp,此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一
个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数
中的局部变量,临时数据已经调试信息等。*/
00CD18B9  push        ebx  //将寄存器ebx的值压栈,esp-4
00CD18BA  push        esi  //将寄存器esi的值压栈,esp-4
00CD18BB  push        edi  //将寄存器edi的值压栈,esp-4
/*上面3条指令保存了3个寄存器的值在栈区,这3个寄存器的在函数随后执行中可能会被修改,所以先保存寄
存器原来的值,以便在退出函数时恢复。*/
//下面的代码是在初始化main函数的栈帧空间。
//1. 先把ebp-24h的地址,放在edi中
//2. 把9放在ecx中
//3. 把0xCCCCCCCC放在eax中
//4. 将从ebp-0x24h到ebp这一段的内存的每个字节都初始化为CCCCCCCCh
00CD18BC  lea         edi,[ebp-24h]  //把后面有效的地址加载到前面空间里
00CD18BF  mov         ecx,9  
00CD18C4  mov         eax,0CCCCCCCCh /*每一次四个字节,总共出了*/ 
00CD18C9  rep stos    dword ptr es:[edi]  //word是一个字两个字节;dword是两个字,四个字节。
00CD18CB  mov         ecx,0CDC008h  //把0CDC008h放在ecx里
00CD18D0  call        00CD131B  //执行 call指令之前先会把call 指令的下一条指令的地址进行压栈操作

在这里插入图片描述
图示:
在这里插入图片描述
3)核心代码

 int a = 3, b = 6,c = 0;//变量a,b,c的创建和初始化,这就是局部的变量的创建和初始化
00CD18D5  mov         dword ptr [ebp-8],3  
00CD18DC  mov         dword ptr [ebp-14h],6  
00CD18E3  mov         dword ptr [ebp-20h],0  
    c = ADD(a,b);
00CD18EA  mov         eax,dword ptr [ebp-14h]  
00CD18ED  push        eax  
00CD18EE  mov         ecx,dword ptr [ebp-8]  
00CD18F1  push        ecx  
00CD18F2  call        00CD1217  
00CD18F7  add         esp,8  
00CD18FA  mov         dword ptr [ebp-20h],eax  

1).给变量a、b、c创建初始化

int a = 3, b = 6,c = 0;//变量a,b,c的创建和初始化,这就是局部的变量的创建和初始化
00CD18D5  mov         dword ptr [ebp-8],3  //把3放到ebp-8地址里
00CD18DC  mov         dword ptr [ebp-14h],6  //把6放到ebp-14h里
00CD18E3  mov         dword ptr [ebp-20h],0  //把0放到ebp-20h里

在这里插入图片描述
图示:
在这里插入图片描述
2).调用Add函数

 c = ADD(a,b);
00CD18EA  mov         eax,dword ptr [ebp-14h]  //把ebp-14h里的值给eax
00CD18ED  push        eax  //压栈,压一个元素,寄存器eax里压入ebp-14h里面的值
00CD18EE  mov         ecx,dword ptr [ebp-8] //把ebp-8里的值给ecx 
00CD18F1  push        ecx  //压栈,压一个元素,寄存器exc里压入ebp-8里面的值
00CD18F2  call        00CD1217  /*这条指令是去调用ADD函数,把地址00CD18F7存放到地址00CD18F2里(call指令的下一条指令的地址),按一下F11,进入被调函数ADD里(地址00CD1217),调用结束后,来到了下一条指令的地址处*/
00CD18F7  add         esp,8  
00CD18FA  mov         dword ptr [ebp-20h],eax  

图示:
在这里插入图片描述
3).进入ADD函数(在call指令处按F11,然后再按一次F11)
这里我重新进入调试模式,所以地址的位置也就发生了变化,意思还是不变的。

int main()
{
00C518B0  push        ebp  
00C518B1  mov         ebp,esp  
00C518B3  sub         esp,0E4h  
00C518B9  push        ebx  
00C518BA  push        esi  
00C518BB  push        edi  
00C518BC  lea         edi,[ebp-24h]  
00C518BF  mov         ecx,9  
00C518C4  mov         eax,0CCCCCCCCh  
00C518C9  rep stos    dword ptr es:[edi]  
00C518CB  mov         ecx,0C5C008h  
00C518D0  call        00C5131B  
    int a = 3, b = 6,c = 0;
00C518D5  mov         dword ptr [ebp-8],3  
00C518DC  mov         dword ptr [ebp-14h],6  
00C518E3  mov         dword ptr [ebp-20h],0  
    c = ADD(a,b);
00C518EA  mov         eax,dword ptr [ebp-14h]  
00C518ED  push        eax  
00C518EE  mov         ecx,dword ptr [ebp-8]  
00C518F1  push        ecx  
00C518F2  call        00C51217  
00C518F7  add         esp,8  
00C518FA  mov         dword ptr [ebp-20h],eax  

在这里插入图片描述
在这里插入图片描述
在按一下F11,进入ADD函数里
在这里插入图片描述
4).创建ADD函数栈帧
在这里插入图片描述
5).ADD函数的执行过程

 int z = x + y;
00C51795  mov         eax,dword ptr [ebp+8]  //把ebp+8里面的值给eax
00C51798  add         eax,dword ptr [ebp+0Ch]  //eax里面的值加上ebp+0Ch地址里的值
00C5179B  mov         dword ptr [ebp-8],eax //eax的值放到ebp-8地址里
    return z;
00C5179E  mov         eax,dword ptr [ebp-8]  //eax相当于全局的寄存器,ebp-8的值放到寄存器里。

如图:
在这里插入图片描述
6),函数栈帧创建的视图:
在这里插入图片描述

3.函数栈帧的销毁

1)ADD函数栈帧的销毁

00C517A1  pop         edi  //在栈顶弹出一个值,存放到edi中,esp+4
00C517A2  pop         esi  //在栈顶弹出一个值,存放到esi中,esp+4

00C517A3  pop         ebx   //在栈顶弹出一个值,存放到ebx中,esp+4
00C517A4  add         esp,0CCh  /*将esp的地址加上0cch,相当于回收了ADD函数的栈帧空间*/ 
00C517AA  cmp         ebp,esp  //判断有没有溢出
00C517AC  call        00C51244  //call指令里放的是下一个指令的地址
00C517B1  mov         esp,ebp  //ebp里面的值放到esp里
00C517B3  pop         ebp  //出栈,弹出一个元素,dsp+4
00C517B4  ret /*call指令可以实现调用一个子程序,在子程序里使用ret指令,结束子程序的执行并返回主函数,让主函数继续往下执行*/

图示:
在这里插入图片描述
2).ADD函数栈帧销毁后,回到主函数:
在这里插入图片描述
调用完ADD函数,回到main函数的时候,继续往下执行,可以看到:

00C518F7  add         esp,8  //esp直接+8,相当于跳过了main函数中压栈的
00C518FA  mov         dword ptr [ebp-20h],eax  /*将eax中值,存档到ebp-20h的地址处,其实就是存储到main函数中c变量中,而此时eax中就是ADD函数中计算的x和y的和,可以看出来,本次函数的返回值是由eax寄存器带回来的。程序是在函数调用返回之后,在eax中去读取返回值的。*/
    printf("%d\n", c);

在这里插入图片描述

注意:
在这里插入图片描述
总结:
1为什么局部变量不初始化内容是随机的或者是"烫"?
因为在创建函数栈帧的时候,中间的地址的值都是不确定的,而如果访问一个未初始化的变量,指向这些不确定的值,就是随机值。而初始化为0CCCCCCCCh时,遇到0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”。
2.函数调用时参数时如何传递的?传参的顺序是怎样的?
从创建局部变量的函数(比如main函数)栈帧中通过内存访问,储存在eax和ecx中再入栈(相当于临时拷贝)。
3.函数的形参和实参分别是怎样实例化的?
实参是在函数栈帧里通过ebp内存访问储存的值。形参是由ebp内存访问将栈中储存的临时变量。
4.函数调用结束后怎么返回值?
ADD函数中通过将在寄存器(eax)中相加得到的9,在移入ADD函数栈帧中c的地址位置,再将这个地址位置的值传给eax,在销毁ADD函数栈帧后,将eax中的值传给main函数栈帧中创建的c地址位置。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/692641.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

仅在python虚拟环境中安装CUDA、CUDNN、PaddlePaddle-gpu

0、前言 在配置深度学习环境时&#xff0c;若想使用GPU加速&#xff0c;就需要安装CUDA、CUDNN&#xff0c;然而在系统中安装的话&#xff0c;若不同框架需要的版本不同&#xff0c;就会比较麻烦。因此&#xff0c;一种比较方便的做法是&#xff1a;利用conda管理不同的python…

arcgis实现DEM镶嵌和去白点

目录 镶嵌去白点 镶嵌 arcgis中直接选择镶嵌就行&#xff0c;然后选择其中一幅进行输出就好 去白点 镶嵌好之后&#xff0c;就会出现白色的&#xff0c;好像没有数据&#xff0c;为nodata 解决办法&#xff1a;可以使用arcgis中的焦点统计&#xff0c;计算邻域进行插值&…

terser用于ES6的压缩JS工具

https://www.npmjs.com/package/terser uglify-es不再维护&#xff0c;uglify-js也不支持ES6。 terser是uglify-es的一个分支&#xff0c;主要保留了与uglify和uglify-js3. npm install terser -gterser [input files] [options] terser-webpack-plugin 使用terser-webpack-pl…

生成式 AI 将如何颠覆数据分析?

生成式 AI 对数据分析的颠覆式变革 想象这样一个场景&#xff0c;您能够像与人交谈一样和计算机进行交流。在这个场景中&#xff0c;您不需要学习复杂的技术&#xff0c;通过自然语言就能够整理数据、分析复杂的数据集、并生成报告。几年前&#xff0c;这可能还是科幻小说中的画…

手把手教你实现SpringBoot的监控!

任何一个服务如果没有监控&#xff0c;那就是两眼一抹黑&#xff0c;无法知道当前服务的运行情况&#xff0c;也就无法对可能出现的异常状况进行很好的处理&#xff0c;所以对任意一个服务来说&#xff0c;监控都是必不可少的。 就目前而言&#xff0c;大部分微服务应用都是基…

【C#】并行编程实战:实现数据并行(1)

本教程对应学习工程&#xff1a;魔术师Dix / HandsOnParallelProgramming GitCode 到目前为止&#xff0c;我们已经掌握了并行编程、任务和任务并行的基础知识。本章将讨论并行编程的另一个重要方面&#xff0c;即数据并行。 任务并行可以为每个参与线程创建一个单独的…

SpringSecurity认证流程(超级详细)

1 .前言 最近开发项目的时候遇到了和SpringSecurity相关的一些问题&#xff0c;但是之前并没有去了解过SpringSecurity&#xff0c;导致改系统安全权限验证的时候就比较吃力了&#xff0c;目前项目开发大多都直接用脚手架直接开发&#xff0c;系统安全权限验证已经形成了&…

漏洞复现|和信创天云桌面系统存在任意文件上传目录遍历漏洞

一、 阅读须知 一切从降低已有潜在威胁出发&#xff0c;所有发布的技术文章仅供参考&#xff0c;未经授权请勿利用文章中的技术内容对任何计算机系统进行入侵操作&#xff0c;否则对他人或单位而造成的直接或间接后果和损失&#xff0c;均由使用者本人负责。 郑重声明&#x…

2019年全国硕士研究生入学统一考试管理类专业学位联考数学试题——纯题目版

2019 年 1 月份管综初数真题 一、问题求解&#xff08;本大题共 5 小题&#xff0c;每小题 3 分&#xff0c;共 45 分&#xff09;下列每题给出 5 个选项中&#xff0c;只有一个是符合要求的&#xff0c;请在答题卡上将所选择的字母涂黑。 1、某车间计划 10 天完成一项任务&a…

网络变压器/网络滤波器 国产化替代一般需要签订哪一些相关文件

Hqst华强盛导读&#xff1a;网络变压器/网络滤波器 国产化替代一般需要签订哪一些相关文件 在国内推广和应用国产替代网络变压器/滤波器时&#xff0c;需要签定一系列的文件&#xff0c;以确保网络变压器/滤波器的质量和安全&#xff0c;同时遵守国家相关的法律法规和政策规定…

JS实现选择图片剪裁及保存

JS实现选择图片剪裁及保存 以下是一个简单的示例代码&#xff0c;实现了显示一个文件上传框和一个canvas元素。用户可以选择一张图片文件后&#xff0c;该图像将显示在canvas中&#xff0c;并且用户可以通过鼠标拖拽来选取需要剪裁的区域。单击“剪裁”按钮&#xff0c;程序会…

8.11 TCP链接管理与UDP协议

目录 TCP的链接管理 TCP包头 连接的建立——”三次握手” 连接的释放——“四次挥手” 保活计时器 UDP协议 计算机网络体系结构 UDP协议 UDP的主要特点 UDP是面向报文的 TCP的链接管理 TCP包头 连接的建立——”三次握手” TCP 建立连接的过程叫做握手。 采用三报文…

java进阶—重要概念反射

反射概念 反射: 它是java中的一个很重要的概念,是框架设计的灵魂 框架呢&#xff1f;就是一个半成品软件&#xff0c;我们在这半成品上进行开发&#xff0c;比如我们经常提到spring springmvc springboot spingcloud 等等 也许有的小伙伴会说&#xff0c;框架别人都写好了&a…

Tdengine 时序数据库-安装与客户端连接

使用 TDengine 时序数据库的版本是 2.4.0.0 使用的安装RPM的安装方便安装 TDengine-server-2.4.0.0-Linux-x64.rpm 1. 安装指令: rpm -ivh TDengine-server-2.4.0.0-Linux-x64.rpm [rootnode3 server]# rpm -ivh TDengine-server-2.4.0.0-Linux-x64.rpm Verifying... …

python的格式化输出

print中的占位符(%-fomatting) print中的%d,%s等&#xff0c;只是一个占位符&#xff0c;等正确内容到了再查进去。 注意&#xff1a;变量值和打印内容之间不能有逗号隔开。 使用fomat方法 format底层可以理解成一个字典&#xff0c;默认key是按顺序从0开始&#xff0c;指定key…

nginxWebUI runCmd RCE漏洞复现

0x01 产品简介 nginxWebUI是一款图形化管理nginx配置的工具&#xff0c;能通过网页快速配置nginx的各种功能&#xff0c;包括HTTP和TCP协议转发、反向代理、负载均衡、静态HTML服务器以及SSL证书的自动申请、续签和配置&#xff0c;配置完成后可以一键生成nginx.conf文件&#…

网络变压器卷盘(封装载带)封膜压力标准

Hqst华强盛导读&#xff1a;网络变压器封装载带封膜压力标准通常是根据具体产品的封装材料和尺寸来确定的。 网络变压器封装载带封膜压力标准通常是根据具体产品的封装材料和尺寸来确定的。一般来说&#xff0c;封装载带的封膜压力应该足够大&#xff0c;以确保封装过程中封膜…

安卓开发基于KeyStore对数据进行加解密

问题背景 在我们App开发过程中&#xff0c;可能会涉及到一些敏感和安全数据需要加密的情况&#xff0c;比如登录token的存储。我们往往会使用一些加密算法将这些敏感数据加密之后再保存起来&#xff0c;需要取出来的时候再进行解密。 此时就会有一个问题&#xff1a;用于加解密…

计算机网络—应用层

文章目录 网络应用基本原理网络应用体系结构~三种结构网络应用进程通信网络应用需求 WEB应用HTTP协议Cookie技术WEB缓存/代理服务器技术 Email应用SMTP协议Email消息格式&#xff08;SMTP传输消息的格式&#xff09;POP3协议IMAP DNS应用DNS协议 网络应用基本原理 网络应用体系…

adb 工具

小白初如嵌入式的坑&#xff0c;对于串口调试工具&#xff0c;之前一直只知道有SecureCRT这一种方式&#xff1b; 但是在最近一次的使用过程中&#xff0c;发现抢占资源的情况出现&#xff0c;两个进程之间相互打架&#xff0c;这个时候需要 查看top&#xff08;嵌入式板子资…