在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-- 应该指向的是什么地方呢?(通过上面对栈帧的了解,存放临时变量x的位置的下一个位置(p--),应该存放的是call指令的下一条指令的地址)

                            也就是说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

                           我们在add中已经将应该返回到main的地址保存在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函数
请按任意键继续. . .

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值