Ch 14 BCD 數(2):BCD 之輸入與乘法

非聚集 BCD 乘法原始程式

在這一章堣p木偶將延續上一章的觀念,實作一個任意位數之乘法程式。使用者將可以輸入被乘數與乘數,而程式將自動算出乘積並顯示於螢光幕上。這個程式在位址的計算上相當複雜煩人,要有一些耐心。

原始程式如下:

digit           equ     20      ;01 位數
cr              equ     0dh     ;02 歸位字元
lf              equ     0ah     ;03 換行字元
escape          equ     1bh     ;04 Esc 鍵
backspace       equ     08h     ;05 倒退鍵
;***************************************
code    segment
        assume  cs:code,ds:code
        org     100h
        extrn   ubcd_print:near,ubcd_add:near
;---------------------------------------
start:  jmp     begin
;資料區
mes1    db      cr,lf,'輸入被乘數:$'
mes2    db      cr,lf,'輸入乘數:$'
v1      db      digit dup (0),cr        ;16 被乘數
v2      db      digit dup (0),cr        ;17 乘數
product db      digit*2 dup (0),cr      ;18 乘積
len_v1  db      ?                       ;19 被乘數位數
len_v2  db      ?                       ;20 乘數位數
last_ad dw      ?                       ;21 此程式最高位址再加20H

;求此程式的最高位址再加20H
begin:  mov     ax,offset end_prog      ;24 取得 print_cr_lf 副程式最後位址
        add     ax,3bh+39h+20h
        and     ax,0fff0h
        mov     last_ad,ax              ;27 存入 last_ad 變數

        mov     dx,offset mes1  ;29 輸入被乘數
        mov     ah,9
        int     21h
        mov     cx,digit
        mov     si,offset v1
        call    ubcd_input
        mov     len_v1,ch       ;35 將輸入位數存於 len_v1

        mov     dx,offset mes2  ;37 輸入乘數
        mov     ah,9
        int     21h
        mov     cx,digit
        mov     si,offset v2
        call    ubcd_input
        mov     len_v2,ch       ;43 將輸入位數存於 len_v2

        mov     ax,offset v1    ;45 計算乘積
        mov     bx,offset v2
        mov     dx,offset product
        mov     ch,len_v1
        mov     cl,len_v2
        call    ubcd_mul        ;50
        call    print_cr_lf     ;51 換行

;印出『被乘數』『*』『乘數』『=』字樣
        mov     si,offset v1    ;54
        mov     cx,digit
        call    ubcd_print
        mov     dl,'*'
        call    print_char
        mov     si,offset v2
        mov     cl,len_v2
        sub     ch,ch
        call    ubcd_print
        mov     dl,'='
        call    print_char      ;64

;計算乘積位數=『被乘數位數』加『乘數位數』
        mov     cl,len_v1       ;67
        add     cl,len_v2
;印出『乘積』
        mov     si,offset product
        call    ubcd_print      ;71

        mov     ax,4c00h
        int     21h             ;74 結束
;---------------------------------------
;目的:由鍵盤輸入十進位數並轉換成最大位數於高位址的非聚集BCD數
;輸入:SI:非聚集BCD之最低位址
;      CX:非聚集BCD之最大位數,但CH必為0
;輸出:CL:非聚集BCD之最大位數,與輸入值之CL相同
;      CH:實際輸入位數
;      AL:0表示輸入正常,1BH表示按下Esc鍵
;      AH、DX:會被破壞,其餘不變
ubcd_input      proc    near    ;83
        push    bx
        push    di

;於暫存區(temp_ubcd_rev)中輸入非聚集BCD數(最大位數於低位址)
        sub     bx,bx           ;88 BX設為零,爾後BX為temp_ubcd_rev之指標
ipn0:   mov     ah,0
        int     16h             ;90 輸入阿拉伯數字之 ASCII
        cmp     al,escape
        je      ipn4
        cmp     al,cr
        je      ipn1
        cmp     al,backspace
        je      ipn5
        cmp     al,'0'          ;97 檢查是否為阿拉伯數字
        jb      ipn0
        cmp     al,'9'
        ja      ipn0
        mov     dl,al
        call    print_char
        sub     al,'0'          ;103 使之變成BCD的一位數
        mov     temp_ubcd_rev[bx],al
        inc     bx              ;105 指標加一
        cmp     bx,cx           ;106 CX為最大位數
        jne     ipn0

