栈帧作用
栈帧在程序中负责保存程序的局部变量、参数与函数最终执行完后的返回地址。我们在写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函数,一个栈帧的调用过程结束。