函数栈帧的创建及销毁(超详解)

目录

1.预备知识

1.1内存区的划分

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

1.2.1寄存器

1.2.2相关汇编指令 

2.测试前

2.1测试代码及环境 

2.2 main函数也是被其他函数调用的

3.函数栈帧的创建 

4.进入函数内部 

5.形参与实参 

 6.call/jump add函数

7.函数栈帧的销毁 

7.1保存返回值

7.2销毁add的函数栈帧

 7.3回收形参

 7.4返回值输入


1.预备知识

由于本文重在帮助初学者快速理解函数栈帧的创建及销毁,下面所有概念会讲得通俗一些(不使用标准定义) ,并且调试环境为x86,debug版本,vs2022.

1.1内存区的划分

了解内存分区有助于理解程序的运行机制和内存管理,进而更好理解函数栈帧的创建及销毁。

由低地址到高地址依次是:

  1. 代码区(code segment):简单来讲,你写的代码就在这个区域。
  2. 常量区(constant):你写代码中涉及的字符常量,define的常量,就是不变不可修改的在这个区。
  3. 全局/静态区(global/static):存放全局变量和静态变量,这些变量在整个程序运行期间都存在。包括初始化了的,和未初始化的。
  4. 堆区(heap):也是动态内存分配的区域,由程序员手动控制。常用的malloc函数就是利用的这个区。
  5. 栈区(stack):由编译器自动分配和释放,用于存放函数的参数值、局部变量的值等。栈是一种先进后出的数据结构,用于支持程序的执行流程,如函数调用和返回。你可以理解为你程序运行能得出结果,就是靠的他。

除此以外,我们还需要明确, 代码是由低地址到高地址开始执行的,但函数栈帧是从高地址到低地址的,这个我在下面示例中会详细强调。

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

1.2.1寄存器

寄存器大家刚开始也不必想得特别复杂,就理解为与平时的int a,int* a差不多功能的存放数据的就行了,大致有四种寄存器:

1.通用寄存器:主要是用来保存一些数据的,可以理解为“万能”的类似于int的数据类型,通常用于算术运算、逻辑运算以及数据传递等操作。包含eax,ebx,ecx,edx四个主要的寄存器。

eax:保存数据来进行加法乘法运算,也可以保存返回值

ebx:主要用于内存寻址时存放指针或索引的基地址

ecx:存放循环次数

edx:除法运算时,存放余数

2.变址寄存器 :主要是存放地址的,你可以理解为函数内的某种指针,指向栈区

esi:通常被称为源变址寄存器。它主要用于存放要处理的数据的内存地址,特别是在进行字符串操作时,ESI寄存器通常用于指向源字符串的起始地址。

edi:通常被称为目的变址寄存器。与ESI相对应,EDI寄存器通常用于存放目标数据的内存地址,在进行字符串操作时,EDI寄存器通常用于指向目标字符串的起始地址。

3.两个核心寄存器 :栈指针寄存器ESP基址指针寄存器EBP,指向栈区

栈区是由高地址向低地址执行的(结合上图加以理解),所以高地址是栈底,低地址是栈顶。

ebp,指向栈底

esp,指向栈顶

4.EIP :指向下一条即将执行的语句,指向代码区

1.2.2相关汇编指令 

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

2.测试前

2.1测试代码及环境 

环境:x86,debug,vs2022

//代码实现的是计算并打印十六进制A+B的结果#include<stdio.h>int add(int a, int b)
{int c = 0;c = a + b;return c;
}int main()
{int x = 0xA;int y = 0xB;int z = 0;z = add(x, y);printf("%d\n", z);return 0;
}

2.2 main函数也是被其他函数调用的

我们可以看到main函数被invoke_main函数调用, invoke_main函数又被_scrt_common_main_seh函数调用,最后大家会看到,kerner32.dll,实际上就是被操作系统调用了。main函数被其他函数调用,其他函数也会被调用,调用的尽头就是被操作系统调用。

3.函数栈帧的创建 

开始的时候,我们讲详细一些

