栈在计算机语言中扮演了非常重要的角色,在高级语言中,我们所有的本地变量以及函数的调用都是基于栈上分配数据以及栈的调用(而全局变量和静态变量则是在堆上分配),在汇编中的过程以及函数的调用,那么,什么是栈呢?栈其实就是一个容器,是一段数据空间,这段数据空间可以连续(在汇编中通常如此),也可以是非连续(在C/C++中可以通过创建自己的数据结构和动态分配自己的内存来实现栈),它遵循FILO 的规则,即先进栈数据的顺序和出栈顺序相反。栈的这种特质让它能胜任很多工作,比如CPU对Thread Context的现场保护也使用栈来实现等等。
好多汇编的书籍对栈讲的非常清楚,但是,很多人却对栈的认识很模糊,时常在高级语言中碰到一些函数调用,却不清楚这时栈到底是的怎样工作,我们的这篇短文旨在将汇编中栈和高级语言中的函数调用联系在一起,来探究栈工作的方式。
研究高级语言中函数调用是探究栈工作方式的一种很好的方法,而使用递归函数作为研究对象更能让我们彻底地理解栈。下面,我们写一段程序来计算阶乘n ! 的结果(不必非要使用递归算法,但是为了方便我们研究,这里就写成递归算法):
int FactFun (int n)
{
if (n = =1){ return n;}
else { return n * FactFun(n -1);}
}
前面,我们曾提到过,函数的调用是基于栈的调用,那么,它是怎样被具体实现的呢?下面,我们将这段程序翻译为汇编语言,你就明白是怎么回事了:
;----------------------------------------------
; **** The body is factorial function **
;----------------------------------------------
FactFun PROC NEAR32
;procedure integer Factfun(integer)
;prarameters are passed in double words on the stack
push ebp
mov ebp,esp
push edx
cmp Dword Ptr[ebp+8],1
jne elseMore
jmp endIfOne
elseMore:
mov eax,Dword Ptr[ebp+8]
dec eax
push eax
call FactFun
mul Dword Ptr[ebp+8]
add esp,4
endIfOne:
pop edx
pop ebp
ret
FactFun ENDP
;-------------------------------------------------
注意,这个函数有一个传入参数和一个返回值,通常情况下,我们将返回值放入Eax 寄存器,在调用函数之前,通常要将参数压栈,代码片段如下:
。。。。。。
push eax
call FactFun
add esp,4
。。。。。。
首先,将传入的参数n压入栈,然后再将下一条指令的地址作为函数调用返回地址压入栈,然后进 入函数体
图 1 为我们第一次调用后栈中的数据图,在函数体的调用中,栈通常要保护一些数据,一些函数体中用到寄存器的内容,这样能有效防止在调用过程中跟改了数据而导致的不可预测的错误。在我们的例子中,每一个数据都占据 double words 的空间(当然也可以占据 2 个 bytes的空间),在函数体中,我们有一个判断语句:
cmp Dword Ptr[ebp +8],1
jne elseMore
这两个语句的意思是,当传入的参数大于1时,我们要跳转到elseMore,这时,我们又开始第二次递归调用,这时,栈的数据图如图2 所示。递归程序一直压栈,直到我们传入的参数parameter 等于1 ,这个时候,调用就开始退栈,每一次退栈就会执行函数体中Call FactFun 之后的语句即:
。。。。。。
call FactFun
mul Dword Ptr[ebp +8]
add esp,4
endIfOne:
pop edx
pop ebp
ret
执行上述语句进行计算,每一次ret 之后,计算结果都存在eax 中,这样,只要将传入的参数(在Dword Ptr[ebp+8] 中) m,和返回值相乘就得到 m* FactFun(m -1) 的结果,这样层层退栈,直到参数为我们最先传入的参数值 n ,就可以得到计算结果,放入eax 中,返回值可以直接中这个寄存器中取得。我们也可以借助下图来描述我们来帮助我们建立一个递归函数过程
从上面的分析中,我们可以知道,当栈空间一定的时候,过多的数据压入(譬如,本例中n 的值过大)有可能导致栈空间不够用,即这就是我们常说的栈溢出,windows 2000中系统给应.用程序分配的栈空间大小通常为1M,也就说,超过这个范围就会产生Stack Overflow。这从另外一方面给了我们一些警示,对于栈开销太大的计算应该通常考虑其他的计算方法代替,一是为了减小栈开销,二是为了效率(譬如递归,效率通常要比迭代底)。