Ch 07 顯示十進位數


在說了許多 ASCII 碼之後,也大致瞭解十六進位數字,在這一章裡,我想來談談電腦是如何把在電腦中儲存的十六進位數,轉換成十進位數。

數字系統中,已說明了十進位與十六進位的關係,但是那是以人腦的想法為出發點,若想把這個想法轉換成組合語言程式,並不能像在數字系統那篇附錄裡的做法。

要把十六進位數(就是含有0、1、2、3……9、A、B……F的字)轉換成只有0、1、2……9的阿拉伯數字。這個問題其實可以用除法來解決。因為不管是十進位或是十六進位,其『量值』是相同的,例如假設你有170cm高,以十進位表示是170cm,以十六進位表示是AA,但是你還是170cm高。若把AAh(也就是170d)除以100d,其商必然為1,且餘數為70d。看到這兒,想必你已經知道,要把十六進位變成十進位,就是使用連續除法。現在來看看實例:

;***************************************
code    segment
        assume  cs:code,ds:code
        org     100h
;---------------------------------------
start:  jmp     short begin
hex_n   dw      0ffeeh  ;07 要轉換的數字
begin:  mov     dx,hex_n;08 到17行印出 FFEEh=
        xchg    dl,dh   ;09 DL、DH互相交換
        mov     cl,4
        call    dl_reg  ;11 印出 FF
        mov     dl,dh
        call    dl_reg  ;13 印出 EE
        mov     dl,'h'
        call    ok
        mov     dl,'='
        call    ok

        mov     cx,10000
        mov     ax,hex_n
        sub     dx,dx
        div     cx
        xchg    ax,dx   ;23 AX=商,故與DX交換
        mov     bx,ax   ;24 印出阿拉伯數時會使AX數值改變,故餘數保存於BX
        call    print   ;25 印出十進位的萬位數

        mov     ax,bx
        sub     dx,dx
        mov     cx,1000
        div     cx
        xchg    ax,dx
        mov     bx,ax
        call    print   ;33 印出千位數

        mov     ax,bx
        sub     dx,dx
        mov     cx,100
        div     cx
        xchg    ax,dx
        mov     bx,ax
        call    print   ;41 印出百位數

        mov     ax,bx
        sub     dx,dx
        mov     cx,10
        div     cx
        xchg    ax,dx
        mov     bx,ax
        call    print   ;49 印出十位數

        mov     dx,bx
        call    print   ;52 印出個位數

        mov     dl,'d'
        call    ok

        mov     ax,4c00h
        int     21h     ;58 結束
;---------------------------------------
dl_reg  proc    near
        mov     bh,dl
        shr     dl,cl
        call    print   ;63 印出DL較高的四個位元所代表的數字
        mov     dl,bh
        and     dl,0fh
        call    print   ;66 印出DL較低的四個位元所代表的數字
        ret
dl_reg  endp
;---------------------------------------
;print 副程式
;輸入:DL-由 0 到 F 的十六進位數
;輸出:在螢幕上印出 DL 內的 ASCII 碼
print   proc    near
        add     dl,30h  ;74 加上 30H
        cmp     dl,'9'  ;75 比較看看是否超過 39H
        jbe     ok      ;76 沒超過直接印出
        add     dl,7    ;77 若超過再加上 7
ok:     mov     ah,2    ;78
        int     21h     ;79 印出
        ret
print   endp
;---------------------------------------
code    ends
;***************************************
        end     start

將它存成HEX2DEC1.ASM,並組譯、連結、改成HEX2DEC1.COM ( 註一 ),執行。底下就是執行結果,黃色是我們的所寫的程式名,天空藍的字就是執行結果

h:\homepage\asm_program>hex2dec1
FFEEh=65518d
h:\homepage\asm_program>

注意!在DOS中,大小寫是不分的,因此執行程式時,輸入大寫或小寫均不影響。

接下來解說這個程式。程式由第8行開始,首先將十六進位的 FFEEh 載入到 DX 暫存器中,此時 DH=FFh 而 DL=EEh,然後要印出『FFEEh=』的字樣,故先交換 DH、DL 的內容。


