1、所谓的代码段、(堆栈段)栈段、数据段,不过是我们(程序员)对内存的一种逻辑划分,在计算机看来,段及段中的数据不过是存储的内存中的数据。如果要维护这种逻辑划分,我们就要说明哪些内存单元作是栈段,哪些内存单元作为代码段,等等。比如,代码段,我们要告诉CPU内存中的某个范围里的数据,存放的都是代码,那么我们需要把该数据在内存单元的物理地址告诉CPU,方法是设置CS:IP,CPU便可以根据CS和IP两个寄存器里的内容计算出执行代码的物理地址,然后根据该物理地址找到代码数据在内存中的具体存放位置。
再比如栈段,假设有一段内存,如果想使得我们对该内存的读写数据操作的体验就像在使用一个栈数据结构一样,就需要把该段内存的地址告知ss:sp,这样,我们就可以用入栈出栈操作来访问这段内存了。注意,我们改变的仅仅是读写内存的行为(通过入栈push和出栈pop),本质上还是在从内存里读写数据。
2、pop操作会导致sp+=2,push操作会导致sp+=2。
3、因为16位寄存器的限制,我们知道一个栈的最大容量是64kb(也就是SP的变化范围),通常的时候我们不需要这么大内存作为栈使用,我们可以这么做:
数据段从内存中申请了16个字节的空间,然后将这段空间的段地址(data标识)赋给了ss,栈空的时候sp指向栈栈空间最底端的字单元(ss:sp的操作以字为一个单位)的下一单元——当然这一个字单元不属于本栈空间。栈满的时候——也即栈里有数据了,不论何时只要栈里有数据,sp都会指向栈顶的数据——sp的值为0。那么有了栈之后,我们能否就这样放心地使用它,而不用考虑栈越界的问题了呢?不行,CPU没让我们省心。
栈满时,sp=0,将一个字入栈,sp-=2,然后将字存放在:ss:sp处,这明显是越界操作,但CPU不管,同理,当栈空时,将一个字出栈,它会将ss:sp指向的数据取出(CPU认为目前栈里有合法数据),然后sp+=2,这明显也是越界操作。两种操作都不安全,应该怎么办呢,很遗憾,唯一的办法就是:作为程序员,我们要小心,避免这种操作的发生。
有一种情况比较特殊:
至第10行,我们定义了一个栈,容量是16个字节,初始为空,sp指向栈底字单元下一个字单元,对于个16字节即8个字的栈来说,栈底偏移为000eh,那么一个字单元便是0010h。第12行代码进行了一个非法操作:从空栈里出数据,当然,这种非法仅仅是逻辑上的非法,在程序员看来这应当避免,但CPU不在乎,CPU依然将ss:0010处的数据出栈了,然后sp+=2。现在问题来了,如果我们定义了一个空的栈,它足足有64kb,CPU还样做的话,sp岂不要指出其他段?这时候CPU再也不能无动于衷了,无论如何也不能让sp指的范围超过0000~FFFF,更何况sp作为一个16位的寄存器,也压根不能保存大于FFFF的偏移量。具体来说CPU是这么做的:如果一个栈空间达到64kb,且是空的,sp值为0。从取模的角度来讲,0字单元依然是fffe字单元的下一个字单元。关于sp为何为0,可以这样考虑:
假设该栈的段地址是1000,若这个栈里只有一个数据,则该数据肯定存放在1000:fffe处,现在将这个唯一的字出栈,sp+=2,sp只能装得下16位数据(fffe+2>16位),sp最终结果是fffe+2然后对总容量(64kb)取模得sp=0。第18行代码执行完后,会发现sp=0000,此时若将0018h入栈,sp-=2,在取模的意义下,sp=fffe,然后在1000:fffe处存放0018h。21代码执行后,我们发现sp=fffe。
现在回头来看第一份代码,我们定义了一个16字节的栈,会有越界问题,向上越界(往一个满了栈push数据)和向下越界(从空栈pop数据),无论怎么越界,范围都在64kb内,这是16位sp寄存器所决定的。容量小于64kb的栈越界会读写非本栈的内存,容量为64kb的栈越界会导致栈的数据被覆盖,比如64kb的栈满了,sp指向0处,如果再将一个字入栈,sp=-2使得sp=fffe,从而将以前该位置的数据覆盖,如果是空栈,sp也指向0,pop操作会读出无效数据。程序员应当控制代码不让越界发生,以防逻辑错误。
举例:
mov ax,1000h
mov ss,ax
mov sp,0
通过上面的分析可知,上面三行代码的意思是定义一个栈,状态为空,sp指向栈底字单元下一个单元,sp=0,所以该栈的容量是64kb。