深入理解《hello world》是如何实现的

简介: 深入理解《hello world》是如何实现的

函数栈桢的创建和销毁

❤️ :热爱编程学习,期待一起交流。 🙏:博主是河牧院大一在读学生,水平有限,如发现错误,期待指点!(2466200050)

🌳:以下是我对main函数栈帧的创建与销毁一些拙见,期待大佬们指教。

前言


了解函数栈桢的目的是提高我们的基础知识功底,了解反汇编和内存,而不是会printf一个hello world,但不知道他是如何实现的,知其所以然。虽然学了这些并不能提高我们的算法能力,但看看到底是怎么运行的,有助于后期的学习,使我们向看代码是内存这个境界更近一步。

正如图上达克效应一样,横坐标代表技术。纵坐标代表自信。我们并没有想象中的那么优秀,要学会从他人眼里看到自己的影子。也许你现在处于愚昧之巅,也许你处在绝望之谷,不过这都是正常的,只要我们认清自己,持续输出,一定会走向开悟之坡。


C语言是由函数构成的

C语言是由函数为单元模块构成的,不信的话,可以想象一下,每一次我们用C语言编写程序的时候,都需要写一个main()。而main()就是一个函数。

就拿最简单的打印一句hello world来说,不就是在main函数里面的的吗。

所以我们了解函数栈帧的创建和销毁,其实就在研究一个代码是如何实现的。

我们在这里要用到汇编代码来演示函数栈帧的创建与销毁。

栈帧概念

C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。


栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。


首先应该明白,栈帧是存放在内存中的栈区的。栈帧是栈区分配给进程的内存区。


栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。


寄存器

ebp:栈底指针

esp:栈顶指针

esp和ebp是用来维护函数的。


寄存器有很多,但这里我们主要用到一下两种寄存器,ebp和esp。

这两个寄存器存放的是地址。用来维护函数栈帧。所以我们可以把它俩个可以叫做指针。指针不就是用来存放地址的嘛。

我们平时用C语言写的每一个程序都包含了函数,而每一个函数的调用,都要在栈区创建一个空间。这个空间区域就叫做函数栈帧。而ebp和esp就是用来维护这片区域的。


hello world是如何实现

这里我们需要明白

main函数是一个程序的入口,main函数有且只能有一个。

我们在进入这个程序的时候,首先需要调用main函数,你没听错,就是调用它

那么是谁调用的main函数呢?它是被一个简称叫做CRT的函数所调用的。(CRT是我自己命名的简称,我只要知道main函数是被它调用的就好了。)

#include<stdio.h>
int main()
{
  printf("hello world");
  return 0;
}
  • 看到这段在main函数里面的代码。有人就乐了,不就是一个printf就可以打印出来了吗?
  • 是的,没错,的确是一个printf就打印出来了。
  • 但是在执行printf这句语句的时候,编译器要做大量的工作。这里的工作就是指函数栈帧的创建

我们转到汇编代码

  • 这里我用的是vs2019编译器下转到反汇编查看的。
  • 红色的框就是ebp指针和esp指针从维护CRT函数转变到维护main函数的过程。
  • 绿色的框就是我们程序代码。

🌳 main函数栈帧的创建(开始调用main函数)

下面是esp和ebp维护CRT函数栈帧的图片

首先进行第一条指令


  • push的意思就是压栈,就是将ebp放到栈顶。
  • 注意:这是为了将来main函数返回之后,esp寄存器和ebp寄存器还能来回来维护CRT函数的,这里push的ebp就是为以后调用完返回CRT函数埋下的伏笔。

* 压栈的同时esp指针也会随着向上走。CRT函数的栈帧也随之增加

* 接着执行第二条指令

mov是move的缩写,意思是将esp赋值给ebp,实质是将esp里面的地址放到ebp里面。(也就是两个指针指向了同一位置)

