匯編語言(王爽第三版)實驗10:編寫子程序


實驗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中你看不到底層是怎么操作的,其實跟這個類似。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM