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语句来提前结束函数的执行。


相关文章
|
7月前
|
存储 编译器 程序员
C语言之反汇编查看函数栈帧的创建与销毁(一)
C语言之反汇编查看函数栈帧的创建与销毁(一)
C语言之反汇编查看函数栈帧的创建与销毁(一)
|
7月前
|
安全 C语言 C++
奇怪的函数调用
奇怪的函数调用
58 0
|
存储 安全
!!!教你如何搞懂字符串函数(详细,后期会讲函数栈帧)(下)
!!!教你如何搞懂字符串函数(详细,后期会讲函数栈帧)(上)
|
编译器
!!!教你如何搞懂字符串函数(详细,后期会讲函数栈帧)(中)
!!!教你如何搞懂字符串函数(详细,后期会讲函数栈帧)(上)
|
2月前
|
存储 编译器 C语言
烫烫烫手的结构体大小计算来咯,很烫哦,慢慢消化。自定义类型(一)
烫烫烫手的结构体大小计算来咯,很烫哦,慢慢消化。自定义类型(一)
20 2
|
7月前
|
存储 编译器 程序员
C语言之反汇编查看函数栈帧的创建与销毁(二)
C语言之反汇编查看函数栈帧的创建与销毁(二)
!!!教你如何搞懂字符串函数(详细,后期会讲函数栈帧)(上)
!!!教你如何搞懂字符串函数(详细,后期会讲函数栈帧)
|
存储 编译器 程序员
|
编译器
学C的第十一天【查看汇编代码一步步了解 函数栈帧(栈区局部变量)的创建和销毁 讲解】-1
函数栈帧的创建和销毁 越高级的编译器,越不容易学习和观察该过程 同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现