int main()
{
00A025B0 55                   push        ebp  
00A025B1 8B EC                mov         ebp,esp  
00A025B3 81 EC E4 00 00 00    sub         esp,0E4h  
00A025B9 53                   push        ebx  
00A025BA 56                   push        esi  
00A025BB 57                   push        edi  
00A025BC 8D 7D DC             lea         edi,[ebp-24h]  
00A025BF B9 09 00 00 00       mov         ecx,9  
00A025C4 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00A025C9 F3 AB                rep stos    dword ptr es:[edi]  
00A025CB B9 08 C0 A0 00       mov         ecx,0A0C008h  
00A025D0 E8 4B ED FF FF       call        00A01320  int x = 0xA;
00A025D5 C7 45 F8 0A 00 00 00 mov         dword ptr [ebp-8],0Ah  int y = 0xB;
00A025DC C7 45 EC 0B 00 00 00 mov         dword ptr [ebp-14h],0Bh  int z = 0;
00A025E3 C7 45 E0 00 00 00 00 mov         dword ptr [ebp-20h],0  z = add(x, y);
00A025EA 8B 45 EC             mov         eax,dword ptr [ebp-14h]  
00A025ED 50                   push        eax  
00A025EE 8B 4D F8             mov         ecx,dword ptr [ebp-8]  
00A025F1 51                   push        ecx  
00A025F2 E8 2C EA FF FF       call        00A01023  
00A025F7 83 C4 08             add         esp,8  
00A025FA 89 45 E0             mov         dword ptr [ebp-20h],eax  printf("%d\n", z);
00A025FD 8B 45 E0             mov         eax,dword ptr [ebp-20h]  
00A02600 50                   push        eax  
00A02601 68 CC 7B A0 00       push        0A07BCCh  
00A02606 E8 AB ED FF FF       call        00A013B6  
00A0260B 83 C4 08             add         esp,8  return 0;
00A0260E 33 C0                xor         eax,eax  
}
00A02610 5F                   pop         edi  
00A02611 5E                   pop         esi  
00A02612 5B                   pop         ebx  
00A02613 81 C4 E4 00 00 00    add         esp,0E4h  
00A02619 3B EC                cmp         ebp,esp  
00A0261B E8 24 EC FF FF       call        00A01244  
00A02620 8B E5                mov         esp,ebp  
00A02622 5D                   pop         ebp  
00A02623 C3                   ret  

eip代表的即是即将执行的下一条语句,我们F10右键进入反汇编,eip的值为main函数进去后的第一条指令。

eip=00A025B0时,F10,代码走起,push ebp,相当于把ebp自己的地址压入栈中,esp 自己的地址向低地址改变时,esp的值是ebp的地址。(ebp,esp的值是他们所在内存区的地址,而非内存区存放的值)回来看时esp push 后我发现有歧义,我的意思是push  ebp后esp的地址,esp push 前同理。

 

eip=00A025B1,F10,代码走起 ,mov  ebp,esp ,相当于将esp的值(注意是esp的地址,而非内存区的值)给ebp,即ebp与esp指向同一个地方

 

eip =  00A025B3,F10,代码走起,sub  esp,0E4h,也就是将esp向低地址移动0E4h(228)

 

eip = 00A025B9,00A025BA,00A025BB,开始push ebx,esi,edi,跟push  ebp是一个思路。分别将ebx,esi,edi的值压入栈中,esp所在内存区的值就是他们的值 。注意,push的内容在内存中是连续存放的,esp都相差4.

 

 eip = 00A025BC,F10,代码走起,lea  edi,[ebp - 24h],也就是将edi的值改成了ebp - 24h,相当于改变了edi 的指向。跟mov很像,只是mov进行的是数据的计算,而lea进行的是地址的计算。

eip = 00A025BF,F10,代码走起,mov  ecx,9,也就是将循环次数设置为9

eip = 00A025C4 ,F10,代码走起,mov  eax,0CCCCCCCCh ,也就是eax存入CCCCCCCC

eip = 00A025C9,F10,代码走起,rep stos    dword ptr es:[edi],也就是重复次数为9,将eax的值向高地址开始“赋值”,值为CCCCCCC,4*9=36,即最后到了edi + 36上,恰好是到了ebp为止

