棧是一種具有特殊的訪問(wèn)方式的存儲(chǔ)空間。它的特殊性就在于,最后進(jìn)入這個(gè)空間的數(shù)據(jù),最先出去。
可以用一個(gè)盒子和3本書(shū)來(lái)描述棧的這種操作方式。
一個(gè)開(kāi)口的盒子就可以看成一個(gè)棧空間,現(xiàn)在有3本書(shū),《高等數(shù)學(xué)》、《C語(yǔ)言》、《軟件工程》,把它們放到盒子中,操作的過(guò)程如圖所示。


現(xiàn)在的問(wèn)題是,一次只允許取一 本, 我們?nèi)绾螌?本書(shū)從盒子中取出來(lái)?
顯然,必須從盒子的最上邊取。這樣取出的順序就是:《軟件工程》 、《C 語(yǔ)言》、《高等數(shù)學(xué)》,和放入的順序相反,如圖3.8所示。


從程序化的角度來(lái)講,應(yīng)該有一個(gè)標(biāo)記,這個(gè)標(biāo)記一.直指示著盒子最上邊的書(shū)。
如果說(shuō),上例中的盒子就是一個(gè)棧,我們可以看出,棧有兩個(gè)基本的操作:入棧和出棧。入棧就是將一個(gè)新的元素放到棧頂,出棧就是從棧頂取出一個(gè) 元素。棧頂?shù)脑乜偸亲詈笕霔#枰鰲r(shí),又最先被從棧中取出。棧的這種操作規(guī)則被稱為: LIFO(Last In First Out,后進(jìn)先出)。
CPU提供的棧機(jī)制
現(xiàn)今的CPU中都有棧的設(shè)計(jì),8086CPU也不例外。8086CPU提供相關(guān)的指令來(lái)以棧的方式訪問(wèn)內(nèi)存空間。這意味著,在基于8086CPU編程的時(shí)候,可以將一段內(nèi)存當(dāng)作棧來(lái)使用。
8086CPU提供入棧和出棧指令,最基本的兩個(gè)是PUSH(入棧)和POP(出棧)。比如,push ax表示將寄存器ax中的數(shù)據(jù)送入棧中,pop ax表示從棧項(xiàng)取出數(shù)據(jù)送入ax。8086CPU的入棧和出棧操作都是以字為單位進(jìn)行的。
下面舉例說(shuō)明,我們可以將10000H~1000FH這段內(nèi)存當(dāng)作棧來(lái)使用。


mov ax, 0123H
push ax
mov bx, 2266H
push bx
mov cx, 1122H
push cx
pop ax
pop bx
pop сх
字型數(shù)據(jù)用兩個(gè)單元存放,高地址單元存放高8位,低地址單元存放低8位。
其一,我們將1000H~1000FH這段內(nèi)存當(dāng)作棧來(lái)使用,CPU執(zhí)行push和pop指令時(shí),將對(duì)這段空間按照棧的后進(jìn)先出的規(guī)則進(jìn)行訪問(wèn)。但是,一個(gè)重要的問(wèn)題是,CPU如何知道10000H~1000FH這段空間被當(dāng)作棧來(lái)使用?
其二,push ax等入棧指令執(zhí)行時(shí),要將寄存器中的內(nèi)容放入當(dāng)前棧頂單元的上方,成為新的棧頂元素; pop ax 等指令執(zhí)行時(shí),要從棧項(xiàng)單元中取出數(shù)據(jù),送入寄存器中。顯然,push、 pop 在執(zhí)行的時(shí)候,必須知道哪個(gè)單元是棧頂單元,如何知道呢?
CPU如何知道當(dāng)前要執(zhí)行的指令所在的位置?我們現(xiàn)在知道答案,那就是CS、IP中存放著當(dāng)前指令的段地址和偏移地址?,F(xiàn)在的問(wèn)題是: CPU如何知道棧頂?shù)奈恢?顯然,也應(yīng)該有相應(yīng)的寄存器來(lái)存放棧頂?shù)牡刂罚?086CPU 中,有兩個(gè)寄存器,段寄存器SS和寄存器SP,棧頂?shù)亩蔚刂反娣旁赟S中,偏移地址存放在SP中。任意時(shí)刻,SS:SP 指向棧頂元素。push 指令和pop指令執(zhí)行時(shí),CPU從SS和SP中得到棧頂?shù)牡刂贰?/p>
現(xiàn)在,我們可以完整地描述push和pop指令的功能了,例如push ax。
push ax的執(zhí)行,由以下兩步完成。
SP=SP-2, SS:SP指向當(dāng)前棧頂前面的單元,以當(dāng)前棧頂前面的單元為新的棧頂;
將ax中的內(nèi)容送入SS:SP指向的內(nèi)存單元處,SS:SP 此時(shí)指向新棧頂。
圖3.10描述了8086CPU對(duì)push指令的執(zhí)行過(guò)程。

