王爽《匯編語言》第四版 超級筆記
第3章 寄存器(內存訪問)
3.1 內存中的存儲:字(word)
CPU中,用16位寄存器來存儲一個字。高8位存放高位字節,低8位存放低位字節。
在內存中存儲時,由於內存單元是字節單元(一個單元存放一個字節),則一個字要用兩個地址連續的內存單元來存放,這個字的低位字節存放在低地址單元中,高位字節存放在高地址單元中。
在圖3.1中,我們用0、1兩個內存單元存放數據 20000(4E20H)。
0、1兩個內存單元用來存儲一個字,這兩個單元可以看作一個起始地址為0的字單元(存放一個字的內存單元,由0、1兩個字節單元組成)。
對於這個字單元來說,0號單元是低地址單元,1號單元是高地址單元,則字型數據4E20H的低位字節存放在0號單元中,高位字節存放在1號單元中。
同理,將2、3號單元看作一個字單元,它的起始地址為2,在這個字單元中存放數18(0012H),則在2號單元中存放低位字節12H,在3號單元中存放高位字節00H。
我們提出字單元的概念:字單元,即存放一個字型數據(16位)的內存單元,由兩個地址連續的內存單元組成。
高地址內存單元中存放字型數據的高位字節,低地址內存單元中存放字型數據的低位字節。
任何兩個地址連續的內存單元,N號單元和N+1號單元,可以將它們看成兩個內存單元,也可看成一個地址為N的字單元中的高位字節單元和低位字節單元。
3.2 DS 和 [address]
CPU要讀寫一個內存單元的時候,必須先給岀這個內存單元的地址,在8086PC中,內存地址由段地址和偏移地址組成。
8086CPU中有一個DS寄存器,通常用來存放要訪問數據的段地址。比如我們要讀取10000H單元的內容,可以用如下的程序段進行。
mov bx,1000H
mov ds,bx
mov al,[0]
上面的3條指令將10000H(1000:0)中的數據讀到al中。
下面詳細說明指令的含義:
mov al,[0]
前面我們使用mov指令,可完成兩種傳送:
①將數據直接送入寄存器;
②將一個寄存器中的內容送入另一個寄存器。
也可以使用mov指令將一個內存單元中的內容送入一個寄存器中。
從哪一個內存單元送到哪一個寄存器中呢?
在指令中必須指明。寄存器用寄存器名來指明,內存單元則需用內存單元的地址來指明。
顯然,此時mov指令的格式應該是:mov 寄存器名,內存單元地址。
“[•••]”表示一個內存單元,“[•••]”中的0表示內存單元的偏移地址。
我們知道, 只有偏移地址是不能定位一個內存單元的,那么內存單元的段地址是多少呢?
指令執行時,8086CPU自動取ds中的數據為內存單元的段地址。
如何把一個數據送入寄存器呢?
我們以前用類似“mov ax,1”這樣的指令來完成,從理論上講,我們可以用相似的方式:mov ds,1000H,來將1000H送入ds。
可是,現實並非如此,8086CPU不支持將數據直接送入段寄存器的操作,ds是一個段寄存器,所以 mov ds,1000H 這條指令是非法的。
那么如何將1000H送入ds呢?
只好用一個寄存器來進行中轉,即先將1000H送入一個一般的寄存器如bx,再將bx中的內容送入ds。
為什么8086CPU不支持將數據直接送入段寄存器的操作?
這屬於8086CPU硬件設計的問題,我們只要知道這一點就行了。
怎樣將數據從寄存器送入內存單元?
從內存單元到寄存器的格式是:“mov 寄存器名, 內存單元地址”,從寄存器到內存單元則是:“mov 內存單元地址,寄存器名”。
10000H 可表示為1000:0,用ds存放段地址1000H,偏移地址是0,則mov [0],al 可完成從al到10000H的數據傳送。
完整的幾條指令是:
mov bx,1000H
mov ds,bx
mov [0],al
3.3 字的傳送
8086CPU是16位結構,有16根數據線,所以,可以一次性傳送16位的數據,也就是說可以一次性傳送一個字。
只要在mov指令中給出16位的寄存器就可以進行16位數據的傳送了。 比如:
mov bx,1000H
mov ds,bx
mov ax,[0] ;1000:0處的字型數據送入ax
mov [0],ex ;cx中的16位數據送到1000:0處
內存中的情況如圖3.3所示,寫出下面的指令執行后內存中的值,思考后看分析。
3.4 mov、add、sub 指令
mov、add、sub指令,它們都帶有兩個操作對象。
到現在,我們知道,mov指令可以有以下幾種形式。
mov 寄存器,數據 比如:mov ax,8
mov 寄存器,寄存器 比如:mov ax,bx
mov 寄存器,內存單元 比如:mov ax,[0]
mov 內存單元,寄存器 比如:mov [0],ax
mov 段寄存器,寄存器 比如:mov ds,ax
我們可以根據這些己知指令進行下面的推測。
(1)既然有“mov 段寄存器,寄存器”,從寄存器向段寄存器傳送數據,那么也應該有“mov 寄存器,段寄存器”,從段寄存器向寄存器傳送數據。
一個合理的設想是: 8086CPU內部有寄存器到段寄存器的通路,那么也應該有相反的通路。
有了推測,我們還要驗證一下。進入Debug,用A命令,如圖3.4所示。
圖3.4中,用A命令在一個預設的地址0B39:0100處,用匯編的形式mov ax,ds寫入指令,再用T命令執行,可以看到執行的結果,段寄存器ds中的值送到了寄存器ax中。 通過驗證我們知道,“mov 寄存器,段寄存器”是正確的指令。
(2)既然有“mov 內存單元,寄存器”,從寄存器向內存單元傳送數據,那么也應該有“mov 內存單元,段寄存器”,從段寄存器向內存單元傳送數據。
比如我們可以將段寄存器cs中的內容送入內存10000H處,指令如下。
mov ax,1000H
mov ds,ax
mov [0],cs
在Debug中進行試驗,如圖3.5所示。
圖3.5中,當CS:IP指向0B39:0105的時候,Debug顯示當前的指令mov [0000],cs,因為這是一條訪問內存的指令,Debug還顯示出指令要訪問的內存單元中的內容。
由於指令中的CS是一個16位寄存器,所以耍訪問(寫入)的內存單元是一個字單元,它的偏移地址為0,段地址在ds中,Debug在屏幕右邊顯示岀“DS:0000=0000”,我們可以知道這個字單元中的內容為0。
mov [0000],cs執行后,CS中的數據(0B39H)被寫入1000:0處,1000:1單元存放0BH,1000:0單元存放39H。
最后,用D命令從1000:0開始查看指令執行后內存中的情況,注意1000:0、1000:1兩個單元的內容。
(3)“mov 段寄存器,內存單元”也應該可行,比如我們可以用10000H處存放的字型數據設置ds(即將10000H處存放的字型數據送入ds),指令如下。
mov ax,1000H
mov ds,ax
mov ds,[0]
可以自行在Debug中進行試驗。
add和sub指令同mov一樣,都有兩個操作對象。
它們也可以有以下幾種形式。
add 寄存器,數據 比如:add ax,8
add 寄存器,寄存器 比如:add ax,bx
add 寄存器,內存單元 比如:add ax,[0]
add 內存單元,寄存器 比如:add [0],ax
sub 寄存器,數據 比如:sub ax,9
sub 寄存器,寄存器 比如:sub ax,bx
sub 寄存器,內存單元 比如:sub ax,[0]
sub 內存單元,寄存器 比如:sub [0],ax
它們可以對段寄存器進行操作嗎?
比如“add ds,ax”,請自行在Debug中試驗。
3.5 數據段
對於8086PC機,在編程時,可以根據需要,將一組內存單元定義為一個段。
我們可以將一組長度為N(N<=64KB)、地址連續、起始地址為16的倍數的內存單元當作專門存儲數據的內存空間,從而定義了一個數據段。
比如用123B0H~123B9H這段內存空間來存放數據,我們就可以認為,123B0H~123B9H這段內存是一個數據段,它的段地址為123BH,長度為10個字節。
如何訪問數據段中的數據呢?
將一段內存當作數據段,是我們在編程時的一種安排,可以在具體操作的時候,用ds存放數據段的段地址,再根據需要,用相關指令訪問數據段中的具體單元。
比如,將123E0H~123B9H的內存單元定義為數據段。現在要累加這個數據段中的前3個單元中的數據,代碼如下。
mov ax,123BH
mov ds,ax ;將123BH送入ds中,作為數據段的段地址
mov al,0 ;用al存放累加結果
add al,[0] ;將數據段第一個單元(偏移地址為0)中的數值加到al中
add al,[1] ;將數據段第二個單元(偏移地址為1)中的數值加到al中
add al,[2] ;將數據段第三個單元(偏移地址為2)中的數值加到al中
3.1~3.5 小 結
(1)字在內存中存儲時,要用兩個地址連續的內存單元來存放,字的低位字節存放在低地址單元中,高位字節存放在高地址單元中。
(2)用mov指令訪問內存單元,可以在mov指令中只給出單元的偏移地址,此時,段地址默認在DS寄存器中。
(3)[address]表示一個偏移地址為addiess的內存單元。
(4)在內存和寄存器之間傳送字型數據時,高地址單元和高8位寄存器、低地址單元和低8位寄存器相對應。
(5)mov、addx sub是具有兩個操作對象的指令。jmp是具有一個操作對象的指令。
(6)可以根據自己的推測,在Debug中實驗指令的新格式。
3.6 棧、CPU提供的棧機制
在這里,我們對棧的研究僅限於這個角度:棧是一種具有特殊的訪問方式的存儲空間。
它的特殊性就在於,最后進入這個空間的數據,最先出去。
可以用一個盒子和3本書來描述棧的這種操作方式。
一個開口的盒子就可以看成一個棧空間,現在有3本書,《高等數學》、《C語言》、《軟件工程》,把它們放到盒子中,操作的過程如圖3.7所示。
現在的問題是,一次只允許取一本,我們如何將3本書從盒子中取岀來?
顯然,必須從盒子的最上邊取。
這樣取出的順序就是:《軟件工程》、《C語言》、《高等數學》,和放入的順序相反,如圖3.8所示。
從程序化的角度來講,應該有一個標記,這個標記一直指示着盒子最上邊的書。
如果說,上例中的盒子就是一個棧,我們可以看出,棧有兩個基本的操作:入棧和出棧。
入棧就是將一個新的元素放到棧頂,出棧就是從棧頂取出一個元素。
棧頂的元素總是最后入棧,需要出棧時,又最先被從棧中取出。棧的這種操作規則被稱為:LIFO(Last In First Out,后進先出)。
現今的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 ex,1122H
push ex
pop ax
pop bx
pop ex
注意,字型數據用兩個單元存放,高地址單元存放高8位,低地址單元存放低8位。
push ax的執行,由以下兩步完成。
1、SP=SP-2, SS:SP指向當前棧頂前面的單元,以當前棧頂前面的單元為新的棧頂;
2、將ax中的內容送入SS:SP指向的內存單元處,SS:SP此時指向新棧頂。
pop ax的執行過程和push ax剛好相反,由以下兩步完成。
1、將SS:SP指向的內存單元處的數據送入ax中;
2、SP=SP+2, SS:SP指向當前棧頂下面的單元,以當前棧頂下面的單元為新的棧頂。
圖3.10描述了8086CPU對push指令的執行過程。
從圖中我們可以看出,8086CPU中,入棧時,棧頂從高地址向低地址方向增長。
3.7 棧頂超界的問題
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次pop ax后,從棧中彈出8個字,棧空,SS:SP指向10020H;
再次執行pop ax:sp=sp+2,SS:SP指向10022H,棧頂超出了棧空間。此后,如果再執行push指令,10020H、10021H中的數據將被覆蓋。
上面描述了執行push、pop指令時,發生的棧頂超界問題。
可以看到,當棧滿的時候再使用push指令入棧,或棧空的時候再使用pop指令出棧,都將發生棧頂超界問題。
棧頂超界是危險的,因為我們既然將一段空間安排為棧,那么在棧空間之外的空間里很可能存放了具有其他用途的數據、代碼等,這些數據、代碼可能是我們自己程序中的,也可能是別的程序中的(畢竟一個計算機系統中並不是只有我們自己的程序在運行)。
但是由於我們在入棧出棧時的不小心,而將這些數據、代碼意外地改寫,將會引發一連串的錯誤。
我們當然希望CPU可以幫我們解決這個問題,比如說在CPU中有記錄棧頂上限和棧底的寄存器,我們可以通過填寫這些寄存器來指定棧空間的范圍,然后,CPU在執行push指令的時候靠檢測棧頂上限寄存器、在執行pop指令的時候靠檢測棧底寄存器保證不會超界。
不過,對於8086CPU,這只是我們的一個設想(我們當然可以這樣設想,如果CPU是我們設計的話,這也就不僅僅是一個設想)。
實際的情況是,8086CPU中並沒有這樣的寄存器。
8086CPU不保證我們對棧的操作不會超界。
這也就是說,8086CPU只知道棧頂在何處(由SS:SP指示),而不知道我們安排的棧空間有多大。
這點就好像CPU只知道當前要執行的指令在何處(由CS:IP指示),而不知道要執行的指令有多少。
從這兩點上我們可以看出8086CPU的工作機理,它只考慮當前的情況:當前的棧頂在何處、當前要執行的指令是哪一條。
我們在編程的時候要自己操心棧頂超界的問題,要根據可能用到的最大棧空間,來安排棧的大小,防止入棧的數據太多而導致的超界;
執行岀棧操作的時候也要注意,以防棧空的時候繼續出棧而導致的超界。
3.8 push、pop 指令
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中取得。
push ax是入棧指令,它將在棧頂之上壓入新的數據。
一定要注意:它的執行過程是,先將記錄棧頂偏移地址的SP寄存器中的內容減2,使得SS:SP指向新的棧頂單元,然后再將寄存器中的數據送入SS:SP指向的新的棧頂單元。
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提供的棧操作機制。
棧的綜述
(1) 8086CPU提供了棧操作機制,方案如下。
在SS、SP中存放棧頂的段地址和偏移地址;
提供入棧和出棧指令,它們根據SS:SP指示的地址,按照棧的方式訪問內存單元。
(2)push指令的執行步驟:①SP=SP-2;②向SS:SP指向的字單元中送入數據。
(3)pop指令的執行步驟:①從SS:SP指向的字單元中讀取數據;②SP=SP+2。
(4)任意時刻,SS:SP指向棧頂元素。
(5)8086CPU只記錄棧頂,棧空間的大小我們要自己管理。
(6)用棧來暫存以后需要恢復的寄存器的內容時,寄存器出棧的順序要和入棧的順序相反。
(7)push、pop實質上是一種內存傳送指令,注意它們的靈活應用。
棧是一種非常重要的機制,一定要深入理解,靈活掌握。
3.9 棧 段
對於8086PC機,在編程時,可以根據需要,將一組內存單元定義為一個段。
我們可以將長度為N(N<=64KB)的一組地址連續、起始地址為16的倍數的內存單元,當作棧空間來用,從而定義了一個棧段。
比如,我們將10010H~1001FH這段長度為16字節的內存空間當作棧來用,以棧的方式進行訪問。這段空間就可以稱為一個棧段,段地址為1001H,大小為16字節。
將一段內存當作棧段,僅僅是我們在編程時的一種安排,CPU並不會由於這種安排,就在執行push、pop等棧操作指令時自動地將我們定義的棧段當作棧空間來訪問。
如何使得如push、pop等棧操作指令訪問我們定義的棧段呢?
前面我們己經討論過,就是要將SS:SP指向我們定義的棧段。
任意時刻,SS:SP指向棧頂元素,當棧為空的時候,棧中沒有元素,也就不存在棧頂元素,所以SS:SP只能指向棧的最底部單元下面的單元,該單元的地址為棧最底部的字單元的地址+2。棧最底部字單元的地址為1000:FFFE,所以棧空時,SP=0000H。
一個棧段最大可以設為多少?為什么?
首先從棧操作指令所完成的功能的角度上來看,push、pop等指令在執行的時候只修改SP,所以棧頂的變化范圍是0~FFFFH,從棧空時候的SP=0,一直壓棧,直到棧滿時SP=0;
如果再次壓棧,棧頂將環繞,覆蓋了原來棧中的內容。所以一個棧段的容量最大為64KB。
我們可以將一段內存定義為一個段,用一個段地址指示段,用偏移地址訪問段內的單元。這完全是我們自己的安排。
我們可以用一個段存放數據,將它定義為“數據段”;
我們可以用一個段存放代碼,將它定義為“代碼段”;
我們可以用一個段當作棧,將它定義為“棧段”。
我們可以這樣安排,但若要讓CPU按照我們的安排來訪問這些段,就要:
對於數據段,將它的段地址放在DS中,用mov、add、sub等訪問內存單元的指令時,CPU就將我們定義的數據段中的內容當作數據來訪問;
對於代碼段,將它的段地址放在CS中,將段中第一條指令的偏移地址放在IP中,這樣CPU就將執行我們定義的代碼段中的指令;
對於棧段,將它的段地址放在SS中,將棧頂單元的偏移地址放在SP中,這樣CPU在需要進行棧操作的時候,比如執行push、pop指令等,就將我們定義的棧段當作棧空間來用。
CPU將內存中的某段內容當作代碼,是因CS:IP指向了那里;
CPU將某段內存當作棧,是因為SS:SP指向了那里。
我們一定要清楚,什么是我們的安排,以及如何讓CPU按我們的安排行事。
一段內存,可以既是代碼的存儲空間,又是數據的存儲空間,還可以是棧空間,也可以什么也不是。
關鍵在於CPU中寄存器的設置,即CS、IP,SS、SP,DS的指向。
要非常清楚CPU的工作機理,才能在控制CPU按照我們的安排運行的時候做到游刃有余。