;將temp_ubcd_rev之數字轉換成大位數在高位元的BCD數
ipn1:   or      bx,bx           ;110 檢查是否只輸入Enter鍵
        je      ipn6            ;111 是則跳至ipn6標記處
        sub     di,di           ;112 DI由暫存區的低位址處開始
        mov     ch,bl           ;113 設定實際輸入位數
ipn2:   mov     al,temp_ubcd_rev[di]    ;114 取得暫存區BCD數字
        mov     [si+bx-1],al    ;115 存入父程式指定位址
        inc     di
        dec     bx
        jnz     ipn2
ipn3:   sub     al,al           ;119 正常返回父程式
ipn4:   pop     di
        pop     bx
        ret

;按下Backspace(倒退鍵)
ipn5:   or      bx,bx           ;125 檢查是否已經到最前面
        je      ipn0
        mov     dl,al           ;127 游標倒退一格
        call    print_char
        mov     dl,' '          ;129 消去螢幕上的舊資料
        call    print_char
        mov     dl,backspace    ;131 游標倒退一格
        call    print_char
        dec     bx              ;133 temp_ubcd_rev之指標減一
        jmp     ipn0

;只輸入Enter鍵,即使用者輸入『0』
ipn6:   mov     ch,1
        mov     byte ptr [si],0
        jmp     ipn3

print_char:
        mov     ah,2
        int     21h
        ret

;暫時存放輸入BCD數處,此數字大位數在低位址處
temp_ubcd_rev   db      digit dup (0)   ;147
ubcd_input      endp
;---------------------------------------
;計算兩非聚集BCD數之乘積        ;150
;輸入:AX:被乘數位址
;      BX:乘數位址
;      DX:乘積位址
;      CH:被乘數位數
;      CL:乘數位數
;輸出:於DX所指的位址列出乘積
;      AX、BX、CX、DX、SI、DI均被破壞
ubcd_mul        proc    near
        mov     l_adr_v2,bx     ;159 將被乘數、乘數最低位址分別存於
        mov     l_adr_v1,ax     ;160 l_adr_v1、ladr_v2
        sub     bx,bx           ;161 計算被乘數最大位數所在位址,並存於
        mov     bl,ch           ;162 h_adr_v1
        add     ax,bx
        mov     h_adr_v1,ax
        mov     bl,cl           ;165 計算乘數最大位數所在位址,並存於
        add     bx,l_adr_v2     ;166 h_adr_v2
        mov     h_adr_v2,bx
        add     cl,ch           ;168 計算乘積位數,並存於 len_p
        sub     ch,ch
        mov     len_p,cx
        push    dx              ;171 保存乘積位址,待第二個大迴圈才使用(222行)
        mov     bx,dx
m0:     mov     byte ptr [bx],ch;173 清除乘積的垃圾資料
        inc     bx
        loop    m0
        
;第一個大迴圈,處理每一位乘數去乘被乘數
        mov     di,last_ad      ;178 DI指向暫存區(存放每一位乘數相乘後的結果)
        mov     si,l_adr_v2     ;179 每次計算乘數一位數乘被乘數結果之迴圈開始
m1:     sub     ax,ax           ;180 清除AX、DH使往後的AAM、AAA指令能正確運算
        mov     dh,al

        mov     cx,si           ;183 小位數的填零部分,CX為填零的個數
        sub     cx,l_adr_v2
        mov     bx,l_adr_v1     ;185 計算乘數每位數乘被乘數部分
        push    cx
        jcxz    m3
m2:     mov     [di],al
        inc     di
        loop    m2              ;190
		
m3:     mov     al,[bx]         ;192 BX指向被乘數
        mul     byte ptr [si]
        aam
        add     al,dh           ;195 加上前一次的進位
        aaa
        mov     dh,ah           ;197 進位存於DH
        mov     [di],al
        inc     bx
        inc     di
        cmp     bx,h_adr_v1
        jne     m3              ;202
		
        mov     [di],dh         ;204 處理進位部分
        inc     di              ;205

        pop     ax              ;207 處理大位數填零部分
        mov     cx,len_p        ;208 CX為填零的個數
        sub     bx,l_adr_v1
        sub     cx,ax
        sub     cx,bx
        dec     cx
        jcxz    m5
m4:     mov     byte ptr [di],0
        inc     di
        loop    m4              ;216

