實驗10:編寫子程序
一. 子程序:顯示字符串
實驗要求:在屏幕的8行3列,用綠色顯示data段中的字符串。
名稱:show_str
功能:在指定的位置,用指定的顏色,顯示一個用0結束的字符串。
參數:(dh)=行號(0-24取值范圍);(dl)=列號(0-79取值范圍);(cl)=顏色(是一個二進制排列組合的值);ds:si指向字符串的首地址。
實驗目的:
1.熟練掌握在dos屏幕上輸出字符的基本操作。掌握顯示緩沖區范圍。
2.為什么定義字符串用0來結尾?
3.熟練掌握從內存中讀取字節單元內容和字單元內容;並將該內容寫入我們期望的內存中。
4.掌握8位乘法和16位乘法的操作。
程序分析:
在實驗九中我們可以知道一些基本信息:
命令提示符窗口或dos窗口,我們可以顯示80X25的字符(我的機器行數多,命令提示符窗口,跟設置有關)。每行80個字符,一共是25行。它們在內存中是在一個內存段中存儲的,這個內存區域叫做顯示緩沖區。從物理地址B8000H~BFFFH這個32K的內存區域就是顯示緩沖區。
在顯示緩沖區中,偶數字節單元表示的是字符,奇數字節單元表示的是字符的屬性(顏色、閃爍等)。也就是說在顯示緩沖區中,每2個字節負責屏幕上一個字符的顯示(包括顯示的屬性)。
在顯示緩沖區內寫入的字符,立即就顯示在屏幕上。
因為每行要顯示80個字符,故從0000H~009FH是顯示的第一行(共160個字節)。每行可以類推。也就是說行的偏移量是160個字節。
為什么在定義字符串時候,結尾有個0?
講解:因為在匯編和其它語言中,字符串存儲在內存中,長度不一致,我們統一規定在每個字符串的結尾有個數字0,就代表了這個字符串結束了。規定(也是便於管理)
在寫入顯示緩沖區中時,我們為什么使用[bx+di+idata]的方式?
在子程序中,我們通過計算得出了在特定行和特定列(由主程序中的dl和dh參數傳入)的基於b800:0000的偏移地址是(bx);(di)代表了從這個偏移地址開始,每個字符的偏移地址。(idata)代表了每個字符的二個字節(一個是字符本身,一個是字符的顏色屬性)。
代碼如下:
assume cs:code
data segment
db 'Welcome to masm!', 0 ;內存data段中定義一個字符串
data ends
code segment
main: ;字符串參數
mov dh, 8 ;屏幕的行數
mov dl, 3 ;所在行的列數
mov ch, 0 ;ch清零,防止高8位不為零。
mov cl, 2 ;顏色屬性(此處應是二進制數0000 0010)
mov ax, data
mov ds, ax
mov si, 0 ;將ds:si指向字符串
call show_str
mov ax, 4c00H
int 21H
;show_str功能 :按行和列及字符屬性顯示字符串
;入口參數:dh-行數、dl-列數、cl-字符屬性、ds:[si]指向字符串。
;返回值:無
show_str: push dx
push cx
push si ;將子程序用到的寄存器入棧
mov ax, 0b800H
mov es, ax ;設置顯示緩沖區內存段
mov ax, 0 ;(ax)= 0,防止高位不為零
mov al, 160 ;0a0H- 160字節/行
mul dh ;相對於0b800:0000第dh行偏移量
mov bx, ax ;將第(dh)行的偏移地址送入bx,bx代表行偏移
mov ax, 0
mov al, 2 ;列的標准偏移量是2個字節
mul dl ;同一行列的偏移量,盡量使用乘法,(al)=列偏移
add bx, ax ;最終獲得偏移地址(bx)=506H
mov di,0 ;將di作為每個字符的偏移量
mov al, cl ;將字符屬性寫入al中
mov ch, 0 ;將cx高8位設置為0
show: mov cl, ds:[si] ;將字符串單個字符讀入cl中
jcxz ok ;判斷字符串是否為零。
mov es:[bx+di+0], cl ;在顯示緩沖區中寫入字符
mov es:[bx+di+1], al ;在顯示緩沖區中寫入字符屬性
add di, 2
inc si
jmp short show
ok: pop si ;字符串字符為0,結尾
pop dx
pop cx ;恢復寄存器
ret
code ends
end main
程序體會:
1).參數的傳遞.。此程序有3個參數,它們是:dh)=行號(0-24取值范圍);(dl)=列號(0-79取值范圍);(cl)=顏色。參數的傳遞方式是傳遞值。
通過修改這3個參數,我們可以方便的將data段中定義的字符串顯示在我們需要的位置。
2).子程序的調用,我們可以多次調用該子程序,用於顯示特定的字符串。只要知道字符串的地址,我們不必關心子程序內部是怎樣運算的。也就是說只要我們指定ds:si的指向就可以了。那么我們使用實驗10的代碼,改寫實驗9的程序就輕松了。只需要在主程序中添加參數和再次call下就行了。
3).只要給出相關的參數值,調用子程序,我們就可以達到我們預期的目的(子程序設計的目的)
4).還是要熟悉從內存中讀入一個字符,我們采用的是ds:[si] 方式;寫入到顯示緩沖區中(同樣是內存,它們沒有任何的區別,CPU把所有的設備都內存化了),我們采用了
es:[bx+di+idata]的方式。由於ds寄存器被data段占用了,目前只有es寄存器可用,只好把es當做了顯存的段寄存器,bx代表了行偏移、di代表了列偏移、idata(值是0和1)代表了列的2個字節(2個字節代表一個字符的顯示)的偏移。
5)這個程序是固定了行和列,我們也可以通過程序來提示行和列,做到人機交互,這個本章沒有涉及。呵呵。
6)關於接口的問題,我們查找其他資料了解。
二. 解決除法溢出的問題
問題提出:
考慮下面代碼1:
mov bh, 1
mov ax, 1000
div bh
程序分析:由於除數是(bh),故div是執行的8位除法,(ax)/(bh)=1000(結果);結果的商(1000)應該存放在al中,結果的余數(0)存放在ah中;從代碼我們得知,它的結果的商是1000,al是8位寄存器,保存的數值(0~255如果按照無符號數運算)超出了存儲范圍。
考慮下面代碼2:
mov ax, 1000H
mov dx, 1
mov bx, 1
div bx
程序分析:由於除數是(bx),故div是執行的16位除法,被除數應該是:(dx)高16位,與(ax)低16位組合在一起=11000H,故11000H/(bx)=11000H(結果)。結果的商(11000H)存入ax中,結果的余數(0)存入到dx中;由於ax寄存器不能存儲11000H數值,導致溢出。
除法溢出:除法操作時,由於運算結果商的值過大,超出ax寄存器的存儲范圍,導致ax寄存器不能存儲該值。CPU將引發一個內部錯誤:除法溢出。
展示下如何導致除法的溢出。(在debug中直接演示)
解決方法:編程一個子程序。
名稱:divdw
功能:進行不會產生溢出的除法運算,被除數為dword型,除數為word型,結果為dword型。
參數:(ax)=dword型被除數的低16位
(dx)=dword型被除數的高16位
(cx)=除數
返回值:(ax)=dword型結果的低16位
(dx)=dword型結果的高16位
(cx)=除數
例子:計算1000000/10(F4240H/0AH)
mov ax, 4240H
mov dx, 000FH
mov cx, 0AH
call divdw
程序分析:
1)首先判斷這個無符號數值的除法運算;1000000==F4240H這個數ax寄存器肯定存儲不下,1000000/10==F4240H/0AH=186A0H(結果),結果ax也存儲不下,結果的余數(0)dx倒是可以存儲。這種情況CPU會發生內部錯誤:除法溢出。
2)我們將被除數(一個雙字單元,4個字節)的高16位(000FH)存儲在dx中,將低16位(4240H)存儲在低16位中;除數(0AH)存儲在cx中。
3)調用子程序divdw,子程序的返回值,高16位存儲在dx中,將低16位存儲在低16位中;除數依然不變存儲在cx中。
4)考慮將被除數和除數定義在內存data段中,通過內存讀入到寄存器中,這樣符合設計思想。在此例子中,似乎王老師希望直接在寄存器賦值。
5)我們不必糾結這個公式的推算,這是數據結構中算法負責研究的事情,我們只管負責把這個推算的公式匯編語言代碼化。
6)分析這個公式:
X是被除數:(范圍[0~FFFFFFFFH]),也就是0F4240H
N是除數:(范圍[0~FFFFH]),也就是0AH
H:高16位(范圍[0~FFFFH]),對於被除數來說就是000FH
L:低16位(范圍[0~FFFFH]),對於被除數來說就是4240H
int():取商,int(H/N)也就是求H/N結果的商。
rem():取余,rem(H/N)也就是求H/N結果的余數。
公式:X/N=int(H/N)*65536+[rem(H/N)*65536+L]/N
噢!MY GOD!你還能不能再把這個爛公式簡單點???這個有點彎彎繞。大部分人看不懂這個公式。
解讀這個公式(希望我們不要把時間花費在這個上面,這是數據結構的問題):
明確65536是什么東東?10000H;等價於左移16位,也就是說此例中代表了高位寄存器。
X/N代表,你懂的!!! X(被除數)代表一個dw類型的雙字單元,N(除數)代表一個字單元。
int(H/N)*65536代表了X/N結果高16位
[rem(H/N)*65536+L]/N代表了X/N結果的低16位
+代表了將它們的組合一起,代表了一個dw類型的雙字單元。
程序分析:
首先:我們先求H/N,這個有結果了,公式的二個部分都有結果了,商=int(H/N);
余數:rem(H/N)。
要使用div指令,div cx,最初:(ax) =4240H(低16位);(dx)=000FH(高16位);(cx)=0AH(除數)。
由於ax中目前有值,先將其壓棧保存。
然后(ax)=(dx)(高16位的數值),(dx)=0000H,(cx)不變,也就是說0000 000FH/000AH。結果的商為0001H,存儲在了ax中,此時(ax)=0001H,結果的余數存儲在dx中了,此時(dx)=0005H。
然后我們求出int(H/N)*65536,結果的高16位:
H/N的結果有了,商是0001H(等價於int(H/N)),保存在了ax中。這個數我們另存儲在一個寄存器中(由於后面需要ax參與運算)。此例中我們把這個值先保存在了bx中;
為毛*65536?表明了它是一個數據(2個字單元)的高16位。也就是說0001H代表了最終結果的高16位的數值。
其次我們求出[rem(H/N)*65536+L]/N ,運算結果商代表最終結果的低16位,運算結果的余數,代表最終結果的余數:
理解公式的后部分:同理:[rem(H/N)*65536+L]也代表一個數值:高16位是rem(H/N)*65536(使用這個表示),其實存儲在高16位寄存器變量中的數值是rem(H/N);為什么*65536,表示它左移了16位,代表高16位。低16位是L,就是最初存儲在ax中的那個數值。將它們二者組合后,形成一個新的數值,這個新的數值與N(始終保持不變的cx變量)做除法運算,運算結果的商=(ax),余數=(dx)。也就是說此時的(ax)就是低16位的數值。
通俗的講就是將H/N的余數(等價於rem(H/N))作為高16位;將L作為低16位;
將它們組合成一個數值,與cx做除法運算。此時我們的H/N運算結果的余數存儲在dx中了,是0005H,正好,就是我們需要的數值;L代表了低16位的值,這個值在棧中呢,彈棧到ax(把它恢復就可以了pop ax),那么(dx)=0005H,(ax)=4240H,(cx)=0AH,將它們組合后形成一個dw型的雙字數值:0005H+4240H=00054240H。也就是00054240H/0AH;結果的商存儲在ax中(此時ax=86A0H),余數是0存儲在dx中。
最終結果的高16位的值是:0001H(我們把它存儲在了bx中),把它送入dx中去;低16位就是ax值,除數cx值不變。也就是說最終結果是186A0H。
匯編代碼如下:
assume cs:code
code segment
start:
mov ax, 4240H ;被除數,低16位
mov dx, 000FH ;被除數,高16位
mov cx, 0AH ;除數
call divdw ;調用divdw子程序,做不溢出的除法運算。
mov ax, 4c00H
int 21H
divdw: ;子程序開始
push ax ;將被除數低16位先壓棧保存。
mov ax, dx ;(ax)=(dx)
mov dx, 0000H ;
div cx ;此時(dx)=0000H,(ax)=000FH,組合成0000000FH。
mov bx, ax ;將H/N結果的商先保存在bx中,(bx)=0001H
pop ax ;將L值彈棧到ax
div cx ;此時(dx)=0005H,(ax)=4240H,組合成54240H
mov cx, dx ;返回值(cx)等於最終結果的余數
mov dx, bx ;最終結果高16位值=(bx)
ret
code ends
end start
在debug中運行結果是:
AX=86A0 BX=0001 CX=0000 DX=0001 SP=FFFE BP=0000 SI=0000 DI=0000
DS=0B56 ES=0B56 SS=0B66 CS=0B66 IP=0022 NV UP EI PL NZ NA PO NC
結果分析:與F4240H/0AH=186A0H(商)結果一樣,余數:(cx)=0000H
實驗目的:
1)考察對於一個較大的數值的存儲,無論是在寄存器中還是在內存中。
2)熟悉div除法指令的內涵,它的操作數存儲的寄存器是那些?運算的結果又存儲在那些寄存器中。
3)合理例如棧空間保存一些臨時的數值。
改進程序:這個程序看着有點累,如果將這個被除數存儲在內存中,代碼就顯得好理解些,有興趣的自行修改。
三。數值顯示
問題提出:
編程,將data段中定義的數據以十進制的方式顯示出來(在計算機屏幕上)
data segment
dw 123, 12366, 1, 8 , 3, 38
data ends
編程分析:
1)確定123, 12366, 1, 8 , 3, 38這些數在內存中是以二進制形式存儲的(二進制補碼),2個字節存儲一個數字。12366=(0011 0001 0111 1010B)=(317AH)
2)在計算機屏幕上顯示的數字、字符、其他符號,一律按照字符方式顯示,都是按照ASCII碼來處理的。也就是說在屏幕上顯示的1它不代表數值1,而是字符1。我們遇到的問題變成了怎樣把二進制代碼(補碼方式存儲的)變成ASCII碼;
提示:0~9字符在ASCII碼中是30H~39H,是否有規律?0=30H、1=30 H +1、……9=30H+9。
3)在底層顯卡顯示方面,由於我們知道了CPU只要在顯卡的顯存中寫入期望的數據,它就顯示在屏幕上,那么我們就可以利用實驗10第一個子程序了。
4)怎樣將一個十進制的數值轉變為表示十進制數的字符串,並且字符串以0為結尾符號。例如:將數值12666轉變成一個字符串“12666”,也就是說得到1,2,6,6, 6的ASCII碼,他們分別是31H、32H、36H、36H,36H。怎樣得到十進制的各個數字呢?我們可以使用將12666除以10,然后取余數,將余數倒序后,就得到了12666的各個位的數字了,對於12666搞個循環,5次,就將它們搞定了。這里注意余數的順序。
怎樣轉換成ASCII碼?字符0(ASCII碼30H),同理,字符3就是30H+3,總結:30H+余數就是對應的ASCII碼。
但是對於我們不知道的一個十進制數字,怎么判斷各位的值求出來呢?只要保證結果的商是0,那么這個數除以10肯定結束了。這樣我們可以使用jcxz指令判斷(CX)是否為0(將結果的商每次送入到cx中),作為結束循環條件。
5)由於(ax)/10的求商(求各個位的數字)的順序是倒序的(原理看書吧!)。怎樣把它的順序給倒過來?我們可以采用棧的結構,利用棧的先進后出的原理,彈棧時將棧頂的值(也就是數字的最高位的值)先寫入內存data段中。這樣就解決了字符順序的問題。
也可以判斷該數字一共有幾個數字組成,然后在寫入內存時,si的值是從大到小遞減也可以。
還是利用系統給你的棧結構吧。那個是免費了,不用費事了!
6)編寫子程序dtoc,功能是將ax中的存儲十進制數值(傳入參數(ax))轉換成對應的ASCII碼,並將這些字符按順序寫入到內存data段中。
7)調用子程序show_str,顯示該data段的字符串。完成在計算機屏幕中顯示12666這個字符串的功能。
匯編代碼如下:
assume cs:code
data segment
db 10 dup (0) ;初始化10個字節,置零
data ends
code segment
start: mov ax, 12666 ;將顯示的數字賦值給ax
mov bx, data
mov ds, bx
mov si, 0 ;將ds:si指向data內存段
call dtoc ;調用dtoc子程序
;為調用show_str做准備
mov dh, 8 ;屏幕的行數
mov dl, 3 ;所在行的列數
mov ch, 0 ;ch清零,防止高8位不為零。
mov cl, 2 ;顏色屬性(此處應是二進制數0000 0010)
call show_str ;調用show_str子程序將字符串顯示?
mov ax, 4c00H
int 21H
;-----
;dtoc功能:將一個數字轉換成字符串,並寫入data段中。
;入口參數:ax, ds
;返回值:無
;-----
dtoc: ;保護寄存器變量值,因為下面的變量子程序都用到。
push ax
push cx
push bx
push si
mov si, 0 ;偏移地址置零
mov bx, 10 ;除數=10
change: mov dx, 0 ;涉及到16位除法,先將存儲余數的變量置零
div bx ;將(ax)/(bx)
mov cx, ax ;將除法運算結果的商賦值給cx,用於條件判斷 jcxz last ;判斷cx是否為0?或商為零?
add dx, 30H ;將每個位的數字轉換成ASCII碼值
push dx ;將ASCII碼值壓棧保存
inc si
jmp short change
last: ;最后一次除法,商為0,(dx)=余數時,沒有轉換並壓棧。故。。。。。。
add dx, 30H ;將數字轉換成ASCII碼
push dx ;將字符值壓棧
inc si ;最后一次也要轉換並壓棧
;將棧中數據倒序寫入內存data段中
mov cx, si ;(si)=字符串共幾個字符,設置循環計數器cx。
mov si, 0
s: pop ds:[si] ;彈棧,並寫入data內存段。
inc si
loop s
exit: ;恢復寄存器,並返回主調程序。
pop si
pop bx
pop cx
pop ax
ret
;------
;show_str功能 :按行和列及字符屬性顯示字符串
;入口參數:dh-行數、dl-列數、cl-字符屬性
;返回值:?
;------
show_str: push dx
push cx
push si ;將子程序用到的寄存器入棧
mov ax, 0b800H
mov es, ax ;設置顯示緩沖區內存段
mov ax, 0 ;(ax)= 0,防止高位不為零
mov al, 160 ;0a0H- 160字節/行
mul dh ;相對於0b800:0000第dh行偏移量
mov bx, ax ;將第(dh)行的偏移地址送入bx,bx代表行偏移
mov ax, 0
mov al, 2 ;列的標准偏移量是2個字節
mul dl ;同一行列的偏移量,盡量使用乘法,(al)=列偏移
add bx, ax ;最終獲得偏移地址(bx)=506H
mov di,0 ;將di作為每個字符的偏移量
mov al, cl ;將字符屬性寫入al中
mov ch, 0 ;將cx高8位設置為0
show: mov cl, ds:[si] ;將字符串單個字符讀入cl中
jcxz ok ;判斷字符串是否為零。
mov es:[bx+di+0], cl ;在顯示緩沖區中寫入字符
mov es:[bx+di+1], al ;在顯示緩沖區中寫入字符屬性
add di, 2
inc si
jmp short show
ok: pop si ;字符串字符為0,結尾
pop dx
pop cx ;恢復寄存器
ret
code ends
end start
程序理解:
1)子程序show_str的代碼沒有任何改變,拿來直接用就可以了。注意在匯編語言編程中代碼段的框架結構。
2)如果遇到的數字數值過大?可以考慮實驗第二個子程序:divdw來解決問題。此時也應該考慮這個數字位數多,在data段中多初始化內存空間;
3)合理利用系統提供的棧結構,或程序員創建的棧結構,提高臨時存儲數據的效率。
4)有時間看看ASCII的有關資料。幫助你理解字符及字符串。
5)我們在寫入data內存段時,結尾沒有0,這個不必糾結,在我們初始化data時,都置零了,也就是說“12666”后面有零。字符串后面有0,為什么?我們以前介紹了。
C語言隨想:看來我們還是懷念C,為了在屏幕上顯示字符串,費勁太大了。C語言一個語句就搞定了。但在C中你看不到底層是怎么操作的,其實跟這個類似。