eip = 00A025CB,F10,代码走起,mov   ecx,0A0C008h,即将0A0C008h的值放进ecx中。

 

 eip = 00A025D0, call   00A01320,即先压入返回地址00A025D5(int x = 0xA;),然后跳转到00A01320去执行。我们在add函数时再详细讲解。

至此,函数栈帧就已基本开辟完毕。

4.进入函数内部 

eip = 00A025D5,00A525DC,00A025E3,执行x,y,z的定义dword ptr [ebp-8],0Ah,dword ptr [ebp-14h],0Bh,dword ptr [ebp-20h],0,相当于将值保存到ebp - 8或14h或20h当中。

 

5.形参与实参 

由于笔者在调试时,不小心退出过一次,重新调试,代码的地址要变,故下列eip会与上不同,但原理一致,而且是同样的代码,故图的是不会发生改变的,相对地址不会变,变的只是地址

	z = add(x, y);
00A025EA 8B 45 EC             mov         eax,dword ptr [ebp-14h]  
00A025ED 50                   push        eax  
00A025EE 8B 4D F8             mov         ecx,dword ptr [ebp-8]  
00A025F1 51                   push        ecx  
00A025F2 E8 2C EA FF FF       call        00A01023  
00A025F7 83 C4 08             add         esp,8  
00A025FA 89 45 E0             mov         dword ptr [ebp-20h],eax  

eip = 00A025EA ~00A025F1,先将y的值放入eax中,再压栈,然后再将x的值放入eax中,再压栈,形参就形成了。

 6.call/jump add函数

eip = 00A025F2,call        00A01023,先压入返回的地址00A025F7(相当于push操作),再调用00A01023处的函数

 

00A01023 E9 B8 07 00 00       jmp         add (0A017E0h)

 eip = 00A01023 ,就直接跳进去了

int add(int a, int b)
{
00A017E0 55                   push        ebp  
00A017E1 8B EC                mov         ebp,esp  
00A017E3 81 EC CC 00 00 00    sub         esp,0CCh  
00A017E9 53                   push        ebx  
00A017EA 56                   push        esi  
00A017EB 57                   push        edi  
00A017EC 8D 7D F4             lea         edi,[ebp-0Ch]  
00A017EF B9 03 00 00 00       mov         ecx,3  
00A017F4 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00A017F9 F3 AB                rep stos    dword ptr es:[edi]  
00A017FB B9 08 C0 A0 00       mov         ecx,offset _AB3779B8_Project2024_2_22@c (0A0C008h)  
00A01800 E8 1B FB FF FF       call        @__CheckForDebuggerJustMyCode@4 (0A01320h)  int c = 0;
00A01805 C7 45 F8 00 00 00 00 mov         dword ptr [c],0  c = a + b;
00A0180C 8B 45 08             mov         eax,dword ptr [a]  
00A0180F 03 45 0C             add         eax,dword ptr [b]  
00A01812 89 45 F8             mov         dword ptr [c],eax  return c;
00A01815 8B 45 F8             mov         eax,dword ptr [c]  
}
00A01818 5F                   pop         edi  
00A01819 5E                   pop         esi  
00A0181A 5B                   pop         ebx  
00A0181B 81 C4 CC 00 00 00    add         esp,0CCh  
00A01821 3B EC                cmp         ebp,esp  
00A01823 E8 1C FA FF FF       call        __RTC_CheckEsp (0A01244h)  
00A01828 8B E5                mov         esp,ebp  
00A0182A 5D                   pop         ebp  
00A0182B C3                   ret 

7.函数栈帧的销毁 

是不是感觉前面都很熟悉,简直跟main函数一模一样,没错,都说,main函数也是被其他函数调用,所以被调用的add函数也就差不多了。也可以说得上是创建的逆过程,于是我们直接进入销毁阶段。

7.1保存返回值

eip = 00A01815 ,mov   eax,dword ptr [c]  ,就是先将返回值保存在寄存器中,到时候,再将eax中的值给实参z。

7.2销毁add的函数栈帧

eip = 00A01818 ~00A0182B,其实就相当于从栈底逐渐弹栈,把高的拿完了,再拿低的。add  esp ,加的值其实恰好使esp的值等于了ebp,但vs2022又做了一个安全检查,最后返回