m5:     inc     si              ;218 指向乘數的下一位
        cmp     si,h_adr_v2     ;219 檢查乘數是否都已算完
        jne     m1

;第二個大迴圈,處理每一位乘數乘積之和
        mov     cx,h_adr_v2
        sub     cx,l_adr_v2     ;224 CX為乘數位數,即要相加次數
        pop     si              ;225 取回乘積位址
        mov     bx,last_ad      ;226 BX為暫存區位址
m6:     push    cx
        mov     dx,si
        mov     ax,dx
        push    bx
        mov     cx,len_p
        mov     ch,cl
        call    ubcd_add        ;233 DX=DX+BX
        pop     bx
        pop     cx
        add     bx,len_p
        loop    m6              ;237
        ret
l_adr_v1        dw      ?       ;239 被乘數最低位址
l_adr_v2        dw      ?       ;240 乘數最低位址
h_adr_v1        dw      ?       ;241 被乘數最高位址
h_adr_v2        dw      ?       ;242 乘數最高位址
len_p           dw      ?       ;243 乘積位數
ubcd_mul        endp            ;244
;---------------------------------------
print_cr_lf     proc    near
        mov     dl,cr
        call    print_char
        mov     dl,lf
        call    print_char
        ret
print_cr_lf     endp
;---------------------------------------
end_prog:                       ;254 程式最高位址
code    ends
;***************************************
        end     start

這個程式的流程很簡單,重點在兩個新的副程式 ( ubcd_input 與 ubcd_mul ),還有計算程式結束位址。好吧,我們先先來『解剖』ubcd_input 副程式吧。

ubcd_input 副程式

這個副程式是用來讓使用者由鍵盤輸入一個數字,並且轉換成非聚集的 BCD 數。如同前面所言,人類的習慣是由大位數開始輸入 ( 或書寫 ),再依次遞減,但是電腦上先輸入的字卻放在低位址,於是人先輸入的數字卻變成在低位址了,這會產生矛盾。為了解決此一問題,於是小木偶設計了一個暫存區,名為 temp_ubcd_rev ( rev 是 reverse 之簡寫,即顛倒之意 ),當使用者輸入數字時,每一位數都按照先後順序依次由低位址排向高位址,待使用者輸入完畢後再由第 110 行到第 119 行的程式處理,將暫存區中顛倒的非聚集 BCD 數變成正常的 BCD 數。

在整個過程中,BX 暫存器所扮演的角色至為重要,它是代表 temp_ubcd_rev 的指標。一開始 BX 之值為零,代表它指到 temp_ubcd_rev 的最低位址 (最前面),而後每當使用者正確地輸入一位數 (由程式第 97 行到第 102 行檢查並印在螢幕上),BX 之值就增加一。如下圖所示:

基底相對定址

基底相對定址

當使用者輸入一個數字後,就得將這個數字存入 temp_ubcd_rev 暫存區內,至於是存入暫存區的那一個位址則是由 BX 之值決定。程式第 104 行就是執行這件事。

mov     temp_ubcd_rev[bx],al

上式程式是一種前面沒提到過的定址方式,稱基底相對定址。temp_ubcd_rev[bx] 所指的位址,是 temp_ubcd_rev 所在位址後的第幾個位址,至於是之後的那一個位址要看 BX 的數值。如果 BX 為零,該位址就是 temp_ubcd_rev 這個位址,如果 BX 為一,該位址就是 temp_ubcd_rev 這個位址的下一個位址(高一個位址)。用 DEBUG 來觀察程式 104 行的情形,當 ubcd_input 副程式要我輸入時,我按下『9』這個鍵,在執行

mov     temp_ubcd_rev[bx],al

這行程式之前,先觀察 temp_ubcd_rev 暫存區,這個暫存區在位址 1C8E:0259 開始的 20 個位元組處 (可以由 MASM.EXE 組譯後副檔名為 LST 的檔案得知),但我只觀察前幾個。

-d 259 L7 [Enter]
1C8E:0250                             00 00 00 00 00 00 00

執行前都是零。好,看看 mov temp_ubcd_rev[bx],al 這個指令,竟然變成『MOV [BX+0259],AL』了,事實上,這個 0259 就是 temp_ubcd_rev 的所在位址。

-r [Enter]
AX=0209  BX=0000  CX=0014  DX=0139  SP=FFF8  BP=0000  SI=011F  DI=0000
DS=1C8E  ES=1C8E  SS=1C8E  CS=1C8E  IP=0216   NV UP EI PL NZ NA PE NC
1C8E:0216 88875902      MOV     [BX+0259],AL                       DS:0259=00
-t [Enter]