注意:此时ebp和esp没有在维护CRT函数了,但他的函数栈帧没有销毁,因为栈空间的使用只能先销毁低地址,再销毁高地址。即图中的从上往下销毁。

  • 第三条指令

  • sub是减法的英文。意思是将esp这个地址减去C0这个16进制数字。C0其实就是十进制的192.可以在电脑上打开计算器,切换为程序员模式进行计算。(esp-192)
  • h是一个标识符,不用管它
  • * esp-192就是esp指针从低地址移向了高地址。而此时就呈现出来了一个main函数栈帧的雏形。ebp和esp之间的就是他的雏形。

* 接下来执行第四,五,六条指令。(可以不用管这三条指令)

  • 这里是连续三次(push)压栈,为了保存这三个寄存器。压栈的同时,esp指针位置随之上升。main函数的栈帧也随之扩大。

  • 到这里main函数的函数栈帧初步创建成功。

🌳 main函数栈帧的初始化

  • 这四步骤就是将ebp到esp之间的空间初始化为cc cc cc cc。
  • 这些cc cc cc cc是随机值。

* 然后执行这两条命令

  • 这里的call是调用的意思。就是调用printf函数来打印hello world。这里我就不赖细究printf函数怎么调用了。触及到博主目前知识水平极限了。
  • 保存002e1339这个地址的作用是为了调用完printf函数后返回主调函数,执行下一条指令。

🌳函数栈帧的销毁

printf函数栈帧的销毁

  • 调用完printf函数后,printf函数它的栈帧就要销毁了。
  • add的意思是让esp加4,即栈顶指针向下移动,来实现栈帧销毁。

  • 这个xor指令的意思为异或,两个相同的数异或就为0(实质是设置返回值为0)


main函数的栈帧销毁

  • main函数栈帧的销毁遵循后进先出的原则
  • 首先连续执行三个pop(弹出),把三个寄存器还原回去。


  • 我们让esp加上0C0h,那么esp将会往下移动到ebp的位置。
  • 官方语言是:降低栈顶esp的位置,此时局部变量空间被释放。
  • 此时main函数的栈帧就销毁了。


  • 接着的话esp和ebp指针会重新去维护调用main函数的函数栈帧

总结

每一次的函数调用都要在栈区开辟相应的栈帧空间。并将空间的内容进行初始化。

为什么平时写程序时没有赋值的值都是随机值?因为局部变量就是在函数栈帧里创建的。局部变量的值是我们初始化的cc cc cc cc。cc cc cc cc就是随机值

函数调用是怎么返回的?我们首先push了一个ebp寄存器,这样调用完之后就可以根据ebp这个地址返回了。

如果你觉得我的文章对你有帮助🎉欢迎关注🔎点赞👍收藏⭐️留言📝。


相关文章
|
5月前
|
Dart 开发工具 C++
Dart第一个程序hello,world
Dart第一个程序hello,world
|
5月前
|
存储 传感器 算法
Hello World CGAL 5.4入门
Hello World CGAL 5.4入门
|
5月前
|
Java C语言 C++
实现Hello,World!的方式
实现Hello,World!的方式
|
5月前
使用101中语言,输出hello world。
使用101中语言,输出hello world。
|
6月前
|
编译器 开发工具 C语言
单一程序:打印 Hello World
【5月更文挑战第14天】单一程序:打印 Hello World。
30 1
|
6月前
|
Android开发
开发Hello World 程序
开发Hello World 程序
|
IDE 编译器 程序员
编写第一个 C++ 程序:Hello World 示例
"Hello World"程序是学习任何编程语言的第一步,也是你将学习的最简单的程序之一。你所要做的就是在屏幕上显示消息"Hello World"。现在让我们看看程序:
194 0
|
开发框架 Java 开发者
Hello World细节-自动配置|学习笔记
快速学习Hello World细节-自动配置
Hello World细节-自动配置|学习笔记
|
JavaScript 前端开发 开发者
Hello World 程序|学习笔记
快速学习 Hello World 程序
|
测试技术
软件测试面试题:已知一个字符串为“hello_world_yoyo”, 如何得到一个队列 [“hello“,“world“,“yoyo“]
软件测试面试题:已知一个字符串为“hello_world_yoyo”, 如何得到一个队列 [“hello“,“world“,“yoyo“]
412 0