Ch 08 十六進位變十進位


HEX2DEC2程式

在這一章裡,我打算寫一個程式,它可以讓使用者由鍵盤輸入一個16位元(也就是從0000h到FFFFh)的十六進位數,程式會計算出該數的十進位數值並印在銀幕上。

在這個程式裡,我將會說明如何在組合語言中如何處理鍵盤輸入,然後處理成為十六進位的數字,存於暫存器中。

這個程式名叫 HEX2DEC2.COM,為何有兩個2呢?『2』英文讀音和『to』同音,故 HEX2DEC 的意思就是『HEX TO DEC』,HEX、DEC 分別表示十六進位和十進位﹔第二個『2』表示有別於上一章的程式 HEX2DEC.COM。

原理

要用組合語言得到鍵盤輸入的『字母』,必須使用中斷 16h 服務程式,這個中斷服務程式並不是由 DOS 提供的,而是由主機板上的 BIOS 提供的,因此屬於 BIOS 服務程式。INT 16h 主要的服務對象是鍵盤,INT 13h 是磁碟機,INT 14h 是通訊埠(就是 COM1、COM2 等等或是 RS-232),INT 10h 是螢幕,INT 17h 是印表機,這些都屬於 BIOS 服務程式。BIOS 服務程式就像是 DOS 服務程式一樣(即 21h 中斷),差別是 BIOS 服務程式是燒錄在一個唯讀記憶體內。

INT 16h 的服務程式不像 INT 21h 這麼多,只有三個:

在一般鍵盤上,共有 101 個鍵,每當使用者按下一個鍵時,鍵盤的電路會送電子訊號給主機板,主機板會解讀成兩個數字。其中一個稱為『掃描碼』,每一個鍵的掃描碼都不同,也就是說程式可以由掃描碼判斷那一個鍵被按下。另一個數字是鍵所代表的 ASCII 碼,當然有些鍵例如 F1、F2 這些鍵並沒有 ASCII 碼。

這樣做的好處是,有些字可以用兩種方式按出來,例如阿拉伯數字的『1』你可以使 Num Lock 燈亮起再按鍵盤右邊的鍵,這個鍵還代表 End,或者直接按鍵盤左上方的鍵,這個鍵也可以按出『!』,但是這兩個鍵的掃描碼不同,程式就可以確實知道使用者到底按出那一個鍵。雖然掃描碼不同,但是所按出來阿拉伯數字『1』的 ASCII 碼卻是一樣的。

執行 INT 16H/AH=0 這個 BIOS 中斷服務程式後,會停下來等使用者按下一個鍵,當使用者按下一個鍵後,AH 會被存入這個鍵的掃描碼,AL 會存入被按下鍵的 ASCII 碼。

取得使用者按下的數字之後,還得經過處理才能成為輸入的十六進位數。還記得如果有一個十六進位數 021Bh,其數值就是 2x162+1x16+11 吧?(如果不記得請參考數字系統註一。)我們的處理方法類似上面,每當輸入一位數字時,原先的數值乘以 16 就變成十六進位了。有關十六進位轉換成十進位前幾章已說明,就不贅述了。

原始程式列表

當我們按下任何一個鍵,電腦會把該鍵的掃描碼及 ASCII 碼分別送到 AH、AL 暫存器,而我的程式就可以取得輸入的文字了。程式如下:
;***************************************
code    segment
        assume  cs:code,ds:code
        org     100h
;---------------------------------------
start:  jmp     short begin
hex_n   dw      ?
begin:  sub     bx,bx   ;08 使BX為零,以儲存輸入的十六進位數
next:   mov     ah,0
        int     16h     ;10 呼叫BIOS鍵盤服務程式
        cmp     al,'='  ;11 若輸入等號表示要開始轉換了
        je      ok1
        mov     dl,al   ;13 將輸入的ASCII碼存入DL以利印在螢幕上
        cmp     al,'0'  ;14 檢查輸入的ASCII碼是否為阿拉伯數字
        jb      next
        cmp     al,'9'
        ja      alpha   ;15 若否,則檢查是否為A到F的英文字
        sub     al,'0'  ;18 減去30h,以得到十六進位數
        jmp     short ok0
alpha:  and     al,0dfh ;20
        mov     dl,al
        sub     al,37h  ;22 檢查是否為A到F的英文字
        cmp     al,0ah
        jb      next
        cmp     al,0fh
        ja      next    ;26

ok0:    push    ax      ;28 
        call    ok      ;29 在螢幕上印出輸入的字
        pop     ax
        mov     cl,4    ;31 將BX乘以16d
        shl     bx,cl
        cbw
        add     bx,ax   ;34 使BX加上輸入的16進位數
        mov     dh,bh   ;35 檢查是否超過BX容量
        and     dh,0f0h
        jnz     ok1     ;37 超過了
        jmp     next    ;38 沒超過

