探究C语言函数栈帧的创建和销毁

news2024/11/16 7:01:35

引言

在C语言程序中,每当一个函数被调用时,系统都会在栈上为该函数分配一块内存空间,这块内存空间就被称为栈帧。

栈帧中包含了函数执行所需的所有信息,如局部变量、参数、返回地址等。栈帧的创建和销毁是函数调用的核心部分,它们确保了函数能够正确地执行和返回。

本文将在VS2013环境下,通过实践操作,对比较简单的C语言程序进行调试的基础上,对C语言函数栈帧的创建和销毁过程进行详细的论述,并探讨函数中局部变量的创建、参数的传递、形参的引用以及返回值等过程。

一、概念

我们在写c语言程序时,通常会把独立的功能封装为一个个函数,所以C语言程序是以函数为基本单位的。而函数的传参、调用和返回值等问题都和函数栈帧有关。

1.栈

栈是一个线性数据结构,遵循先进后出的规则,可将数据从栈顶压入,也可将数据从栈顶弹出。在windows操作系统中,栈是由高地址向低地址使用的。

2.函数栈帧

每一个函数调用,都要在栈区上开辟一块空间,这块空间就是函数栈帧。

这块空间用来存放:

(1)函数参数和返回值;

(2)局部变量;

(3)保存上下文信息。

3.寄存器

(1)eax:通用寄存器,保留临时数据,常用于返回值;

(2)ebx:通用寄存器,保留临时数据;

(3)ebp:栈底寄存器;

(4)esp:栈顶寄存器;

(5)eip:指令寄存器,保存当前指令的下一条指令的地址。

ebp和esp是维护函数栈帧的两个寄存器,哪个函数被调用,它们就指向哪个函数的栈帧空间进行维护,来标识哪个函数正在被使用,为对这个函数的操作提供支持。

4.汇编命令

(1)mov:数据转移指令;

(2)push:数据入栈;

(3)pop:数据弹出至指定位置;

(4)sub:减法命令;

(5)add:加法命令;

(6)call:函数调用,压入返回地址,转入目标函数;

(7)jump:通过修改eip,转入目标函数,进行调用;

(8)ret:恢复返回地址。

5.使用的C程序

二、main函数的运行

1.main函数被调用过程

main函数并不是最初被程序调用的函数,main函数也是通过被其他函数调用而被使用的。调用main函数的具体过程,是由编译器决定的。

main函数由__tmainCRTstartup函数调用,而__tmainCRTstartup函数又由mainCRTstartup函数调用。

在栈区内存中,空间是由高地址向低地址使用的,所以程序开始运行后,mainCRTstartup函数先在较高地址处创建栈帧,然后调用__tmainCRTstartup函数并创建栈帧,最后再调用main函数并为其创建相应的栈帧空间。

2.main函数栈帧的创建

main函数在运行时也会在栈区内存上开辟一块空间,这个空间由ebp和esp两个寄存器来维护。ebp指向栈底的较高地址,esp指向栈顶的较低地址,两个寄存器记录的地址之间就是内存划分给main函数,供main函数使用调配的空间。

可以经过反编译,得到main函数栈帧创建的汇编代码:

为main函数创建栈帧的时候,ebp和esp正在维护__tmainCRTstartup函数创建的栈帧。

为main函数创建栈帧的过程如下:

(1)00C21410  push    ebp

push指令进行压栈,把ebp中的地址压入栈顶,此时维护栈顶的指针esp值减小4,把刚刚压入的元素纳入__tmainCRTstartup函数的栈帧空间;

(2)00C21411  mov    ebp,esp

mov指令将栈底指针移动到栈顶;

(3)00C21423  sub    esp,0E4h

sub指令让esp中的值减去一个数,来到更低地址的位置,此时esp和ebp两个寄存器就离开了原先__tmainCRTstartup函数的栈帧空间,指向了一块新的栈区空间,这块空间就是为main函数预申请的栈帧空间;

(4)00C21419  push    ebx

     00C2141A  push    esi

     00C2141B  push    edi

push指令将ebx、esi、edi的值压入栈;

(5)00C2141C  lea    edi,[ebp+FFFFFF1Ch]

