欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

在VC6.0下,探索栈帧的那些事

程序员文章站 2024-03-23 21:15:16
...

首先我们需要了解一下栈结构:

栈-->入栈(push)和出栈(pop)都是从一端进行的,也就是所谓的“先进后出”(或“后进先出”)

对于计算机来说最重要的就是CPU了:

它主要的工作,就是:读取指令-->分析指令-->执行指令

对此我们再了解一下CPU中常用的寄存器:

EAX、EBX、ECX、EDX --> 通用寄存器

EIP(PC)--> 程序计数器(存放当前正在执行指令的下一条指令的地址)

ESP --> 栈顶

EBP --> 栈底

注意:esp和ebp之间就是一个函数的栈帧。这个栈帧的大小,是通过对函数内定义的临时变量所需空间的大小而开辟出来的。因此,函数内定义的任何临时变量,都位于栈中(说具体点就是位于该函数的栈帧中)。

我们再看一个最重要的:内存的划分(主要了解栈帧,因此我将它放大了)

我们知道内存是局部划分的,各个区域我也已经标注好了,需要注意的是堆区和栈区是相对扩充的!!!

我画的内存是高地址在上,低地址在下

在VC6.0下,探索栈帧的那些事


接下来对栈帧的研究我是在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

此时寄存器和内存中是这样的

在VC6.0下,探索栈帧的那些事

此时栈中是这样的(这里的main_ebp,main_esp就是ebp和esp,只是为了强调这是main函数的栈帧)

在VC6.0下,探索栈帧的那些事

继续按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

此时寄存器和内存是这样的

在VC6.0下,探索栈帧的那些事

此时栈中是这样的

在VC6.0下,探索栈帧的那些事

接下来就是执行call指令

注意:call指令会完成两步 --> 保护call的下一指令的地址

                                                      跳转于目标函数的入口处

0040108E   call        @ILT+0(_add) (00401005)

第一步 --> 保存下一指令的地址

在VC6.0下,探索栈帧的那些事

第二步 -->跳转于目标函数的入口处(通过jmp指令修改EIP的值)

00401005   jmp         add (00401020)
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,44h

上述call执行结束后,寄存器和内存是这样的

在VC6.0下,探索栈帧的那些事

此时栈中是这样的

在VC6.0下,探索栈帧的那些事

继续按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的值 --> 临时变量是在调用函数传参之前就形成的 --> 可以看出形参实例化是从右至左进行的

此时寄存器和内存是这样的

在VC6.0下,探索栈帧的那些事

此时栈中是这样的(这里的add_ebp,add_esp就是ebp和esp,只是为了告诉你这是add函数的栈帧

在VC6.0下,探索栈帧的那些事

add执行结束就该return返回结果了

函数调用结束之后是需要将该函数的栈帧空间释放的,那么都已经释放了,计算出的z值是怎么得到的呢??

00401041   mov         eax,dword ptr [ebp-4]

看上述指令,此时计算的z值是会保存在eax中的,所以后面这个结果是通过该寄存器得到的^_^

继续按F11,程序继续执行

00401047   mov         esp,ebp

此时栈中是这样的

在VC6.0下,探索栈帧的那些事

继续按F11,程序继续执行

00401049   pop         ebp

此时栈中是这样的

在VC6.0下,探索栈帧的那些事

接下来就是执行ret指令

注意:ret指令会完成两步 --> 将当时保存的地址(既call指令的下一条指令的地址)出栈

                                                    将弹出的数据修改EIP

此时栈中是这样的

在VC6.0下,探索栈帧的那些事

继续按F11,程序继续执行

00401093   add         esp,8

此时栈中是这样的

在VC6.0下,探索栈帧的那些事

继续按F11,程序继续执行

00401096   mov         dword ptr [ebp-0Ch],eax

此时寄存器和内存是这样的

在VC6.0下,探索栈帧的那些事

此时栈中是这样的

在VC6.0下,探索栈帧的那些事

以上过程完成了对一个函数的调用,既实现了为一个函数开辟栈帧到释放栈帧的过程。。。

以上需注意的是,释放栈帧只是将指向它的指针去除了,使这段空间成为了无指向空间(也就是可被再次利用的空间),但里面的值并未删除或修改。因此,当你刚刚释放了栈帧还想用栈里面的数据时,是可以找到的,只不过这段空间可能随时被其他程序利用并修改内部的值




做一个小小的扩展:

原本是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函数
请按任意键继续. . .