目錄
王爽《彙編語言》第四版 超級筆記
第5章[BX]和 loop 指令
5.1 [BX]
5.2 loop 指令
5.3 Debug和彙編編譯器masm對指令的不同處理
5.4 loop和[bx]的聯合應用
5.5 段字首及使用
5.6 一段安全的空間
1、[bx]和 記憶體單元的描述
[bx]是什麼呢?和[0]有些類似,[0]表示記憶體單元,它的偏移位址是0。
比如在下面的指令中(在Debug中使用):
mov ax,[0]
将一個記憶體單元的内容送入ax,這個記憶體單元的長度為2位元組(字單元),存放一個字,偏移位址為0,段位址在ds中。
mov al,[0]
将一個記憶體單元的内容送入al,這個記憶體單元的長度為1位元組(位元組單元),存放一個位元組,偏移位址為0,段位址在ds中。
要完整地描述一個記憶體單元,需要兩種資訊:
①記憶體單元的位址;
②記憶體單元的長度(類型)。
用[0]表示一個記憶體單元時,0表示單元的偏移位址,段位址預設在ds中,單元的長度(類型)可以由具體指令中的其他操作對象(比如說寄存器)指出。
[bx]同樣也表示一個記憶體單元,它的偏移位址在bx中,比如下面的指令:
mov ax,[bx]
将一個記憶體單元的内容送入ax,這個記憶體單元的長度為2位元組(字單元),存放一個字,偏移位址在bx中,段位址在ds中。
mov al,[bx]
2、loop
英文單詞“loop”有循環的含義,顯然這個指令和循環有關。
3、我們定義的描述性的符号:"()”
我們将使用一個描述性的符号“()”來表示一個寄存器或一個記憶體單元中的内容。比如:
(ax)表示ax中的内容、(al)表示al中的内容;
(20000H)表示記憶體20000H單元的内容(()中的記憶體單元的位址為實體位址);
注意,“()”中的元素可以有3種類型:
①寄存器名;
②段寄存器名;
③記憶體單元的實體位址(一個20位資料)。
我們看一下(X)的應用,比如:
(1) ax中的内容為0010H,可以這樣來描述:(ax)=0010H; (2) 2000:1000處的内容為0010H,可以這樣來描述:(21000H)=0010H; (3) 對于mov ax,[2]的功能,可以這樣來描述:(ax)=((ds)x16+2); (4) 對于mov [2],ax的功能,可以這樣來描述:((ds)x16+2)=(ax); (5) 對于add ax,2的功能,可以這樣來描述:(ax)=(ax)+2; (6) 對于add ax,bx的功能,可以這樣來描述:(ax)=(ax)+(bx); (7) 對于push ax的功能,可以這樣來描述: (sp)=(sp)-2 ((ss)x16+(sp))=(ax) (8) 對于pop ax的功能,可以這樣來描述: (ax)=((ss)x16+(sp)) (sp)=(sp)+2
“(X)”所表示的資料有兩種類型:①位元組;②字。
是哪種類型由寄存器名或具體的運算決定。
4、約定符号idata表示常量
我們在Debug中寫過類似的指令:mov ax,[0],表示将ds:0處的資料送入ax中。
指令中,在裡用一個常量0表示記憶體單元的偏移位址。
以後,我們用idata表示常量。比如:
mov ax,[idata] 就代表 mov ax,[1]、mov ax,[2]、mov ax,[3] 等。
mov bx,idata 就代表 mov bx,1、mov bx,2、mov bx,3 等。
mov ds,idata 就代表mov ds,1、mov ds,2 等,它們都是非法指令。
功能:bx中存放的資料作為一個偏移位址EA,段位址SA預設在ds中,将SA:EA處的資料送入ax中。即:(ax)=((ds)x16+(bx))。
mov [bx],ax
功能:bx中存放的資料作為一個偏移位址EA,段位址SA預設在ds中,将ax中的資料送入記憶體
SA:EA處。即:((ds)x16+(bx))=(ax)。
問題5.1
程式和記憶體中的情況如圖5.1所示,寫出程式執行後,21000H-21007H單元中的内容。