ok1:    mov     dl,'h'
        call    ok
        mov     dl,'='
        call    ok
        mov     hex_n,bx
                        
        mov     cx,10000;46開始轉換成十進位數
        mov     ax,hex_n
        sub     dx,dx
        div     cx
        xchg    ax,dx   ;50 AX=商,故與DX交換
        mov     bx,ax   ;51 印出阿拉伯數時會使DX數值改變,故餘數保存於BX
        call    print   ;52 印出十進位的萬位數

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

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

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

        mov     dx,bx
        call    print   ;79 印出個位數
        mov     dl,'d'
        call    ok

        mov     ax,4c00h
        int     21h
;---------------------------------------
;print 副程式
;輸入:DL-由 0 到 F 的十六進位數
;輸出:在螢幕上印出 DL 內的 ASCII 碼
print   proc    near
        add     dl,30h  ;90 加上 30H
        cmp     dl,'9'  ;91 比較看看是否超過 39H
        jbe     ok      ;92 沒超過直接印出
        add     dl,7    ;93 若超過再加上 7
ok:     mov     ah,2    ;94
        int     21h     ;95 印出
        ret
print   endp
;---------------------------------------
code    ends

;***************************************
        end     start

產生並執行HEX2DEC2.COM

將它用文書處理程式存成 HEX2DEC2.ASM,然後在Win95/98/Me的DOS模式或DOS中用我們所建立的批次檔來組譯、連結,然後執行:
H:\HomePage\SOURCE>hex2dec2 [Enter]
ABCDh=43981d
H:\HomePage\SOURCE>
當你在 DOS 中執行這個程式( DOS 要執行程式時並不像 Windows 9x 一樣用滑鼠按兩下,而是要輸入程式的檔名),會停下來等你輸入,程式會自動檢查你輸入的是不是在 0 到 9 或是 A 到 F 之間,若不是則沒有反應。這個程式最大只能轉換 0FFFFh,所以如果你輸入超過,也會自動截去,如果你想轉換小於 1000h 的數,可以在輸入三位之後按『=』鍵。

程式說明

在整個程式中,我用 hex_n 儲存使用者所輸入的十六進位數,因此在第七行先定義這個變數。因為最大要處理 0FFFFh 的數,所以要用一個字組的容量,定義字組用『dw』。在這個程式無法事先預測使用者輸入什麼數,所以用『?』代替,如果有設定值,也可以用數字。DW 這個指令,並不是使 8086 CPU 動作的指令,而是組譯器的指令,故也是虛指令的一種。

在輸入十六進位的過程,小木偶用 BX 暫時存放十六進位數,因此第八行先將 BX 清除變為零。第九、十行是等使用者輸入一個字的 BIOS 中斷服務程式。

第 11 行到第 19 行是檢查是否輸入『=』或阿拉伯數字 0 到 9 或英文字 A 到 F,若不是上面幾個字就回到 BIOS 中斷繼續等使用者輸入,若是的話還要印出輸入什麼字並作處理。

處理時得分三部份來作。第一,如果是『=』的話就直接跳到 ok1: 標記處,ok1: 標記是處理已經完成輸入後的程式。第二,如果是阿拉伯數字的話,就要將 BX 的數值先乘以 16,再加上剛輸入的阿拉伯數字(這裡有一個 80X86 新指令 CBW,請看底下說明),注意!阿拉伯數字還得先從 ASCII 碼變成數值。第三,如果是英文字的話,要先轉換成大寫,接下來的處理過程和阿拉伯數字一樣。

英文字母『A』的 ASCII 碼是 41h,二進位為 0100 0001B﹔而『a』的 ASCII 碼是 61h,二進位為 0110 0001B,兩者的差別只在第五個位元,大寫者為 0,小寫者為 1。故將暫存器的數值和 1101 1111B(=0dfh) 作 AND 運算就能強迫變成大寫英文字母。

ok1: 標記處(第 40 行)到第 43 行是印出『h=』這兩個字。第 44 行以後就是印出 hex_n 變數的十進位數值了。

新的指令:CBW

這個 80X86 指令是把 AL 暫存器的內容改成 16 位元,並存於 AX 暫存器中,就是 convert byte to word 的意思。只是 AL 是八位元的,要如何變成 16 位元呢?

CBW 依照以下的兩點規則:
第一、如果 AL 的最高位元是 0 的話(即 AL 小於 80h,或是第四章註一的正數),執行此指令之後 AH 會變為零,AL 之值不變。
第二、如果 AL 的最高位元是 1 的話(負數),執行此指令之後 AH 會變為 0ffh,AL 之值不變。

新的指令:PUSH 和 POP

在第 28、30 行有兩個新的 8086 指令,這兩個指令的作用是和堆疊有關。PUSH 是將資料推入堆疊,同時會使 SP 暫存器之值減 2,其語法如下:
PUSH    暫存器
PUSH    變數
因為每次進入堆疊的數都是一個字組,因此這個,推入堆疊後暫存器和變數的數值並不會改變。

