【C语言】你知道程序是如何调用函数的吗?

news2024/11/17 1:52:57

目录

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存放栈底指针(其值是地址)

汇编指令用途
movmov 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 的值:

        示意图如下:

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

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

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

相关文章

祝贺誉天多位学员通过HCIE-Datacom,喜提誉天助跑奖学金

华为HCIE-Datacom自发布以来&#xff0c;就一直牵动着数通学员们的心。不论是版本之间的取舍&#xff0c;还是新增模块的学习&#xff0c;都一度引发热议。为了帮助各位小伙伴完成过渡顺利通过考试&#xff0c;誉天除了在师资、设备、教研等方面下足功夫之外&#xff0c;还专门…

12. 【gRPC系列学习】失败重试Retry原理

本节分析gRPC的失败重试机制,从原理、源码上进行深入分析,下节编写测试代码,验证Retry机制。 官方文档在失败重试方面的讲解比较透彻,下面内容是对官方文档的整理、归类、总结。 1. 两种重试策略简介 1)retry策略 retry策略:发送RPC,服务端应用层返回指定的异常码后,…

ES6--》了解并使用模块化规范

目录 ES6模块化规范 模块化的分类 ES6模块化的语法 ES6模块化规范 ES6模块化规范是浏览器端与服务器端通用的模块化开发规范。它的出现极大的降低了前端开发者的模块化学习成本&#xff0c;开发者不需要再额外的学习AMD、CMD或CommonJS等模块化规范。 ES6模块化规范定义&am…

激活海量数据价值,实现生产过程优化

背景 在全球掀起的新一轮工业转型浪潮中&#xff0c;智能制造面临巨大发展机遇。得益于云计算、大数据和人工智能技术的加持&#xff0c;工业转型升级进入新阶段&#xff0c;人们逐渐意识到由数据驱动催生的新商业模式所带来的巨大价值&#xff0c;数据和算法模型的结合与碰撞…

KMP算法,686. 重复叠加字符串匹配,

首先&#xff0c;要明白&#xff0c;如果b串的第一个字母b[0]在a串中没有找到&#xff0c;那么不管a串复制多少次&#xff0c;b串都不会是a的子串。 如果b串的第一个字母b[0]在a串中能找到&#xff0c;那么我们看一下a串需要复制几次呢&#xff1f; 总结,可以发现这四种情况是…

数据库系统概论第六章(关系数据理论)知识点总结(3)—— 范式知识点总结

本专栏收录了数据库的知识点&#xff0c;而从本文起&#xff0c;将讲述有关于关系数据理论中的第一范式、第二范式、第三范式以及BC范式有关知识点&#xff0c;提供给有需要的小伙伴进行学习&#xff0c;本专栏地址可以戳下面链接查看 &#x1f388; 数据库知识点总结&#xff…

windows CMD “ZEBAR ZPL 命令打印“驱动打印机

一、命令预览 copy WS20_8891898136BB520221221003.txt "\\10.165.98.33\IP-print-ZDesigner ZT411-203dpi ZPL" 二、难点打印机名称 1. copy 命令必须为共享打印机&#xff0c;及打印机共享名称&#xff0c;可以参考(163条消息) 发送ZPL指令到斑马打印机&#xff…

【Java对象拷贝机制】「实战开发专题」高性能使用CGlib实现Bean拷贝(BeanCopier)指南

对象拷贝现状 业务系统中经常需要两个对象进行属性的拷贝&#xff0c;不能否认逐个的对象拷贝是最快速最安全的做法&#xff0c;但是当数据对象的属性字段数量超过程序员的容忍的程度&#xff0c;代码因此变得臃肿不堪&#xff0c;使用一些方便的对象拷贝工具类将是很好的选择…

训练seq2seq模型的一些Tips——李宏毅机器学习笔记

Copy Mechanism 有时我们并不需要decoder创造一些东西出来&#xff0c;有些内容是可以从encoder复制而来。 最早具有复制能力的模型&#xff1a;Pointer Network 例如&#xff1a; chat-bot Summarization 至少要训练百万篇文章 Guided Attention Monotonic Attention Lo…

PPT怎么转化成PDF?这两种方法教你快速解决!