AX=0209  BX=0000  CX=0014  DX=0139  SP=FFF8  BP=0000  SI=011F  DI=0000
DS=1C8E  ES=1C8E  SS=1C8E  CS=1C8E  IP=021A   NV UP EI PL NZ NA PE NC
1C8E:021A 43            INC     BX
-d 250 L7 [Enter]
1C8E:0250                             09 00 00 00 00 00 00   .......!........

因為一開始,BX 為零,所以 BCD 數,9,填在 1C8E:0259 處。其實上述程式也可以改寫成:

mov     si,offset temp_ubcd_rev
add     bx,si
mov     [bx],al

執行結果是一樣的,只是多用了一個 SI 暫存器。ubcd_input 副程式中還有處理使用者按下 Enter 鍵、Esc 鍵以及其他『不合法』的按鍵,但這些並不難,小木偶就不再說明了。

ubcd_mul 副程式

這個副程式的原理,說穿了也不值幾文錢,其實就是小學時老師所教的直式乘法。回想一下,假如要求 98765*123,你會怎麼算?我想大部分的人會採下面的算法:

被乘數         9 8 7 6 5
  乘數   X        1 2 3
--------------------------
             2 9 6 2 9 5
           1 9 7 5 3 0
           9 8 7 6 5
--------------------------
  乘積   1 2 1 4 8 0 9 5

小木偶所寫的程式也是利用這個方法,由乘數的個位數起,乘上被乘數,再換乘數的十位數乘上被乘數,直到乘數所有位數都乘上被乘數以後,再全部相加起來,就是乘積了。所以,整個 ubcd_mul 副程式的架構就顯現了,有兩個大迴圈。第一個大迴圈處理乘數的每一位數乘被乘數,得到上式兩條線之間的數。第二個大迴圈處理每一位數乘被乘數所得的數相加,也就是把兩條線之間的乘積相加,就得到答案了。

好,再來看看第一個大迴圈內,每一位乘數去乘被乘數會發生那些事情。小木偶用特殊濾鏡照出下式:

被乘數         9 8 7 6 5
  乘數   X        1 2 3
--------------------------
         0 0 2 9 6 2 9 5
         0 1 9 7 5 3 0 0
         0 9 8 7 6 5 0 0
--------------------------
  乘積   1 2 1 4 8 0 9 5

仔細觀察上式乘法,當乘數的每一位數乘被乘數時 (例如乘數的個位數 3 乘以 98765 這種情形),可分為四個步驟。第一步是最右邊的自動填零 (橘色部分),至於填多少位零,是看乘數的位數而定,如果乘數的個位數乘被乘數,那不須填零;乘數的十位數乘被乘數,要填一個零;乘數的百位數乘被乘數,要填兩個零,依此類推。第二步是中間綠色部分是實際進行乘法運算的部分。第三步是進位部分 (白色部分)。最後一步是大位數的填零部分 (最左邊橘色部分)。換句話說,每一位乘數去乘以被乘數要有四個小迴圈去處理。

在第一個大迴圈堙A程式第 178、179 行堣p木偶用 SI 暫存器去記錄正在處理的乘數位址,當每次處理完四個小迴圈後,SI 暫存器就增加一,指向下一位乘數,當然在處理同一位乘數時,這 SI 不能改變。小木偶又用 DI 暫存器來指向暫存區位址,這個暫存區的目的是用來暫時存放乘數的每一位數去乘被乘數所得的數值之用,因這個暫存區的大小會隨被乘數語乘數之位數有關,程式設計師無法在事前估計,故小木偶放在程式的最尾端,待會再細說。

好了,程式第 183 至 190 行是處理小位數的填零部分﹔第 192 至 202 行是處理相乘部分﹔第 204、205 兩行是處理進位﹔第 208 至 216 行是處理大位數的填零部分。

值得一提的是第 192 至 202 行是處理相乘部分。在每一次計算一位乘數乘被乘數前都要先使 AL、DH 歸零,這兩行程式在第 180、181 行。該位乘數是在 SI 所指位址的數值,而被乘數位於 BX 所指的數值,BX 會由被乘數的個位數起,逐次指向大位數,而每次相乘後得考慮進位,進位數存於 DH。小木偶想其他細節,也不用贅述了。

