在說了許多 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 假指令是用來定義字組 ( word,DW 是 define word 之意 )的,在MASM 6.x 版以上組譯時,也可以用 WORD ( 亦即 DW 和 WORD 是一樣的意思 )。一個字組的大小有 16 位元,可以表示由 0 開始到 65535 為止,共 65536 個整數。它的語法是:
變數名 dw 數值
如果一開始並不知道該變數的數值,可以用『?』代替數值,如果想定義兩個變數,也可以用
變數名 dw 數值1,數值2
這個指令很簡單,就是將後面的兩個運算元之值互相交換,這兩個運算元可以同時是暫存器,也可以一個是暫存器另一個是變數,但不可兩個同時是變數。
再接下來的第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 的十進位值了,有許多方法可以做,但這裡採用除法,因此先來介紹除法指令,你瞭解除法指令後,就知道要怎麼寫了。
除法方式 | 被除數 | 除數 | 商數 | 餘數 | 範例 | ||
運算 | 運算前 | 運算後 | |||||
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 相容的方式組譯。要注意這兩個參數的大小寫,以及參數要在被組譯檔名之前。所以不一定版本高就是好,版本高可能功能較多,但卻不一定用得著,您還是要了解背後的原理方能運用自如,否則就像小孩耍大刀,搞不好還弄傷自己。