lea指令把ebp-0E4h加载进edi中,这其实就是压入ebx、esi、edi三个元素前esp的地址;

(6)00C21422  mov    ecx,39h

     00C21427  mov    eax,0CCCCCCCCh

mov指令将39h、0CCCCCCCCh两个值分别放入ecx和eax两个寄存器当中;

(7)00C2142C  rep stos    dword ptr es:[edi]

rep stos指令,在这里是将从edi中的地址开始,向下39h次,每次改变dword(4个字节)的空间,全部改为eax的值,这个操作把为main函数开辟的栈帧空间中的值全部初始化为cccccccc(每4个字节)。

3.main函数中局部变量的创建

(1)00C2142E  mov    dword ptr [ebp-8],0Ah

     00C21435  mov    dword ptr [ebp-14h],14h

     00C2143C  mov    dword ptr [ebp-20h],0

mov指令将值10放入ebp-8的位置,创建了整型变量a;将值20放入ebp-14h的位置,创建了整型变量b;将值0放入ebp-20h的位置,创建了整型变量c.

注意到整型变量a创建在ebp-8的位置上,而随后的整型变量b创建在ebp-14h(ebp-20)的位置上,之间相隔8个字节,共2个整型的空间,这就是平时我们会观察到,前后紧邻创建的两个变量,在内存空间上却并不是紧邻的根本原因。

在代码中因为越界或其他因素访问到没有被初值初始化的内存空间,打印出随机值时,经常出现烫烫烫烫的字样,就是因为函数栈帧中初始存放的值是cccccccc(4个字节),这些值在打印的时候被译为烫烫烫烫。

4.main函数中为Add函数传参

(1)00C21443  mov    eax,dword ptr [ebp-14h]

     00C21446  push    eax

     00C21447  mov    ecx,dword ptr [ebp-8]

     00C2144A  push    ecx

mov和push指令在这里把ebp-14h中的值20,也就是整型变量b的值,放入到了寄存器eax中,然后压入栈顶,把栈顶指针减少;之后又把ebp-8中的值10,也就是整型变量a中的值放入了寄存器ecx中,然后压入栈顶,把栈顶指针减少。这两步操作其实就是在传参,把main函数中实参部分,整型变量a和整型变量b的值,传递给Add函数中的形参整型变量x和整型变量y,也就是说,这里的寄存器eax和ecx就相当于放着整型变量x和整型变量y。

另外从这个传参过程中也可以很清楚的看到,函数参数的传递是从右向左进行的的,先传递的整型变量b,再传递的整型变量a。

5.创建Add函数的函数栈帧

call指令将call指令的下一条指令的地址压入栈中,自己通过jmp指令来找到Add函数。这里就是在开始调用Add函数,同时把下一条指令的地址记录下来,方便调用完成后返回到应当执行的位置,从而使得main函数完成对Add函数的调用后,接下来的代码能准确运行。

6.Add函数中对形参的引用

为Add函数创建好栈帧,并且创建好整型变量z变量后,开始执行z=x+y对应的汇编代码。

可以看到在这之前是没有创建出变量x和y的,这里调用x和y的时候,使用的是ebp+8和ebp+0Ch的值。从上面可以知道,这两个地址中的值其实就是上面传参时压入main函数栈帧的,存放在寄存器eax和ecx中的值。

所以说,平时我们传递参数的时候,如果采用传值调用,那么因为在被调函数中实际访问和操作的是寄存器eax和ecx中的值,而与传值过去的变量无关,所以在被调函数中对形参的操作不影响原函数中变量的值。

而如果采用传址调用,那么寄存器eax和ecx中存放的就是原函数中变量的地址,程序通过这个地址,就找到原函数中的变量,从而使得被调函数中的操作,改变原函数中变量的值。

在Add函数还未调用的时候,参数就已经先传递过去了,是在main函数的堆栈中,然后才是对Add函数栈帧的创建与初始化。

所以有一句非常形象且正确的话:形参是实参的一份临时拷贝。

7.Add函数返回值,程序返回main函数

