9.为什么有时候会“烫烫烫”——之函数栈桢

简介: 9.为什么有时候会“烫烫烫”——之函数栈桢

1. 什么是函数栈帧

我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。 那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧 有关系。

函数栈帧(stack frame)就是函数调用过程中在程序的调用(call stack)所开辟的空间,这些空间 是用来存放:

  • 函数参数和函数返回值
  • 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
  • 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)

2. 理解函数栈帧能解决什么问题呢?

理解函数栈帧有什么用呢?问题答案见文末,先不妨带着问题思考一下吧

只要理解了函数栈帧的创建和销毁,以下问题就能够很好的理解了:

  1. 局部变量是如何创建的?
  2. 为什么局部变量不初始化内容是随机的?
  1. 函数调用时参数时如何传递的?
  2. 传参的顺序是怎样的?
  3. 函数的形参和实参分别是怎样实例化的?
  4. 函数的返回值是如何带会的?

让我们一起走进函数栈帧的创建和销毁的过程中。  

3. 函数栈帧的创建和销毁解析

3.1 什么是栈?

栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函 数,没有局部变量,也就没有我们如今看到的所有的计算机语言。


在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可 以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出 栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。


在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据 从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。 在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。 在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的。

3.2 认识相关寄存器和汇编指令

相关寄存器

  • eax:通用寄存器,保留临时数据,常用于返回值
  • ebx:通用寄存器,保留临时数据
  • ebp:栈底寄存器
  • esp:栈顶寄存器
  • eip:指令寄存器,保存当前指令的下一条指令的地址

相关汇编命令

  • mov:数据转移指令
  • push:数据入栈,同时esp栈顶寄存器也要发生改变
  • pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
  • sub:减法命令
  • add:加法命令
  • call:函数调用,1. 压入返回地址 2. 转入目标函数
  • jump:通过修改eip,转入目标函数,进行调用
  • ret:恢复返回地址,压入eip,类似pop eip命令

3.3 解析函数栈帧的创建和销毁

3.3.1 预备知识

首先我们达成一些预备知识才能有效的帮助我们理解,函数栈帧的创建和销毁

1. 每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。

2. 这块空间的维护是使用了2个寄存器: esp 和 ebp , ebp 记录的是栈底的地址, esp 记录的是栈顶 的地址

如图所示:

3.3.2 函数的调用堆栈

函数调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到 ,main 函数调用之前,是由 invoke_main 函数来调用main函数。

那我们可以确定, invoke_main 函数应该会有自己的栈帧, main 函数和 Add 函数也会维护自己的栈 帧,每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间。

那接下来我们从main函数的栈帧创建开始讲解:

3.3.4 准备环境

为了让我们研究函数栈帧的过程足够清晰,不要太多干扰,我们可以关闭下面的选项,让汇编代码中排 除一些编译器附加的代码

3.3.5 转到反汇编

调试到main函数开始执行的第一行,右击鼠标转到反汇编。得到的代码整理如下:

转化为伪代码和图片可以得到:

小知识:烫烫烫~

