【函数栈帧的创建和销毁】 -- 神仙级别底层原理,你学会了吗?(1)

简介: 1.函数的调用方式相信你对调用函数一点都不陌生,但是在调用函数的过程中,却存在着很多你无法见到的东西,这是底层信息,想要理解透彻,就得深入底层去观察。本文以一个最简单的加法函数为例,深入讲解内存空间中的每一条指令。

1.函数的调用方式

相信你对调用函数一点都不陌生,但是在调用函数的过程中,却存在着很多你无法见到的东西,这是底层信息,想要理解透彻,就得深入底层去观察。

本文以一个最简单的加法函数为例,深入讲解内存空间中的每一条指令。

int Add(int x, int  y)
{
  int z = 0;
  z = x + y;
  return z;
}
int main()
{
  int a = 10;
  int b = 20;
  int c = Add(a, b);
  printf("%d\n", c);
  return 0;
}

这是源码,以该源码为例。

首先,我们进入调式

0c7bbf2175f8428b9f9f547639b7b58f.png

按如下图所示进行操作。

转到反汇编后,开始观察每一条代码的执行指令,在开始之前,先提出几个常见的问题:

1.局部变量是怎么创建的?
2.局部变量未初始化为什么是随机的?
3.函数是怎么传参的?传参的顺序是怎么样的?

4.形参和实参是什么关系?
5.函数的调用是怎么做的?
6.函数调用结束后是怎么返回的?

以上问题,都会通过下面的函数栈帧一一为你解答。

以下的讲解都是以低地址处在相对高的位置,高地址在相对低的位置,如下图:

39abea162816472197940f30b2773369.png

记住,每一个函数的调用,都会在栈区开辟一块内存空间。

栈空间的使用习惯是:从高地址向低地址使用。

我们在main函数被调用时,会在栈区开辟一块内存空间,如下图:


7d40ce8bd582419c91a9655ec9f996a9.png

实际上,main函数也是被其他函数调用的,所以,在main函数开辟栈空间之前,一定会先开辟调用main函数的函数的栈空间。

VS2019的环境下:

10cb17d27c8048fd962ab16858417834.png

通过调用堆栈我们可以看到,main函数是被一个叫做invoke_main的函数调用的,

9c29e3fa839448c4973697f58470916f.png

而该函数又是被一个叫做main_result 的函数调用的

6d78c39a0c1545b9a50877f6e24865d5.png

这样逐层调用下去。所以,main函数也是被编译器中的其他函数调用的。

具体的函数调用多少,取决于不同的编译器实现。

所以,调用main函数的函数先在栈区开辟一块空间。

2.函数在栈区上的动作

首先,回到反汇编代码中,

d4dca4479c224e7a83d869564c21b4e9.png

在执行第一条 int a = 10语句之前,有许许多多的反汇编代码。

先看第一条:

ebp是一个寄存器,push ebp,是将寄存器压栈,压入栈空间的顶部。

那么寄存器是什么呢?压栈是什么呢?

先看下图:


4fbb5fb1b689468182c12737ac89501c.png

在栈空间中,一块函数栈空间是由寄存器来维护和使用的。


两个不同的寄存器足以维护它们之间的栈空间,并且寄存器和函数地址是毫不相干的,寄存器是一个真实存在的东西,任何代码任何地方都可以使用它。


在main函数的栈空间中,使用的方式是从栈顶往栈底压栈的,所以ebp和esp两个寄存器可以形象地称为栈底指针和栈顶指针。

在执行了第一条汇编指令后,ebp寄存器中的值就被压到了调用main函数的函数的栈空间顶部。

不是ebp本身被压栈,ebp寄存器是个真实存在的东西,不可能会被真的压栈,压栈压得是ebp存放的值。


8617335a2f2d465ab065f2be325ef860.png

我们查看寄存器的值发现,ebp的值存放的是一个地址,该地址就是上图中的栈底指针所指向的那个地方的地址:

如下图:

18083cf02e554aa4bef0c7bf02077978.png

而在压栈结束后,esp这个寄存器指向的地址会往上走,也就是会往低地址处走,因为它是栈顶指针。

如下图:

da15ddf1e7fb44689572266889b4a52c.png

我们可以验证一下:


9b0a6ecdd40e4853ac7104b40d40a044.png

esp的值从0x00D0FADC变成了0x00D0FAD8

证实了上述的动作。

可能你会有个疑问:将ebp压栈有什么用呢?

将ebp压栈是为了记录ebp和esp最开始维护的栈空间的地址,以后ebp被调用到其他地方的时候,栈空间的地址仍然被记录,很有效的防止栈空间丢失的现象。

在执行完第一条汇编语句后,接下来执行第二条汇编语句:

d96a4e101e0f46df9592026bafd22a26.png

该汇编语句的意思是: 将esp的值move 到ebp ,也就是把esp的值赋给ebp。

也就是说,ebp此时指向了esp指向的地址:如下图:

676d3480cd674be4a344353003981caf.png

此时ebp和esp都指向了同一个位置,


7617e056890e44028069627422de775a.png

执行第二个语句后,esp和ebp存放的值相同了。

