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

2023-11-01

目录

1.函数栈帧的含义

概念 

要用到的汇编语言的知识

示例

2.理解栈帧

2.1 main函数栈帧的创建

2.2 局部变量的创建

2.3 函数传参

2.4 调用函数

2.5 函数返回 


        一个.c文件在调用函数的时候(包括main 函数),其内存中的栈区有什么变化?要压栈、出栈哪些寄存器呢?函数的参数是如何进行传递的呢?函数调用结束之后栈区又是如何变化的呢?本文通过使用汇编语言,对这些内容进行了较为详细的剖析。

1.函数栈帧的含义

概念 

        首先,栈的概念想必不需要过多解释,那么什么是栈帧?引用百度百科:C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。从这句话中,可以提炼以下几点信息:

· 栈帧是一块因函数运行而临时开辟的空间。
· 每调用一次函数便会创建一个独立栈帧。
· 栈帧中存放的是函数中的必要信息,如局部变量、函数传参、返回值等。
· 当函数运行完毕栈帧将会销毁。

        我们知道,C语言的内存区分成了 静态区、栈区、堆区,函数栈帧无疑是在栈区创建和销毁的,所以其要符合栈“后进先出”的特点。

要用到的汇编语言的知识

        在这里使用汇编语言方面知识的原因是:通过它,我们可以深入底层了解一个程序是如何运行的,在何时——什么东西压栈,什么东西出栈,寄存器(汇编语言中一些用来暂时存储数据的东西)如何变化等等。这些都是C语言无法直观体现的,我们可以通过Visual Stdio 的在调试时的反汇编功能,将C语言代码转换成汇编语言代码,以便更好地观察。(另,C语言也是汇编语言编写的。)所以,简单地说,本文主要是在分析汇编语言的执行过程。

        我们首先要了解几个汇编语言方面的东西,其中ESP和EBP时专门维护函数栈帧的,分别指向栈顶和栈底:

寄存器  用途
EAX 累加寄存器:用于乘除法、函数返回值
EBX 用于存放内存数据指针
ECX 计数器
EDX 用于乘除法、IO指针
ESP 存放栈顶指针(其值是地址)
EBP 存放栈底指针(其值是地址)

汇编指令 用途
mov mov A,B 将数据B移动到A
push 压栈
pop 出栈
call 函数调用
add 加法
sub 减法
rep 重复
lea 加载有效地址

示例

        比如,我们写下一个如下的C语言程序,非常容易,只有main() 函数和一个 Add() 函数,主函数里面调用了 Add() 。

#include<stdio.h>

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	return 0;
}

        那么,一开始调用主函数的时候,主函数的函数栈帧就压栈;然后在主函数里面,调用了Add() 函数,此时Add() 的函数栈帧也要压栈。那么现在面临一个问题,是维护Add() 函数,还是维护main() 函数,亦或两者都维护?
        其实,从平时使用Visual Stdio 调试的时候就可以看出来,当主函数内部调用一个函数A,按F11分步调试,会进入函数A的内部,函数A调用结束,会返回主函数。同理,实际上从进入函数A,一直到A 函数调用结束,这个过程都在维护函数A。所以,在main() 函数内部调用 Add() 函数之后,会出现如下图所示的情况,ebp和esp来维护Add() 函数的栈帧当Add() 函数调用结束,它的函数栈帧自然就会出栈,此时ebp、esp又会返回维护main() 函数的栈帧

2.理解栈帧

2.1 main函数栈帧的创建

        实际上,main() 函数也是由其他函数调用的,其调用链条如下:

        创建main函数的函数栈帧代码如下,汇编语言的注释是用 ; 所以这里 ; 后面的内容是注释,帮助理解代码。

006117A0  push        ebp                 ; ebp 压栈
006117A1  mov         ebp,esp             ; 将 esp里面的值 赋给ebp
006117A3  sub         esp,0E4h            ; esp减去0E4h(十六进制),得到的结果赋给esp
006117A9  push        ebx                 ; ebx 压栈
006117AA  push        esi                 ; esi 压栈
006117AB  push        edi                 ; edi 压栈
006117AC  mov         edi,[ebp-24h]       ; 将ebp往上数,第24h(16进制)个字节开始,向下4个字节的值赋给edi
006117AF  mov         ecx,9               ; 这里到结束的意思是:
006117B4  mov         eax,0CCCCCCCCh      ; 将0CCCCCCCCh 赋值给某块空间,这块空间从附加段中 edi 指向的位置开始
006117B9  rep stos    dword ptr es:[edi]  ; 一共执行九次(0CCCCCCCCh 是四个字节的内容,每次操作四个字节,所以一共操作了36字节)

