深入理解函数的调用过程--栈帧的创建和销毁

首先我们来了解一下这两个概念:

函数的调用过程:每一次函数调用都是一个过程,这个过程我们通常称之为函数的调用过程

函数栈帧:函数调用过程要为函数开辟栈空间,用于本次函数的调用中临时变量的保存,现场保护。这块栈空间,我们称之为函数栈帧

在研究函数的栈帧之前,我们先得知道了解一下这么几个寄存器:

EAX:是“累加器”,它是很多加法,乘法指令的缺省寄存器

EBX:是“基地址寄存器”,在内存寻址时存放基地址

ECX: 是“计数器”,是重复REP前缀指令和LOOP指令的内定计数器

EDX:总是用来存放整数除法产生的余数

ESI/EDI: 分别叫做“源/目标索引寄存器”,因为在很多字符串操作指令中,DS:ESI 指向源串,而ES:EDI 指向目标串

EBP:存放了指向函数栈帧栈底的地址

ESP:存放了指向函数栈帧栈顶的地址

EIP:程序计数器,用来存放正在执行指令的下一条指令的地址

下来我们来看一段简单的代码:

#include 

int add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}

int main(void)
{
	int a = 10;
	int b = 20;
	int ret = add(a, b);
	printf("ret = %d\n", ret);
	return 0;
}

我在VC++ 6.0下编写这段代码,按F10进入调试模式,在菜单栏的view 选项中找到Debug Windows,打开Registers,Memory,Call Stack.接下来看下面这张巨大无比的栈帧图,,,,,,,,,,,,,,,,,,,,,

深入理解函数的调用过程--栈帧的创建和销毁_第1张图片

看了这个巨大无比的栈帧图之后,相信你肯定基本掌握栈帧了,那么下来来看一个有意思的题:

#include 

void fun(void)
{
	int tmp = 10;
	int *p = (int *)(*(&tmp + 1));
	*(p - 1) = 20;
}

int main(void)
{
	int a = 0;
	fun();
	printf("a = %d\n", a);
	return 0;
}

这个程序我简单画个图,就能知道结果是啥了

深入理解函数的调用过程--栈帧的创建和销毁_第2张图片

那么利用栈帧还能做什么好玩的事情呢?

我们知道函数内部的局部变量是在该函数的栈帧内部保存的,而且是紧挨着的,那么我们可以先用指针指向一个变量,然后通过指针运算来修改其他变量的数据,下面来看一段简单的代码:

#include 

int main(void)
{
	int a = 10;
	int b = 20;
	int *p = &a;
	printf("b = %d\n", b);
	p--;
	*p = 15;
	printf("b = %d\n", b);
	return 0;
}

两次输出b的值分别是20,15这是为什么呢?请看下面这个图:

深入理解函数的调用过程--栈帧的创建和销毁_第3张图片

我们知道在执行call指令的时候要先把当前这条指令的下一条指令的地址入栈保存,作用是恢复现场,函数调用完毕后可以通过这个地址找到下一条要执行的指令。我们还知道函数名其实就是函数的入口地址,那么我们另写一个函数,我们现在用这个函数的入口地址修改下一条指令的地址,那么函数在返回的时候就不能正常返回,而是找到我们写的函数的入口地址,去执行我们刚才写的函数,接下来上代码看的更清楚一些:

首先我们来看一个正常的函数调用:

#include 
#include 

int add(int a, int b)
{
	int z = a + b;
	printf("我是add函数!\n");
	return z;
}

int main(void)
{
	int a = 10;
	int b = 20;
	int ret = 0;
	printf("我先被输出!\n");
	ret = add(a, b);
	printf("ret = %d\n", ret);
	return 0;
}

那么输出结果是下图,没什么问题

那么,我把代码稍加修改:

#include 
#include 

void *save = NULL;   //用来保存call指令调用add函数的下一条指令的地址

void bug(void)
{
	printf("我是bug函数,我不是用call 指令调用的!\n");
	system("pause");
}