前面我们讲过,一块栈空间是由两个寄存器来维护的,现在两个寄存器都指向了同一个位置,那之前的空间不会丢失了吗?

这就回答了第一个汇编语句:将ebp压栈的作用,此时已经记录了ebp和esp在最开始所维护的空间的地址,保证开辟的栈空间不会被忘记。

接下来执行第三条汇编语句:

6730001860b9455e8b0fede5f3c6d472.png

这条语句的意思是:将esp存放的值减去0E4h, sub就是减法的意思。 0E4h其实是一个16进制数字,只是方便编译器识别而这样设计的,具体这个值是多少我们可以看一下,不过不需要去了解,这是为编译器使用的。

21312e8295754e74b1467422cd6b7a19.png

esp的值减去一个值,结果当然会更小,所以esp会往上走,因为低地址是在上方,所以esp会走到上面的某一个区域。如下图:


74537493fe21423bb87858f707afb078.png

可以验证一下:esp存放的值现在是0x00D0FAD8,执行了该汇编指令后,esp的值是:0x00D0F9F4,明显小于之前的,所以证实了esp往低地址走了。

7a83d51b2ddc40959b4678170cfe6458.png

接下来执行第四条汇编指令:

03c55495fc9c4002a97fdf6a67c72b39.png

ebx也是一个寄存器,该汇编指令就是把ebx压栈。如下图:


436f955abc204cc5b16a2927b1e7f4a9.png

那么在执行完压栈操作后,esp又会往上走一走,


f910362f509942b48545a6d14ba85646.png

执行来看一下:


99e36f4ac5be4735a231ddcdc7e0f5db.png

esp存的地址的的确确又往低地址处走了,之前是F4,现在是F0(地址的后两位),也就是走了4个字节。

那么,介于ebp和esp的那么大一块的空间是干嘛的呢?

下面的汇编指令会给你解答。

接下来继续执行两条压栈的汇编指令


a5b38ed2b3d44bdb96dded2cba5474d5.png

依然是将edi这个寄存器的值压栈,

76bb6e3beea146808def9e6fd513510a.png

随后执行的汇编指令是


285d540118804e75800e308938f07842.png

先看第一个:lea的意思是 load effective address ,加载有效地址,

将ebp-24h的值加载到edi中,ebp的值是一个地址,ebp-24h依然是一个地址。

接下来是mov ecx 9,也就是将9赋值给ecx寄存器。

然后是将0CCCCCCCCh 赋给eax这个寄存器。

这三条语句是为下面这条语句做铺垫的,真正起作用的也是这条语句:

8ce71cc9b5c64097a74f52da34ed0ff7.png

dword的意思是double word,word是字,单词的意思,dword就是两个字,两个单词, 一个字是两个字节,那两个字就是四个字节。

该语句的意思是:

285d540118804e75800e308938f07842.png

相关文章
|
3月前
|
存储 安全 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
|
3月前
|
存储 编译器 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
|
3月前
|
存储 编译器
初识函数栈帧的创建与销毁(笔记)
初识函数栈帧的创建与销毁(笔记)
|
程序员 编译器 C语言
细谈函数栈帧的创建与销毁
我们在写C语言代码时,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。那函数如何调用?函数的返回值如何返回的?函数参数是如何传递的?这些问题都与函数栈帧有关系。
160 0
|
存储 编译器 程序员
C语言代码函数栈帧的创建与销毁(修炼内功)
目录 在前期的学习中我们可能有很多困惑 例如:局部变量是怎么创建的 为什么局部变量的值是随机值 函数是怎么样传参的 传参的顺序是什么 形参和实参的关系是什么 函数调用是怎么做的 函数掉调用结束后怎么返回的 这篇博客我们来修炼自己的内功,掌握好这篇博客的大部分知识就已经很不错了 我们用到VS2013这个编译器,目的是为了看到更详细的函数封装内容 提示不要使用太过高级的编译器,因为越高级的编译器越不容易观察。同时这里需要注意的是在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,不是完全相同的,具体细节取决于编译器
|
存储 编译器
从汇编代码探究函数栈帧的创建和销毁的底层原理(二)
从汇编代码探究函数栈帧的创建和销毁的底层原理
|
C语言 C++
从汇编代码探究函数栈帧的创建和销毁的底层原理(一)
从汇编代码探究函数栈帧的创建和销毁的底层原理
【函数栈帧的创建和销毁】 -- 神仙级别底层原理,你学会了吗?(2)
1.函数的调用方式 相信你对调用函数一点都不陌生,但是在调用函数的过程中,却存在着很多你无法见到的东西,这是底层信息,想要理解透彻,就得深入底层去观察。 本文以一个最简单的加法函数为例,深入讲解内存空间中的每一条指令。
抽丝剥茧C语言(中阶)函数栈帧的创建与销毁——图解(下)
抽丝剥茧C语言(中阶)函数栈帧的创建与销毁——图解
|
编译器 C语言
【函数栈帧的创建和销毁】(超详细图解)(上)
【函数栈帧的创建和销毁】(超详细图解)
70 0
【函数栈帧的创建和销毁】(超详细图解)(上)