今天我们将向您介绍如何将 PowerPoint 演示文稿 (PPT) 转换为 PDF 文件。 我们将通过两种方法来做到这一点&#xff1a; 将 PowerPoint 演示文稿另存为 PDF 文件。使用 奇客免费PDF转换 将 PowerPoint 演示文稿转换为 PDF。方法一&#xff1a;使用 Microsoft PowerPoint 将 P…

合理利用chatGpt之新冠阳性

&#x1f3c6;今日学习目标&#xff1a; &#x1f340;合理利用chatGpt之新冠阳性 ✅创作者&#xff1a;林在闪闪发光 ⏰预计时间&#xff1a;30分钟 &#x1f389;个人主页&#xff1a;林在闪闪发光的个人主页 &#x1f341;林在闪闪发光的个人社区&#xff0c;欢迎你的加入:…

AI 预测到了正确的世界杯胜利者吗 ?

在过去的一个月里&#xff0c;卡塔尔 2022 年世界杯让我们目睹了一个又一个令人震惊的结果&#xff0c;甚至在决赛之前&#xff0c;让所有人的目光都聚焦在法国和阿根廷两支球队上&#xff0c;其实在球迷们心中&#xff0c;早就开始了各种猜测。&#x1f604;现在是时候回顾一下…

基于CNN和LSTM的气象图降水预测示例

我们是否可以通过气象图来预测降水量呢&#xff1f;今天我们来使用CNN和LSTM进行一个有趣的实验。 我们这里使用荷兰皇家气象研究所(也称为KNMI)提供的开放数据集和公共api&#xff0c;来获取数据集并且构建模型预测当地的降水量。 数据收集 KNMI提供的数据集&#xff0c;我…

excel函数公式大全,最常用的6个公式

Excel中的函数引用一些预定义的公式&#xff0c;可以通过输入参数值来计算函数的对应函数&#xff0c;并且函数名称基本上与函数相对应&#xff0c;这很容易记住。在日常工作中&#xff0c;功能可用于数据统计、计算、处理和分析。本文主要介绍EXCEL中一些常用公式&#xff0c;…

视唱练耳训练小程序开发,摆脱传统训练制约性

视唱练耳作为一门综合性的音乐基础理论学科&#xff0c;对于声乐、器乐、舞蹈等音乐学科中的各个方面都起着十分重要的作用&#xff0c;尤其是突出表现在基本理论、基本技能和音乐审美上&#xff0c;对培养和发展学生的乐感、唱奏技巧以及音乐思维等都有着非常重要的意义。世界…

Databend 开源周报 #71

Databend 是一款强大的云数仓。专为弹性和高效设计&#xff0c;自由且开源。 即刻体验云服务&#xff1a;https://app.databend.com。 What’s New 探索 Databend 本周新进展&#xff0c;遇到更贴近你心意的 Databend 。 Features & Improvements Planner 优化集群模…

简单易用的监控告警系统 | HertzBeat 在 Rainbond 上的使用分享

在现有的监控告警体系中 Prometheus AlertManger Grafana 一直是主流&#xff0c;但对于中小团队或个人来说&#xff0c;这种体系显的较为复杂。而 HertzBeat 能让中小团队或个人很快速的搭建监控告警系统&#xff0c;并通过简单的配置实现应用、数据库、操作系统的监控与告警…

k8s HPA升级 KEDA 基于事件驱动的自动伸缩

说明&#xff1a;KEDA有啥用&#xff0c;相对HPA有啥优势。HPA针对于cpu,内存来进行弹性伸缩&#xff0c;有点不太精确。KEDA可以接入prometheus&#xff0c;根据prometheus的数据指标进行弹性伸缩&#xff0c;相比更加的精准实用。 安装k8s环境部署prometheus 创建ns&#xf…

HashMap最全面试题

文章目录一、 存储结构字段结构二、索引计算三、put方法四、扩容机制五、其他一、 存储结构 HashMap的底层数据结构是什么&#xff1f; 在JDK1.7 和JDK1.8 中有所差别&#xff1a; 在JDK1.7 中&#xff0c;由“数组链表”组成&#xff0c;数组是 HashMap 的主体&#xff0c;链…

Django学习Day6

1.ORM故障处理 1)当执行python manager.py makemigrations出现迁移问题时&#xff0c;如何进行解决。 处理方案&#xff1a;在models.py中&#xff0c;为book表的des非空字段设置一个默认值。 2&#xff09;数据库的迁移文件混乱问题 数据库中的django_migrations记录了migra…