C语言之反汇编查看函数栈帧的创建与销毁(一)

简介: C语言之反汇编查看函数栈帧的创建与销毁(一)

一、 什么是函数栈帧?

函数栈帧是用于在计算机程序中实现函数调用的一种数据结构。在函数调用过程中,每个函数都需要在内存中创建一个栈帧,用于存储局部变量、返回地址和参数等。

  • 具体来说,函数栈帧通常包含以下部分:
  • 局部变量表:存储函数的局部变量,包括基本数据类型(如整数、浮点数等)和对象引用(如指针)。
  • 返回地址:存储函数的返回地址,即函数执行完毕后需要跳转到的地址。
  • 参数表:存储函数的输入参数,通常按照传递的顺序排列。
  • 操作数栈:用于存储函数的临时数据和中间结果,通常使用栈结构进行操作。
  • 当一个函数被调用时,会在内存中创建一个新的栈帧,并将其压入调用该函数的栈中。当函数执行完毕后,该栈帧会被弹出栈并销毁。因此,函数栈帧在函数调用过程中起到了存储和传递数据的作用。

函数栈帧的实现方式取决于具体的编程语言和编译器。在一些高级编程语言中,编译器通常会为每个函数自动创建和销毁栈帧,而无需程序员手动管理。而在低级编程语言或手动控制内存分配的情况下,程序员需要手动创建和销毁栈帧。

二、 理解函数栈帧能解决什么问题呢?

理解函数栈帧有什么用呢?

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

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

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

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

3.1、什么是栈?

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

  • 在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈(Push):将数据项添加到栈的顶部。这相当于把数据放到栈的最上面。出栈(Pop):从栈的顶部移除数据项。这相当于移除栈顶的数据项。但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。就像一个桶,先放的东西最后才能拿出、
  • 在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。

在经典的操作系统中,栈总是向下增长(由高地址向低地址)的,在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的

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

3.2.1 相关寄存器

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

3.2.2 相关汇编命令

【mov】:数据转移指令

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

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

【add】:加法命令

【sub】:减法命令

【lea】 :load effective address,加载有效地址

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

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

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

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

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

3.3.1 预备知识

  • 每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间
  • 这块空间的维护是使用了2个寄存器: espebp ,【ebp】 记录的是栈底的地址, esp 记录的是栈顶的地址

如图所示:

  • 函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异,本次演示以VS2022为例。

3.3.2 代码和环境搭建

  • 这段代码,如果我们在VS2019编译器上调试,调试进入Add函数后,我们就可以观察到函数的调用堆栈
#include <stdio.h>
int Add(int x, int y)
{
  int z = 0;
  z = x + y;
  return z;
}
int main()
{
  int a = 10;
  int b = 20;
  int c = 0;
  c = Add(a, b);
  printf("%d\n", c);
  return 0;
}
  • 首先我们来做一些环境的搭建工作

  • 首先直接在键盘上按下F10【笔记本按下Fn + F10】。
  • 以往写代码的时候,我们都知道要写这么一个main函数,程序就是从这里开始运行的
  • 接下去在按下F10后到监视窗口打开【调用堆栈】的窗口

  • 然后就出现了这样的界面。此时我们的main函数就从第13行开始运行了

  • 一直按F10,当调试箭头运行到第【22行】的时候,就会自动进入到exe_common.inl,此时我们就可以观察到底是哪个函数调用了main函数
  • 通过下图可知是invoke_main这个函数调用的,我们了解到这里就可以了~~

  • 然后,关掉这个【调用堆栈】的窗口后,重新调试起来
  • 调出【反汇编】【内存】【监视】这三个窗口

【反汇编】

【内存】

【监视】

好,现在我们的环境已经全部搭建好了

3.3.3 函数栈帧的创建

  • 接下去,我们正式开始分析函数栈帧究竟是如何创建的
  • 去掉符号名,方便看内存

  • 从上图看到已经进入到main函数了
  • main函数是由invoke_main这个函数来进行调用的,所以我们先画出它的函数栈帧

  • 首先看到左边的两个寄存器【esp】和【ebp】,分别用来维护栈顶和栈顶。
  • 对于栈来说是从【高地址】向【低地址】使用的。
  • 好,接下去的话就要执行第一条指令。将栈中push一个ebp,也就是将ebp中的值进行一个压栈的操作,此时的ebp中存放的是invoke_main函数栈帧的ebp