從圖中我們可以看出,8086CPU中,入棧時(shí),棧頂從高地址向低地址方向增長(zhǎng)。
如果將10000H~ 1000FH這段空間當(dāng)作棧,初始狀態(tài)棧是空的,此時(shí),SS=1000H,SP=?

將10000H~1000H這段空間當(dāng)作棧段,SS=100H,??臻g大小為16字節(jié),棧最底部的字單元地址為1000:000任意時(shí)刻,SS:SP 指向棧項(xiàng),當(dāng)棧中只有-個(gè)元素的時(shí)候,SS=1000H, SP=000EH。 棧為空,就相當(dāng)于棧中唯 一的元素出棧, 出棧后,SP=SP+2,SP原來(lái)為000EH, 加2后SP=10H, 所以,當(dāng)棧為空的時(shí)候,SS=1000H,SP=10H。
換一個(gè)角度看,任意時(shí)刻,SS:SP指向棧項(xiàng)元素,當(dāng)棧為空的時(shí)候,棧中沒(méi)有元素,也就不存在棧項(xiàng)元素,所以SS:SP只能指向棧的最底部單元下面的單元,該單元的偏移地址為棧最底部的字單元的偏移地址+2,棧最底部字單元的地址為1000:000E,所以??諘r(shí),SP=0010H。
我們描述pop指令的功能,例如pop ax。
popax的執(zhí)行過(guò)程和push ax剛好相反,由以下兩步完成。
將SS:SP指向的內(nèi)存單元處的數(shù)據(jù)送入ax中;

注意,圖3.2中,出棧后,SS:SP指向新的棧頂1000EH,pop操作前的棧頂元素,1000CH處的2266H依然存在,但是,它已不在棧中。當(dāng)再次執(zhí)行push等入棧指令后,SS:SP移至1000CH并在里面寫(xiě)入新的數(shù)據(jù),它將被覆蓋。
棧頂超界的問(wèn)題
8086CPU用SS和SP指示棧頂?shù)牡刂?,并提供push和pop指令實(shí)現(xiàn)入棧和出棧。
SS和SP只是記錄了棧頂?shù)牡刂?,依靠SS和SP可以保證在入棧和出棧時(shí)找到棧頂。可是,如何能夠保證在入棧、出棧時(shí),棧頂不會(huì)超出??臻g?
圖3.13描述了在執(zhí)行push指令后,棧頂超出棧空間的情況。
圖3.13中,將10010H~1001FH當(dāng)作??臻g,該??臻g容量為16字節(jié)(8字),初始狀態(tài)為空,SS=1000H、 SP=0020H,SS:SP指向10020H;
在執(zhí)行8次push ax后,向棧中壓入8個(gè)字,棧滿,SS:SP 指向10010H;
再次執(zhí)行push?ax:?sp=sp-2,?SS:SP指向1000EH,棧頂超出了棧空間,ax?中的數(shù)據(jù)送入1000EH單元處,將棧空間外的數(shù)據(jù)覆蓋。
圖3.14描述了在執(zhí)行pop指令后,棧頂超出??臻g的情況。
圖3.14中,將10010H~1001FH?當(dāng)作??臻g,該棧空間容量為16字節(jié)(8字),當(dāng)前狀態(tài)為滿,SS=1000H、SP=0010H,SS:SP?指向10010H;