DW 假指令

DW 假指令是用來定義字組 ( word,DW 是 define word 之意 )的,在MASM 6.x 版以上組譯時,也可以用 WORD ( 亦即 DW 和 WORD 是一樣的意思 )。一個字組的大小有 16 位元,可以表示由 0 開始到 65535 為止,共 65536 個整數。它的語法是:

變數名    dw       數值

如果一開始並不知道該變數的數值,可以用『?』代替數值,如果想定義兩個變數,也可以用

變數名    dw       數值1,數值2

XCHG 交換指令

這個指令很簡單,就是將後面的兩個運算元之值互相交換,這兩個運算元可以同時是暫存器,也可以一個是暫存器另一個是變數,但不可兩個同時是變數。


再接下來的第10行到第17行是印出『FFEE=』字樣的程式,因為所要轉換的數共 16位元,每四個位元印一次,而每一次所執行的內容都相像,故寫成『一串的』副程式。這些副程式的最底端是 print 副程式,它是專門負責把 DL 內的數字轉換成 ASCII 碼,印在銀幕上,而在執行此副程式之前,DL 內的值必須是在 00、01、02……09、0A、0B、0C……0F 之間的數字,否則會印出一些看不懂的亂碼來。

而 DL_REG 副程式則是會把 DL 暫存器的內容印在螢幕上,為了配合 print 副程式,所以必須先將 DL 的較高的四個位元向右移位四次,使其較高的四個位元變成 0,這就是第 62 行要做 shr dl,cl 的原因。在印出較高的四個位元之後,緊接著要印出較低的四個位元,故須先將 DL 之值保存起來,這就是第61行執行 mov bh,dl 指令的原因,我將它存在 BH 暫存器中。

在先後印出 DH、DL 的內容後,接著印出『h=』的字來,這裡比較特別的是,我並沒有宣告副程式,就直接呼叫 ok: 標記(見第15行),因為在跳到 ok: 標記(第78行)之後執行一段指令就會遇到 RET 返回第16行,因此不會出錯,這種用法很有彈性,可以節省不少空間,當然也造成維護上的麻煩。要小心使用。

再接下來,就是要求出 FFEEh 的十進位值了,有許多方法可以做,但這裡採用除法,因此先來介紹除法指令,你瞭解除法指令後,就知道要怎麼寫了。


再談 DIV 除法指令

80X86 的除法分兩種,十六位元和三十二位元,若您的 CPU 是 80386 或比它更高級的 CPU 也可以用 64 位元的除法。若是被除數小於 65535d 就採用十六位元的除法,這時被除數固定放在 AX 暫存器中,除數可以是 BH、BL、CH、CL 等八位元的暫存器或變數,計算結果商放在 AL,餘數放在 AH ﹔如果是被除數大於或等於 65536d 就採用三十二位元的除法,此時被除數擺在 DX:AX 中,意思是較高位元的部分放在 DX,較低位元的部分放在 AX 中,除數可以是 BX、CX 或其他十六位元的變數,計算結果商放在 AX,餘數放在 DX。列一張表或許比較清楚:

除法方式 被除數 除數 商數 餘數 範例
運算 運算前 運算後
16位元 AX BH等8位元
暫存器或變數
AL暫存器 AH暫存器 00AAh除以12h
等於09h餘08h
AX=00AAh
BL=12h
AX=0809h
(AL=商,AH=餘)
BL=12h
32位元 DX:AX BX等16位元
暫存器或變數
AX暫存器 DX暫存器 1234FEDCh除以5678h
等於35E7h餘2094h
AX=FEDCh
DX=1234h
BX=5678h
AX=35E7h(商)
DX=2094h(餘)
BX=5678h
64位元
只能用於
386等級以上
EDX:EAX EBX等32位元
暫存器或變數
EAX暫存器 EDX暫存器 0FEDCBA12345000h
除以10000000h
等於FEDCBA1h餘2345000h
EAX=12345000h
EDX=0FEDCBAh
EBX=10000000h
EAX=0FEDCBA1h(商)
EDX=2345000h(餘)
EBX=10000000h