eip = 00A01828 ,ret。ret的是什么呢?刚刚弹了ebp,为什么ebp能回到原来的位置呢?因为push ebp时,esp对应的内存区存放的是ebp原来的地址,弹栈就是能弹回原来的地方,ret对应内存区的地址就是返回地址返回的地址00A025F7。所以F10后,eip能指00A025F7.

 7.3回收形参

eip = 00A025F7,add   esp,8,esp向高地址移动8个单位后,最终回到了原来的地方

 7.4返回值输入

eip = 00A025FA,mov  dword ptr [z],eax,将eax保存的返回值放入参数z中

至此,函数栈帧的销毁就到此结束了。

每个函数栈帧的创建及销毁其实都大同小异 ,对照参考,就能得出。

感谢大佬们的支持,也欢迎能够指出不足!

 

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

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

相关文章

使用transformer来训练自己的大模型实现自定义AI绘图软件的详细操作步骤

使用transformer来训练自己的大模型实现自定义AI绘图软件的详细操作步骤&#xff01;下面的步骤是非常细致的&#xff0c;如果你有一台自己的GPU算力还算可以的服务器主机&#xff0c;想自己训练AI大模型。可以按照如下步骤开展操作。 要使用 Transformer 框架训练属于自己的大…

哪种游泳耳机品牌更好?2024四款甄选高评分榜单好物!

在繁忙的都市生活中&#xff0c;游泳已经成为了许多人释放压力、保持健康的重要方式。而随着科技的进步&#xff0c;游泳耳机也逐渐走进了人们的视野&#xff0c;让音乐与游泳完美结合&#xff0c;为游泳爱好者带来了全新的运动体验。然而&#xff0c;在琳琅满目的游泳耳机市场…

matlab|计及电转气协同的含碳捕集与垃圾焚烧虚拟电厂优化调度

1 主要内容 该程序参考《计及电转气协同的含碳捕集与垃圾焚烧虚拟电厂优化调度》模型&#xff0c;主要实现的是计及电转气协同的含碳捕集与垃圾焚烧虚拟电厂优化调度模型。通过引入碳捕集电厂–电转气–燃气机组协同利用框架&#xff0c;碳捕集的CO2 可作为电转气原料&#xf…

Linux下出现ERROR: 1 Can‘t create/write to filexxxxxx

此类问题大多都是权限问题&#xff0c;将根目录的读写权限设置为最高即可解决 案例&#xff0c;ubantu中安装mysql&#xff0c;出现ERROR: 1 Cant create/write to file /home/utf/server_202402/db/mysql/data/mysql/db.MYI (Errcode: 13) 解决办法&#xff1a;将/home/utf目…

【算法与数据结构】417、LeetCode太平洋大西洋水流问题

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;题目要求雨水既能流向太平洋也能流向大西洋的网格。雨水流向取决于网格的高度。一个比较直接的方式是对…

element ui 安装 简易过程 已解决

我之所以将Element归类为Vue.js&#xff0c;其主要原因是Element是&#xff08;饿了么团队&#xff09;基于MVVM框架Vue开源出来的一套前端ui组件。我最爱的就是它的布局容器&#xff01;&#xff01;&#xff01; 下面进入正题&#xff1a; 1、Element的安装 首先你需要创建…

Java设计模式-结构型-适配器模式

Java设计模式-结构型-适配器模式 本文我们简单说下设计模式中的适配器模式。 一、概述 ​ 与电源适配器相似&#xff0c;在适配器模式中引入了一个被称为适配器(Adapter)的包装类&#xff0c;而它所包装的对象称为适配者(Adaptee)&#xff0c;即被适配的类。适配器的实现就是…

