在VC6.0下,探索栈帧的那些事
首先我们需要了解一下栈结构:
栈-->入栈(push)和出栈(pop)都是从一端进行的,也就是所谓的“先进后出”(或“后进先出”)
对于计算机来说最重要的就是CPU了:
它主要的工作,就是:读取指令-->分析指令-->执行指令
对此我们再了解一下CPU中常用的寄存器:
EAX、EBX、ECX、EDX --> 通用寄存器
EIP(PC)--> 程序计数器(存放当前正在执行指令的下一条指令的地址)
ESP --> 栈顶
EBP --> 栈底
注意:esp和ebp之间就是一个函数的栈帧。这个栈帧的大小,是通过对函数内定义的临时变量所需空间的大小而开辟出来的。因此,函数内定义的任何临时变量,都位于栈中(说具体点就是位于该函数的栈帧中)。
我们再看一个最重要的:内存的划分(主要了解栈帧,因此我将它放大了)
我们知道内存是局部划分的,各个区域我也已经标注好了,需要注意的是堆区和栈区是相对扩充的!!!
我画的内存是高地址在上,低地址在下
接下来对栈帧的研究我是在VC6.0中实现的(在不同的平台下,原理是一样的只是偏移量会不同)
#include<stdio.h>
#include<windows.h>
int add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a = 0xAAAAAAAA;
int b = 0xBBBBBBBB;
int ret = add(a, b);
printf("%d\n", ret);
system("pause");
return 0;
}
以上代码运行后进行调试,转成汇编语言,打开寄存器,打开内存
12: int a = 0xAAAAAAAA;
00401078 mov dword ptr [ebp-4],0AAAAAAAAh
13: int b = 0xBBBBBBBB;
0040107F mov dword ptr [ebp-8],0BBBBBBBBh
此时寄存器和内存中是这样的
此时栈中是这样的(这里的main_ebp,main_esp就是ebp和esp,只是为了强调这是main函数的栈帧)
继续按F11,程序继续执行,现在分析一下call之前的指令
14: int ret = add(a, b);
00401086 mov eax,dword ptr [ebp-8]
00401089 push eax
0040108A mov ecx,dword ptr [ebp-4]
0040108D push ecx
此时寄存器和内存是这样的
此时栈中是这样的
接下来就是执行call指令
注意:call指令会完成两步 --> 保护call的下一指令的地址
跳转于目标函数的入口处
0040108E call @ILT+0(_add) (00401005)
第一步 --> 保存下一指令的地址
第二步 -->跳转于目标函数的入口处(通过jmp指令修改EIP的值)
00401005 jmp add (00401020)
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,44h
上述call执行结束后,寄存器和内存是这样的
此时栈中是这样的
继续按F11,程序继续执行
6: int z = x + y;
00401038 mov eax,dword ptr [ebp+8]
0040103B add eax,dword ptr [ebp+0Ch]
0040103E mov dword ptr [ebp-4],eax
注意:main函数中a先入栈b再入栈。当调用函数时形成的临时变量,是先取出b的值再取出a的值 --> 临时变量是在调用函数传参之前就形成的 --> 可以看出形参实例化是从右至左进行的
此时寄存器和内存是这样的
此时栈中是这样的(这里的add_ebp,add_esp就是ebp和esp,只是为了告诉你这是add函数的栈帧)
add执行结束就该return返回结果了
函数调用结束之后是需要将该函数的栈帧空间释放的,那么都已经释放了,计算出的z值是怎么得到的呢??
00401041 mov eax,dword ptr [ebp-4]
看上述指令,此时计算的z值是会保存在eax中的,所以后面这个结果是通过该寄存器得到的^_^
继续按F11,程序继续执行
00401047 mov esp,ebp
此时栈中是这样的
继续按F11,程序继续执行
00401049 pop ebp
此时栈中是这样的
接下来就是执行ret指令
注意:ret指令会完成两步 --> 将当时保存的地址(既call指令的下一条指令的地址)出栈
将弹出的数据修改EIP
此时栈中是这样的
继续按F11,程序继续执行
00401093 add esp,8
此时栈中是这样的
继续按F11,程序继续执行
00401096 mov dword ptr [ebp-0Ch],eax
此时寄存器和内存是这样的
此时栈中是这样的
以上过程完成了对一个函数的调用,既实现了为一个函数开辟栈帧到释放栈帧的过程。。。
以上需注意的是,释放栈帧只是将指向它的指针去除了,使这段空间成为了无指向空间(也就是可被再次利用的空间),但里面的值并未删除或修改。因此,当你刚刚释放了栈帧还想用栈里面的数据时,是可以找到的,只不过这段空间可能随时被其他程序利用并修改内部的值
做一个小小的扩展:
原本是main调用add,add执行完后返回main。现在我想让main调用add,add执行结束后返回bug函数,最终通过bug返回到main --> 也就是说我现在想指定函数返回的位置
我就把我的add函数改成了这个样子
int add(int x, int y)
{
int *p = &x;
int z = 0;
p--;
g_ret = (void *)*p;
*p = (int)bug;
z = x + y;
printf("add begin run...\n");
return z;
}
也就是说p此时应该指向的是add本应该返回的地址(g_ret = (void *)*p;这个先不看,后面了解)
接下来把bug函数的入口地址赋值给*p又是什么呢??
其实就是让add返回的时候返回到bug函数中,这不就实现了我们的目的了嘛。。。
再看看我的bug函数
void bug()
{
int x = 0;
int *p = &x;
p += 2;
*p = (int)g_ret;
//*((int *)(&x + 2)) = g_ret;
printf("i am bug, i catch you!!\n");
system("pause");
}
add返回到了bug现在是不是应该让bug返回到main呢。
分析一下:add函数中,我们是通过临时变量x确定返回地址的,现在bug函数中没有临时变量怎么办呢?
(参照上面add函数中,我定义变量z的栈帧图)其实之前我们了解到:在add中,z地址的上一个地址放的是main_ebp,这个地址的上一个地址放的就是add应该返回的地址了
也就是说我们在bug函数中定义一个变量x,通过将它的地址上移两个就能找到应该存放返回地址的地方了。这个时候我们只需要将应该返回到main的地址放在这个地方就可以了
那么,应该返回到main的地址又在哪里呢??
void *g_ret = NULL;
我们看一下在add函数中这行没有分析的代码:g_ret
= (void *)*p;此时我们就需要定义一个全局变量g_ret
此时只需要将该地址放入bug的返回地址位置就可以了,bug中注释掉的部分就是对上述的简写,我定义p分布写是为了便于我理解过程。。。
最后研究一下我的main函数(重点了哈)
注意一点:起初我调用add函数再进行返回,是完成了call指令和ret指令的。现在我的bug函数是没有经过调用直接返回到main的,也就是说,bug只进行了ret指令,并没有进行call指令。。。
上面我们已经看到了call指令和ret指令所干的事情,call中有一件事是push下一指令的地址,ret中有一件事是pop保存的call指令的地址。现在我们只进行了ret,也就是pop了却没有push,这就会导致我们的esp变大了一个地址的大小,会造成栈出现问题程序崩塌(因为没有经过压栈直接出栈了,这不就多出栈了一步么)
这个问题我就通过我的main解决了
int main()
{
int a = 0xAAAAAAAA;
int b = 0xBBBBBBBB;
int ret = 0;
printf("main begin run...\n");
ret = add(a, b);
printf("you should run here!!\n");
__asm
{
sub esp,4
}
system("pause");
return 0;
}
我在main中写入了这样的代码(__asm是在高级语言中插入汇编语言)
__asm
{
sub esp,4
}
我们刚刚不是说我们的esp多pop了一下,那我现在给esp - 4这不就恢复了esp应该有的值了嘛^-^
这样就算彻底的实现了main --> add -->bug --> main -->程序结束。。。
程序运行起来后就是这样的
main begin run... //执行main函数
add begin run... //执行add函数
i am bug, i catch you!! //回到bug函数
请按任意键继续. . .
you should run here!! //回到main函数
请按任意键继续. . .
上一篇: iOS - 性能优化
下一篇: 浅谈iOS isa指针