堆栈调用之初步探索
jefby.
Xidian University.
堆栈是一种数据结构,相信很多人都写过简单的栈操作(push,pop),但是对于其在函数调用方面的应用却很少有人真正了解,也就是说如何构建函数堆栈,如何进行函数调用及返回,函数执行时期堆栈结构到底是怎样的??相信学过汇编语言的人多少会有些了解,但是我相信只是大部分人的认识都只是停留在表面,那么请跟我一起来深入研究下程序执行时的堆栈情况,相信真正理解了这个原理之后,对你以后的代码编写,编程内功的提升都有着巨大的帮助作用。
下面从最通用的Intel体系结构入手,开始探索下堆栈细节。图一所示是程序栈情况。
图 1
当执行函数调用时,为了便于演示,定义函数void fun(int x,int y);
假设在main函数中调用此语句,例子程序如下:
0x3FFFFFFF fun(1,2);//语句1
0x40000003 a = 1;//语句2
函数fun定义如下(为了展示函数调用的方法以及堆栈的结构)
void fun(intx,int y)//仅为了展示,代码无实际意义
{
Int a,b;//语句3
a = add(x,y);//语句4
b = x – y;//语句5
}
函数add定义如下:
int add(intx,int y)//仅为了展示,代码无实际意义
{
return x + y;//语句6
}
图 2
图2所示为函数执行到语句6时的堆栈结构,当第一次调用函数fun时,首先将参数从右向左入栈(在VC、VS2008中)默认的调用方式cdcel,然后执行call fun,此时,首先将返回地址0x40000003压入堆栈,并将fun函数的地址赋给EIP,转而执行fun函数,首先函数fun被译为一般源码为:
push EBP
mov EBP,ESP
即首先保存旧的EBP值,然后将EBP指向当前ESP所指向的位置,如图2所示,然后ESP-8,给局部变量a,b分配存储空间,然后又开始调用函数add,此时,首先从右向左将参数压入堆栈,然后开始执行call add,将add的地址赋值给EIP,同时将返回地址压入堆栈,从而执行add,同fun函数类似,保存旧的EBP,同时将EBP指向当前ESP所指向的位置,开始执行语句6,此句将a+b的和赋值给EAP,并通过EAP返回,此时将EAP的值赋值给变量a;当执行ret时,首先,执行语句
Push ESP,EBP ;回收分配的栈空间
Pop EBP ; 恢复保存在堆栈中的旧EBP值
同时将返回地址即[ESP]赋值给EIP,继而执行语句5,此时的堆栈如图3所示:
图 3
语句5执行完成之后,fun函数也执行完了,那么进入ret语句,此时与add函数ret语句类似,执行语句
Push ESP,EBP ;回收分配的栈空间
Pop EBP ; 恢复保存在堆栈中的旧EBP值
同时将返回地址0x40000003赋值给EIP,此时堆栈如图4所示,
图 4
即堆栈已经被清空,按照不同的调用标准(_stdcall,_cdcel等),如果是_stdcall的话是被调用者清理堆栈,如果是_cdcel的话是调用者清理堆栈,这里的清理堆栈主要负责清理的是函数调用时传递的参数(实参)。先写到这里吧,太晚了,后面会继续整理一些更加高级的用法,敬请期待。有什么问题可以email我,邮件地址jef199006@gmail.com。