基于springboot+vue的桂林旅游景点导游平台(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

Spring Security 重点解析

Spring Security 重点解析 文章目录 Spring Security 重点解析1. 简介2. 依赖3. 登录认证3.1 登录校验流程3.2 Spring Security 默认登录的原理3.2.1 Spring Security 完整流程3.2.2 登录逻辑探究 3.3 自定义改动3.3.1 自定义用户密码校验3.3.2 自定义 UserDetails 获取方式 F1…

基于Spring Boot的安康旅游网站的设计与实现,计算机毕业设计(带源码+论文)

源码获取地址&#xff1a; 码呢-一个专注于技术分享的博客平台一个专注于技术分享的博客平台,大家以共同学习,乐于分享,拥抱开源的价值观进行学习交流http://www.xmbiao.cn/resource-details/1760645517548793858

SpringSecurity + OAuth2 详解

SpringSecurity入门到精通 ************************************************************************** SpringSecurity 介绍 **************************************************************************一、入门1.简介与选择2.入门案例-默认的登录和登出接口3.登录经过了…

不做内容引流,你凭什么在互联网上赚钱?

孩子们放寒假了&#xff0c;待在家里不是看电视&#xff0c;就是拿着手机刷视频&#xff0c;脸上是各种欢快和满足。只是一切换到写作业模式&#xff0c;孩子是各种痛苦表情包&#xff0c;家长则是使出浑身解数&#xff0c;上演亲子大战。可见娱乐常常让人愉悦&#xff0c;而学…

鼠标事件和滚轮事件

1. 介绍 QMouseEvent类用来表示一个鼠标事件&#xff0c;当在窗口部件中按下鼠标或者移动鼠标指针时&#xff0c;都会产生鼠标事件。利用QMouseEvent类可以获知鼠标是哪个键按下了&#xff0c;还有鼠标指针的当前位置等信息。通常是重定义部件的鼠标事件处理函数来进行一些自定…

ubuntu使用LLVM官方发布的tar.xz来安装Clang编译器

ubuntu系统上的软件相比CentOS更新还是比较快的&#xff0c;但是还是难免有一些软件更新得不那么快&#xff0c;比如LLVM Clang编译器&#xff0c;目前ubuntu 22.04版本最高还只能安装LLVM 15&#xff0c;而LLVM 18 rc版本都出来了。参见https://github.com/llvm/llvm-project/…

【STM32】Keil RTE使用记录

0 前言 最近因为任务需要&#xff0c;再次开始研究STM32&#xff0c;打算过一遍之前记录的笔记&#xff0c;在创建工程模板时&#xff0c;突然发现一个之前被自己忽略的东西&#xff0c;那就是创建项目时会弹出的Run-Time Environment&#xff0c;抱着好奇的心态去找了一些资料…

防御保护--入侵防御系统IPS

目录 DFI和DPI技术 --- 深度检测技术 入侵防御&#xff08;IPS&#xff09; 签名 入侵防御策略的配置 内容安全&#xff1a;攻击可能只是一个点&#xff0c;防御需要全方面进行 IAE引擎 DFI和DPI技术 --- 深度检测技术 DPI--深度包检测技术--主要针对完整的数据包&#xff0…

冒泡排序法的名字由来,排序步骤是什么,最坏情况下的排序次数如何计算得来的呢?

问题描述&#xff1a;冒泡排序法的名字由来&#xff0c;排序步骤是什么&#xff0c;最坏情况下的排序次数如何计算得来的呢&#xff1f; 问题解答&#xff1a; 冒泡排序法的名字来源于排序过程中较大的元素会像气泡一样逐渐“冒”到序列的顶端&#xff0c;而较小的元素则会逐…

员工离职倾向分析工具

很多公司都担心员工离职&#xff0c;尤其是工龄久的老员工&#xff0c;为什么呢&#xff1f; 很多离职员工带走上家机密&#xff0c;还有的辞职后开公司成为了上家企业的对手公司等等&#xff0c;这类事件非常常见&#xff0c;因此员工离职是一个敏感的话题。 员工离职的原因 …

基于springboot+vue的植物健康系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

力扣随笔之两数之和 Ⅱ -输入有序数组(中等167)

思路&#xff1a;在递增数组中找出满足相加之和等于目标数 定义左右两个指针&#xff08;下标&#xff09;从数组两边开始遍历&#xff0c;若左右指针所指数字之和大于目标数&#xff0c;则将右指针自减&#xff0c;若左右指针所指数字之和小于目标数&#xff0c;则左指针自加&…