栈帧详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Neil4/article/details/62416903
一. 理解栈帧
栈帧是什么,我们基本的理解是栈帧是栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。通俗来说栈帧就时C语言函数在调用的过程中的调用原理,就是当我们执行一个函数操作的时候,它的内部是如何实现的呢。
二 .关于栈帧的背景知识
1. 寄存器
第一个寄存器ebp,基址寄存器,也叫做栈底寄存器。
第二个寄存器esp,是栈顶寄存器。
第三个寄存器pc指针,也叫做程序计数器,它永远指向当前指令的下一条指令。
2. 计算机运算的基本过程
取指令--分析指令--执行指令
但程序执行的过程中,pc指针指向下一个指令,那么当一个函数执行的时候,它会指向这个 函数,另外任何一个函数都有自己的ebp和esp,即是任何一个函数都有一个栈底和栈顶。但是在我们的cpu中,ebp和esp不可能有很多,可是我们的函数却可以很多,所以在函数执行的时候,ebp和esp都被新的函数覆盖掉了,那个原来的ebp和esp都应该保存,这样之后我们在执行完一个函数之后才可以回到上面一个函数。所以我们的ebp和esp始终指向当前函数的栈底和栈顶。
3. 栈地址的生长方向
栈的生长方向是从高地址往低地址生长的,那么随着我们函数的调用的层层深入,我们的ebp和esp会越来越小。
4. 程序地址空间
这里先画一个草图,让大家看一下一个程序在运行的时候需要哪些地址空间。

今天我们主要讨论的是栈区里面的使用原理,下面就是栈帧调用的详细解析
三. 栈帧调用原理
这里讨论函数调用的原理,我们主要画一个栈和代码区的一个图。下面的这幅图简明的说明了栈帧调用的机理。
在函数的最开始执行的应该是调用main函数,那么此时应该为main函数开辟一个存储空间,并且有一个ebp和一个esp指向这个存储空间的栈顶和栈底,另外还有pc指针指向代码区中的main函数中的下一条指令。这里再次说明一下,栈里面是由高地址往低地址生长,请看下图,其中红色箭头代表main函数的指针,蓝色箭头代表指向fun函数的指针,这里我们假设首先执行main函数,然后执行fun函数,首先使用红色指针,然后使用蓝色指针。


这里说明了一个问题就是,当程序执行不同的函数时,我们的ebp、esp和pc指针是变化的,并且执向当前函数的栈帧区,因为我们执行完当前函数之后还希望返回原来的函数,那么我们在改变这三个指针之前一定要先保存这三个指针。上图只是说明我们要保存三个指针变量但是没有说明具体是如何保存的,还有就是函数中是要开辟参数的,我们的草图也没有说明参数的空间时如何开辟的。
接下来我们通过具体的代码来详细的说明一下,在编译器的内部是如何进行栈帧的变化的。首先看下面的一段代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<windows.h>

int fun(int a, int b)
{
int c = 0xcccccccc;
return c;
}

int main()
{
int a = 0xaaaaaaaa; //这里赋值为0xaaaaaaaa的原因是,在一会的查看汇编时候,
//显示的都是16进制,这里这样赋值为了我们容易观察
int b = 0xbbbbbbbb;
int ret = fun(a, b);
printf("You shoud running here!\n");
system("pause");
return 0;
}


对于这段代码,我们可以按F10进入调试,然后选择菜单栏的调试,选择反汇编,然后一次次按F10知道定义a变量的时候,看下面的截图。

这里看红色框里面,实际上在汇编语言中,定义并初始化a,b变量的时候实际上是把一个值放入到一个内存当中去,中间用到了mov指令。那么我们此时也应该在主函数的栈帧区为a,b变量分别开辟空间,请看下图。

接下来我们基础回到我们的汇编代码,请看我给大家的截图

这里补充说明的一点,汇编代码中的call实际上就是调用函数的过程,但是我们还需要明白的一点就是,当我们调用这个函数的时候,数据应该是已经创建好的,所以我们这里在call指令调用这个函数的前面是一个传递参数的工作。这里解释一点就是eax,ebx...都是通用寄存器。第一个红框里面mov指令,把b变量放置在eax中,然后执行push指令,该指令的作用是把b变量放置在栈顶,第二个红色框中,把a变量放置在ecx中,然后push变量a入栈。push完成过后就形成了形参实例化的a和b的临时变量。从上面的实例化的过程中,我们不难发现一个问题就是,参数在实例化的时候的顺序是从右往左的。
现在我们再来看看栈帧图是如何压栈的。

这里的压栈,即汇编代码中的push,是首先把指针往下移动,然后把数据形参b放进栈顶,然后再使指针往下移动,然后再放入数据形参a到栈顶位置。
把a,b的形参压如栈顶之后,才真正开始执行call指令,call指令的作用是,第一个作用是把当前指令的下一条指令的地址压入栈中,当前指令使call指令,它的下一条指令是add,从上面的反汇编图中可以看到,这里我们还可以看到add的指令地址为01011449,那么这个地址就被压入栈中;call的第二个作用是跳转(jmp)到下一个函数的入口。我们按F11就可以进入到jmp这个函数中去,jmp的一个作用是修改pc指针。接下来按F10就可以执行我们的fun函数。
这个时候我们进入到了它的fun函数内部,继续查看它的反汇编代码,请看下面的反汇编的截图

第一句,push ebp即是把main函数的栈底指针压栈。接下来是mov ebp,esp它的作用就是把ebp指向当前函数的栈底。再下一条指令,sub esp,0CCh,这条指令作用是使esp往下移一段空间,这样做之后,我们的ebp和esp又指向了一个新的空间,这个空间就是我们的fun函数的栈帧空间。再来看上图中的最下面的两个红色框中,这个时候fun函数已经执行完毕,mov esp,ebp,就是把ebp中的内容放在esp中去,那么此时的结果就是esp又指向了存放main函数的ebp的地址空间,请看黑色箭头。接下来又执行了pop ebp,就是把esp当前所指向的内容放在了ebp中,当前esp的内容正是主函数的ebp,那么此时ebp又指向了main函数的最开始的ebp中去了,然后esp往上退回一个地址空间,那么此时esp就是main:ret,也就是刚刚的那个call指令的下一条指令add指令。接下来反汇编代码中最后一个指令ret,这个指令被集成了,我们无法看到,它的作用是继续pop,它的其中一个操作是pop pc,那么就是把当前esp中的内容放在了pc指针中,此时pc指针指向的就是add指令,接着再让esp退回一段地址空间,所以ret执行完毕之后,esp又指回了临时变量a。请看下面的栈帧图

接下来我们按F10这样又回到了主函数当中去了,那个此时执行了add指令,请看下面的反汇编截图

紧接着它做了下一步操作add esp,8,就是把esp中的内容加上8,加上8之后,esp刚好指向了main函数原来的esp,至此esp和ebp都回到了原来的main的栈帧结构中了。
请看最后一张图,简化后的图


阅读更多
换一批

没有更多推荐了,返回首页