返回值的时候,mov指令把ebp-8地址处的值,也就是整型变量z的值,放入寄存器eax当中,这里需要注意的是,寄存器是不会随函数的销毁而销毁的。

edi、esi、ebx三个元素也是Add函数栈帧的一部分,pop指令把edi、esi、ebx三个元素从栈顶依次弹出。

mov指令把寄存器ebp中的值赋给寄存器esp。

pop指令把ebp处的元素弹出并且赋给ebp,这里存放的值就是调用Add的函数,也就是main函数原先的栈底地址。所以经过这次pop,栈顶指针和栈底指针,又指向了main函数的栈顶和栈底,返回对main函数的维护。

ret指令返回的时候,其实相当于从栈顶指针处pop掉一个元素,而这个元素就是上面call指令压入的地址,是call指令下一条指令的地址。所以通过ret这个指令,就使内存上返回到了原先调用Add时的状态,并且走到了调用完成后的下一步。

此时传递的形参还在,之后执行的add指令使esp增加8个字节,弹出了传递的两个形参。

mov指令将eax中的值放入ebp-20h中,也就是变量c中,到这里完成了Add函数返回值的操作。

我们知道在一个函数中创建的局部变量,在函数销毁的时候也会同步销毁,而返回值的时候函数已经执行完毕被销毁掉了,那么用函数中的变量返回一个值,是怎么做到的呢?就是通过不随函数销毁而销毁的寄存器完成的,先把局部变量的值放入寄存器,通过寄存器将值返回,就避免了返回失败的问题。

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

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

相关文章

【华为HCIA数通网络工程师真题-数据通信与网络基础】