POP 的功用和 PUSH 相反,它是由堆疊取出數並存入後面的暫存器或變數,同時使 SP 暫存器的數值增加 2。它和 PUSH 一樣,暫存器和變數的長度都必須是十六位元。其語法如下:

POP     暫存器
POP     變數
在使用 PUSH 和 POP 時要注意要注意,最好能成對使用,因為如果在結束程式或副程式時,都必須到堆疊中取出要返回的位址,如果堆疊內的資料有錯,就會造成當機。

第 28、30 行的這一對 PUSH 和 POP 的主要目的是保存 AX 內的數值,因為 ok: 標記的程式會破壞 AX 的內容。在程式中常常用這種方式來保存數值。

新的指令:SHL

這個指令的語法和 SHR 相同,作用也幾乎相同,不同的是 SHL 是使跟在後面的暫存器或變數內容向左移,SHR 是向右移。

SHL 除了使暫存器內的二進位數向左移數個位元外,還有乘法的功能。它可以立即算出二的冪方的乘法。例如當需要將 AX 內的數值乘以 2 時可以用

shl     ax,1

要使 AX 內的數值乘以 4 時可以用

shl     ax,2

要使 AX 內的數值乘以 8 時可以用

shl     ax,3

這種情形其實和我們十進位相似,例如當 25 向左移一位,就變成 250。差別只在一個是二進位,一個是十進位。程式第 31、32 行的原理就在這堙C當然 SHR 也有除以 2 的冪方的功用。

或許您會問,80X86 不是也有 MUL 與 DIV 指令嗎?為何不用這兩個指令呢?原因是這兩個指令所花時間太長了,所以如果乘除二的冪方數用 SHL 或 SHR 較為迅速。( DIV 指令曾在第六章與第七章介紹,至於乘法指令請看本章註二。 )


註一:鍵盤上的一些鍵,例如 Caps Lock、Num Lock、Alt,的狀態是由 INT 16H/AH=2 來取得,結果存於 AH 暫存器中,AH 有 8 個位元其意義如下表。
位元 設為一時之意義
0 按下右邊的 Shift 鍵
1 按下左邊的 Shift 鍵
2 按下 Ctrl 鍵
3 按下 Alt 鍵
4 在 Scroll Lock 狀態
5 在 Num Lock 狀態(數字)
6 在 Caps Lock 狀態(大寫)
7 在插入狀態

註二:

MUL 指令

80x86 家族的乘法指令有兩種,MUL 與 IMUL,前者是用來計算無號數,後者則是用來計算有號數的。此處僅介紹 MUL,至於 IMUL 用法請參閱第 35 章。MUL 的語法如下:

        MUL     暫存器或變數

80x86 的 MUL 指令可以用來計算 8、16、32 位元的乘法,所得的乘積大小應為 16、32、64 位元,分別存於 AX、DX:AX、EDX:EAX 暫存器中。當指令 MUL 所接運算元,暫存器或變數,大小為八位元時,就表示此運算是 AL 乘以此八位元的暫存器或變數,而所得之乘積存於 AX 暫存器堙A原先 AH 之值會消失;當 MUL 所接的運算元是 16 位元時,就表示是 AX 乘以此 16 位元的運算元,而所得之乘積存於 DX:AX 暫存器堙A原先 DX 之值會消失;當 MUL 所接的運算元是 32 位元時,就表示是 EAX 乘以此 32 位元的運算元,而所得之乘積存於 EDX:EAX 暫存器堙A原先 EDX 之值會消失。整理如下表:

乘法方式被乘數 乘數乘積 範例
運算前運算後
8 位元AL8 位元暫存器
或變數
AX AX=0105
BX=0106
MUL BL
AX=001E
BX=0106
16 位元AX16 位元暫存器
或變數
DX:AX AX=0200,DX=FFFF
BX=0106
MUL BX
DX=0002,AX=0C00
BX=0106
32 位元
只能用於
386等級以上
EAX32 位元暫存器
或變數
EDX:EAX EAX=80000001
EDX=88887777
EBX=00000002
MUL EBX
EDX=00000001
EAX=00000002
EBX=00000002

例如,如果要計算 9 乘以 8,可以用底下的方式:

        mov     al,9
        mov     bl,8
        mul     bl

這是因為 9、8 都不超過 255 ( 還記得吧?八位元的暫存器或記憶體寬度可以表示 0 到 255 範圍的正數 ),因此被乘數、乘數都存於八位元的暫存器堙A但是依據 80x86 MUL 語法,被乘數只能用 AL,所以第一行為『mov al,9』,至於乘數,8,可放於任何一個八位元的暫存器,如 AH、BL、BH……都可以,也可以放在以 DB 所定義的八位元記憶體變數堙A最後再以指令『mul bl』計算,所得乘積會在 AX 暫存器堙C

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


回到首頁到第七章到第九章