第一行 

        我们来开始逐句剖析上方代码,首先执行第一行(图中红色圆圈圈出来的黄色箭头,表示已经执行完其上一行,按F10调试就执行当前行),由于是压栈操作,所以esp的值会有所变化,如下图右边监视窗口,esp的值(十六进制显示的)相较之前改变了,所以变成红色:

        如下,ebp压栈,同时esp上移:

第二行

         该行是将esp的值赋给ebp,效果也如下图,右边监视窗口的红色部分所示。

        如下,将esp的值赋给ebp之后,ebp和esp指向同一块地方:
 

第三行

        该行是将esp的值减去0E4h(十六进制),得到的结果赋给esp,如下图。

         如图所示,由于图中从下往上是地址高处到地址低处,所以esp值变小,实际上图中是上移。并且,现在ebp和esp维护的空间,就是main函数的函数栈帧:

第四行

        压栈,压入ebx,改变栈顶指针esp的值。

        如下,压栈,esp上移:

第五行

        压入esi,改变esp的值。

        如下,和上一步类似:
 

第六行

        压入edi,改变esp的值。 

        和上一步也类似:

第七行

        将[ebp-24h] 表示的地址赋值给edi。

        这里就是把 edi 里面的值改变,从函数栈帧看不出什么,看上面的监视图就可以直到确实是改变了。

最后三行

        如之前代码里的注释所说。

        如下,两个箭头指示的值是一样的,其代表的是edi所表示的地址,从该地址开始,往后9个dw(double word 双字,一个双字等于四个字节)的内容,都赋值为cccccccc (十六进制)。

        这三行代码效果如下:

        整个过程可以用一张动图生动形象地展示:

2.2 局部变量的创建

        接下来,我们在汇编代码中鼠标右击,然后将下图红色箭头所指示的"显示符号名" 的勾去掉。

        发生改变的是下图中红色圆圈圈出来的,可以看出,原本所有的变量名,都变成了寄存器减去某个十六进制数字。他们实际上是等价的,即变量的地址就等于替换后的地址

        接下来分析局部变量创建过程。
        首先,创建变量a,代码如下,其含义就是,将0Ah 这个十六进制数字,从ebp的地址低八位处开始放,占四个字节

002C17C5  mov         dword ptr [ebp-8],0Ah  

        如下图,可以通过两个红色箭头看到,右边监视的ebp的值就是左边 地址处的箭头指向的地址,说明这就是ebp的地址,然后减去八位,再根据栈从下往上使用以及Visual Stdio小端存储的特点,就成了内存区里面红色方框框出来的内容。(注意,比如 cc cc cc cc cc占据的是一个字节的空间,四个cc 就占据四个字节,而汇编语言中,地址-1,只跳过一个c,所以ebp-8是跳过8个c,即四个字节)

        如下,图中一个小格子代表四个字节,不难看出,变量a存储的位置,在栈底指针往上跳过四个字节的地方。

        然后创建局部变量b,通过内存图可以看出,变量b和变量a是间隔八个字节的:

        如下图:

2.3 函数传参

        代码如下:

002C17D3  mov         eax,dword ptr [ebp-14h]   ; 将变量b的值赋给eax
002C17D6  push        eax                       ; eax压栈
002C17D7  mov         ecx,dword ptr [ebp-8]     ; 将变量a的值赋给ecx
002C17DA  push        ecx                       ; ecx压栈

        执行前两行代码,确实将变量b的值赋给了eax,然后eax引起的压栈导致了esp改变:

        如下图,不要忘了eax里面的值和变量b是一样的哦:

        执行后两行代码:

        其效果和前两行类似,同时ecx里面存的是变量a的值:

2.4 调用函数

        上面的内容执行完之后,要执行如下语句,其意思是,执行 002C10B4 地址处的内容:

002C17DB  call        002C10B4  

        然后我们将其滑倒该地,发现是这样的,意思是跳到002C1740地址处:

         又找到该地址,发现如下,所以,通过这两步调用Add() 函数,如下红色部分,和创建main函数的函数栈帧类似,实际上就是创建了Add() 的函数栈帧

        效果如下,建立了Add函数的函数栈帧:

        红色个方框后面两行代码不是很重要,是用来检查bug的,如下代码和图片:

002C1757  mov         ecx,2CC003h  
002C175C  call        002C130C  

 

2.5 函数返回 

        函数返回

002C1761  mov         eax,dword ptr [ebp+8]   
002C1764  add         eax,dword ptr [ebp+0Ch]  

        第一行代码: 将ebp+8 地址处的数据放到eax 。

        第二行代码:将ebp+0Ch 地址处的数据和eax相加,结果存到eax里面。

        如下图中,由于图片从下往上是地址从高到低,所以图片中ebp+8是在ebp下方。实际上就是ecx和eax的值相加,然后存到eax里面。eax里面存储变量b的值,ecx里面存储变量a的值,最后eax的值就是变量a、b之和。并且eax是不会随着Add() 函数的函数栈帧销毁而改变值。

        通过监视也可以看出,eax的值变成0x0000001e,转换成十进制就是30。
 
        此时已经拿到返回值,存储在eax里面,还要执行以下几行代码:

00AA13F1  pop         edi  
00AA13F2  pop         esi  
00AA13F3  pop         ebx  
00AA13F4  mov         esp,ebp  
00AA13F6  pop         ebp  
00AA13F7  ret  

         就是出栈、赋值等等,结果如下,回到了调用Add() 函数之前的状态:

        然后执行main() 函数后续代码代码,如下图红色框出:

        第一行:esp加8,即esp在途中向下移动四个字节。

        第二行,将eax的值赋给ebp-20h 地址处。

        执行完之后,调试图如下,通过对比两个红色方框的内容,左边红色方框的地址,和右边&c 的值一样,说明那就是变量c 存储的地方,其值也是变量c 的值:

        示意图如下:

        通过对函数栈帧创建、销毁过程的剖析使我们不仅了解计算机做了什么,还了解了它是如何做的。通过函数栈帧尝试解析递归等问题相信也会更加直观。由于本人水平有限,不足之处还请大家多多指教。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C语言】函数栈帧的创建和销毁 的相关文章

