栈帧详解

栈帧作用

栈帧在程序中负责保存程序的局部变量、参数与函数最终执行完后的返回地址。我们在写C语言代码时经常会接触到的一个概念“局部变量”就是由栈帧来负责保存维护的。

从数据结构上来说,是一种先进后出的数据接管,可以把他想象成一个水桶,要存入的数据就是一个个的盘子,现在将盘子按顺序一个一个放入桶中,当要用到盘子要取出盘子的时候,最后一个放入的盘子肯定第一个被取出,第一个放入的盘子肯定最后一个被取出。

栈帧相关的寄存器与汇编指令

esp:始终指向栈顶,随时发生变化
ebp:负责保存栈的基地址
eax:常用于保存函数的返回值

push:入栈操作
***pop***出栈操作

举例说明

C语言代码

#include<stdio.h>

int fun(int u,int p)
{
	int y;
	int x;
	y= u;
	x = p;
	return x+y;
}
int main(int argv, char*argc[])
{
	fun(1,2);
	return 0;
}

fun函数反编译结果

int fun(int u,int p)
{
0093F010  push        ebp  
0093F011  mov         ebp,esp  
0093F013  sub         esp,0D8h  
0093F019  push        ebx  
0093F01A  push        esi  
0093F01B  push        edi  
0093F01C  lea         edi,[ebp-0D8h]  
0093F022  mov         ecx,36h  
0093F027  mov         eax,0CCCCCCCCh  
0093F02C  rep stos    dword ptr es:[edi]  
0093F02E  mov         ecx,offset _BD926964_源@cpp (09A2003h)  
0093F033  call        @__CheckForDebuggerJustMyCode@4 (093ADB0h)  
	int y;
	int x;
	y= u;
0093F038  mov         eax,dword ptr [u]  
0093F03B  mov         dword ptr [y],eax  
	x = p;
0093F03E  mov         eax,dword ptr [p]  
0093F041  mov         dword ptr [x],eax  
	return x+y;
0093F044  mov         eax,dword ptr [x]  
0093F047  add         eax,dword ptr [y]  
}
0093F04A  pop         edi  
0093F04B  pop         esi  
0093F04C  pop         ebx  
0093F04D  add         esp,0D8h  
0093F053  cmp         ebp,esp  
0093F055  call        __RTC_CheckEsp (093AE5Ah)  
0093F05A  mov         esp,ebp  
0093F05C  pop         ebp  
0093F05D  ret  

main函数反编译代码

int main(int argv, char*argc[])
{
0093F080  push        ebp  
0093F081  mov         ebp,esp  
0093F083  sub         esp,0C0h  
0093F089  push        ebx  
0093F08A  push        esi  
0093F08B  push        edi  
0093F08C  lea         edi,[ebp-0C0h]  
0093F092  mov         ecx,30h  
0093F097  mov         eax,0CCCCCCCCh  
0093F09C  rep stos    dword ptr es:[edi]  
0093F09E  mov         ecx,offset _BD926964_源@cpp (09A2003h)  
0093F0A3  call        @__CheckForDebuggerJustMyCode@4 (093ADB0h)  
	fun(1,2);
0093F0A8  push        2  
0093F0AA  push        1  
0093F0AC  call        fun (093C007h)  
0093F0B1  add         esp,8  
	return 0;
0093F0B4  xor         eax,eax  
}
0093F0B6  pop         edi  
0093F0B7  pop         esi  
0093F0B8  pop         ebx  
0093F0B9  add         esp,0C0h  
0093F0BF  cmp         ebp,esp  
0093F0C1  call        __RTC_CheckEsp (093AE5Ah)  
0093F0C6  mov         esp,ebp  
0093F0C8  pop         ebp  
0093F0C9  ret  

现在先来逐句分析fun函数的反编译代码
首先

0093F010  push        ebp 

这一句将ebp入栈,而ebp中在这之前一直存储的是main函数的栈基地址,相当于是把原来的main函数的栈基地址入栈

0093F011  mov         ebp,esp 

esp始终指向栈顶,在这里将esp的值存入ebp,相当于ebp现在指向的基地址不再是main函数的栈基地址,而变成了一个新的栈基地址,在这里也就是fun函数的栈基地址

0093F013  sub         esp,0D8h

这一句,由于汇编中栈是由高到低来存储数据的,像这样
在这里插入图片描述在这里就相当于让栈顶指针减去0x0D8表示在我这个图上就是向上拓展0x0D8个单元,为什么要向上拓展,当然是为fun函数中的局部变量开辟内存空间,我在这里只声明了两个变量怎么说也应该是0x08为什么会变成0x0D8,我个人认为这与编译器有关,我这的编译器会自动分配一块足够大倍数的内存空间用于存放局部变量

	int y;
	int x;
	
y= u;
0093F038  mov         eax,dword ptr [u]  
0093F03B  mov         dword ptr [y],eax  
x = p;
0093F03E  mov         eax,dword ptr [p]  
0093F041  mov         dword ptr [x],eax  

在这里
[u] 应该是[ebp+8],指向参数u
[p]应该是[ebp+12],指向参数p
(因为参数入栈顺序是从右至左,所以参数p先入栈,所以他要加的值也要大于参数u
[x]应该是[esp],指向局部变量x
[y]应该是[esp+4],指向局部变量y
至于为什么会变成这样,当然要把锅甩给vs的反编译器了
为什么要ebp+8才能指向参数u
因为参数u和参数p是在main函数内入栈的,也就是这两句

	fun(1,2);
0093F0A8  push        2  
0093F0AA  push        1  

而在两步入栈操作后调用了call指令来调用子程序,而call指令在依据标号跳转的时候(而"call far ptr 标号"还会将cs寄存器也入栈)会将eip寄存器入栈(+4),然后在jmp进fun子程序中而fun子程序中又会将ebp入栈(+4)所以一共加起来是8,二参数p比u早入栈所以又在参数u之上所以想要访问参数p还要在参数u的基础上再加4也就是12。
为什么esp+4才能指向变量y?
因为变量x后声明后入栈,所以esp当前指向变量x,而变量y在x之上,所以要+4才能访问到。

	return x+y;
0093F044  mov         eax,dword ptr 

通常情况下,会把函数返回值放入eax寄存器。

0093F05A  mov         esp,ebp  
0093F05C  pop         ebp  
0093F05D  ret  

由于ebp寄存器从一开始的 mov ebp,esp后值就再没变过,一直指向main函数调用fun函数之后的栈底的位置所以执行完mov esp,ebp后的状态就像这样
在这里插入图片描述esp栈顶有回到了一开始的地方,也就是main函数栈调用fun函数后最后指向的地方,如此一来fun函数的局部变量就被全部释放掉了

pop ebp

z这一句再将原来main函数栈的基地址取出还给ebp寄存器,ret退出子程序重新再回到main函数,一个栈帧的调用过程结束。

猜你喜欢

转载自blog.csdn.net/weixin_43815930/article/details/104823536