IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 【C语言】C程序是如何开始的?函数参数是如何传递的?函数栈帧又是怎么回事? -> 正文阅读

[C++知识库]【C语言】C程序是如何开始的?函数参数是如何传递的?函数栈帧又是怎么回事?

C语言函数是如何调用的?参数又是如何传递的?什么是函数栈帧?

开门见山,先看一段简单代码,从这个简单代码着手,我们一步一步解开C语言函数调用参数传递的谜团…(鉴于本人也在不断探索学习,如有错误,愿虚心请教,相互学习

Add(int x, int y)

{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	printf("%d\n", c);
	return 0;
}

这段代码对于任何一个C学习者来说都不是难事,按照我们往常的理解,一眼就可以看出其中门道:“这不就是函数参数的传值调用嘛!
没错,此处函数调用方式方为C语言中两种函数参数传递方式的一种,今日我们先行讨论函数传值调用的具体底层逻辑,对于传址调用,其底层函数维护方式与传值调用雷同,只是在指针变量的维护上稍有不同,具体我们日后再叙。

言归正传,谈到底层,我们到底从哪里入手呢?
有人说:讲底层?你给我从main函数开始!

好!今天我们就从main函数的调用开始!!!(忽然觉得自己好刚)

几乎每一个C学习者的第一个代码都是从"Hello,World!"开始的,身为小白的我们会按照书中所示的那几行代码,认真的敲下几行代码:

#include<stido.h>
int main
{
   printf("Hello,world\n");
   return 0;
}

可当我们学习过函数的相关内容我就会知道,函数是需要定义的,函数需要参数,返回值类型等要素条件。那么这个main是哪里来的?怎么我们每一个C程序的启动都要从它开始?

有人说:main 就和 printf 等函数一样,都是“与生俱来”的。
这个说法宏观上讲并没有什么问题,可main 函数的作用可不仅仅是在打印输出这种功能上。更多的是,main函数给我们提供的是第一个入口,是整个程序的起点。
正好比无穷的宇宙,究其本源到底是什么?
而对于C程序,是怎么创造起点的?

第一个问题,尚属人类未解之谜。

针对第二个问题,我们可以通过VS编译器来找到答案:

在VS2013的编译环境下,通过调试窗口的调用栈堆,我们可以发现:
main 函数被 __tmainCRTStartup调用,__tmainCRTStartup又被 tmainCRTStartup调用着

此处的“发现”可以说明:
main函数并不是凭空产生的,他由其他隐式函数调用并发挥着功能。

创建函数,首先就得知道函数在哪里存在着:

在计算机内存中,栈区、堆区和静态区和其所对应存放的内容是这样一种关系:
内存示意图 其中自定义函数,和其对应的形参在创建时应在内存的栈区存放。主函数main也不例外,创建时也要从栈区创建。由malloc等函数申请的动态内存则存放在堆区。全局变量,以及static修饰的静态变量则存放在内存的静态区。
栈区示意图对于一个栈来说,低地址对应在栈区顶部,高地址对应在栈区底部。压栈时先进入高地址栈底,出栈时则从低地址栈顶弹出。

在正式介绍main函数创建过程之前,还需要一些准备工作。首先要知道在底层汇编中,通常是以寄存器的形式进行操作的。这里不对具体CPU进行说明,在VS编译环境下寄存器通常由eax,ebx,ecx,edx等组成。他们起到存放在变量和值的作用。
这里还介绍两个特别的寄存器:ebp和esp
ebp又称为栈底指针,esp又称为栈顶指针
两者的作用就是维护当前函数的栈区,使之发挥功能。
__tmainCRTStartup由上述VS2013编译器环境下的main函数引出条件我们可知,主函数main的创建是由__tmainCRTStartup函数调用而来,那么在main函数作为正式入口进入整个程序之前,必存在着上图所示的esp/ebp维护空间以供__tmainCRTStartup函数存在,并调用主函数main。

为了破解main函数的起源,在VS2013环境下,对开始那段代码进行了反汇编操作。
经过反汇编后:
在这里插入图片描述
我们可以看到,首先在栈顶(低地址)push进ebp的值,然后将esp(栈顶指针)移动到ebp处,这时再将esp减去0E4h空间,由于地址大小是上低下高,所以esp在栈顶低地址处,做减法就向上远离了ebp所在位置。至此esp指向低地址栈顶处,ebp在高地址栈底处
在这里插入图片描述至此,主函数main所需要的就空间创建了。(这里需要特别提示一点,每当在栈区push进一个东西,栈顶指针esp就会向上(低地址、栈顶)移动一个单位,所以当push进ebp值的时候esp指针是自动向上移动的)

然后依次进行push进其他寄存器单元入栈
在这里插入图片描述
接下来是这四句汇编指令
lea edi,[ebp-0E4h]
mov ecx,39h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]