在執(zhí)行8次popax后,從棧中彈出8個(gè)字,???,SS=SP指向10020H;
再次執(zhí)行pop ax: sp sp+2, SS:SP指向1022H棧頂超出了??臻g。此后,如果再執(zhí)行push指令,10020H、 10021H 中的數(shù)據(jù)將被覆蓋。
上面描述了執(zhí)行push、 pop指令時(shí),發(fā)生的棧頂超界問(wèn)題。可以看到,當(dāng)棧滿的時(shí)候再使用push指令入棧,或??盏臅r(shí)候再使用pop指令出棧,都將發(fā)生棧頂超界問(wèn)題。
棧頂超界是危險(xiǎn)的,因?yàn)槲覀兗热粚⒁欢慰臻g安排為棧, 那么在??臻g之外的空間里很可能存放了具有其他用途的數(shù)據(jù)、代碼等,這些數(shù)據(jù)、代碼可能是我們自己程序中的,也可能是別的程序中的(畢競(jìng)一個(gè)計(jì)算機(jī)系統(tǒng)中并不是只有我們自己的程序在運(yùn)行)。但是由于我們?cè)谌霔3鰲r(shí)的不小心,而將這些數(shù)據(jù)、代碼意外地改寫(xiě),將會(huì)引發(fā)連串的錯(cuò)誤。
我們當(dāng)然希望CPU可以幫我們解決這個(gè)問(wèn)題,比如說(shuō)在CPU中有記錄棧頂上限和棧底的寄存器,我們可以通過(guò)填寫(xiě)這些寄存器米指定棧空間的范圍,然后,CPU在執(zhí)行push指令的時(shí)候靠檢測(cè)棧頂上限寄存器、在執(zhí)行pop指令的時(shí)候靠檢測(cè)棧底寄存器保證不會(huì)超界。
不過(guò),對(duì)于8086CPU,這只是我們的一個(gè)設(shè)想(我們當(dāng)然可以這樣設(shè)想,如果CPU是我們?cè)O(shè)計(jì)的話,這也就不僅僅是一個(gè)設(shè)想)。實(shí)際的情況是,8086CPU 中并沒(méi)有這樣的寄存器。
8086CPU不保證我們對(duì)棧的操作不會(huì)超界。這也就是說(shuō),8086CPU只知道棧頂在何處(由SS:SP指示),而不知道我們安排的??臻g有多大。這點(diǎn)就好像CPU只知道當(dāng)前要執(zhí)行的指令在何處(由CS:IP 指示),而不知道要執(zhí)行的指令有多少。從這兩點(diǎn)上我們可以看出8086CPU的工作機(jī)理,它只考慮當(dāng)前的情況:當(dāng)前的棧頂在何處、當(dāng)前要執(zhí)行的指令是哪一條。
我們?cè)诰幊痰臅r(shí)候要自己操心棧頂超界的問(wèn)題,要根據(jù)可能用到的最大??臻g,來(lái)安排棧的大小,防止入棧的數(shù)據(jù)太多而導(dǎo)致的超界;執(zhí)行出棧操作的時(shí)候也要注意,以防??盏臅r(shí)候繼續(xù)出棧而導(dǎo)致的超界。
push、pop指令
前面我們一直在使用push ax和pop ax,顯然push和pop指令是可以在寄存器和內(nèi)存(??臻g當(dāng)然也是內(nèi)存空間的一部分, 它只是段可以以一種特殊的方式進(jìn)行訪問(wèn)的內(nèi)存空間。)之間傳送數(shù)據(jù)的。
push和pop指令的格式可以是如下形式:
push寄存器? ;將一個(gè)寄存器中的數(shù)據(jù)入棧
pop寄存器? ;出棧,用一個(gè)寄存器接收出棧的數(shù)據(jù)
當(dāng)然也可以是如下形式:
push段寄存器? ;將一個(gè)段寄存器中的數(shù)據(jù)入棧
pop段寄存器? ;出棧,用一個(gè)段寄存器接收出棧的數(shù)據(jù)
push和pop也可以在內(nèi)存單元和內(nèi)存單元之間傳送數(shù)據(jù),我們可以:
push內(nèi)存單元? ;將一個(gè)內(nèi)存字單元處的字入棧(注意:棧操作都是以字為單位)
pop內(nèi)存單元? ;出棧,用一個(gè)內(nèi)存字單元接收出棧的數(shù)據(jù)
mov ax, 1000H
mov ds,ax? ;內(nèi)存單元的段地址要放在ds中
push [0]? ;將1000:0處的字壓入棧中
pop [2]? ;出棧,出棧的數(shù)據(jù)送入1000:2處
指令執(zhí)行時(shí),CPU要知道內(nèi)存單元的地址,可以在push、pop指令中只給出內(nèi)存單元的偏移地址,段地址在指令執(zhí)行時(shí),CPU 從ds中取得。
編程,將10000H~1000FH這段空間當(dāng)作棧,初始狀態(tài)棧是空的,將AX、BX、DS中的數(shù)據(jù)入棧。
mov ax, 1000H
mov ss, ax? ;設(shè)置棧的段地址,Ss=1000H, 不能直接向段寄存器 SS中送入;數(shù)據(jù), 所以用ax中轉(zhuǎn)。
mov sp, 0010H? ;設(shè)置棧頂?shù)钠频刂?,因棧為空,所以sp=0010H。上面的3條指令設(shè)置棧頂?shù)刂贰>幊讨幸约鹤⒁鈼5拇笮 ?/p>
push ax
push bx
push ds
將10000H~1000FH這段空間當(dāng)作棧,初始狀態(tài)棧是空的;
設(shè)置AX=001AH,BX=001BH;
將AX、BX中的數(shù)據(jù)入棧;
然后將AX、BX清零;
從棧中恢復(fù)AX、BX原來(lái)的內(nèi)容。
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 的機(jī)器碼為2個(gè)字節(jié),
;mov ax,0 的機(jī)器碼為3個(gè)字節(jié)。
sub bx,bx
pop bx? ;從棧中恢復(fù) ax、bx原來(lái)的數(shù)據(jù), 當(dāng)前棧頂?shù)膬?nèi)容是bx
pop ax;中原來(lái)的內(nèi)容: 001BH,ax 中原來(lái)的內(nèi)容001AH在棧頂
;的下面,所以要先pop bx,然后再pop ax。