再談談第二個大迴圈。平常吾人是利用直式加法由個位數、十位數依次往大位數加上去,但小木偶是利用上一章的加法副程式,ubcd_add,來處理。該加法副程式每次只能求兩數相加,所以小木偶就使位於兩線之間的數依次和積相加,並使其和再存於積內,積的位址由 SI 來指定,兩線之間的數之位址由 BX 指定。以 98765*123 為例來說,就是先使 00296295 和 SI 所指的位址相加,並使其和存於 SI 中,然後再使 01975300 和上述SI 所指的位址相加,又使其和存於 SI 中,最後再使 09876500和 SI 所指的位址相加,並使其和存於 SI 中。

在呼叫 ubcd_add 副程式時,必須把被加數、加數及和的位址分別存放於 AX、BX 及 DX 暫存器中,故相加時,DX 和 AX 相等,都等於 SI,而 BX 是由兩線之間的數依次增加,增加的量視乘積位數而定。

取得程式結尾位址

這兩個副程式有個共同的地方,那就是設計者無法先預知使用者會用去多少的資料,因此無法先規劃適當大小的暫存區。遇到這種問題,小木偶想有兩種方式解決 (如有其他方法也請告知我),一是事先預設一塊很大的記憶體空間讓使用者盡情輸入,但是這樣會使得執行檔變大。第二種方法就是將這些資料移到程式最後面,如此就不怕使用者輸入太多資料而造成程式碼為資料所覆蓋而出錯(據說黑帽駭客的緩衝區溢位的原理就是如此)。

在此小木偶採用第二種方法。要取得程式碼最後一個位址,其實不難,只要在原始程式最後面加上一個標記,就可以用假指令 offset 取得該位址 (所以其實標記也是一種位址)。在這個程式的第 24 到 27 行,就是取得程式最後位址並存於 last_ad 變數中。而第 25 行是因為這個程式呼叫了兩個外部副程式,所以還必須再加上這兩個副程式的大小,3BH 和 39H (這兩個副程式的大小可以由 LIB.EXE 所建立的列表檔看出來,上一章堛 MYASMLIB.LST 檔)。至於加上 20H 是留一些緩衝空間,並沒有特別的意義。

組譯方法

這個程式的原理已經說完了,底下來點兒輕鬆的,那就是如何組譯他呢?假設您把上面程式存成 UBCD_MUL.ASM 檔,那麼依下面步驟組譯:

H:\HomePage\SOURCE>..\masm50\masm ubcd_mul; [Enter]
Microsoft (R) Macro Assembler Version 5.00
Copyright (C) Microsoft Corp 1981-1985, 1987.  All rights reserved.


  50774 + 399578 Bytes symbol space free

      0 Warning Errors
      0 Severe  Errors

H:\HomePage\SOURCE>..\masm50\link ubcd_mul,,,myasmlib [Enter]

Microsoft (R) Overlay Linker  Version 3.60
Copyright (C) Microsoft Corp 1983-1987.  All rights reserved.

LINK : warning L4021: no stack segment

H:\HomePage\SOURCE>..\masm50\exe2bin ubcd_mul ubcd_mul.com [Enter]

這樣就可以得到 UBCD_MUL.COM 可執行檔了。這個程式必須用到上一章的 MYASMLIB.LIB 程式庫,請參考上一章的方法製作他。最後如果您跟我一樣認為 ubcd_input 和 ubcd_mul 或許很有用,那也可以把這兩個副程式加入 MYASMLIB.LIB 程式庫堙C

結論

上一章的程式已經很複雜了,而這個程式比上一章有過之無不及,經過兩個這樣複雜的程式,您有沒有什麼心得呢?我想這兩章的程式其實只不過是手段,並非目的。我要告訴您的並不是這些程式的步驟,而是面對寫作複雜程式時的方法,那就是先仔細觀察,把一個大問題分解成小問題,再把小問題分成更小的問題,直到能夠用程式寫出來,再加以『組裝』起來就能解決大問題了。

但話又說來了,雖然我主要目的並不是告訴你細節的步驟,不過這兩章還是脫不了主題,計算 BCD 數的指令 AAA、AAM、DAA 等等,假如您不瞭解這幾個指令,不瞭解記憶體定址,您還是沒有辦法寫出這些程式,只不過我認為這些是末節,並非大方向。


回到首頁到第十三章到第十五章