本文我们单考虑内存中的栈区域,考虑函数的调用、参数的传递和局部变量的使用——这一切在汇编的抽象级别是如果进行的。
(Contents from Assembly Language For x86 Processors Sixth Edition By Kip R.Irvine)
首先一上来作者用了这么一段话非常精炼的总结了stack的几乎所有功能,其实要想完全掌握这个概念,脑中只要把调用子程序时生成stack的动态模型给记住即可。为了便于大家对stack模型的理解,我根据ebp相对固定的特点,将stack模型分为两部分,分别是ebp之下的空间和ebp之上的空间。
1. EBP之下的空间
1.1 Prologue
在一进入子程序的时候,我们首先要做的是设置栈顶的位置,数据入栈出栈有esp掌控位置的变化,但我们也需要一个固定不变的位置来在内存中标记当前stack的具体位置,这项工作就由ebp寄存器来完成。
首先保存ebp的值,然后将ebp指向esp
1.2 Saving and Restoring Registers
Ideally,the registers in question should be pushed on the stack just after setting EBP to ESP,and just before reserving space for local variables.
通过书中的这句话我们可以很清晰的看到将寄存器的值push进栈(保存其值以便返回前恢复)这个步骤具体是在什么时候进行的,而我们之所以要保存一些寄存器的值,是因为在子程序的调用过程中,我们可能会需要借用一些寄存器来处理相应的数据,为了保护寄存器原来的值不被这次调用破坏,我们会选择先将其保存起来。这一行为的专业术语我们称之为保护现场。
1.3 Local Variables
Mysub proc push ebp mov ebp,esp sub esp,8 mov DWORD PTR [ebp-4],10 mov DWORD PTR [ebp-8],20 mov esp,ebp pop ebp ret Mysub endp
在我们想创建局部变量之前,首先要为他们在stack中开辟一段空间,而这个任务就由sub esp,8这条指令来完成,其内存模型如下图:
显而易见,ebp保持位置不变,esp进行相应的移动为局部变量的存储创造空间。那么这些创造出来的空间在子程序调用结束后该怎么处理呢?请见1.4
1.4 Epilogue
在runtime stack中为local variable和一些要保存原来值的registers预留了空间,在子程序调用结束后,这些空间必须要被释放出来,如果不释放,那么当pop ebp时,ebp就不能得到stack为他保留的值了。因此epilogue这一环节的意义可以总结为释放stack中ebp和esp之间的空间,从而使得ret 指令在调用结束后能将程序返回到正确的地址。
2. EBP之上的空间
2.1 Stack Parameters
push 6 push 5 call AddTwoNum
上述语句的执行会产生下面的stack:
在调用子程序(subroutine)的时候,调用者会将实参push进栈,以便稍后的调用过程中使用。注意我们传递参数这一步是在调用(call)之前进行的,比如如下代码,要在调用计算两数和的子程序之前就将我们需要的参数push进栈。调用子程序的call指令就相当于将子程序的返回地址压栈。
2.2 参数空间的释放
我们看这样一个例子,在main函数中调用Example1函数,再在Example1中调用AddTwo函数,然后ret,通过这个例子来说明子程序返回时释放参数空间的重要性
我们先看一下程序的栈模型:
在AddTwo子程序调用结束后,ret指令会返回到当前ESP所指向的地址,也就是之前传递的参数5和6的位置,这就导致了错误的产生——我们没有跳转到return address上去啊!解决这种错误有两种方法,一是在caller中释放该调用的空间,另一个是在子程序中自行完成,二者的实现分别如下:
在caller中释放参数空间:
在子程序中释放空间:
(这种方法也被称为STDCALL Calling Convention)
通常情况下也就是子程序的连续或循环调用可能会出现此种错误,需要注意