栈是一种具有特殊的访问方式的存储空间。它的特殊性就在于,最后进入这个空间的数据,最先出去。
可以用一个盒子和3本书来描述栈的这种操作方式。
一个开口的盒子就可以看成一个栈空间,现在有3本书,《高等数学》、《C语言》、《软件工程》,把它们放到盒子中,操作的过程如图所示。
现在的问题是,一次只允许取一 本, 我们如何将3本书从盒子中取出来?
显然,必须从盒子的最上边取。这样取出的顺序就是:《软件工程》 、《C 语言》、《高等数学》,和放入的顺序相反,如图3.8所示。
从程序化的角度来讲,应该有一个标记,这个标记一.直指示着盒子最上边的书。
如果说,上例中的盒子就是一个栈,我们可以看出,栈有两个基本的操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个 元素。栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。栈的这种操作规则被称为: LIFO(Last In First Out,后进先出)。
CPU提供的栈机制
现今的CPU中都有栈的设计,8086CPU也不例外。8086CPU提供相关的指令来以栈的方式访问内存空间。这意味着,在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
8086CPU提供入栈和出栈指令,最基本的两个是PUSH(入栈)和POP(出栈)。比如,push ax表示将寄存器ax中的数据送入栈中,pop ax表示从栈项取出数据送入ax。8086CPU的入栈和出栈操作都是以字为单位进行的。
下面举例说明,我们可以将10000H~1000FH这段内存当作栈来使用。
mov ax, 0123H
push ax
mov bx, 2266H
push bx
mov cx, 1122H
push cx
pop ax
pop bx
pop сх
字型数据用两个单元存放,高地址单元存放高8位,低地址单元存放低8位。
其一,我们将1000H~1000FH这段内存当作栈来使用,CPU执行push和pop指令时,将对这段空间按照栈的后进先出的规则进行访问。但是,一个重要的问题是,CPU如何知道10000H~1000FH这段空间被当作栈来使用?
其二,push ax等入栈指令执行时,要将寄存器中的内容放入当前栈顶单元的上方,成为新的栈顶元素; pop ax 等指令执行时,要从栈项单元中取出数据,送入寄存器中。显然,push、 pop 在执行的时候,必须知道哪个单元是栈顶单元,如何知道呢?
CPU如何知道当前要执行的指令所在的位置?我们现在知道答案,那就是CS、IP中存放着当前指令的段地址和偏移地址。现在的问题是: CPU如何知道栈顶的位置?显然,也应该有相应的寄存器来存放栈顶的地址,8086CPU 中,有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。任意时刻,SS:SP 指向栈顶元素。push 指令和pop指令执行时,CPU从SS和SP中得到栈顶的地址。
现在,我们可以完整地描述push和pop指令的功能了,例如push ax。
push ax的执行,由以下两步完成。
SP=SP-2, SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
将ax中的内容送入SS:SP指向的内存单元处,SS:SP 此时指向新栈顶。
图3.10描述了8086CPU对push指令的执行过程。
从图中我们可以看出,8086CPU中,入栈时,栈顶从高地址向低地址方向增长。
如果将10000H~ 1000FH这段空间当作栈,初始状态栈是空的,此时,SS=1000H,SP=?
将10000H~1000H这段空间当作栈段,SS=100H,栈空间大小为16字节,栈最底部的字单元地址为1000:000任意时刻,SS:SP 指向栈项,当栈中只有-个元素的时候,SS=1000H, SP=000EH。 栈为空,就相当于栈中唯 一的元素出栈, 出栈后,SP=SP+2,SP原来为000EH, 加2后SP=10H, 所以,当栈为空的时候,SS=1000H,SP=10H。
换一个角度看,任意时刻,SS:SP指向栈项元素,当栈为空的时候,栈中没有元素,也就不存在栈项元素,所以SS:SP只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2,栈最底部字单元的地址为1000:000E,所以栈空时,SP=0010H。
我们描述pop指令的功能,例如pop ax。
popax的执行过程和push ax刚好相反,由以下两步完成。
将SS:SP指向的内存单元处的数据送入ax中;
注意,图3.2中,出栈后,SS:SP指向新的栈顶1000EH,pop操作前的栈顶元素,1000CH处的2266H依然存在,但是,它已不在栈中。当再次执行push等入栈指令后,SS:SP移至1000CH并在里面写入新的数据,它将被覆盖。
栈顶超界的问题
8086CPU用SS和SP指示栈顶的地址,并提供push和pop指令实现入栈和出栈。
SS和SP只是记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶。可是,如何能够保证在入栈、出栈时,栈顶不会超出栈空间?
图3.13描述了在执行push指令后,栈顶超出栈空间的情况。
图3.13中,将10010H~1001FH当作栈空间,该栈空间容量为16字节(8字),初始状态为空,SS=1000H、 SP=0020H,SS:SP指向10020H;
在执行8次push ax后,向栈中压入8个字,栈满,SS:SP 指向10010H;
再次执行push ax: sp=sp-2, SS:SP指向1000EH,栈顶超出了栈空间,ax 中的数据送入1000EH单元处,将栈空间外的数据覆盖。
图3.14描述了在执行pop指令后,栈顶超出栈空间的情况。
图3.14中,将10010H~1001FH 当作栈空间,该栈空间容量为16字节(8字),当前状态为满,SS=1000H、SP=0010H,SS:SP 指向10010H;
在执行8次popax后,从栈中弹出8个字,栈空,SS=SP指向10020H;
再次执行pop ax: sp sp+2, SS:SP指向1022H栈顶超出了栈空间。此后,如果再执行push指令,10020H、 10021H 中的数据将被覆盖。
上面描述了执行push、 pop指令时,发生的栈顶超界问题。可以看到,当栈满的时候再使用push指令入栈,或栈空的时候再使用pop指令出栈,都将发生栈顶超界问题。
栈顶超界是危险的,因为我们既然将一段空间安排为栈, 那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己程序中的,也可能是别的程序中的(毕竞一个计算机系统中并不是只有我们自己的程序在运行)。但是由于我们在入栈出栈时的不小心,而将这些数据、代码意外地改写,将会引发连串的错误。
我们当然希望CPU可以帮我们解决这个问题,比如说在CPU中有记录栈顶上限和栈底的寄存器,我们可以通过填写这些寄存器米指定栈空间的范围,然后,CPU在执行push指令的时候靠检测栈顶上限寄存器、在执行pop指令的时候靠检测栈底寄存器保证不会超界。
不过,对于8086CPU,这只是我们的一个设想(我们当然可以这样设想,如果CPU是我们设计的话,这也就不仅仅是一个设想)。实际的情况是,8086CPU 中并没有这样的寄存器。
8086CPU不保证我们对栈的操作不会超界。这也就是说,8086CPU只知道栈顶在何处(由SS:SP指示),而不知道我们安排的栈空间有多大。这点就好像CPU只知道当前要执行的指令在何处(由CS:IP 指示),而不知道要执行的指令有多少。从这两点上我们可以看出8086CPU的工作机理,它只考虑当前的情况:当前的栈顶在何处、当前要执行的指令是哪一条。
我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
push、pop指令
前面我们一直在使用push ax和pop ax,显然push和pop指令是可以在寄存器和内存(栈空间当然也是内存空间的一部分, 它只是段可以以一种特殊的方式进行访问的内存空间。)之间传送数据的。
push和pop指令的格式可以是如下形式:
push寄存器 ;将一个寄存器中的数据入栈
pop寄存器 ;出栈,用一个寄存器接收出栈的数据
当然也可以是如下形式:
push段寄存器 ;将一个段寄存器中的数据入栈
pop段寄存器 ;出栈,用一个段寄存器接收出栈的数据
push和pop也可以在内存单元和内存单元之间传送数据,我们可以:
push内存单元 ;将一个内存字单元处的字入栈(注意:栈操作都是以字为单位)
pop内存单元 ;出栈,用一个内存字单元接收出栈的数据
mov ax, 1000H
mov ds,ax ;内存单元的段地址要放在ds中
push [0] ;将1000:0处的字压入栈中
pop [2] ;出栈,出栈的数据送入1000:2处
指令执行时,CPU要知道内存单元的地址,可以在push、pop指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU 从ds中取得。
编程,将10000H~1000FH这段空间当作栈,初始状态栈是空的,将AX、BX、DS中的数据入栈。
mov ax, 1000H
mov ss, ax ;设置栈的段地址,Ss=1000H, 不能直接向段寄存器 SS中送入;数据, 所以用ax中转。
mov sp, 0010H ;设置栈顶的偏移地址,因栈为空,所以sp=0010H。上面的3条指令设置栈顶地址。编程中要自己注意栈的大小。
push ax
push bx
push ds
将10000H~1000FH这段空间当作栈,初始状态栈是空的;
设置AX=001AH,BX=001BH;
将AX、BX中的数据入栈;
然后将AX、BX清零;
从栈中恢复AX、BX原来的内容。
mov ax, 1000Hmov ss, ax
mov sp, 0010H ;初始化栈顶,栈的情况如图3.15(a)所示
mov ax, 001AH
mov bx, 001BH
push ax
push bx ;ax、bx入栈,栈的情况如图3.15(b)所示
sub ax, ax ;将ax清零,也可以用mov ax,0,
;sub ax,ax 的机器码为2个字节,
;mov ax,0 的机器码为3个字节。
sub bx,bx
pop bx ;从栈中恢复 ax、bx原来的数据, 当前栈顶的内容是bx
pop ax;中原来的内容: 001BH,ax 中原来的内容001AH在栈顶
;的下面,所以要先pop bx,然后再pop ax。
从上面的程序我们看到,用栈来暂存以后需要恢复的寄存器中的内容时,出栈的顺序要和入栈的顺序相反,因为最后入栈的寄存器的内容在栈顶,所以在恢复时,要最先出栈。
将10000H~1000FH这段空间当作栈,初始状态栈是空的;
设置AX=001AH,BX=001BH;
利用栈,交换AX和BX中的数据。
mov ax, 1000H
mov ss, ax
mov sp, 0010H ;初始化栈顶,栈的情况如图3.16(a)所示
mov ax, 001AH
mov bx, 001BH
push ax
push bx ;ax、bx入栈,栈的情况如图3.16(b)所示
pop ax ;当前栈项的数据是bx中原来的数据: 001BH;
;所以先pop ax, ax=001BH;
pop bx ;执行pop ax后,栈顶的数据为ax原来的数据:
;所以再pop bx, bx=001AH;
push、 pop实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与mov指令不同的是,push和pop指令访问的内存单元的地址不是在指令中给出的,而是由SS:SP指出的。同时,push和pop指令还要改变SP中的内容。
我们要十分清楚的是,push 和pop指令同mov指令不同,CPU执行mov指令只需一步操作,就是传送,而执行push、 pop指令却需要两步操作。执行push 时,CPU的两步操作是:先改变SP,后向SS:SP 处传送。执行pop时,CPU的两步操作是:先读取SS:SP处的数据,后改变SP。
注意,push, pop 等栈操作指令,修改的只是SP。也就是说,栈顶的变化范围最大为: 0~FFFFH。
提供:SS、SP指示栈顶:改变SP后写内存的入栈指令;读内存后改变SP的出栈指令。这就是8086CPU提供的栈操作机制。
栈的综述
8086CPU提供了栈操作机制,方案如下。
在SS、SP中存放栈顶的段地址和偏移地址;提供入栈和出栈指令,它们根据SS:SP指示的地址,按照栈的方式访问内存单元。
push指令的执行步骤:SP=SP-2;向SS:SP指向的字单元中送入数据。
pop 指令的执行步骤:从SS:SP指向的字单元中读取数据:SP=SP+2。
任意时刻,SS:SP 指向栈顶元素。
8086CPU只记录栈项,栈空间的大小我们要自己管理。
用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。
push、pop 实质上是一种内 存传送指令,注意它们的灵活应用。