程序员内功心法之函数栈帧的创建和销毁

简介: 程序员内功心法之函数栈帧的创建和销毁

1、本节目标

C语言绝命七连问,你能回答出几个?

  1. 局部变量是如何创建的?
  2. 为什么局部变量不初始化其内容是随机的?
  3. 有些时候屏幕上输出的"烫烫烫"是怎么来的?
  4. 函数调用时参数时如何传递的?传参的顺序是怎样的?
  5. 函数的形参和实参的关系是什么?
  6. 函数的返回值是如何带回的?
  7. 函数是怎样在栈区上开辟和释放空间的?

想要对上面的这六个问题做出准确深入的回答,我们需要学习函数栈帧的创建和销毁相关知识,在正式进入函数栈帧之前,我们需要了解一些相关的寄存器和汇编指令

2、相关寄存器

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

3、相关汇编指令

mov:数据转移指令。

sub:减法命令。

add:加法命令。

push:数据入栈,同时esp栈顶寄存器也要发生改变。

pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变。

call:函数调用,1. 压入返回地址 2. 转入目标函数。

jump:通过修改eip,转入目标函数,进行调用。

lea:传递地址指令,用于加载有效地址。

ret:恢复返回地址,压入eip,类似pop eip命令。

4、什么是函数栈帧

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

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

同时,每一次函数调用,编译器都会为该函数分配一块空间,而这块空间就被称为这个函数的函数栈帧;并且,这块空间是由两个寄存器来维护的:esp寄存器(记录栈顶的地址)和ebp寄存器(记录栈底的地址)。

2020062310470442.png

5、什么是调用堆栈

函数调用堆栈是反馈函数调用逻辑的。我们以main函数的调用为例:

2020062310470442.png

6、函数栈帧的创建和销毁

我们以一段程序为例讲解函数栈帧:(注意: 函数栈帧的创建和销毁过程,在不同的编译器上实现的方法和细节会有所差异,一般来说,越新的编译器对函数栈帧的封装就越严密,本次演示以VS2019为例。)

演示代码

#include<stdio.h>
int Add(int x, int y)
{
  int z = 0;
  z = x + y;
  return z;
}
int main()
{
  int a = 3;
  int b = 5;
  int ret = 0;
  ret = Add(a, b);
  printf("%d", ret);
  return 0;
}

(1)、main函数栈帧的创建与初始化

2020062310470442.png

20200623104134875.png

20200623104650275.png

(2)、main函数的核心代码

2020062310470442.png

20200623104134875.png

20200623104650275.png

(3)、Add函数的调用过程

2020062310470442.png

代码执行到Add函数的时候,就要开始创建Add函数的栈帧空间了。

在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。

1. 将main函数的 ebp 压栈。

2. 计算新的 ebp 和 esp。

3. 将 ebx , esi , edi 寄存器的值保存。

4. 计算求和,在计算求和的时候,我们是通过 ebp 中的地址进行偏移访问 到了函数调用前压栈进去的参数,这就是形参访问。

5. 将求出的和放在 eax 寄存器中准备带回。

2020062310470442.png

20200623104134875.png

(4)、Add函数栈帧的销毁

2020062310470442.png

20200623104134875.png

20200623104650275.png

(5)、调用完成

调用完Add函数,回到main函数的时候,继续往下执行,可以看到:

2020062310470442.png

add esp,8: esp直接+8,相当于pop了main函数之前压栈的a’和b’。


mov dword ptr [ebp-20h] eax:将eax中的值存放到ebp-0x20的地址处,其实就是存储到main函数中ret变量中,而此时eax中就是Add函数中计算的x和y的和,可以看出来,本次函数的返回值是由eax寄存器带回来的。程序是在函数调用返回之后,在eax中去读取返回值的。

7、对开篇问题的解答

当我们完整的了解了函数栈帧创建和销毁的过程后,我们就可以回答开篇提到的问题了:

局部变量是如何创建的?

局部变量的创建是当局部变量所在的函数的栈帧创建完成并初始化后,在该栈帧内为局部变量分配空间的。


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

因为函数栈帧在创建完成之后,编译器会把该栈帧空间的内容全部初始化为一个值,而这个值是随机的,且在不同编译器下该值可能是不同的。(VS下该随机值为0xcc cc cc cc)


有些时候屏幕上输出的"烫烫烫"是怎么来的?