從上面的程序我們看到,用棧來(lái)暫存以后需要恢復(fù)的寄存器中的內(nèi)容時(shí),出棧的順序要和入棧的順序相反,因?yàn)樽詈笕霔5募拇嫫鞯膬?nèi)容在棧頂,所以在恢復(fù)時(shí),要最先出棧。
將10000H~1000FH這段空間當(dāng)作棧,初始狀態(tài)棧是空的;
設(shè)置AX=001AH,BX=001BH;
利用棧,交換AX和BX中的數(shù)據(jù)。
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? ;當(dāng)前棧項(xiàng)的數(shù)據(jù)是bx中原來(lái)的數(shù)據(jù): 001BH;
;所以先pop ax, ax=001BH;
pop bx? ?;執(zhí)行pop ax后,棧頂?shù)臄?shù)據(jù)為ax原來(lái)的數(shù)據(jù):
;所以再pop bx, bx=001AH;

push、 pop實(shí)質(zhì)上就是一種內(nèi)存?zhèn)魉椭噶?,可以在寄存器和?nèi)存之間傳送數(shù)據(jù),與mov指令不同的是,push和pop指令訪問(wèn)的內(nèi)存單元的地址不是在指令中給出的,而是由SS:SP指出的。同時(shí),push和pop指令還要改變SP中的內(nèi)容。
我們要十分清楚的是,push 和pop指令同mov指令不同,CPU執(zhí)行mov指令只需一步操作,就是傳送,而執(zhí)行push、 pop指令卻需要兩步操作。執(zhí)行push 時(shí),CPU的兩步操作是:先改變SP,后向SS:SP 處傳送。執(zhí)行pop時(shí),CPU的兩步操作是:先讀取SS:SP處的數(shù)據(jù),后改變SP。
注意,push, pop 等棧操作指令,修改的只是SP。也就是說(shuō),棧頂?shù)淖兓秶畲鬄? 0~FFFFH。
提供:SS、SP指示棧頂:改變SP后寫(xiě)內(nèi)存的入棧指令;讀內(nèi)存后改變SP的出棧指令。這就是8086CPU提供的棧操作機(jī)制。
棧的綜述
8086CPU提供了棧操作機(jī)制,方案如下。
在SS、SP中存放棧頂?shù)亩蔚刂泛推频刂?提供入棧和出棧指令,它們根據(jù)SS:SP指示的地址,按照棧的方式訪問(wèn)內(nèi)存單元。
push指令的執(zhí)行步驟:SP=SP-2;向SS:SP指向的字單元中送入數(shù)據(jù)。
pop 指令的執(zhí)行步驟:從SS:SP指向的字單元中讀取數(shù)據(jù):SP=SP+2。
任意時(shí)刻,SS:SP 指向棧頂元素。
8086CPU只記錄棧項(xiàng),??臻g的大小我們要自己管理。
用棧來(lái)暫存以后需要恢復(fù)的寄存器的內(nèi)容時(shí),寄存器出棧的順序要和入棧的順序相反。
push、pop 實(shí)質(zhì)上是一種內(nèi) 存?zhèn)魉椭噶?,注意它們的靈活應(yīng)用。