Ch 35 有號數


原理

如何定義有號數

在數學上,整數可分成正整數、負整數與零,我們以『+』或『-』表示這整數是正整數還是負整數,如果是正數,一般前面不加上正號。而在電腦科學上,我們只能用數個位元的儲存空間 ( 一般是 8、16、32、64 位元 ) 來表示這些整數。不過在日常生活中還是以正整數較為常用,因此在電腦中大部分應用堙A我們還是數字看成正數而忽略正負號,稱之為無號數 ( unsigned numbers )。當然了,有時我們還是希望能夠進行正數或負數的運算。與無號數反面的就是有號數 ( signed numbers ) 了,所謂有號數乃是指具有正負號的數,我們以最高位元設定這個數是正數還是負數,當最高位元為 0 時,表示此數為正數;當最高位元為 1 時,表示此數為負數。這最高位元稱為符號位元。

因為用了一個位元來表示正負數,所以同樣是 8 位元,無號數可以表示 0∼255 的範圍,但有號數所能表示的只有 -128 到 +127,其他的請參見下表:

儲存空間無號數範圍有號數範圍 組合語言中
所用分配空間
8 位元 ( 位元組,byte )0 到 255 -128 到 +127DB 或 BYTE
16 位元 ( 字組,word )0 到 65535 -32768 到 +32767DW 或 WORD
32 位元 ( 雙字組,double word )0 到 4294967295 -2147483648 到 +2147483647DD 或 DWORD
64 位元 ( 四字組,quad word )0 到 18446744073709551615 -9223372036954775808 到 +9223372036954775807DQ 或 QWORD

在數學上,我們知道等值不等號的兩數互為相反數 ( 什麼?相反數是什麼?相反數就是兩數之和為零者即為相反數 ),例如 +1 與 -1 兩數即為等值不等號,此二數相加結果為零。由此原理可推知,假如我們要定義負數,最好是符合上面的規則。試想假如把 1 存入 AL 暫存器堙A那麼 AL 加上多少會變成零呢?答案是 0FFH 吧,因此我們把 0FFH 定成 -1,同理把 0FEH 定成 -2……,可以參考下圖:

有號數與二進位數

在上圖中小木偶把 0∼0FFH 這 128 個數值排成圓環狀,如果 AL 暫存器的數值由零開始遞增到 0FFH ( 見紅色的數 ),在上方以淡藍色的數字即為所代表的無號數,而下方以黃色數字為其有號數。由圖上可以看到,當 AL 暫存器的數值由 0 到 7FH 時,不管是無號數或有號數都為正數,即 0 到 127;但是當最高位元為 1 時 ( 由 80H 到 0FFH 時 ),則有號數與無號數所代表的意義是不同的。可能讀者會問,既是如此,那麼同樣一個十六進位數到底是代表無號數還是有號數呢?以 0FFH 為例,這個問題是說,0FFH 到底是代表 255 還是 -1 呢?其實這是老問題了,在組合語言堙A暫存器或記憶體的資料全賴程式如何去解釋這些資料。

