Bootstrap

函数私底下都在干什么?图解函数栈帧的创建与销毁


前言(什么是函数栈帧?)

函数栈帧是什么?相信很多人第一次听到时都一脸懵逼。我们不妨百度一下。

C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。
栈帧也叫过程活动记录。也就是函数的活动记录。函数执行的环境。函数参数、函数的局部变量、函数执行完后返回到哪里等等。
首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。


想要了解函数栈帧到底是什么一个东西,那么就必须要进入反汇编来观察,我们会发现反汇编界面有着很多CPU指令,想要弄清楚函数栈帧那么就必须搞懂这些指令。这对我们理解函数栈帧有着很大的帮助,本文按照顺序来介绍这些指令并梳理创建与销毁函数栈帧的过程。
首先我们写好一段代码:

#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 = 10;
	int b = 20;
	int c = 0;
	c = Add(a, b);
	printf("%d\n", c);
	return 0;
}

在编译器下按F10进入调试界面,然后鼠标右击进入反汇编界面:
在这里插入图片描述

我们会发现在反汇编界面有着许多的cpu指令,那么下面就简单介绍一下cpu指令和寄存器。

寄存器的介绍

数据寄存器(EAX EBX ECX EDX)

  • 寄存器EAX通常称为累加器(Accumulator),用累加器进行的操作可能需要更少时间。累加器可用于乘、除、输入/输出等操作,它们的使用频率很高;
  • 寄存器EBX称为基地址寄存器(Base Register)。它可作为存储器指针来使用;
  • 寄存器ECX称为计数寄存器(Count Register)。在循环和字符串操作时,要用它来控制循环次数;在位操作中,当移多位时,要用CL来指明移位的位数;
  • 寄存器EDX称为数据寄存器(Data Register)。在进行乘、除运算时,它可作为默认的操作数参与运算,也可用于存放I/O的端口地址。

指针寄存器(ESP EBP)

  • 寄存器EBP称为基址指针寄存器(栈底指针)。
  • 寄存器ESP称为堆栈指针寄存器(栈顶指针)。

变址寄存器(ESI EDI)

  • 寄存器ESI称为源变址寄存器 (Source Index);
  • 寄存器EDI称为目的变址寄存器(Destination Index)。

函数栈帧的创建与销毁过程详解

1.push指令

push指令是压栈的操作,就是将一块内存地址压入栈中(从高地址开始压入):
在这里插入图片描述

在调用main函数之前,main函数还被其他函数所调用,这里不做赘述,很明显我们看到,ebp被压入栈中,push指令就是这么简单。

esp和ebp

这里的esp和ebp分别是两个寄存器,里面存放的是地址,这两个地址用用来维护函数栈帧的,esp维护栈顶,ebp维护栈底,当每次开辟新的内存空间时,esp会自动跳到栈顶。所以esp为栈顶指针而ebp是栈底指针

2.mov指令

move指令是将地址中的数据赋给其他的地址例如:

009818B1  mov         ebp,esp  

就是将esp所存储的值也就是esp的地址赋给ebp,那么此时ebp的位置如下
在这里插入图片描述

3.sub指令和add指令

sub指令是减值操作,例如:

009818B3  sub         esp,0E4h

这里就是esp的地址减去0E4h这么多的地址,那么此时esp指针应该上移:
相反add是加的指令
在这里插入图片描述
那么此时esp栈顶指针和ebp栈底指针之间所维护的空间就是main函数的栈帧,至于地址空间大小,一定是符合要求的,编译器是为我们考虑好的,所以我们不必担心开辟出来的空间不够啊之类的问题。
在这里插入图片描述

再次压入三个地址(寄存器地址)。

3.lea指令

lea指令和mov有着些许相似的地方,mov是赋内存中的值,而lea是赋地址。
例如:
lea edi,[ebp-24h]
就是将[ebp-24h]这个地址值直接赋给edi,而不是把[ebp-24h]处的内存地址里的数据赋给eax。

mov指令则恰恰相反
例如:
mov ebp,esp
则是把内存地址为esp处的数据赋给eax。
在这里插入图片描述

4.rep stos指令

009818BC  lea         edi,[ebp-24h]  
009818BF  mov         ecx,9  
009818C4  mov         eax,0CCCCCCCCh 
009818C9  rep stos    dword ptr es:[edi] 

rep指令的目的是重复其上面的指令.ECX的值是重复的次数.
STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址.
这段代码的意思也就是将ebp-24h向上的地址,执行循环9次,双字(一个字等于两个字节)也就是四个字节的内容全部初始化为0CCCCCCCCh 。
在这里插入图片描述
edi的地址
这里也就是将main函数的栈帧初始化为cccccccc

4.call指令

call指令用来调用函数,并且记录下行代码地址,这里开始进入main函数
开始执行有效代码:

  1. 将a初始化为10:十六进制0a就是10
  2. 将b初始化为20:
  3. 将c初始化为0:

在这里插入图片描述
b和c的位置都间隔2个地址空间(8字节),这是编译器所决定的

在这里插入图片描述

传参

下面执行c=Add(a,b),传参
在这里插入图片描述
ebp-14h也就是b的位置,也就代表编译器是从右往左边传参

在这里插入图片描述

调用Add函数并记录下一行代码地址

在这里插入图片描述

那么指令执行!
在这里插入图片描述

进行运算,我们可以看出其实参数加减是在寄存器中运行的,最后eax寄存器的值就是30,是把值都放在eax累加器的,那么为什么形参是实参的临时拷贝也就解释的通了。改变形参不会改变实参。

在这里插入图片描述

5.pop指令

pop指令就是出栈,销毁函数栈帧
此时esp和ebp指向同一位置,Add函数栈帧被销毁
ebp内存地址弹出,那么此时ebp栈底指针也就要返回,指向在main函数中的ebp
在这里插入图片描述

此时,esp和ebp返回到main函数里面!

6.ret指令

返回到记录的call指令记录的下一条代码的位置
在这里插入图片描述
返回,那么就会弹出call指令记录的位置执行esp+8,那esp的位置就是:
在这里插入图片描述

ebp-20h就是c的位置,eax的值就是30,将30赋值给c,那么c的值就是30了
至此,函数栈帧的创建与销毁逻辑大概就介绍完毕。

总结

想要解释好函数栈帧画图是必不可少的环节,图文相结合才能更好地理解函数栈帧,还要对于cpu的指令进行一定了解。
函数栈帧算是比较底层的东西,希望本文对你以后的学习能有所帮助!

;