深入理解函数调用--函数栈帧

简介: 深入理解函数调用--函数栈帧

前言(为什么要深入了解函数栈帧)

学习函数栈帧可以让我们清晰的理解在代码运行的过程中,函数的调用、局部变量的生成与销毁、以及内存分布到底是怎么一回事。如果说使用函数就像是品尝佳肴的话,那么弄清楚函数栈帧就是把这个菜是怎么做的弄清楚,先放盐?还是先放酱油?等等。每一步都影响着最后菜品的味道以及品相。而如果要想成为一名优秀的厨师(程序员),我们就要明白其中的道道。理解底层代码的运作同时也体现一名程序员的内力深度。

相关知识点:

什么是寄存器?

寄存器:寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。

常见的寄存器:esp,edp,eax,ebx,ecx,edx,ebp,esp,edi.

这里我们重点介绍esp和edp寄存器。

(1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。

(2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

通俗的来说,esp和ebp一同维护了一个栈区,当调用函数时,内存会为该函数开辟一定的空间,也就是栈帧,esp指向栈顶,ebp指向栈底,当该栈区push一个元素时,esp的地址变小(栈区地址从上往下是由低到高的)。

汇编指令。

这里我只列出来常用的一部分:

常见汇编指令

push 把数据压入堆栈
pop 把数据弹出堆栈
add 将两个数相加,并将结果存储在指定的位置。
sub 从一个数中减去另一个数,并将结果存储在指定的位置。
mov 将数据从一个位置复制到另一个位置。
lea (Load Effective Address) 将有效地址加载到目标寄存器中,而不是加载实际的数据值。

汇编指令是干嘛的?

首先我们要弄明白什么是汇编与反汇编。

汇编和反汇编是与计算机指令相关的两个概念。

汇编是将高级语言或者人类可读的指令转换为机器语言的过程。在汇编过程中,程序员使用特定的汇编语言编写程序,然后使用汇编器将其转换为机器语言指令,以便计算机能够执行。

反汇编是将机器语言指令转换回汇编语言或者人类可读的指令的过程。在反汇编过程中,计算机程序将机器语言指令解析为对应的汇编语言指令,以便程序员或者分析人员能够理解和修改程序的行为。

所以汇编指令其实是一种较为底层的,能较为直接的与机器交流的语言,每种指令都有着其特定的作用。所以使用汇编语言,程序员可以直接访问和控制寄存器、内存和其他硬件资源,提供了对计算机底层操作的灵活控制和高效性。这也是为什么我下面会用观察反汇编语言的形式来了解函数栈帧。

编译器如何看到反汇编的代码呢?

我用的是vs2022,以下是该版本编译器打开反汇编窗口的详细方式:

好了,前戏做足了,现在开始步入正题:

测试代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
 
int add(int x, int y) {
  int z = 0;
  z = x + y;
  return z;
}
 
int main() {
  int a = 1;
  int b = 2;
  int c = 0;
  c = add(a, b);
  printf("%d\n", c);
  return 0;
}

什么是函数栈帧?

函数栈帧(Function Stack Frame)是在程序执行过程中,用来保存函数调用信息和局部变量的数据结构。它包含了函数的参数、返回地址和局部变量等信息。通俗的来说,就是在调用函数的时候系统给你开辟在栈区的一部分空间,专门用来存放局部变量等。比如说main()函数,它其实也是被调用的,打开反汇编我们可以看到:

函数栈帧的创建:

当一个函数被调用时,一个新的函数栈帧就会被创建在栈(stack)上,此时函数的参数会被压入栈中,并且执行流会跳转到被调用函数的入口。在被调用函数中,局部变量也会在栈帧中分配内存空间。

函数栈帧的销毁?

当被调用函数的执行完成后,函数栈帧会被销毁,栈指针会恢复到调用该函数之前的位置,继续执行原有函数的代码。函数栈帧的创建和销毁过程遵循“先进后出”的原则,因为栈是一种后进先出(LIFO)的数据结构。

函数调用结束时:

把返回值z的值反在寄存器中。

随着栈顶不断弹出数据,add函数建立的栈帧被释放。(包括里面的函数信息,局部变量)

esp与ebp又回到了原来的main函数栈区,esp还是指向栈顶。

这时候变量c的值被寄存器中存放的值覆盖,而这时候寄存器里的值其实就是add(a,b)的结果。

所以,这也是为什么局部变量在函数结束时还能把返回值传递过来,因为寄存器在中间起着桥梁作用。

函数调用时形参和实参的关系?

我们来看:

add函数内:

在生成a,b变量时,在main函数的栈帧内,我们把这两个变量各自放在了不同的区域内,在调用add函数时,从右往左依次将这两个变量的值存在了eax、ecx寄存器中,所以说这里函数里的x,y与a,b只是同值,但不同地址,介质是寄存器,这也是为什么说形参是实参的一份临时拷贝。

相关文章
|
6月前
|
存储 编译器 C语言
【深入理解函数栈帧:探索函数调用的内部机制】
【深入理解函数栈帧:探索函数调用的内部机制】
|
5月前
|
编译器
函数栈帧的创建和销毁
函数栈帧的创建和销毁
28 0
|
6月前
|
存储 编译器 容器
函数栈帧的创建和销毁讲解
函数栈帧的创建和销毁讲解
38 0
|
6月前
|
容器
函数栈帧的创建和销毁介绍
函数栈帧的创建和销毁介绍
38 0
|
存储
函数栈帧的创建和销毁(下)
函数栈帧的创建和销毁(下)
50 0
|
12月前
|
存储 缓存 编译器
函数栈帧的创建与销毁
函数栈帧的创建与销毁
37 0
|
12月前
|
存储 C语言 C++
你知道函数栈帧的创建和销毁吗?
你知道函数栈帧的创建和销毁吗?
73 0
|
编译器 C语言 容器
函数栈帧的创建和销毁(一)
函数栈帧的创建和销毁
115 1
|
存储 编译器
函数栈帧的创建和销毁(上)
函数栈帧的创建和销毁
50 0
|
存储 编译器 C语言
C/函数栈帧
C/函数栈帧