随机推荐

  • 雷达手势识别技术概述

    前言 不必害怕未知 无需恐惧犯错 做一个Creator 目录 前言 雷达技术特点 毫米波雷达 实现过程 手势信号预处理 手势特征提取与分类识别算法 雷达技术特点 随着雷达技术的快速发展和广泛应用 雷达手势识别已成为人机交互技术领域的一个重要
  • LoadRunner解决动态验证码问题

    对于这个问题 通常我们可以采取以下三个途径来解决该问题 1 第一种方法 也是最容易想到的 在被测系统中暂时屏蔽验证功能 也就是说 临时修改应用 无论用户输入的是什么验证码 都认为是正确的 这种方法最容易实现 对测试结果也不会有太大的影响 当
  • Linux命令之sync

    概述 sync 命令可以强制将内存中的文件缓冲写入磁盘 更新块信息 在 linux unix 系统中 在文件或数据处理过程中一般先放到内存缓冲区中 等到适当的时候再写入磁盘 以提高系统的运行效率 这样虽然可以提高磁盘写入数据的效率 但是也带
  • STM32高级定时器中心对齐PWM模式,频率设置的分享

    有关STM32高级定时器中心对齐PWM输出的实验记录 计算PWM的频率公式 f PCLK2 TIM Prescaler 1 TIM Period 1 2 条件TIM ClockDivision 0 而不是f PCLK2 TIM Presca
  • 单链表的定义,插入与删除,查找,建立。

    链表分为 单链表 双链表 循环链表 静态链表 一 单链表的定义 在内存空间中 各个节点在逻辑上相邻 但在物理上不相邻 在单个的结点内部需要存放 数据域 和 指针域 存放指向下一个结点的指针 优点 不要求一大片连续空间 改变容量方便 缺点 不
  • InVideo AI:用人工智能轻松制作视频

    简介 InVideo AI 是一款在线视频制作工具 使用人工智能来帮助用户快速 轻松地制作高质量的视频 该工具提供多种功能 包括 链接 ai invideo io 仪表盘 历史记录 创建视频 选择模板 youtube explainer 加
  • 关于华硕飞行堡垒8笔记本网卡启动不了(Inter(R) Wi-Fi 6 AX201)该设备无法启动 代码10

    今天打开笔记本莫名奇妙的 连不上WIFI了 网线可以联网 查看设备管理器 网卡亮感叹号 查看详情 提示 该设备无法启动 代码10 代码10有三个情况 1 驱动不合 概率少 2 系统不合 小概率 3 坏了 大概率 解决方案也就是 1 重装网卡
  • proc文件系统下各参数解析

    文章目录 一 proc文件系统 1 1 proc pid 1 1 1 proc pid arch status 1 1 2 proc pid attr 1 1 2 1 proc pid attr current 1 1 2 2 proc p
  • 【华为OD机试真题2023B卷 JS】比赛的冠亚季军

    华为OD2023 B卷 机试题库全覆盖 刷题指南点这里 比赛的冠亚季军 知识点数组编程基础链表分治 时间限制 1s 空间限制 256MB 限定语言 不限 题目描述 有N 3 lt N lt 10000 个运动员 他们的id为0到N 1 他们
  • 如何使用pandas读取csv文件中的某一列数据

    使用pandas读取csv文件中的某一列数据 可以这样做 先导入pandas模块 import pandas as pd 使用pd read csv函数读取csv文件 df pd read csv 文件名 csv 使用df 列名 读取某一列
  • 矩阵的转置(c++)

    将一个m n的二维数组的行和列元素互换 如下图所示 注 T代表转置 输入格式 输入矩阵的行和列数 用空格隔开 接下来输入矩阵的数据 1
  • Tomcat debug模式启动

    在 CATALINA HOME bin startup bat中添加以下任意一行配置 SET JAVA OPTS Xdebug Xrunjdwp transport dt socket address 8787 server y suspe
  • gitlab第一次上传项目

    1 git config global user name git的name 2 git config global user email git的邮箱 3 ssh keygen t rsa C git的邮箱 三次回车 会生成一个id rs
  • .torrent文件如何使用

    遇到 torrent文件 我们需要对应的下载软件 以迅雷为例打开后点击 新建 添加链接或口令 添加BT任务 选中已有 torrent文件即可下载 百度网盘的离线下载也可以进行 torrent文件的下载
  • C语言入门第十九篇,文件操作

    文件操作是通过c语言编程实现对文本文件的控制 比如读入文件内容 处理文件内容等等 相信很多学校的学生在学习的时候老师就会叫大家用c语言做一个学生的成绩管理系统或者是什么的系统 如果不涉及到数据库 那多半是用文件来做 我们这篇主要讲文件的读出
  • Python基础 - os.walk()详细使用

    Python os walk 详细使用 转自 Python os walk 详细使用 小菠萝测试笔记的博客 CSDN博客 os walk 方法简单介绍 主要用来遍历一个目录内各个子目录和子文件 是一个简单易用的文件 目录遍历器 可以帮助我们
  • 手动配置DHCP服务

    目录 一 安装DHCP服务 编辑 二 更改网卡配置 配置网卡信息 3 配置ensp SW2配置 SW1配置 一 安装DHCP服务 进入 进入dhcp的配置文件中进行配置 vim etc dhcp dhcpd conf 这里面是空文件 但是会
  • QT day 1

    作业 widget cpp include window h include
  • Python(1)生成目录及超链接

    Python 1 生成目录及超链接 coding utf 8 import xlsxwriter 导入模块 import os 新建txt文档 f open PCL bat a f write echo off f write ntree
  • 【C语言】函数栈帧的创建和销毁

    目录 1 函数栈帧的含义 概念 要用到的汇编语言的知识 示例 2 理解栈帧 2 1 main函数栈帧的创建 2 2 局部变量的创建 2 3 函数传参 2 4 调用函数 2 5 函数返回 一个 c文件在调用函数的时候 包括main 函数 其内