下面的程序输出“烫”这么一个奇怪的字,是因为main函数调用时,在栈区开辟的空间的其中每一 个字节都被初始化为0xCC,而arr数组是一个未初始化的数组,恰好在这块空间上创建的,0xCCCC(两 个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”。  

接下来我们再分析main函数中的核心代码:

上面已经告诉大家看反汇编的方法啦,代码太长就不放在文章里面了,大家可以自己实现,对照下面的图进行理解:

拓展了解:

其实返回对象时内置类型时,一般都是通过寄存器来带回返回值的,返回对象如果时较大的对象时,一 般会在主调函数的栈帧中开辟一块空间,然后把这块空间的地址,隐式传递给被调函数,在被调函数中 通过地址找到主调函数中预留的空间,将返回值直接保存到主调函数的。


具体可以参考《程序员的自我 修养》一书的第10章。   到这里已经给大家完整的演示了main函数栈帧的创建,Add函数站真的额创建和销毁的过程,相信大家 已经能够基本理解函数的调用过程,函数传参的方式,也能够回答文章开始处的问

Q&A

局部变量是如何创建的?

局部变量是在函数或代码块内部声明的变量。当程序执行到包含局部变量声明的函数或代码块时,该变量会被创建并分配内存空间。局部变量只在声明它的函数或代码块内部可见,超出其作用域范围后就会被销毁

为什么局部变量不初始化内容是随机的?


栈上的数据并不会被自动初始化为特定的值。当程序执行到声明局部变量的语句时,编译器会为该变量分配一块内存空间,但并不会对其进行初始化操作。因此,该内存空间中原本存储的是之前使用过的数据,导致局部变量的内容是随机的。


函数调用时参数时如何传递的?

在传值调用中,实参的值被复制到形参中,函数内部对形参的修改不会影响实参的值。

传参的顺序是怎样的?

按照声明的顺序来确定的,,而不是按照实参在函数调用中的顺序。

函数的形参和实参分别是怎样实例化的?

形参的实例化:函数的形参是在函数定义或声明时指定的参数,用于接收函数调用时传递的实参的值。

实参的实例化:实参是函数调用时传递给函数的值,它们可以是常量、变量或表达式。


函数的返回值是如何带会的?


在栈桢中,函数的返回值通常存储在栈的某个位置上,例如在栈桢的顶部或者是返回地址的下一个位置。当函数执行完毕后,程序会将该位置上的值取出,并传递给调用函数。由于该值存储在main栈桢中了,在函数调用结束后并不会立即被销毁,因此函数的返回值也就不会被销毁,可以被传递给调用函数。


tips: 如果函数声明的返回类型是void,则表示函数没有返回值,可以使用return语句来提前结束函数的执行。


相关文章
|
Unix 编译器 Linux
【计算机基础 ELF文件】深入探索ELF文件:C++编程中的关键组成部分
【计算机基础 ELF文件】深入探索ELF文件:C++编程中的关键组成部分
1020 0
|
Python
通过阿里云服务器的公网IP访问django网站
在安全组设置,添加可以访问的端口 搭建django网站,可以参考官方的教程 设置网站的setting.py ALLOWED_HOSTS = ['*'] 启动网站服务 python manage.py runserver 0.
5492 0
|
机器学习/深度学习 人工智能 计算机视觉
YOLOv11 正式发布!你需要知道什么? 另附:YOLOv8 与YOLOv11 各模型性能比较
YOLOv11是Ultralytics团队推出的最新版本,相比YOLOv10带来了多项改进。主要特点包括:模型架构优化、GPU训练加速、速度提升、参数减少以及更强的适应性和更多任务支持。YOLOv11支持目标检测、图像分割、姿态估计、旋转边界框和图像分类等多种任务,并提供不同尺寸的模型版本,以满足不同应用场景的需求。
YOLOv11 正式发布!你需要知道什么? 另附:YOLOv8 与YOLOv11 各模型性能比较
|
消息中间件 存储 Java
吃透 RocketMQ 消息中间件,看这篇就够了!
本文详细介绍 RocketMQ 的五大要点、核心特性及应用场景,涵盖高并发业务场景下的消息中间件关键知识点。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
吃透 RocketMQ 消息中间件,看这篇就够了!
|
存储 缓存 NoSQL
Redis与数据库同步指南:订阅Binlog实现数据一致性
本文由开发者小米分享,探讨分布式系统中的一致性问题,尤其是数据库和Redis一致性。文章介绍了全量缓存策略的优势,如高效读取和稳定性,但也指出其一致性挑战。为解决此问题,提出了通过订阅数据库的Binlog实现数据同步的方法,详细解释了工作原理和步骤,并分析了优缺点。此外,还提到了异步校准方案作为补充,以进一步保证数据一致性。最后,提醒在实际线上环境中需注意日志记录、逐步优化和监控报警。
1828 3
|
安全 Shell Linux
记录VulnHub 靶场——Escalate_Linux渗透测试过程
本文档描述了一次靶场环境的搭建和渗透测试过程。首先,提供了靶机环境的下载链接,并建议在VMware或VirtualBox中以NAT模式或仅主机模式导入。接着,通过Kali Linux扫描发现靶机IP,并用Nmap扫描开放端口,识别出80、111、139、445、2049等端口。在80端口上找到一个shell.php文件,通过它发现可以利用GET参数传递cmd命令。
1074 0
|
安全 Linux 开发工具
1.Anaconda 安装配置,轻轻松松上手Python
1.Anaconda 安装配置,轻轻松松上手Python
422 0
|
SQL Go 数据库
【Sentinel Go】新手指南、流量控制、熔断降级和并发隔离控制
【2月更文挑战第12天】随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
1357 0

热门文章

最新文章