2 的補數 ( two's complement )

什麼是 2 的補數呢?簡單的說就是把原來的二進位中的各位元反轉 ( 反轉是指 1 變成 0;0 變成 1 ),然後再加 1。例如 1 的 2 的補數為何?可以這樣算:

0000 0001
  ↓反轉各位元
1111 1110
  ↓再加 1
1111 1111

最後得到 0FFH,換句話說 1 和 0FFH 互為 2 的補數。對照上面的圖片,當可發現一個公式:互為 2 的補數的兩數恰好為相反數。上面的方法是以二進位來思考,如果以十六進位來思考,可以以 0FH 去減掉每位的數值,最後再加一。例如要求 7DH 的二的補數,可以這樣做:

F-7=8
F-D=2

得 82H,最後再加一,得 83H,故 7DH 的 2 的補數為 83H,對照上圖,你也可發現 7DH(+125) 與 83H(-125) 也互為相反數 ( 你也可以改成二進位依據反轉各位元再加一驗算看看 )。

有關 2 的補數還有一項定理,那就是 2 的補數運算具有可逆性,亦即一數經過兩次二的補數運算會恢復原數。例如以上面 7DH 為例,它經過 2 的補數運算後得 83H,再經過一次運算:

F-8=7
F-3=C

最後再加一變成 7DH,是不是又恢復原數了呢?由於 2 的補數這兩個定理,使得我們在求某一無號數的等值異號數時,可以利用 2 的補數來運算。

二的補數也可以用另一種方法求得:

  1. 由第 0 位元開始向高位元檢查是否為零,如果為零則該數的二的補數填零。
  2. 依次向高位元直到找到某位元為一時,把該數的二的補數填入一。
  3. 其餘高位元依次反轉。

例如 0011 0100 的二的補數可以這樣算:先由標示紅色的第零位元開始,其二的補數為 0,再來是白色的,其二的補數為 0,再來是藍色的,不過這次是 1,因此把它填入 1,接下來的位元依序反轉,因此最後得到的答案是 1100 1100。

上面小木偶以 8 位元為例,說明了有號數的定義以及為何如此定義,也說明了如何使用 2 的補數。對於 16、32、64 位元的整數來講,也是一樣的。

說了這麼多二的補數的求法即原理,那麼二的補數究竟有何用途呢?原來二的補數除了可用來求相反數之外 ( 或說求二進位的負數 ),還可以用在把減法當成加法來運算。例如 100-20 可以看成 100+(-20),20 的二的補數不就是 -20 嗎?因此在設計 CPU 的電路上,只要在 CPU 堨[上把數值變成二的補數的線路,就可以不用去設計減法的電路。

NEG 指令

NEG 指令是 8086 指令集中的一個指令,這個指令的語法是:

NEG     目的運算元

此指令把目的運算元作 2 的補數運算 ( 即把所有位元反轉再加一 ) 之後的結果,再存回目的運算元堙A因此目的運算元會被破壞。目的運算元可以是 8 或 16 位元的暫存器或變數,若在 80386 等級以上的 CPU,目的運算元可以使用 32 位元的暫存器或變數。

NOT 指令

NOT 指令也是 8086 指令集中的一個指令,其語法是:

NOT     目的運算元

此指令把目的運算元作 1 的補數運算 ( 即把所有位元反轉 ) 之後的結果,再存回目的運算元堙A因此目的運算元會被破壞。目的運算元可以是 8 或 16 位元的暫存器或變數,若在 80386 等級以上的 CPU,目的運算元可以使用 32 位元的暫存器或變數。

十六進位有號數與十進位數

如果要把十六進位有號數轉換成十進位數的步驟如下:

  1. 如果該十六進位有號數為負,求出它的 2 的補數;如果為正,則保留原數不變。經過此步驟所得的數必為正數。
  2. 把第一步的結果轉換成十進位數。
  3. 如果原十六進位有號數為負,則在前面加上“-”號,否則不加任何符號。

如果要把十進位有號數轉換成十六進位數的步驟如下:

  1. 先把十進位數的絕對值轉變成十六進位數。
  2. 如果原十進位數為負,則再進行 2 的補數運算;如果原十進位數為正,則保留原數。經此步驟後的結果即為所求。

前面提過,我們以數個位元,可能是八個、16 個、32 個甚至是 64 個位元來儲存整數,在許多運算中,常常需要把這些儲存空間變大,例如把存在 AL 暫存器堛漸蕪蒱ぅ峟t整數變成存在 AX 暫存器堙C有關這些功能,80x86 指令集提供了四種指令:

CBW 指令

這個指令的語法是

        CBW

其後沒有運算元,因為它固定把 AL 的符號位元拷貝到 AH 暫存器 ( 我們亦可以想成『符號擴充』)。例如原來 AX 為 1B23H,經 CBW 運算後,AX 變為 0023H;如果 AX 原來為 1B89H ( AL=-119d ),經 CBW 運算後,AX 變成 FF89H ( AX=-119d )。故其 CBW 可以把八位元的數值,變成十六位元且其正負號不變,英文為『convert byte to word』。

CWD 指令

這個指令的語法是

        CWD

CWD 是把 AX 暫存器的符號位元擴充到 DX,亦即把存在 AX 暫存器的有號數擴充變成 DX:AX。CWD 的英文是『convert word to doubleword』。

CDQ 指令

這個指令的語法是

        CDQ

CDQ 是把 EAX 暫存器的符號位元擴充到 EDX,亦即把存在 EAX 暫存器的有號數擴充變成 EDX:EAX。CWQ 的英文是『convert doubleword to quadword』,它必須在 80386 等級或更高級的 CPU 才可以使用。

CWDE 指令

這個指令的語法是

        CWDE

CWDE 是把 AX 暫存器的符號位元擴充到 EAX,亦即把存在 AX 暫存器的有號數擴充變成 EAX。CWDE 指令與 CWD 指令擴充的符號位元是不同的。CWDE 必須在 80386 等級或更高級的 CPU 才可以使用。


範例:兩數相減

光說不練是不行的,底下小木偶舉一個例子來說明有號數的運算。這個例子可由使用者輸入十進位的兩數 ( 可為正或負數 ),程式會計算兩數之差並顯示於螢幕上。程式如下:

;SUB-本程式可讓使用者輸入兩個數字,然後計算此二數之差,顯示於螢幕
        .386
;***********************************************************
code    segment use16
        assume  cs:code,ds:code
        org     100h
;-----------------------------------------------------------
start:  jmp     short begin
msg0    db      'Input minuend : $'             ;輸入被減數及減數,必
msg1    db      0dh,0ah,'Input subtrahend : $'  ;須在-2147483648與
msg2    db      7,'Input error!$'               ;2147483647之間
answer  db      0dh,0ah,40 dup (' ')
buffer  db      12,13 dup (0)
n1      dd      ?
n2      dd      ?

begin:  mov     dx,offset msg0
        call    input
        jc      error
        mov     n1,edx
        mov     di,offset answer+2
        call    hex2dec
        mov     al,'-'
        stosb

        mov     dx,offset msg1
        call    input
        jc      error
        mov     n2,edx
        call    hex2dec
        mov     al,'='
        stosb

        mov     edx,n1
        sub     edx,n2
        call    hex2dec
        mov     al,'$'
        stosb

        mov     dx,offset answer
        jmp     short ok
error:  mov     dx,offset msg2
ok:     mov     ah,9
        int     21h
        mov     ax,4c00h
        int     21h
;-----------------------------------------------------------
input   proc    near
        mov     ah,9
        int     21h
        mov     dx,offset buffer
        mov     ah,0ah
        int     21h

        sub     eax,eax
        mov     cl,'+'          ;CL記錄使用者輸入的正負號
        mov     edx,eax         ;EDX為使用者輸入的十六進位有號數
        mov     si,offset buffer+2
;把輸入的十進位有號數,變成十六進位之絕對值,存於EDX
ipt0:   lodsb
        cmp     al,'-'          ;判斷使用者輸入正數還是負數
        jne     ipt1
        mov     cl,al
        jmp     ipt0
ipt1:   cmp     al,'+'
        je      ipt0
        cmp     al,0dh
        je      ipt2
        cmp     al,'0'
        jb      ipt4
        cmp     al,'9'
        ja      ipt4
        sub     al,'0'
        shl     edx,1           ;這四行運算使EDX乘以10倍
        mov     ebx,edx
        shl     edx,2
        add     edx,ebx
        add     edx,eax
        jmp     ipt0
;若使用者輸入的為負數,則把絕對值變成2的補數
ipt2:   cmp     cl,'-'
        jne     ipt3
        neg     edx
ipt3:   clc
        jmp     short ipt5
ipt4:   stc
ipt5:   ret
input   endp
;-----------------------------------------------------------
hex2dec proc    near
;把EDX內所存的十六進位有號數變成十進位有號數,存於DI所指的位址
        jmp     short h2d0
tmp     db      10 dup (' ')
h2d0:   test    edx,80000000h   ;檢查是否為負數
        jz      h2d1
        mov     al,'-'          ;若為負數,則取其2的補數,並
        neg     edx             ;於DI所指位址存入負號
        stosb
h2d1:   push    di
        mov     ebx,10          ;使EDX每次除以10,所得的餘數
        mov     eax,edx         ;分別為十進位的個位數、十位數、
        mov     di,offset tmp+9 ;百位數……,把這些餘數暫時存於
        mov     cx,bx           ;tmp字串中(由高位址開始存)
h2d2:   sub     edx,edx
        div     ebx
        add     dl,'0'
        mov     [di],dl
        or      eax,eax
        jz      h2d3
        dec     di
        loop    h2d2
h2d3:   mov     si,di           ;把tmp字串中的每一位數,存入
        pop     di              ;DI所指位址
        sub     bx,cx
        mov     cx,bx
        inc     cx
        rep     movsb
        ret
hex2dec endp
;-----------------------------------------------------------
code    ends
;***********************************************************
        end     start

把上述原始碼存於 SUB.ASM 堙A然後用 MASM 5.x 或 MASM 6.x 組譯,如果你用 MASM 5.x 組譯的話,須經過三個步驟:

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

  51564 + 336180 Bytes symbol space free

      0 Warning Errors
      0 Severe  Errors

E:\HOMEPAGE\SOURCE>link sub; [Enter]

Microsoft (R) Personal Computer Linker  Version 2.40
Copyright (C) Microsoft Corp 1983, 1984, 1985.  All rights reserved.

Warning: no stack segment

E:\HOMEPAGE\SOURCE>exe2bin sub sub.com [Enter]

E:\HOMEPAGE\SOURCE>

如果使用 MASM 6.x 組譯,則只需一個步驟:

E:\HOMEPAGE\SOURCE>ml /AT sub.asm [Enter]
Microsoft (R) Macro Assembler Version 6.11
Copyright (C) Microsoft Corp 1981-1993.  All rights reserved.

 Assembling: sub.asm

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

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

E:\HOMEPAGE\SOURCE>

不管用 MASM 5.x 或 MASM 6.x 都應該能順利組譯成功,如果有錯誤,可能是找不到程式位置,請用 path 指令設好 MASM.EXE、ML.EXE、LINK.EXE 等程式所在子目錄。組譯好之後可執行 SUB.COM

E:\HomePage\SOURCE>sub [Enter]
Input minuend : 10 [Enter]
Input subtrahend : 100 [Enter]
10-100=-90
E:\HomePage\SOURCE>

我想所有的原理都在上面『原理』的段落中敘述過了,因此不再說明了,底下來談談溢位 ( overflow ) 的問題。


溢位

SUB.ASM程式有兩個錯誤的地方,例如,如果你對 SUB 作以下輸入:

E:\HOMEPAGE\SOURCE>sub [Enter]
Input minuend : 3 [Enter]
Input subtrahend : -2147483648 [Enter]
3--2147483648=-2147483645   →第一個溢位錯誤的例子
E:\HOMEPAGE\SOURCE>sub [Enter]
Input minuend : 2147483648 [Enter]
Input subtrahend : 3 [Enter]
-2147483648-3=2147483645   →第二個溢位錯誤的例子
E:\HOMEPAGE\SOURCE>

在上面第一個例子中,任何學過國一數學的人都知道 3 減去 -2147483648 所得差應為 2147483651 才對,並非 -2147483645。那麼為什麼會產生錯誤呢?原來用 32 位元的空間要表示有號數,這有號數只能在 -2147483648 到 +2147483647 之間,現在所得結果超過這範圍了,就產生溢位 ( overflow ) 錯誤了。

所謂溢位,是指計算的結果超過暫存器或記憶體所能表示的範圍。在 80x86 家族的 CPU 堙A如果有溢位的情形產生,CPU 會自行將溢位旗標設為 1,即 OV ( 請參閱附錄二 )。會發生溢位的情形有三種:

  1. 兩同號數相加或兩異號數相減,可能會產生溢位,例如:

            mov     eax,2147483647
            add     eax,2

    所得結果應為 2147483649,但超過了 32 位元有號數的範圍,因此產生溢位。更詳細的例子在此處

  2. 乘除運算時,所得的積或商超過運算元存放範圍,例如:

            mev     eax,2147483640
            imul    eax,2

    所得結果應為 4294967280,也超過了 -2147483648 到 +2147483647,因此也會產生溢位。

  3. 在移位或旋轉指令時,最高位元值受到更動 ( 0 變成 1 或 1 變成 0 ) 時,例如:

            mov     ax,7000h    ; AX=0111 0000 0000 0000
            shl     ax,1        

    運算後,AX 變成 1110 0000 0000 0000,因此也會使溢位旗標設為 OV。

了解溢位產生的原因後要解決第一個問題,應該是不難的。只要在兩數相減之後,檢查是否有溢位即可。

第二個例子發生錯誤的原因是使用者輸入的有號數超過了 -2147483648 到 +2147483647 範圍 ( 使用者輸入 +2147483648 ),以致於在 input 副程式堙A計算使用者所輸入的『2147483648』字串換成有號數時,會變成 80000000h,變成負數了。這是一種邏輯上的錯誤,要解決這個問題,可以檢查當使用者輸入是正數時,最高位元應為 0;反之若使用者輸入負數,最高位元應為 1。改寫後的程式,SUB1.ASM 如下:( 為了節省篇幅,只列出一部份 )

……
msg2    db      7,0dh,0ah,'Input error!$'       ;2147483647之間
msg3    db      'ovwerflow!'
answer  db      0dh,0ah,40 dup (' ')
lenmsg3 equ     offset answer-offset msg3
buffer  db      12,13 dup (0)
……
        mov     edx,n1
        sub     edx,n2
        jo      ovflow          ;檢查兩有號數相減,是否溢位
        call    hex2dec         ;沒發生溢位,印出答案
        jmp     short ok0

ovflow: mov     ecx,lenmsg3     ;溢位發生表示答案是錯的
        mov     si,offset msg3
        rep     movsb

ok0:    mov     al,'$'
        stosb
……
input   proc    near
        mov     ah,9
        int     21h
        mov     dx,offset buffer
        mov     ah,0ah
        int     21h

        sub     eax,eax
        mov     cl,'+'
        mov     edx,eax
        mov     si,offset buffer+2
;把輸入的十進位有號數,變成十六進位之絕對值,存於EDX
ipt0:   lodsb
        cmp     al,'-'          ;判斷使用者輸入正數還是負數
        jne     ipt1
        mov     cl,al
        jmp     ipt0
ipt1:   cmp     al,'+'
        je      ipt0
        cmp     al,0dh
        je      ipt2
        cmp     al,'0'
        jb      ipt5
        cmp     al,'9'
        ja      ipt5
        sub     al,'0'
        shl     edx,1           ;這四行運算使EDX乘以10倍
        mov     ebx,edx
        shl     edx,2
        add     edx,ebx
        add     edx,eax
        jmp     ipt0
;若使用者輸入的為負數,則把絕對值變成2的補數
ipt2:   cmp     cl,'-'
        jne     ipt3
        neg     edx
        test    edx,80000000h   ;檢查使用者輸入負數時,最高位元
        jz      ipt5            ;是否為1
        jmp     short ipt4
ipt3:   test    edx,80000000h   ;檢查使用者輸入正數時,最高位元
        jnz     ipt5            ;是否為0
ipt4:   clc
        jmp     short ipt6
ipt5:   stc
ipt6:   ret
input   endp
……

四則運算的溢位

ADD 指令與溢位

CPU 是如何知道運算結果是否溢位呢?小木偶想以下面例子來說明:

   0111 1111 = 07FH = 127D
+ 0000 0010 = 002H =   2D
-------------------------------------------
   1000 0001 = 081H = 129D(無號數) 或 -127D(有號數)

上面是八位元的有號數加法,這個加法運算後所得之和超過 -128 到 +127,因此溢位旗標會被設定。讓我們來探討這運算的過程。二進位的加法運算一如十進位,回想十進位加法,如果個位相加結果小於或等於 9,則不進位;反之超過 9,則僅僅寫出和的個位數而進位。例如:

   24
+ 39
------
   63

4 加 9 得 13,寫 3 進一。二進位加法也是如此,上面第 0 位元相加,得 1 ( 如黃色字 ),第 1 位元相加超過 1,寫上個位數 ( 如紅色字 ) 且進位,第三位元相加再加上進位的一,所得還是超過 1,故寫上 0 ( 藍色字 ) 且進位,如此相加一直到第 7 位元結束為止。以上是 ADD 指令的運作,但是如果是把這相加的兩數看成有號數,有號數的第七位元其實僅代表正負號,照理說不應該把第七位元做加法運算,不過 ADD 指令可不管那麼多,不管有號或無號數都照樣運算,而且 ADD 指令不管是有號數還是無號數都可以算出和來。因此情形複雜得多,考慮所有運算的情形可分成下列三種情形:

  1. 假如正數與正數相加:這時兩數的第七位元都為零,且所得之和應為正數 ( 也就是和的第七位元應為 0 ),可以分成兩種情形來討論。第一種情形是兩數相加時,第六位元不發生進位 ( 藍色的 1 ),像下圖左式,又因為本是兩正數相加,所以第七位元相加仍為零 ( 黃色的 0),這種情形相加不會有溢位發生。

    第二種情形是第六位元相加發生進位到第七位元,像上圖右式,照理說第七位元是符號位元應不參與運算,或又因為是兩正數相加,和的第七位元應為 0,但因第六位元進位結果和變成 1,和變為負數,這樣就會發生溢位。

  2. 假如負數與負數相加:這時兩數的第七位元都為一,且所得之和應為負數 ( 也就是和的第七位元應為 1 ),也可分為兩種情形來討論。第一種情形是第六位元相加不發生進位,像下圖左式,那麼第七位元相加結果第七位元為零,且會發生進位,這種情形是會發生溢位。

    第二種情形是第六位元相加發生進位,像上圖右式,又因為第七位元是兩個一相加再加上進位,結果第七位元仍是一,且第七位元也發生進位,這種情形不會發生溢位。

  3. 假如正數與負數相加:因為正負數會相消,所以不管正數絕對值較大還是負數絕對值較大,都不會有溢位的情形發生。這時也分成兩種情形。第一種情形是第六位元相加不發生進位,像下圖左式,這時第七位元是一與零相加也不會進位。

    第二種情形是第六位元相加發生進位,像下圖右式,這時第七位元是一與零相加再加上第六位元進位的一,結果第七位元相加後會發生進位。

整理上面所有有號數相加的六種情形,您當可發現,如果是第六位元相加之結果與第七位元相加之結果,同時進位或同時不進位,就不會發生溢位;反之如果一為進位,一為不進位,就會發生溢位。CPU 可以檢測第六位元相加之結果與第七位元相加之結果是否同時進位或同時不進位,用術語來說,就是使這兩個進位值作 XOR 運算,就可判斷是否溢位了。

SUB 指令與溢位

減法,SUB,可以看成是被減數與減數的 2 的補數之和,請看下面幾個例子:

   100-20=100+(-20)
  -100-20=-100+(-20)
 100-(-20)=100+20
-100-(-20)=-100+20

上面四個式子中,前兩個式子的減數是 20,其 2 的補數為 -20,因此可看成是被減數與減數的 2 的補數之和。而最後兩個算式中的減數,-20,的 2 的補數是 20,也可以看成是被減數與減數的 2 的補數之和。因此在減法中,溢位旗標的設定加法是一樣的。

MUL、IMUL 指令與溢位

MUL 指令只用於無號數之運算,因此執行完 MUL 後的溢位旗標不重要常忽略,其語法為:

        MUL     來源運算元

來源運算元是乘數,它可以是 8、16 或 32 位元的暫存器或存於 8、16 或 32 位元記憶體的變數,而被乘數則是和乘數一樣長度的 AL、AX、EAX,80x86 會把被乘數與乘數相乘然後把乘積存於 AX、DX:AX、EDX:EAX 堙C如下表:

乘法方式被乘數 乘數乘積 範例
運算前運算後
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

例如底下 32 位元乘法:

        mov     eax,80000001h
        mov     edx,88887777h
        mov     ebx,00000002h
        mul     ebx

執行完『mul ebx』後,EDX 為 1,EAX 為 00000002,故所得乘積為 100000002H,原來的 EDX 值會消失不見。由此看來 MUL 僅僅做 EAX 與 EBX 之乘積,且把 EAX 當做正數。MUL 運算完後如果乘積的高位元部份不為零,溢位旗標與進位旗標均會被設為一;反之,如果乘積的高位元部份為零,溢位旗標與進位旗標均會被設為零。例如上面的例子,因為 EDX 不為零,故溢位旗標會被設定。

如要計算有號數的乘法,必須用 IMUL 指令,IMUL 的語法與 MUL 相同,但是它會把被乘數的符號保留到乘積堙C例如上面的例子,把『MUL EBX』改成『IMUL EBX』,所得結果,EDX:EAX 為 FFFFFFFF:00000002。

IMUL 的語法有三種形式,可以只接一個運算元,或兩個、或三個運算元:

  1. IMUL    暫存器或記憶體變數
  2. IMUL    16位元暫存器,16位元暫存器或變數
    IMUL    32位元暫存器,32位元暫存器或變數
    IMUL    16位元暫存器,8或16位元常數
    IMUL    32位元暫存器,8或32位元常數
  3. IMUL    16位元暫存器,16位元暫存器或變數,8或16位元常數
    IMUL    32位元暫存器,32位元暫存器或變數,8或32位元常數

第一種語法的規則與 MUL 相同,就不贅述。第二種語法 IMUL 後面兩個運算元,第一個運算元稱為目的運算元,第二個稱為來源運算元,IMUL 會把目的運算元當成被乘數,來源運算元當成乘數,相乘之後的乘積存於目的運算元。第三種語法則是在 IMUL 之後接三個運算元,第一個運算元是目的運算元,第二、三個運算元是來源算算元,IMUL 是把第二個運算元當成被乘數,第三個運算元當成乘數,相乘之後的乘積存於第一個運算元,兩個來源運算元不會被破壞 ( 第三個運算元必定是常數,當然不會改變 )。

IMUL 處理溢位旗標的方式是把有號數乘積存於一暫時變數堙A然後把此暫時變數的較低位元存於目的運算元的低位元 ( 此處較低位元指的是乘積所需位元之一半,例如乘數是 8 位元,則被乘數也是 8 位元,故乘積應為 16 位元,較低位元即乘積之較低的 8 位元 ),若此較低位元的目的運算元的符號擴充之後即為暫時變數,則進位旗標與溢位旗標會被清除;反之,進位旗標與溢位旗標會被設定。待完成旗標設定後,再把正確乘積存於目的運算元。例如:

        mov     al,30h
        mov     bl,4h
        imul    bl

運算後之乘積為 00C0h,存於暫時變數堙A較低位元 C0h 存於 AL 擴充符號後為 FFC0h ( 因 AL 的第七位元為 1,擴充符號之後 AH 所有位元均為一 ),與 00C0h 不同,故溢位旗標與進位旗標都被設定。再看另一例子:

        mov     eax,30h
        mov     ebx,4h
        imul    ebx

運算後之乘積為 000000C0h,存於暫時變數堙A較低位元 00C0h 存於 AX 擴充符號後為 000000C0h,與暫時變數相同,故溢位旗標與進位旗標都會被清除。再看另一例子:

        mov     eax,0FFFFFFFFh
        mov     ebx,4h
        imul    ebx

運算後之乘積為 FFFFFFFCh,存於暫時變數堙A較低位元 FFFCh 存於 AX 擴充符號後為 FFFFFFFCh,與暫時變數相同,故溢位旗標與進位旗標都會被清除。再看另一例子:

        mov     al,30h
        mov     bl,-4h
        imul    bl

運算後之乘積為 FF40h,存於暫時變數堙A較低位元 40h 存於 AL 擴充符號後為 0040h ( 因 AL 的第七位元為 0,擴充符號之後 AH 所有位元均為 0 ),與 FF40h 不同,故溢位旗標與進位旗標都被設定。

DIV、IDIV 指令與溢位

DIV 的語法在第七章已經提過,此處不再贅述,這堥蚖◆◎蒂鴘滷“峞C當 DIV 運算時,可能會發生兩種錯誤,一種是除以零的錯誤;一種商數太大,以致於無法存入目的運算元堙C第一種情形顯而易見,在數學上除以零是無意義的,因此這種情形會造成錯誤發生。第二種情形是商數太大,例如下面的例子:

        mov     ax,1000h
        mov     bl,1
        div     bl

在數學上雖然最後商數應該是 1000H,但是對於 80x86 家族的 CPU 而言,這是 8 位元的除法,商數只能放在 AL 暫存器堙A顯然空間是不夠的,所以會造成溢位。對於這兩種錯誤,80x86 都會產生 INT 0 的錯誤,INT 0 即第零號中斷,它會在螢幕上印出『Divide overflow』,然後就結束程式,回到系統。如何避免這兩種錯誤呢?對於避免產生除以零的錯誤,只要在做除法之前檢查除數是否為零即可,例如:

        cmp     bl,0
        jz      no_divide
        div     bl
        ………
no_divide:

對於如何避免第二種錯誤,有一種較為消極的方法,就是把除法改成較多位元的運算例如 32 位元;還有一種方法,就是修改中斷,小木偶打算下一章介紹。

IDIV 指令的語法與 DIV 相同,所不同的地方在於 IDIV 是進行有號數的除法,既是進行有號數除法,就得在意被除數與除數的符號,有時可以用 CBW、CWD、CDQ 等指令,很快的把 AL、AX、EAX 的符號位元擴充到 AX、DX:AX、EDX:EAX,以進行 IDIV 運算。IDIV 和 DIV 一樣,也可能會發生除以零及所得商太大的問題,解決方法也類似。另外,IDIV 與 DIV 都不會影響進位旗標與溢位旗標。


回到首頁到第三十四章到第三十六章