函数的栈帧创建之后,其空间中的每一个字节都被初始化为一个随机值,如果这个随机值为 0xcc (比如VS下),且如果我们定义的是一个未初始化的数组,而这个数组恰好在这块空间上创建,那么打印此数组的内容时屏幕上输出的就是烫烫烫 。(因为0xCCCC(两个连续排列的0xCC)的汉字编码是“烫”)


函数调用时参数时如何传递的?传参的顺序是怎样的?

我们在调用目标函数之前,就会在本函数的栈顶上从右向左依次压入需要传递的参数,然后再创建好被调函数的栈帧后通过栈底寄存器的偏移量来访问形参,所以被调函数的形参不是在被调函数的栈帧空间中创建的,而是在调用函数的栈帧中创建的,且传参的顺序是从右至左。


函数的形参和实参的关系是什么?

形参是实参的一份临时拷贝,二者虽处于同一个函数的栈帧空间内,但存储位置不同,形参的改变不会影响实参。


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

函数的返回值通过eax寄存器带回。


函数是怎样在栈区上开辟和释放空间的?

函数通过改变esp和edp的指向来创建和销毁空间 (即形成函数栈帧),空间销毁并不会清除该空间中的数据,下一次使用该空间时新数据直接覆盖原数据即可。


相关文章
|
7月前
|
存储 C语言
打通你学习C语言的任督二脉-函数栈帧的创建和销毁(上)
打通你学习C语言的任督二脉-函数栈帧的创建和销毁(上)
50 0
|
11月前
|
存储 编译器 程序员
|
存储 编译器 程序员
C语言代码函数栈帧的创建与销毁(修炼内功)
目录 在前期的学习中我们可能有很多困惑 例如:局部变量是怎么创建的 为什么局部变量的值是随机值 函数是怎么样传参的 传参的顺序是什么 形参和实参的关系是什么 函数调用是怎么做的 函数掉调用结束后怎么返回的 这篇博客我们来修炼自己的内功,掌握好这篇博客的大部分知识就已经很不错了 我们用到VS2013这个编译器,目的是为了看到更详细的函数封装内容 提示不要使用太过高级的编译器,因为越高级的编译器越不容易观察。同时这里需要注意的是在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,不是完全相同的,具体细节取决于编译器
|
编译器 C语言
抽丝剥茧C语言(中阶)函数栈帧的创建与销毁——图解(上)
抽丝剥茧C语言(中阶)函数栈帧的创建与销毁——图解
抽丝剥茧C语言(中阶)函数栈帧的创建与销毁——图解(下)
抽丝剥茧C语言(中阶)函数栈帧的创建与销毁——图解
|
存储 监控 编译器
【C语言进阶】函数栈帧的创建和销毁(内功修炼)
目录 前言 一、基础知识 1.1 什么是栈区? 1.2 寄存器 1.3 测试代码和一些其它的 二、函数栈帧的创建和销毁的过程 2.1 _tmainCRTStartup函数(调用main函数)栈帧的创建 2.2 main函数栈帧的创建 2.3 main函数内执行有效代码 2.4 Add函数栈帧的创建 2.5 Add函数内执行有效代码 2.6 Add函数栈帧的销毁 2.7 main函数代码继续执行 三、所需反汇编代码总览 四、总结
259 0
【C语言进阶】函数栈帧的创建和销毁(内功修炼)
|
编译器 C语言
探秘函数栈帧:『 揭开函数栈帧创建与销毁的神秘面纱 』(二)
探秘函数栈帧:『 揭开函数栈帧创建与销毁的神秘面纱 』
147 0
|
存储 程序员 C语言
探秘函数栈帧:『 揭开函数栈帧创建与销毁的神秘面纱 』(一)
探秘函数栈帧:『 揭开函数栈帧创建与销毁的神秘面纱 』
82 0
探秘函数栈帧:『 揭开函数栈帧创建与销毁的神秘面纱 』(三)
探秘函数栈帧:『 揭开函数栈帧创建与销毁的神秘面纱 』
46 0
|
存储 编译器 C语言
C生万物 | 反汇编深挖【函数栈帧】的创建和销毁
从汇编角度深度挖掘函数栈帧建立和销毁的全过程,保姆式教学,超详细解说
174 0
C生万物 | 反汇编深挖【函数栈帧】的创建和销毁

热门文章

最新文章