由栈帧结构引出的一个小bug

        上一篇已经谈过在C语言的函数调用过程中函数栈帧结构的变化,对于栈帧结构变化不清楚的可以去找上一篇文章(链接地址:http://blog.csdn.net/chenkaixin_1024/article/details/53286425)

        这里我们来研究一个有趣的东西,也是函数栈帧结构的一方面应用。

首先我们来看一张图:


        这张图是main函数调用fun函数的栈帧图,从这张图中我们可以发现临时变量a,它的位置很特殊,由于在创建临时变量时是与形参列表中的形参顺序恰好相反的,第一个参数最后一个入栈,最后一个参数第一个入栈,所以第一个参数的在实例化后,所在的地址是所有参数中最低的,而且它们是连续的,所以不妨大胆的设想一下,通过对第一个参数的地址进行一些操作,那么就可以很容易的访问其他参数,甚至可以对它们进行一些改变。

看下面代码:

#include <stdio.h>
#include <windows.h>
int fun(int x,int y)
{
	int c=0xcccccccc;
	int* p=&x;
	p++;
	*p=0xdddddddd;
	printf("%x\n",y);
	return c;

}
int main()
{
	int a=0xaaaaaaaa;
	int b=0xbbbbbbbb;
	int ret=fun(a,b);
	system("pause");
	return 0;
}
程序运行结果如下:

    很明显,这个程序通过使用指针p对原本第二个参数进行了修改,这也就很好的证明了我们之前的设想。

    回过头再看上面的栈帧图,关于第一个临时变量a,我们还注意到一点,那就是:从临时变量a往下,紧挨着的便是我们pc也就是当前语句的下一条语句地址的保存位置,那同样的是不是也能通过函数形参列表中的第一个参数来修改pc呢?

看如下代码:

#include <stdio.h>
#include <windows.h>
void bug()
{
	printf("haha, I am a little bug!!\n");
	Sleep(2000);
}
int fun(int x,int y)
{
	int c=0xcccccccc;
	int* p=&x;
	printf("I'm fun!\n");
	Sleep(2000);
	p--;
	*p=(int)bug;
	return c;

}
int main()
{
	int a=0xaaaaaaaa;
	int b=0xbbbbbbbb;
	int ret=fun(a,b);
	printf("you should run here!\n");
	system("pause");
	return 0;
}
运行结果如下:

        程序崩溃了,但是仔细观察,我们发现这个程序运行了“I'm fun!”语句,也运行了“haha, I am a little bug!!”,唯独没有运行“you should run here!”这条语句。所以我们可以发现,通过函数形参列表中的第一个参数来修改pc这个方法是可行的,但是程序为什么会崩溃呢?分析运行结果,我们发现这个程序确实是进入bug()函数,而且还停顿了2秒才崩溃,说明真正使得程序崩溃的在bug()函数之外。

        那么回过头再看一下栈帧图,分析一下在函数调用过程中栈帧结构的变化。

在bug函数返回之后,势必要恢复调用bug函数的函数的栈帧结构,但是问题来了,bug函数是被谁调用的呢?

        有人说是fun函数,你看不是有*p=(int)bug;这条语句吗,这里我们要分析一下,首先函数调用根本不是这么调用的好不好!那这条语句又是用来干什么的呢?前面说过了我们这个程序是通过修改pc来让在fun函数返回到main函数之后,不执行原定的下一条语句,而是转去其他地方,所以这里仅仅是对pc的修改。

       那么我们发现这个程序好像并没有调用bug函数,那为什么又会执行bug函数中的语句呢?

       这里我们就要好好的研究一下在函数返回的时候,栈帧结构究竟发生了什么变化(以上面栈帧图中的main函数与fun函数为例)

fun在执行完所有语句后返回,那么就要恢复原本main函数的栈帧结构,让esp指向ebp的位置,然后将之前保存的ebp出栈,并赋给ebp,这样就形成了main函数的栈帧结构,然后下一步很关键,esp指向了pc的位置,要接下去执行语句,必定要获得下一条语句的地址,并让pc 指向它,所以这里将保存的pc出栈,赋给pc指针,进入下一条语句。

       然后我们类比到我们这个程序,我们这里的所保存的pc已经被偷偷修改掉了,被改成了bug函数语句的地址,所以在pc读取它时候,就自动跳转到bug函数去了。这就是程序中没有调用bug函数,却执行了bug函数的语句的原因。

       那么程序又为什么会崩溃呢?这个问题还没有解释。

       这里我们要说的一点就是bug函数也是函数,只要是函数就要形成栈帧结构,而且这个栈帧结构是紧紧挨着main函数的,这里可能有人就要问了,为什么是紧挨着main函数的,main函数又没有调用bug函数,这里我们就要注意在fun函数返回形成main函数栈帧结构的同时,他自己的栈帧结构就不存在了,而原本fun函数中所设置的所有东西都被设为无效了,是可以被覆盖的,回过头来,那么当它返回的时候,也必定要遵循上面的规则,但是我们分析一下,在bug函数返回的时候,要将保存的ebp出栈没问题(形成栈帧结构之前都会保存之前的ebp,这是在被调用函数的汇编语句中明摆着放那的),但是要将保存的pc出栈,这里就很莫名了,bug函数并非通过调用进去的,所以并没有保存pc,但是这里却出栈了,很明显这里把不是pc(也就是当前语句的下一条语句的地址)的东西出栈了,并当成pc读取了,那程序自然就崩溃了。

      那么我们可不可以把这程序修改一下,让这个程序变的正常一些,不要崩溃。

要让程序正常运行,关键是要在bug函数返回时给它一个正确的pc地址,所以代码如下:

#include <stdio.h>
#include <windows.h>
void* main_ret=NULL;
void bug()
{
     int flag = 0;
     int*p = &flag;
     p += 3;
     printf("haha, I am a little bug!!\n");
     Sleep(2000);
     *p = main_ret;
     printf("begin return to main from bug!\n"); 
     Sleep(2000);
}
int fun(int x,int y)
{
	int c=0xcccccccc;
	int* p=&x;
	printf("I'm fun!\n");
	Sleep(2000);
	p--;
	main_ret=*p;
	*p=(int)bug;
	return c;
}
int main()
{
	int a=0xaaaaaaaa;
	int b=0xbbbbbbbb;
	int ret=fun(a,b);
	_asm{
		sub esp, 4
	}
	printf("you should run here!\n");
	system("pause");
	return 0;
}
运行结果如下:

程序正常运行,分析这个代码:

        这里我们通过指针p修改了pc之前,先把原本的pc保存在一个void*类型名字为main_ret的全局指针变量当中,而在bug函数中,我们又将这个pc写入,让bug函数在返回时能够出栈正确的pc(这里很明显是让他返回到main函数的下一条语句),这样程序就能正常读取要执行的语句,但是这个程序需要注意两点:

1.bug函数中为什么要定义flag,并让指针p指向它,并在之后还要指向+3的位置

这里是因为bug函数参数列表为空,并没有所谓的第一个参数,所以就不能以第一个参数为标准找到被保存的pc的位置应该在的位置,但是我们可以通过测试,得出在函数中定义的第一个变量的地址与原本应该是被保存的pc的位置之间的差值,从而与第一个参数的修改方法类比。而测试的方法就是求出bug函数中定义的第一个变量的地址,然后求出fun函数中p - -之后的地址(这个就是pc应该被保存在的地址)【这里的原因是因为bug函数与fun函数的栈帧结构都是紧紧挨着main函数的】。这里必须要测试,因为编译器不同,求出来的差值也会不同。

2.

	_asm{
		sub esp, 4
	}
这条语句是用来干什么的?

bug函数要将修改后的pc出栈,但是我们要考虑一点,这个程序在形成bug函数的栈帧结构的时候,并没有保存pc将它入栈,而我们也仅仅是将原本应该是被保存的pc的位置修改成可用的pc,也就是说这里原本没有pc的,但你硬是要把它修改成pc并把它出栈,你可以修改成功也可以出栈成功,但是你这里把原本不该出栈的东西出栈了,是不是破坏了栈帧结构原本应有的平衡,对应的解决办法就是在这之后,再给它入栈一次,也就相当于将pc向下偏移,而之所以偏移4,是因为被保存的pc是地址,地址是4个字节。而上面的语句的意思就是加入一条汇编sub esp,4

        到这里,这个小bug是不是很有趣呢?在没有进行函数调用的时候,硬给程序塞进去了一个函数,并且成功执行了,这个bug可不是个小bug。而通过这个例子,是不是对栈帧结构有了更深的理解呢?


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值