lea=load effictive address显示有效地址
rep指令的目的是重复其上面的指令.ecx的值是重复的次数
stos指令的作用是将eax中的值拷贝到es:edi指向的地址
dword表明是double双字节也就是四个字节的内容

总结起来的作用就是从edi这个位置开始,向下进行ecx=39h次的0cccccccch赋值(int3中断)
其效果就是edi以下 ebp以上的所有内容被赋值为0cc cc cc cch
main函数栈帧此时main函数的栈帧,就已经开辟完毕。
变量的赋值初始化之后在ebp,esp维护的主函数空间内对变量a,b进行赋值,在栈底指针ebp上8个字节处对a进行赋值10,在ebp上20个字节处对b赋值20。
至此对于main函数的空间开辟以及对变量赋值过程结束。

接下来进入函数Add的传参操作。
在这里插入图片描述可以看到,在传递参数时,先将ebp以上的20个字节的位置的内容赋值给寄存器eax。ebp以上20个字节的位置是谁?答案就是b !虽然我们没有对调用函数过程进行完整的剖析,但从这一行汇编指令中我们猛地发现:在向函数传递参数时,往往由右侧开始,向左侧进行。在Add(a,b)函数中,先向Add函数传递b的值,再传递a的值。
传参此时汇编代码来到
在这里插入图片描述call 指令的出现意味着Add函数的空间开辟准备开始,可见call指令的下一条指令的地址是002618CA,执行call指令之后,该地址自动push进栈,esp指针向上移动一个单位。当call指令结束时,返回的地址恰是下一条汇编指令的地址。
在这里插入图片描述F11进入call,开启正式开辟Add函数空间:
Add函数栈帧开辟
看到这个界面时,是不是有几分熟悉?
没错!Add函数与main函数开辟空间步骤几乎完全相同
只有一处略有不同,只有开辟空间的大小与main函数有所不同,空间由编译器自动生成,使其能满足其所需空间。

但是,此处有一点不容忽略,我们刚刚传递的参数并没有存在与Add开辟的函数空间中。从宏观来看,结构应该是这样的(我在这里我省略了Add函数初始化内容与具体的局部变量赋值,应该知道用cccccccch初始化):
在这里插入图片描述可见,形参x,y并没有在Add函数所开辟的空间中,这样恰印证了,我们常在传值调用时,将形式参数认为是实际参数的临时拷贝。因此改变形参并不会改变实际参数的值。

初始化完成之后,在ebp以上8个字节位置处存放z的值,对z进行初始化赋值为0。

在Add函数的求和过程中,即为老生常谈的mov,add操作,此处将ebp向下8个字节位置的内容(a=10)赋值给eax寄存器,又将ebp向下12个字节的内容(b=20)加到eax寄存器上,最终将eax的值赋给z所在空间位置

在这里插入图片描述

当完成加和运算之后,因为函数内所有参数会在函数调用结束时销毁,所有将z的值赋给eax来存储,以便返回到主函数中进行输出,这项操作也印证了为什么函数调用结束仍可以向主函数返回值
在这里插入图片描述三次pop操作,使edi,esi,ebx依次弹出栈区,此时esp栈顶指针指向了ebx向下的一个空间位置。
在这里插入图片描述
最终,函数要走向销毁:
在这里插入图片描述
销毁操作可谓是极其巧妙!只需将epb的值赋给esp,栈底指针的值赋给栈顶指针,二者重合,Add函数空间自然就会被销毁。

之后,pop ebp将ebp的值存放到ebp中(注意此处第一个ebp是指创建Add函数时的起始那个push edp,第二个ebp是创建main函数时的push edp)
此时ebp又回到了主函数所在的栈区空间的起始地址,如图

在这里插入图片描述ret汇编指令的操作使 操作回到002618C地址对应的汇编指令处即call指令的下一个指令的地址对应处:
在这里插入图片描述与此同时,栈顶存放的 002618C pop出栈,esp指针指向 对应的向下一个空间位置。然后esp指针加8对形式参数进行销毁,至此esp又与ebp维护了主函数main所在位置空间。
在这里插入图片描述再对c进行赋值,即完成了从主函数main调用Add函数再返回主函数main的过程。

之后的printf及return 0销毁主函数过程与Add函数调用销毁过程雷同,不再赘述。

至此,本文推演了主函数main的创建,以及简单传值函数Add函数的参数传递、求和及返回主函数并销毁的全过程。

总结:C程序从一个被其他函数调用发生的main函数开始。参数传递过程,往往是从右侧参数向左侧参数依次进行的。而由esp和ebp维护栈区函数空间的行为称为函数栈帧。

希望本文对你有些许的帮助,希望我们可以共同进步。

转载请标明出处~

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-08-07 11:49:05  更:2021-08-07 11:49:22 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/9 17:34:36-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码