思考後看分析。
注意,inc bx 的含義是bx中的内容加1,比如下面兩條指令:
mov bx,1
inc bx
執行後,bx=2。
分析:
(1)先看一下程式的前3條指令:
mov ax,2000H
mov ds,ax
mov bx,1000H
這3條指令執行後,ds=2000H,bx=1000H。
(2)接下來,第4條指令:
指令執行前:ds=2000H,bx=1000H,則mov ax,[bx] 将把記憶體2000:1000處的字型資料送入ax中。該指令執行後,ax=00beH。
(3)接下來,第5、6條指令:
這兩條指令執行前bx=1000H,執行後bx=1002H。
(4)接下來,第7條指令:
指令執行前:ds=2000H,bx=1002H,則mov [bx],ax 将把ax中的資料送入記憶體2000:1002處。
指令執行後,2000:1002單元的内容為BE,2000:1003單元的内容為00。
(5)接下來,第8、9條指令:
這兩條指令執行前bx=1002H,執行後bx=1004H。
(6)接下來,第10條指令:
指令執行前:ds=2000H,bx=1004H,則mov [bx],ax 将把ax中的資料送入記憶體2000:1004處。
指令執行後,2000:1004單元的内容為BE,2000:1005單元的内容為00。
(7)接下來,第11條指令:
這條指令執行前bx=1004H,執行後bx=1005H。
(8)接下來,第12條指令:
mov [bx],al
指令執行前:ds=2000H,bx=1005H,則mov [bx],al 将把al中的資料送入記憶體2000:1005處。
指令執行後,2000:1005單元的内容為BE。
(9)接下來,第13條指令:
這條指令執行前bx=1005H,執行後bx=1006H。
(10)接下來,第14條指令:
指令執行前:ds=2000H,bx=1006H,則mov [bx],al 将把al中的資料送入記憶體2000:1006處。
指令執行後,2000:1006單元的内容為BE。
程式執行後,記憶體中的情況如圖5.2所示。
loop指令的格式是:loop标号,CPU執行loop指令的時候,要進行兩步操作:
①(cx)=(cx)-l;
②判斷CX中的值,不為零則轉至标号處執行程式,如果為零則向下執行。
從上面的描述中,可以看到,CX中的值影響着loop指令的執行結果。
通常(注意,我們說的是通常)我們用loop指令來實作循環功能,CX中存放循環次數。
下面我們通過一個程式來看一下loop指令的具體應用。
程式設計計算2^12。
分析:212=2x2x2x2x2x2x2x2x2x2x2x2,若設(ax)=2,可計算(ax)=(ax)x2x2x2x2x2x2x2x2x2x2x2,最後(ax)中為212的值。Nx2可用N+N實作,程式如下。
可見,按照我們的算法,計算2^12需要11條重複的指令add ax,ax,我們顯然不希望這樣來寫程式,這裡,可用loop來簡化我們的程式。
程式5.1
下面分析一下程式5.1。
(1)标号
在彙編語言中,标号代表一個位址,程式5.1中有一個标号s。它實際上辨別了一個位址,這個位址處有一條指令:add ax,ax。
(2)loop s
CPU執行loop s的時候,要進行兩步操作:
①(cx)=(cx)-l ;
②判斷ex中的值,不為0則轉至标号s所辨別的位址處執行(這裡的指令是add ax,ax),如果為0則執行下一條指令(下一條指令是mov ax,4c00h)。
(3)以下3條指令
執行loop s時,首先要将(cx)減1,然後若(cx)不為0,則向前轉至s處執行add ax,ax。
是以,可以利用cx來控制add ax,ax的執行次數。
下面我們詳細分析一下這段程式的執行過程,從中體會如何用cx和loop s相配合實作循環功能。
從上面的過程中,我們可以總結出用cx和loop指令相配合實作循環功能的3個要點:
(1) 在cx中存放循環次數;
(2) loop指令中的标号所辨別位址要在前面;
(3) 要循環執行的程式段,要寫在标号和loop指令的中間。
用cx和loop指令相配合實作循環功能的程式架構如下。
程式設計,用加法計算123x236,結果存在ax中。思考後看分析。
可用循環完成,将123加236次。可先設(ax)=0,然後循環做236次(ax)=(ax)+123。
程式如下
程式5.2
我們在Debug中寫過類似的指令:
表示将ds:0處的資料送入ax中。
但是在彙編源程式中,指令“mov ax,[0]”被編譯器當作指令“mov ax,0”處理。
下面通過具體的例子來看一下Debug和彙編編譯器masm對形如“mov ax,[0]”這類指令的不同處理。
任務:将記憶體2000:0、2000:1、2000:2、2000:3單元中的資料送入al、bl、cl、dl中。
(1)在Debug中程式設計實作:
(2)彙編源程式實作:
我們看一下兩種實作的實際實施情況:
(1)Debug中的情況如圖5.16所示。
(2)将彙編源程式存儲為compare.asm,用masm、link生成compare.exe,用Debug加載compare.exe,如圖5.17所示。
從圖5.16、圖5.17中我們可以明顯地看出,Debug和編譯器masm對形如“mov ax,[0]”這類指令在解釋上的不同。
我們在Debug中和源程式中寫入同樣形式的指令:"mov al,[0]"、"mov bl,[1]"、"mov cl,[2]"、"mov dl,[3]”,但Debug和編譯器對這些指令中的“[idata]”卻有不同的解釋。
Debug将它解釋為“[idata]”是一個記憶體單元,“idata”是記憶體單元的偏移位址;而編譯器将“[idata]”解釋為“idata”。
那麼我們如何在源程式中實作将記憶體2000:0、2000:1、2000:2、2000:3單元中的資料送入al、bl、cl、dl中呢?
目前的方法是,可将偏移位址送入bx寄存器中,用[bx]的方式來通路記憶體單元。比如我們可以這樣通路2000:0單元:
這樣做是可以,可是比較麻煩,我們要用bx來間接地給出記憶體單元的偏移位址。
我們還是希望能夠像在Debug中那樣,在“[]”中直接給出記憶體單元的偏移位址。這樣做,在彙編源程式中也是可以的,隻不過,要在“[]”的前面顯式地給出段位址所在的段寄存器。比如我們可以這樣通路2000:0單元:
mov ax,2000h mov al,ds:[0]
比較一下彙編源程式中以下指令的含義。
“mov al,[0]”,含義:(al)=0,将常量0送入al中(與mov al,0含義相同);
“mov al,ds:[0]”,含義:(al)=((ds)x16+0),将記憶體單元中的資料送入al中;
“mov al,[bx]”,含義:(al)=((ds)x16+(bx)),将記憶體單元中的資料送入al中;
“mov al,ds:[bx]”,含義:與“mov al,[bx]”相同。
從上面的比較中可以看出:
(1)在彙編源程式中,如果用指令通路一個記憶體單元,則在指令中必須用“[…]”來表示記憶體單元,如果在“[]”裡用一個常量idata直接給出記憶體單元的偏移位址,就要在“[]”的前面顯式地給出段位址所在的段寄存器。比如
如果沒有在“[]”的前面顯式地給出段位址所在的段寄存器,比如
那麼,編譯器masm将把指令中的“[idata]”解釋為“idata”。
(2)如果在“[]”裡用寄存器,比如bx,間接給出記憶體單元的偏移位址,則段位址預設在ds中。當然,也可以顯式地給出段位址所在的段寄存器。
考慮這樣一個問題,計算ffff:0~ffff:b單元中的資料的和,結果存儲在dx中。
我們還是先分析一下。
(1)運算後的結果是否會超出dx所能存儲的範圍?
ffff:0~ffff:b記憶體單元中的資料是位元組型資料,範圍在0~255之間,12個這樣的資料相加,結果不會大于65535,可以在dx中存放下。
(2)我們能否将ffff:0~ffff:b中的資料直接累加到dx中?
當然不行,因為ffff:0~ffff:b中的資料是8位的,不能直接加到16位寄存器dx中。
(3)我們能否将ffff:0~ffff:b中的資料累加到dl中,并設定(dh)=0,進而實作累加到dx中?
這也不行,因為dl是8位寄存器,能容納的資料的範圍在0~255之間,ffff:0~ffff:b中的資料也都是8位,如果僅向dl中累加12個8位資料,很有可能造成進位丢失。
(4)我們到底怎樣将ffff:0~ffff:b中的8位資料,累加到16位寄存器dx中?
從上面的分析中,可以看到,這裡面有兩個問題:類型的比對和結果的不超界。
具體地說,就是在做加法的時候,我們有兩種方法:
1、(dx)=(dx)+記憶體中的8位資料; 2、(dl)=(dl)+記憶體中的8位資料。
第一種方法中的問題是兩個運算對象的類型不比對,第二種方法中的問題是結果有可能超界。
怎樣解決這兩個看似沖突的問題?
目前的方法(在後面的課程中我們還有别的方法)就是得用一個16位寄存器來做中介。
将記憶體單元中的8位資料指派到一個16位寄存器ax中,再将ax中的資料加到dx上,進而使兩個運算對象的類型比對并且結果不會超界。
程式5.5
上面的程式很簡單,不用解釋,你一看就懂。不過,在看懂了之後,你是否覺得這個程式編得有些問題?
它似乎沒有必要寫這麼長。這是累加ffff:0~ffff:b中的12個資料,如果要累加0000:0~0000:7fff中的32K個資料,按照這個程式的思路,将要寫将近10萬行程式(寫一個簡單的作業系統也就這個長度了)。
問題5.4
應用loop指令,改程序式5.5,使它的指令行數讓人能夠接受。
可以看出,在程式中,有12個相似的程式段,我們将它們一般化地描述為:
mov al,ds:[X] ;ds:X指向ffff:X單元 mov ah,0 ;(ax)=((ds)x16+(X))=(ffffXh) add dx,ax ;向dx中加上ffff:X單元的數值
從程式實作上,我們将循環做。
(al)=((ds)x16+X) (ah)=0 (dx)=(dx)+(ax)
一共循環12次,在循環開始前(ds)=ffffh,X=0,ds:X指向第一個記憶體單元。每次循環後,X遞增,ds:X指向下一個記憶體單元。
完整的算法描述如下。
初始化:
(ds)=ffffh X=0 (dx)=0
循環12次:
X=X+1
可見,表示記憶體單元偏移位址的X應該是一個變量,因為在循環的過程中,偏移位址必須能夠遞增。
這樣,在指令中,我們就不能用常量來表示偏移位址。我們可以将偏移位址放到bx中,用[bx]的方式通路記憶體單元。在循環開始前設(bx)=0,每次循環,将bx中的内容加1即可。
最後一個問題是,如何實作循環12次?
我們的loop指令該發揮作用了。更詳細的算法描述如下。
(bx)=0 (cx)=12
最後,我們寫出程式。
程式5.6
在實際程式設計中,經常會遇到,用同一種方法處理位址連續的記憶體單元中的資料的問題。
我們需要用循環來解決這類問題,同時我們必須能夠在每次循環的時候按照同一種方法來改變要通路的記憶體單元的位址。
這時,就不能用常量來給出記憶體單元的位址(比如,[0]、[1]、[2]中,0、1、2是常量),而應用變量。“mov al,[bx]”中的bx就可以看作一個代表記憶體單元位址的變量,我們可以不寫新的指令,僅通過改變bx中的數值,改變指令通路的記憶體單元。
指令“mov ax,[bx]”中,記憶體單元的偏移位址由bx給出,而段位址預設在ds中。
我們可以在通路記憶體單元的指令中顯式地給出記憶體單元的段位址所在的段寄存器。比如:
(1) mov ax,ds:[bx]
(2)mov ax,cs:[bx]
将一個記憶體單元的内容送入ax,這個記憶體單元的長度為2位元組(字單元),存放一個字,偏移位址在bx中,段位址在cs中。
(3)mov ax,ss:[bx]
将一個記憶體單元的内容送入ax,這個記憶體單元的長度為2位元組(字單元),存放一個字,偏移位址在bx中,段位址在ss中。
(4)mov ax,es:[bx]
将一個記憶體單元的内容送入ax,這個記憶體單元的長度為2位元組(字單元),存放一個字,偏移位址在bx中,段位址在es中。
(5)mov ax,ss:[0]
将一個記憶體單元的内容送入ax,這個記憶體單元的長度為2位元組(字單元),存放一個字,偏移位址為0,段位址在ss中。
(6)mov ax,cs:[0]
将一個記憶體單元的内容送入ax,這個記憶體單元的長度為2位元組(字單元),存放一個字,偏移位址為0,段位址在cs中。
這些岀現在通路記憶體單元的指令中,用于顯式地指明記憶體單元的段位址的“ds:”“cs:”“ss:”“es:”,在彙編語言中稱為段字首。
我們考慮一個問題,将記憶體ffff:0~ffff:b單元中的資料複制到0:200~0:20b單元中。
分析一下。
(1)0:200~0:20b單元等同于0020:0~0020:b單元,它們描述的是同一段記憶體空間。
(2)複制的過程應用循環實作,簡要描述如下。
将ffff:X單元中的資料送入0020:X(需要用一個寄存器中轉)
(3)在循環中,源始單元ffff:X和目标單元0020:X的偏移位址X是變量。我們用bx來存放。
(4)将0:200~0:20b用0020:0~0020:b描述,就是為了使目标單元的偏移位址和源始單元的偏移位址從同一數值0開始。
程式5.8
因源始單元ffff:X和目标單元0020:X相距大于64KB,在不同的64KB段裡,程式5.8中,每次循環要設定兩次ds。
這樣做是正确的,但是效率不高。我們可以使用兩個段寄存器分别存放源始單元ffff:X和目标單元0020:X的段位址,這樣就可以省略循環中需要重複做12次的設定ds的程式段。
改進的程式如下。
程式5.9
程式5.9中,使用es存放目标空間0020:0~0020:b的段位址,用ds存放源始空間ffff:0~ffff:b的段位址。
在通路記憶體單元的指令“mov es:[bx],al”中,顯式地用段字首“es:”給出單元的段位址,這樣就不必在循環中重複設定ds。
在8086模式中,随意向一段記憶體空間寫入内容是很危險的,因為這段空間中可能存放着重要的系統資料或代碼。比如下面的指令:
mov ax,1000h mov al,0 mov ds:[0],al
我們以前在Debug中,為了講解上的友善,寫過類似的指令。但這種做法是不合理的,因為之前我們并沒有論證過1000:0中是否存放着重要的系統資料或代碼。
如果1000:0中存放着重要的系統資料或代碼,“mov ds:[0],al”将其改寫,将引發錯誤。
比如下面的程式。
程式5.7
将源程式編輯為p7.asm,編譯、連接配接後生成p7.exe,用Debug加載,跟蹤它的運作,如圖5.18所示。
圖5.18中,我們可以看到,源程式中的“mov ds:[26h],ax”被masm翻譯為機器碼"a3 26 00",而Debug将這個機器碼解釋為"mov [0026],ax"。
可見,彙編源程式中的彙編指令“mov ds:[26h],ax”和Debug中的彙編指令"mov [0026],ax"同義。
我們看一下“mov [0026],ax”的執行結果,如圖5.19所示。
圖5.19中,是在windows2000的DOS方式中,在Debug裡執行“mov [0026],ax”的結果。
如果在實模式(即純DOS方式)下執行程式p7.exe,将會引起當機。産生這種結果的原因是0:0026處存放着重要的系統資料,而“mov [0026],ax”将其改寫。
可見,在不能确定一段記憶體空間中是否存放着重要的資料或代碼的時候,不能随意向其中寫入内容。
同樣不能忘記,我們正在學習的是彙編語言,要通過它來獲得底層的程式設計體驗,了解計算機底層的基本工作機理。是以我們盡量直接對硬體程式設計,而不去理會作業系統。
注意,我們在純DOS方式(實模式)下,可以不理會DOS,直接用彙編語言去操作真實的硬體,因為運作在CPU實模式下的DOS,沒有能力對硬體系統進行全面、嚴格的管理。
但在Windows2000、Unix這些運作于CPU保護模式下的作業系統中,不理會作業系統,用彙編語言去操作真實的硬體,是根本不可能的。硬體已被這些作業系統利用CPU保護模式所提供的功能全面而嚴格地管理了。
在一般的PC機中,DOS方式下,DOS和其他合法的程式一般都不會使用0:200~0:2ff(00200h~002ffh)的256個位元組的空間。
是以,我們使用這段空間是安全的。不過為了謹慎起見,在進入DOS後,我們可以先用Debug檢視一下,如果0:200~0:2ff單元的内容都是0的話,則證明DOS和其他合法的程式沒有使用這裡。
好了,我們總結一下:
(1)我們需要直接向一段記憶體中寫入内容;
(2)這段記憶體空間不應存放系統或其他程式的資料或代碼,否則寫入操作很可能引發錯誤;
(3)DOS方式下,一般情況,0:200~0:2ff空間中沒有系統或其他程式的資料或代碼;
(4)以後,我們需要直接向一段記憶體中寫入内容時,就使用0:200~0:2ff這段空間。