int add(int a, int b)
{
	int z = a + b;
	int *p = &a;      //首先,p指向形参a
	p--;  // 注意 p--,那么此时p指向call指令调用add函数的下一条指令的地址这块内存空间
        save = (void *)(*p);  //这一句就是把call指令调用add函数的下一条指令的地址保存到save中
        *p = (int)bug;  
      //*p = (int)bug; bug函数名即bug函数的入口地址,也就是现在把原先保存的下一条指令的地址改成了bug函数的入口地址,
      //那么add函数在ret时,修改eip为bug函数的入口地址,就会跳转到bug函数中,虽然并没有调用bug函数,
      //很显然,bug函数并不是通过call指令调用的
	printf("我是add函数!\n");
	return z;
}

int main(void)
{
	int a = 10;
	int b = 20;
	int ret = 0;
	printf("我先被输出!\n");
	ret = add(a, b);
	printf("ret = %d\n", ret);
	return 0;
}

结果是什么样的呢?


发现和刚才不同的是,现在进入到bug函数中了,我在bug函数中谢了一句system("pause");,那么接下来我按一下回车

深入理解函数的调用过程--栈帧的创建和销毁_第4张图片

bug函数执行完毕,再按回车,发现回不到main中去了,这是肯定的,我们刚才把下一条指令的地址处修改为bug函数的入口地址,那么bug函数执行完肯定是回不到main中去的,那么现在我们想回到main函数中去怎么办呢?我们知道函数内部的局部变量是在该函数的栈帧内部形成的,先定义的变量首先被压栈保存,那么我们可以在bug函数中定义一个局部变量,用一个指针指向这个变量,那么通过指针运算把我们刚才保存的下一条指令的地址写入到栈帧中,那么在bug函数执行ret指令的时候就可以用这个地址来修改eip从而达到回到main函数的目的,那么我再把代码稍加修改:

#include 
#include 

void *save = NULL;

void bug(void)
{
	int tmp = 0;  //定义一个局部变量tmp
	int *q = &tmp;  //让q指针指向tmp
	q += 2;  //q+=2,  这里很关键q+=2后,q指向main函数的ebp栈底指针上面,这个地方正是原来的形参a,一会附上图更清楚一些
	*q = (int)save;  //修改这块内存空间为我们保存的下一条指令的地址
	printf("我是bug函数,我不是用call 指令调用的!\n");
	system("pause");
}

int add(int a, int b)
{
	int z = a + b;
	int *p = &a;
	p--;
	save = (void *)(*p);
	*p = (int)bug;
	printf("我是add函数!\n");
	return z;
}

int main(void)
{
	int a = 10;
	int b = 20;
	int ret = 0;
	printf("我先被输出!\n");
	ret = add(a, b);
	printf("ret = %d\n", ret);
	return 0;
}

那么经过这么修改过后,又是如何呢?

深入理解函数的调用过程--栈帧的创建和销毁_第5张图片

我们看到此时输出了ret = 0,说明的确回到了main继续执行下面的代码,但是程序发生了错误,这是怎么回事呢?

简单地说,我们虽然可以通过这种方式进入到bug函数中,但是我们不是通过call指令进去的,而是通过修改call指令调用add函数的下一条指令的地址为bug函数的入口地址,当add函数执行ret指令的时候修改eip来达到目的的,那么刚才p+=2,正是修改了原来形参a的位置,bug函数执行完ret后,把call指令调用add函数下一条指令的地址来修改eip,那么程序顺利回到call指令调用add函数的下一条指令处,但是要注意现在,刚才bug函数执行完ret后,esp指针上移了4,因为要pop掉下一条指令的地址,而main函数以为我的形参a,形参b还没有释放掉,通过esp+8,来释放形参a和形参b,但是现在其实形参a的地方已经经过修改被释放掉了,所以现在栈帧不平衡,我们需要在main函数代码加一句

int main(void)
{
	int a = 10;
	int b = 20;
	int ret = 0;
	printf("我先被输出!\n");
	ret = add(a, b);
	__asm
	{
		sub esp,4
	}
	printf("ret = %d\n", ret);
	return 0;
}

那么现在esp+8-4就相当于 esp+4 ,本来要释放形参a和形参b,现在只需要释放形参b那么达到我们的目的,来看运行结果:

深入理解函数的调用过程--栈帧的创建和销毁_第6张图片

再按回车,程序正常退出,至此完成通过栈帧插入一次函数调用。下面附上一张图说明一下:

深入理解函数的调用过程--栈帧的创建和销毁_第7张图片

你可能感兴趣的:(深入理解函数的调用过程--栈帧的创建和销毁)