00EE18D0  push        ebp
  • 随着push入栈的操作,维护栈顶的esp就要往上

  • 然后我们看寄存器的变化

  • 我们再继续执行一下push这句指令,你就会发现【esp】中所存放的地址变小了,原来存的是【ebp】中的值,只是这个存放的形式是倒着存放的,是因为有大小端存储的问题

  • 接下来第二条,【mov】,我们在上面有讲到过是一个数据转移指令,这条指令的含义就是把esp的值存放到ebp中去
00EE18D1  mov         ebp,esp
  • 此时相当于产生了main函数的【ebp】,这个值就是invoke_main函数栈帧的【esp】,从这里开始就要开始维护main函数的函数栈帧了

  • 通过VS再来看一下,【ebp】中就会存放【esp】的地址了

第三条指令

  • 接下来第三条,sub是一条减法命令,那意思就是让esp中的地址减去一个16进制数字【0xe4】,产生新的esp,此时的esp是main函数栈帧的esp
00EE18D3  sub         esp,0E4h

  • 此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数中的局部变量,临时数据以及调试信息
  • 通过图,此时你也可以认为【esp】指向了低地址的一块空间

  • 来看一下寄存器中存放的内存变化

第四、五、六条指令

00EE18D9  push        ebx   //将寄存器ebx的值压栈,esp-4
00EE18DA  push        esi   //将寄存器esi的值压栈,esp-4
00EE18DB  push        edi   //将寄存器edi的值压栈,esp-4
  • 上面3条指令保存了3个寄存器的值在栈区,这3个寄存器的在函数随后执行中可能会被修改,所以先保存寄存器原来的值,以便在退出函数时恢复
  • 那随着寄存器的入栈,维护栈顶的寄存器也将发生变化
  • 此时esp也随着压栈而变化

  • 到VS里来看一下三次的变化:

第七、八、九、十条指令

  • 下面的代码是在初始化main函数的栈帧空间,【非常重要】
00EE18DC  lea         edi,[ebp-24h]   
00EE18DF  mov         ecx,9  
00EE18E4  mov         eax,0CCCCCCCCh  
00EE18E9  rep stos    dword ptr es:[edi] 

上面的这段代码最后4句,等价于下面的伪代码:

edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx = 0; --ecx,edi+=4)
{
  *(int*)edi = eax;
}
  • 首先要来看的就是【lea】就是我们在上面讲到过的【load effective address】加载有效地址的意思,那也就是从【ebp】这个维护栈顶的寄存器减去24h的位置,加载到寄存器【edi】里面去

  • 然后再将9放到【ecx】中去;以及将【0CCCCCCCCh】这块地址存到【eax】中去;
  • 从【edi】所存放的这块地址的开始,每次初始化4个字节的数据,dword值就是4个字节的大小
  • 这4句话的操作就是从edi开始,每次初始化4个字节的数据,总共初始化ecx次,初始化的内容为【0xCCCCCCCC】,总共初始化到ebp的地址结束

  • 到这里,main函数才刚刚被初始化完成
  • 那么里面的cccccccc是初始化的什么内容呢?–>我们来看一下
char arr[20];
printf("%s",arr);

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

C语言之反汇编查看函数栈帧的创建与销毁(二) :https://developer.aliyun.com/article/1427058

相关文章
|
2月前
|
存储 安全 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
|
2月前
|
存储 编译器 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
|
3月前
|
存储 编译器 C语言
C语言:底层剖析——函数栈帧的创建和销毁
C语言:底层剖析——函数栈帧的创建和销毁
|
3月前
|
存储 编译器 C语言
C 语言函数栈帧的概念讲解
C 语言函数栈帧的概念讲解
30 0
|
4月前
|
存储 编译器 程序员
C语言之反汇编查看函数栈帧的创建与销毁(二)
C语言之反汇编查看函数栈帧的创建与销毁(二)
|
2月前
|
存储 自然语言处理 编译器
编译和链接(翻译环境:预编译+编译+汇编+链接​、运行环境)
编译和链接(翻译环境:预编译+编译+汇编+链接​、运行环境)
|
4月前
|
存储 缓存 Linux
C语言编译过程——预处理、编译汇编和链接详解
C语言编译过程——预处理、编译汇编和链接详解
|
12月前
|
C语言
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(三)
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(三)
|
12月前
|
编译器 C语言
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(二)
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(二)
|
12月前
|
存储 自然语言处理 程序员
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(一)
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(一)