DIV 指令一般用在對無號數的計算,無號數所指的就是正數,如果要計算有號數,請參考第 35 章

在使用 DIV 指令之前,最好能事先預估商數的大小,如果太大而超過暫存器的容量,就會出現當機,要小心使用,例如若你用 1234FEDCh 除以 12h,就會當機。筆者初學時,就曾因為每次執行到這個指令就當機,百思不得其解。為了簡化起見在這個程式裡,所能轉換的十六進位數最大不可超過十進位的十萬。或許你會問,如果商真的太大,怎麼辦呢?可以改成用 64 位元除法來計算。


第19行到第25行就是計算並印出萬位數。在第19行,將一萬存於 CX 中,以便之後用 CX 當除數,因為我為了簡化起見,被除數是小於 0FFFFh 的數,故第21行把 DX 設為零。運算完之後將商數以 print 副程式印出來,但是商數存於 AX 中,還記得 print 副程式得把要印出的數放在 DX 中,所以第23行使 DX、AX 互相交換,再把 AX 存放在 BX 中,以免餘數遭到 print 副程式的破壞。

其他印出千位數、百位數、十位數都用類似的方法,而個位數就不用除法再做計算了,因為除以10後的餘數就是個位數了。

你可以修改這個程式的 hex_n 這個變數,再重新組譯、連結、執行,就可以把十六進位數轉換成十進位數,但這樣做每轉換一個十六進位數就得重新修改原始程式、組譯、連結,很是麻煩,下一章將改良這個程式,使它可以由鍵盤輸入十六進位數,再轉換成十進位數。

至於超過十萬的數,就無法用這個程式來轉換,有幾個方法可供使用,有一種就是利用 FPU 的特性,請參考第 23 章到第 25 章。


註一︰常有許多人向小木偶反應,HEX2DEC1.ASM 因為有個未定義的標號『ok:』,所以不能組譯。但事實上在程式第 78 行已經定義了,之所以會造成這樣的錯誤是因為使用 MASM 6.0 及其以後的版本組譯,限制了 call 指令只能呼叫副程式 ( 即以 proc/endp 假指令定義 ),而不能呼叫標記,如此限制是為了增加程式的結構化以及較高的可讀性。當然也造成了一些不便。

解決方法有三。一是改用 MASM 5.x 的版本組譯,這個版本不會限制 call 指令一定要呼叫副程式,它也可以呼叫標記,但是小木偶想要找到這個版本可能也不太容易。第二種做法是另外在原始程式中加入一段副程式,此副程式重複程式第 78 行到第 80 行,然後把所有的 call ok 改成 call 此副程式,但這樣的做法會增加程式碼數個位元組的長度。第三個方法是用 MASM 6.0 以後的版本組譯,但加入『/AT /Zm』兩個參數,實際操作過程如下︰

H:\HomePage\SOURCE>..\MASM611\ml /AT /Zm hex2dec1.asm [Enter]
Microsoft (R) Macro Assembler Version 6.11
Copyright (C) Microsoft Corp 1981-1993.  All rights reserved.

 Assembling: hex2dec1.asm

Microsoft (R) Segmented Executable Linker  Version 5.31.009 Jul 13 1992
Copyright (C) Microsoft Corp 1984-1992.  All rights reserved.

Object Modules [.obj]: hex2dec1.obj/t
Run File [hex2dec1.com]: "hex2dec1.com"
List File [nul.map]: NUL
Libraries [.lib]:
Definitions File [nul.def]:

H:\HomePage\SOURCE>

/AT 參數是表示組譯成 *.COM 檔,/Zm 參數是表示以 MASM 5.1 相容的方式組譯。要注意這兩個參數的大小寫,以及參數要在被組譯檔名之前。所以不一定版本高就是好,版本高可能功能較多,但卻不一定用得著,您還是要了解背後的原理方能運用自如,否則就像小孩耍大刀,搞不好還弄傷自己。


回到首頁到第六章到第八章