文章目录 选择题判断题 选择题 1、在 VRP 平台上,可以通过下面哪种方式访向上条历史命令? 上光标 (ctrlU 为自定义快捷键,ctrlP 为显示历史缓存区的前一条命令,左光标为移动光标) 2、主机 A (1…

TensorRT-常见问题

1、ModelImporter.cpp:779: ERROR: builtin_op_importers.cpp:3608 In function importResize:[8] Assertion failed: scales.is_weights() && "Resize scales must be an initializer!"解决方法:将TensorRT版本升到可以匹配cuda版本的最高版本&a…

多态性(Java)

本篇学习面向对象语言的第三个特性——多态。 目录 1、多态的概念 2、继承多态实现条件 3、重写 4、重新与重载的区别: 5、向上转移和向下转型 5、1向上转型: 5、2 向下转型 1、多态的概念 多态的概念:通俗来说,就是多种形态…

Servlet实践操作

Servlet运行原理 Tomcat 的代码内置了 main 方法,当我们启动 Tomcat 的时候,就是从 Tomcat 的 main 方法开始执行的 被 WebServlet 注解修饰的类会在 Tomcat 启动的时候就被获取并集中管理 Tomcat 通过反射这样的语法机制来创建被 WebServlet 注解修饰…

Day 27:2596. 检查骑士巡视方案

Leetcode 2596. 检查骑士巡视方案 骑士在一张 n x n 的棋盘上巡视。在 **有效 **的巡视方案中,骑士会从棋盘的 左上角 出发,并且访问棋盘上的每个格子 恰好一次 。 给你一个 n x n 的整数矩阵 grid ,由范围 [0, n * n - 1] 内的不同整数组成&…

超神级!Markdown最详细教程,程序员的福音

超神级!Markdown最详细教程,程序员的福音Markdown最详细教程,关于Markdown的语法和使用就先讲到这里,如果喜欢,请关注“IT技术馆”。馆长会更新​最实用的技术!https://mp.weixin.qq.com/s/fNzhLFyYRd3skG-…

linux环境编程基础学习

Shell编程: 相对的chmod -x xx.sh可以移除权限 想获取变量的值要掏点dollar($) 多位的话要加个花括号 运算:expr 运算时左右两边必须要加空格 *号多个含义必须加转义符 双引号可以加反单,但是发过来就不行 …

containerd手动配置容器网络

containerd手动配置容器网络 机器详情nerdctl启动一个不带网络的容器获取容器ID、PID与network namespace路径准备bridge插件的执行配置文件通过下面的命令调用bridge插件准备tuning插件文件执行下面的命令调用tuning插件准备portmap插件文件执行下面的命令调用portmap插件删除…

算法竞赛数论杂题

menji 和 gcd 题目: 一开始以为是只有l不确定,r是确定的,这样的话我们可以枚举r的所有约数,然后对其每个约数x进行判断,判断是否满足题意,具体做法是先让l % x如果 0则该约数可行,如果不可行…

文件扫描工具都有哪些?职场大佬都在用的文本提取工具大盘点~

回想起刚毕业初入职场那阵子,领导让帮忙把纸质文件扫描提取为文本时,还只会傻乎乎地一点点操作,属实是费劲得很! 好在后面受朋友安利,找到了4个能够快速实现文件扫描文字提取的方法,这才让我的办公效率蹭蹭…

[SCAU 课程设计参考] 活动管理程序

(仅供参考!!!!!!) 废话不多说,直接上代码!(但是量有点多,放前面影响观感,所以还是先不放了,文章末尾有链接) 题目的要求: 提要:我的设计只是一个参考,当时还是大一的时候写的,代码比较青涩&a…

[学习笔记]-MyBatis-Plus简介

简介 Mybatis-Plus(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 简言之就是对单表的增删改查有了很好的封装。基本不用再单独写sql语句了。目前此类…

微博舆情分析系统可以继续完善的基于python 前端vue

微博舆情分析系统可以继续完善的,前后端分离,前端基于vue 后端基于python的flask可以说是非常的简洁,支持实时更新数据。界面如图 主要工作点体现在后端实时更新数据跟数据的处理方面上,后续有空会用hadoop来处理海量数据真…

数据库 | 试卷四

1.数据库系统的特点是 数据共享、减少数据冗余、数据独立、避免了数据不一致和加强了数据保护 2.关系模型的数据结构是二维表结构 3.聚簇索引 cluster index 4. 这里B,C都是主属性,所以B->C不是非主属性对码的部分函数依赖 候选键(AC&a…

Jlink下载固件到RAM区

Jlink下载固件到RAM区 准备批处理搜索exe批处理调用jlink批处理准备jlink脚本 调用执行 环境:J-Flash V7.96g 平台:arm cortex-m3 准备批处理 搜索exe批处理 find_file.bat echo off:: 自动识别脚本名和路径 set "SCRIPT_DIR%~dp0" set &qu…

开发者黑板报#65

第65期 AI 谷歌Gemini 终于,GPT-4独霸时代终结了! 过去一个月里,四款大模型横空出世,在各项关键基准测试中与GPT-4相匹敌,甚至更胜一筹。 谷歌Gemini 1.5突破100万个tokens,是GPT-4的近8倍&#xff0c…

【Docker】——安装镜像和创建容器,详解镜像和Dockerfile

前言 在此记录一下docker的镜像和容器的相关注意事项 前提条件:已安装Docker、显卡驱动等基础配置 1. 安装镜像 网上有太多的教程,但是都没说如何下载官方的镜像,在这里记录一下,使用docker安装官方的镜像 Docker Hub的官方链…

易舟云财务软件:开启云记账新时代

在数字化浪潮的推动下,财务管理正经历着深刻的变革。易舟云财务软件,作为一款引领时代的云记账平台,以其卓越的功能和便捷的操作,为企业带来了全新的财务管理体验。 云记账,财务管理的未来趋势 云记账,即基…

【紫光同创盘古PGX-Nano教程】——(盘古PGX-Nano开发板/PG2L50H_MBG324第十一章)模拟波形实验例程说明

本原创教程由深圳市小眼睛科技有限公司创作,版权归本公司所有,如需转载,需授权并注明出处(www.meyesemi.com) 适用于板卡型号: 紫光同创PG2L50H_MBG324开发平台(盘古PGX-Nano) 一:…

Shopify 如何实现 Sticky 功能

Shopify 如何实现 Sticky 功能 介绍 在网页设计中,Sticky 功能是一种常见的技术,它使得网页上的元素在滚动时保持固定位置。这对于创建吸引人的用户体验和提高网站的可用性非常重要。Shopify 作为一个流行的电商平台,提供了丰富的功能和工具…