Ch 22 FPU (1) 簡介


FPU 簡介

FPU 是什麼

FPU 稱為浮點運算器是 floating-point processor unit 的縮寫它是一個處理數學運算的晶片。早在 1979 年英特爾就為了搭配 8086/8088 開發出一個名為 8087 的 FPU,IBM/PC/XT 個人電腦中的主機板上面,在 CPU 插槽附近有一個空著的插槽,就是給 FPU 用的。假如您去買 FPU,可以把 FPU 插在上面,再調整組態開關,就可使用 FPU 了。到了 80286、80386 時代也有與之配合的 FPU,分別稱為 80287、80387。到了 80486 時代,Intel 更把 FPU 整合到 80486 裡面變成單一晶片 ( 那時也有不含 FPU 的 80486,稱之為 SX,而含有 FPU 的稱為 DX)。到了 Pentuim 的時代,FPU 已經完全整合到 CPU 中,使用者完全感覺不到它的存在了。在英特爾開發搭配 x86 中央處理器的 FPU 都稱為 8087、80287、80387,所以也稱為 x87。

講了這麼多,到底這個浮點運算器是幹什麼用的呢?小木偶想我們花了很多時間在講各種資料的處理上,這些都會牽涉到數的計算,直到目前為止,都停留在整數的計算,而帶有小數、很大或很小的數,或者我們想計算三角函數、指數、對數時,一般的 CPU 是不能用一條指令就計算出來,常用的方法是以軟體模擬計算,這種模擬常要花很多時間同時也增加程式碼。所以 Intel 就特別設計了一個處理器,專門做這些數值的計算,因為它專門處理非整數的運算,所以稱之為浮點運算器,又因為是輔助 CPU 運算的所以也有人稱為輔助運算器、共同處理器 ( coprocessor ) 或是數值資料處理器 ( NDP,numberic data processor )。FPU 是採用硬體來計算浮點數、對數、三角函數等複雜運算,所以速度比用 CPU 以軟體模擬還快且精確許多,而且程式碼也小很多。一些著名的軟體,如 AutoCAD、Lotus 1-2-3 都會自動的使用 FPU 加速運算。

我想,由上面的說明,您應當瞭解 CPU 和 FPU 所處理的事是不一樣的,FPU 和 CPU 各有各的指令集,彼此不互相干擾。當 CPU 由記憶體提取指令時,如果發現這個指令是屬於 FPU 的指令,就將該指令所需要的位址計算好,交由 FPU 去處理,而 CPU 就接著去處理下一道指令,所以 CPU 和 FPU 能夠同步運算。但是這裡出現兩個問題,第一,如果下一道指令恰好要用到上一道 FPU 所計算的結果,這時就會產生錯誤;第二,在 FPU 運算結束之前,不能再執行下一道 FPU 指令。程式設計師有責任要注意到第一種錯誤是否可能發生,為保證同步運算,在兩個相連的 FPU 和 CPU 指令且同時存取相同的記憶體位址時,得在 FPU 指令前加上 WAIT 指令,而第二種錯誤的避免責任由組譯器負責,MASM 會在每一條 FPU 指令前自動加上 WAIT 這個指令,以保證與 CPU 能同步。

WAIT/FWAIT 指令

WAIT 指令就是使 CPU 等候 FPU 執行完成的指令,也可以寫成 FWAIT。組譯器會自動在每條 FPU 指令前加上 FWAIT 指令。

x87 的堆疊暫存器

x87 的暫存器可分為五類,堆疊暫存器 ( register stack )、控制字組 ( control word )、狀態字組 ( status word )、標籤字組 ( tag word )、例外指標 ( exception pointer )。雖然看起來很複雜,但是最重要且最常用的是堆疊暫存器。

x87 共有八個堆疊暫存器,分別是 ST、ST(1)、ST(2)、ST(3)……ST(7),這八個暫存器每一個都是 80 位元,用來存放運算時所需要的資料,因此也稱為資料暫存器 ( data register )。x87 許多運算都是先把數值推入堆疊頂端的 ST 暫存器,再對 ST 暫存器作運算。ST 暫存器其實是指 ST(0) 暫存器,在堆疊暫存器最上面,也稱為堆疊頂 ( TOS,top of stack );要注意的是,組譯器稱它為 ST,若寫成 ST(0) 還會出錯,因此在原始碼堣ㄞ鉏g成 ST(0)。

這 8 個堆疊暫存器運作方式如同自助餐廳堆在起一堆的餐盤,當服務生堆上一個新餐盤,原來露在最上面的餐盤就變成第二個,第一個變成新餐盤,並且露在最上面;當取出最上面的餐盤,其餘就都往上移一位置,原來第二個餐盤就露出來了。以術語來說,資料存放在堆疊稱為推入 ( push ),移出頂端的資料稱為彈出 ( pop ),但是在 x87 實際運用上,並不是真的把數值移到上或下一個堆疊暫存器,而是以一個指標來表示那一個堆疊暫存器在頂端,而下一個暫存器就稱為 ST(1)。換句話說,這八個堆疊暫存器只是名稱,並非固定指哪一個暫存器,這八個暫存器,每一個都有可能是 ST(0)。這個指標在 x87 狀態字組的三個 TOP 位元堙A恰好可以表示 0∼7,分別代表八個堆疊暫存器。同時 x87 也允許存取底下的堆疊,不像餐盤只能拿取最上面的餐盤。

小木偶想,舉個例子來說明,可能會更加清楚些。這個例子是假設有個程式依序把兩個整數,987654、123456 推入堆疊。先說明的是,八個堆疊暫存器並沒有固定的名稱,姑且以英特爾的稱法,叫她們 R0、R1、R2……R7 ( 超微則稱之為 FPR0∼FPR7 );另外,所謂的堆疊頂,就是前面提到的 TOS,也就是 ST(0),在組合語言原始碼媦g成 ST。電腦系統一開機,會把狀態字組的 TOP 三位元設為 0,堆疊頂就是 R0,這時 ST(0) 就是 R0、ST(1) 就是 R1……,這時八個堆疊暫存器都是空的,如下面最左邊的圖。當要把 987654 推入 x87 時,先使 TOP 減一,變為 111,這是二進位的「111」,也就是十進位的 7,這時堆疊頂為 R7,ST(0) 就變成 R7、ST(1) 就變成 R0……,數值就存入 R7 堙A如下面中間的圖。如果又再推入一數值,TOP 又先減一,變成 6,堆疊頂就變成 R6,ST(0) 就變成 R6、ST(1) 就變成 R7……第二個數值就存入 R6 堣F,如下面最右邊的圖:

上面的說明,是 x87 在把數值推入堆疊的實際動作;彈出堆疊時,則是先把狀態字組 TOP 三位元所指的堆疊頂的數值複製到目的記憶體中,或另一個堆疊暫存器堙A然後再把堆疊頂標示為空的,最後再把 TOP 三位元加一。

上面是 x87 推入或彈出時實際動作,但這對程式設計師來說,太麻煩的。因此大部分的書都不是照上面的講法,而是照以下的想法思考,會比較單純。大部分的想法是認為 ST(0)∼ST(7) 暫存器固定不動,把數值推入堆疊時,就會使數值移到下一個堆疊,而把新的數值放在堆疊頂;如果又要再把另一數推入堆疊,原先在堆疊的數,全部往下移一個暫存器,再把新的數值放在堆疊頂。以下圖說明,一開始開機時,堆疊暫存器都是空的,首先把 987654 推入堆疊頂,所以 ST 為 987654,其餘仍是空的;第二步把 123456 推入堆疊時,ST 暫存器變為 123456,原先在 ST 暫存器的 987654 被往下推到 ST(1),於是 ST(1) 變為 987654,而其餘仍是空的。您觀察這兩種想法,最後 ST(0) 都是最後推入堆疊暫存器的數,ST(1) 都是最先被推入堆疊暫存器的數。也就是說,即使第二種想法不是真實的動作,但是結果一樣且較為單純,因此在撰寫程式時,都採用第二種想法,而不必去管 R0、R1……或 FPR0、FPR1…,也不必去在意狀態字組的三個 TOP 位元。

NDP 堆疊暫存器想像運作方式


第一個 FPU 組合語言程式

原始程式

好了,小木偶想先簡單寫一個程式來介紹如何操作 x87 的堆疊暫存器。底下小木偶介紹一個簡單的程式可以直接計算 32 位元的整數加法程式,但是為了將注意力集中在 x87 的堆疊暫存器上,所以執行結果必須用 DEBUG.EXE 來觀察。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;***************************************
code    segment
        assume  cs:code,ds:code
        org     100h
;---------------------------------------
start:  jmp     short begin
n1      dd      987654  ;被加數
n2      dd      123456  ;加數
sum     dt      ?       ;聚集的 BCD 數
begin:  finit           ;--st0--;--st1--;--st2--;
        fild    n1      ; 987654;       ;       ;
        fild    n2      ; 123456; 987654;       ;
        fadd            ;1111110;       ;       ;
        fbstp   sum     ;       ;       ;       ;
        int     20h
;---------------------------------------
code    ends
;***************************************
        end     start

短整數與聚集 BCD 數

FPU 可以接受七種型態的數值,在此程式堨u用兩種:短整數 ( 或短式整數,short integer ) 與聚集的 BCD 數。短整數是由雙字組 ( 您也可以說 32 個位元 ) 組成,它是以 2 的補數方式表示整數,其範圍為 -2×109 到 +2×109 (-2147483648 到 2147483647 ),它相當於 BASIC 的單精確度資料型態,在組合語言原始碼堨峞yDD』定義,程式的第 7、8 行就定義了兩個短整數,n1 與 n2。

聚集的 BCD 數 ( packed decimal ) 是由十個位元組組成,可以表示 18 位整數。最高位址的那一個位元組的第 07 個位元表示此數的正負值,如果該位元為一表負值,零表正值,第 0 到 6 位元則未使用。剩下的九個位元組,亦即第 0 個位元組到第 8 個位元組,每個位元組,都以四個位元表示一個阿拉伯數值;故每個位元組可以表示兩個數值,所以可以表示 18 位整數。在組合語言原始碼堨峞yDT』來定義聚集的 BCD 數,DT 的意思是 define ten bytes,程式第 9 行就定義了一個聚集的 BCD 數。待會兒,就可以看到聚集的 BCD 數,如何真正的在記憶體中存放。

現在來看看這個程式中所用到的幾個 FPU 指令,您可以看到四個新的指令,它們都是以『F』開頭的,事實上,凡是 FPU 的指令集都是以『F』開頭,這個『F』當然就是浮點數的意思。

FINIT 指令

FINIT 的功能就是重設 FPU,一般要使用 x87 時,通常都會在程式一開始,先用 FINIT 重設 x87 處理器。FINIT 會重設底下六個 x87 堛獐存器:①把控制字組設為 037Fh ( 遮罩所有例外處理,包含不合法的操作、出現反常值、除以零、產生高過上限、低於下限、精確度錯誤,採用 64 位元有效數,四捨五入法捨入 );②把狀態字組設為 0 ( 清除所有例外旗標、使 TOP 指向 FPR0、);③使標籤字組設為 0FFFFh ( 所有的堆疊暫存器都設為空的 );④使運算元指標、指令指標、指令碼均設為 0。初學者其實可以忽略那些較複雜的說明,如果要知道更詳細的資訊,可以參考附錄二,有關 80x87 暫存器的說明。

FILD 指令

這個指令是用來把整數推入堆疊暫存器內 ( 亦即載入整數到堆疊暫存器 ),至於要推入的整數則寫在 FILD 的後面,您可以將它看成 integer load 的意思,而要推入的整數型態則是由『DW』、『DD』、『DQ』定義,這三個定義分別定義字組整數、短整數、長整數。其語法是

FILD    來源運算元

前面提過,FILD 其實是先使狀態暫存器的 TOP 三位元減一,再把來源運算元推入 ST 三位元所指的堆疊暫存器。但我們以後可以直接想成把來源運算元推入 ST(0),原先在 ST(0) 或其他堆疊暫存器的數值,則是被往下推移一個暫存器。

FADD 指令

FADD 是使兩個運算元相加,其語法是

    FADD    目的運算元,來源運算元

這是把目的運算元 ( 直接接在指令後的變數或堆疊暫存器 ) 與來源運算元 ( 接在目的運算元後的變數或堆疊暫存器 ) 相加,並將結果存入目的運算元。但是目的運算元和來源運算元,常常可以省略,所以其實很少用上面的語法。這種被省略的運算元稱為『隱含運算元』。比較常用的是底下的三種格式:

FBSTP 指令

這是把堆疊頂的數值,先以控制字組內的捨入方式到整數位,再以聚集的 BCD 數的形式,彈出到後面的目的運算元堙A這個目的運算元必須是用『DT』定義的聚集 BCD 數。目的運算元最多可以容納 18 位整數,18 位的聚集 BCD 數佔用 9 個位元組,再加上一個位元組代表符號,00 表示正數,80h 表示負數,這個代表符號的位元組,在最高位址。這個指令您可以記成 BCD store and pop,很明顯的 FBSTP 中的『ST』是 store 之意,P 是 pop 之意,其語法是:

FBSTP   目的運算元

例如,ST 為 8907551746.3682,控制字組為四捨五入,經過 FBSTP 處理後,記憶體為 46 17 55 07 89 00 00 00 00 00。如果 ST 為-96485.33289,控制字組為向負無限大捨入,經過 FBSTP 處理後,記憶體為 86 64 09 00 00 00 00 00 00 80。

以 DEBUG 觀察

小木偶將此程式命名為 FPU1.ASM,並將它變成 FPU1.COM 執行檔,用 DEBUG 看看:

H:\HomePage\SOURCE>debug fpu1.com [Enter]
-d 100 L20 [Enter]
1F90:0100  EB 12 06 12 0F 00 40 E2-01 00 00 00 00 00 00 00   ......@.........
1F90:0110  00 00 00 00 9B DB E3 9B-DB 06 02 01 9B DB 06 06   ................
-r [Enter]
AX=0000  BX=0000  CX=002B  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=1F90  ES=1F90  SS=1F90  CS=1F90  IP=0100   NV UP EI PL NZ NA PO NC
1F90:0100 EB12          JMP     0114
-t [Enter]

AX=0000  BX=0000  CX=002B  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=1F90  ES=1F90  SS=1F90  CS=1F90  IP=0114   NV UP EI PL NZ NA PO NC
1F90:0114 9B            WAIT
-u 114 129 [Enter]
1F90:0114 9B            WAIT
1F90:0115 DBE3                  FINIT
1F90:0117 9B            WAIT
1F90:0118 DB060201              FILD    DWORD PTR [0102]
1F90:011C 9B            WAIT
1F90:011D DB060601              FILD    DWORD PTR [0106]
1F90:0121 9B            WAIT
1F90:0122 DEC1                  FADDP   ST(1),ST
1F90:0124 9B            WAIT
1F90:0125 DF360A01              FBSTP   TBYTE PTR [010A]
1F90:0129 CD20          INT     20

您可以看到,在原始程式堣p木偶並沒有使用 WAIT 指令,但是 MASM 會自動在需要等待 x87 完成的 FPU 指令前,自動加上去,而您可能發現 FADD 指令被 MASM 換成 FADDP ST(1),ST 了,怎麼會這樣呢?我想當我解釋完 FADDP 指令您就會釋疑了。

FADDP 指令

這個指令是使目的運算元加上 ST 暫存器,並彈出 ST 暫存器,而目的運算元必須是堆疊暫存器的其中之一,最後不管目的運算元為何,經彈出一次後,目的運算元會變成上一個堆疊暫存器了。其語法為:

FADDP   ST(?),ST

所以 FADDP ST(1),ST 結果和 FADD 指令省略所有運算元時是一樣的。

在上面用白色字表示的數值為 0F1206,這個數當然是十六進位整數,也就是程式中定義的 n1,您可以以筆算看看,是不是就是 987654?如果您已經不記得怎麼計算,請參考附錄一

用 DEBUG 來追蹤這個程式並無意義,因為 DEBUG 無法觀察 FPU 的暫存器,所以小木偶直接執行到程式尾端,觀察經過 FBSTP 運算後的 sum 變數:

-g 129 [Enter]

AX=0000  BX=0000  CX=002B  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=1F90  ES=1F90  SS=1F90  CS=1F90  IP=0129   NV UP EI PL NZ NA PO NC
1F90:0129 CD20          INT     20
-d 100 L20 [Enter]
1F90:0100  EB 12 06 12 0F 00 40 E2-01 00 10 11 11 01 00 00   ......@.........
1F90:0110  00 00 00 00 9B DB E3 9B-DB 06 02 01 9B DB 06 06   ................

上面紅色的數值就是其結果,是不是和我們運算的一樣呢?


小數的加法

以 FPU 來計算整數,實在是大材小用,底下我們來看看 x87 怎樣計算帶有小數的加法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;***************************************
code    segment
        assume  cs:code,ds:code
        org     100h
;---------------------------------------
start:  jmp     short begin
n1      dd      10.25   ;被加數
n2      dd      2.33    ;加數
sum     dd      ?       ;和
begin:  finit           ;--st0--;--st1--;--st2--;
        fld     n1      ; 10.25 ;       ;       ;
        fld     n2      ;  2.33 ; 10.25 ;       ;
        fadd            ; 12.58 ;       ;       ;
        fstp    sum     ;       ;       ;       ;
        int     20h
;---------------------------------------
code    ends
;***************************************
        end     start

這個程式小木偶命名為 FPU2.ASM,它和 FPU1.ASM 不同之處,僅在於載入與彈出的部分,載入小數或是很大很小的數 (帶有小數的數或以十的冪方表示的數稱為浮點數) 用 FLD 載入,不可用 FILD 否則 FPU 會自動將小數點後的數捨入。

FLD 指令

載入浮點數到 ST 暫存器。而要載入的數可以用『DD』、『DQ』、『DT』來定義。

FSTP 指令

這個指令是用來把 ST 的數以浮點數的方式彈出至後面接的變數堙A而這個變數必須是用『DD』、『DQ』、『DT』其中之一定義的。其語法是:

FSTP    變數名

我想初學者要注意的是把整數載入到 x87,要用 FILD,載入浮點數要用 FLD;把 x87 內的整數存入記憶體,用 FISTP,存入浮點數用 FSTP ( 註一 ),這點很重要,也是常犯的錯誤。好吧,現在用 DEBUG 載入觀察看看。

H:\HomePage\SOURCE>debug fpu2.com [Enter]
-r [Enter]
AX=0000  BX=0000  CX=0025  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=128B  ES=128B  SS=128B  CS=128B  IP=0100   NV UP EI PL NZ NA PO NC
128B:0100 EB0C          JMP     010E

先看看 n1、n2 組譯後變成什麼樣子?

-d 102 Lc [Enter]
128B:0100        00 00 24 41 B8 1E-15 40 00 00 00 00           ..$A...@....

Short Real 短實數

上面白色部分的就是 n1 組譯後的情形,變成 41 24 00 00 了,n2 則變成 40 15 1E B8,sum 是 00 00 00 00,怎麼會變成這樣呢?原來組譯器看到帶有小數點的數值 ( 浮點數 ) 會翻譯成 IEEE 754 格式,而不用十六進位整數處理。用 『DD』定義的浮點數稱為『短實數』,短實數佔有 4 個位元組,共 32 個位元。這 32 位元的最高位元 ( 第 31 位元 ) 表示符號 ( sign ),即此數為正數還是負數。若此位元為零,表示此數為正數;為一,表示此數是負數。第 23 到 30 位元這 8 個位元表示指數部分 ( exponent ),指數部分是以 2 為底數,但在做乘冪之前,指數還得減去基準數 ( bias ),短實數的基準數是 127。第 0 位元到第 22 位元是有效數部份 ( significand ),有效數部份是以 1 開始,依次減半的等比數列 1/2、1/4、1/8、1/16、1/32……的方式排列相加,因為 1 固定所以不表示 ( 註三 ),而從 1/2 開始。綜合上面的各部份,短實數的數值是:

短實數 = (-1)sign×significand×2exponent

以 10.25 為例,組譯器翻譯成 IEEE 754 格式是 41 24 00 00 先變成二進位 0100 0001 0010 0100 0000 0000 0000 0000,第 31 位元 ( 天藍色 ) 為零表示正數,接下來的 8 個位元 ( 白色 ) 換成十進位是 130,減去基準數 127 等於 3,所以指數部分就是 23。而最後面的部分是有效數,小木偶將它排成直列來說明:

1 ==>              1 (固定值,不在IEEE 754格式表示出來)
0 ==>表示 1/2×0  = 0
1 ==>表示 1/4×0  = 0.25
0 ==>表示 1/8×0  = 0
0 ==>表示 1/16×0 = 0
1 ==>表示 1/32×1 = 0.03125
以下皆為零

最後 1+0.25+0.03125 為 1.28125,再乘以指數部份 23 即可得 10.25。

這種方法看起來很複雜,不過我們不需要知道如此瑣碎的事情,我們對浮點數只需要知道三件事,佔用位元組幾個,準確度多少,能表示的範圍多大,這些請看註四

-u 10e 123 [Enter]
128B:010E 9B            WAIT
128B:010F DBE3                  FINIT
128B:0111 9B            WAIT
128B:0112 D9060201              FLD     DWORD PTR [0102]
128B:0116 9B            WAIT
128B:0117 D9060601              FLD     DWORD PTR [0106]
128B:011B 9B            WAIT
128B:011C DEC1                  FADDP   ST(1),ST
128B:011E 9B            WAIT
128B:011F D91E0A01              FSTP    DWORD PTR [010A]
128B:0123 CD20          INT     20
-g [Enter]

Program terminated normally
-d 102 Lc [Enter]
128B:0100        00 00 24 41 B8 1E-15 40 AE 47 49 41           ..$A...@.GIA

同樣的,計算完後仍以浮點數方式表示,見白色部份,為了方便,小木偶建議使用 SYMDEB.EXE 來除錯,雖然它無法觀看堆疊暫存器的數值,但是可以把短實數、長實數和暫時實數三種模式變成十進位的『科學記號』( 有點和真正的科學記號出入 ) 顯示於螢光幕。底下用 SYMDEB 來看看:

H:\HomePage\SOURCE>symdeb fpu2.com [Enter]
Microsoft (R) Symbolic Debug Utility  Version 4.00
Copyright (C) Microsoft Corp 1984, 1985.  All rights reserved.

Processor is [80286]
-ds 102 L3 [Enter]
21FE:0102  00 00 24 41  +0.1025E+2
21FE:0106  B8 1E 15 40  +0.2329999923706055E+1
21FE:010A  00 00 00 00  +0.0E+0
-g [Enter]

Program terminated normally (0)
-ds 102 L3 [Enter]
21FE:0102  00 00 24 41  +0.1025E+2
21FE:0106  B8 1E 15 40  +0.2329999923706055E+1
21FE:010A  AE 47 49 41  +0.1257999992370605E+2

SYMDEB 加強了 dump 指令,ds 就是用短實數方式顯示,dl ( 英文字母的 L,不是阿拉伯數字的 1 ) 是以長實數方式顯示,可以參考附錄六。您可以看到在 010A 處就是 x87 的計算結果,您或許會說,怎麼不是 12.58?這是因為當把浮點數變成 IEEE 754 格式推入 x87 堆疊暫存器時,會產生誤差,這是無可避免的,反之亦復如此。在 x87 的堆疊暫存器媔有 80 位元,當然不能表示所有的數,所以會必定做一些誤差。

設計師所能做的是增加精確度而已,所以此處您應該注意兩件事。第一,您所寫的程式所需精確度為何?如果是不須太準確就用短實數,如果要求很高就用暫時實數。第二,儘量不要把堆疊暫存器的數推入彈出,只有必要時再做,因為這樣不但會降低精確度也浪費時間。


算數指令

在簡單介紹過堆疊暫存器的操作及簡單的概念後,小木偶簡單介紹有關 x87 四則運算指令。

加法指令:FADD、FADDP、FIADD

FPU 提供了三種加法指令,前兩種,FADD、FADDP 前面已敘述過,不再重複,此處僅介紹 FIADD 指令。顧名思義,『I』是指整數 (integer) 之意,FIADD 是把 ST 加上來源運算元,然後再存入 ST 暫存器,來源運算元必須是字組整數或短整數形態的變數。其語法是

FIADD   mem

mem 是字組整數或短整數形態的變數。

減法指令:FSUB、FSUBP、FSUBR、FSUBRP、FISUB、FISUBR

FPU 所提供的減法指令有六種:FSUB、FSUBP、FSUBR、FSUBRP、FISUB、FISUBR。第一個指令,FSUB 指令,它的用法和 FADD 相同,也有三種格式,分成指定兩個運算元、指定一個運算元和不指定運算元三種。第二個指令,FSUBP,它的用法和 FADDP 相同,所以這兩個指令就不再說明。

第三個指令,FSUBR 指令,它和 FSUB 只有一點不同,就是減數與被減數互換,這個『R』字是 reversed 的意思。它也有三種格式:

第四個指令,FSUBPR,它的用法和 FSUBP 相同,只有一點不同,就是減數與被減數互換。例如

FSUBPR  ST(1),ST

這個例子會把 ST-ST(1) 之差存入 ST(1),然後再做一次彈出動作,使得最後 ST 變成原來的 ST-ST(1)。

第五個指令,FISUB,它是整數減法指令,把 ST 減去來源運算元的差,再存入 ST 內,來源運算元必須是字組整數或短整數變數。

第六個指令是,FISUBR,它也是整數減法指令,它和 FISUB 指令相同,差別只在減數與被減數交換。

乘法指令:FMUL、FMULP、FIMUL

這三個指令和 FADD、FADDP、FIADD 相同,只是加法改成乘法而已。

除法指令:FDIV、FDIVP、FDIVR、FDIVRP、FIDIV、FIDIVR

這六個除法指令和減法指令 FSUB、FSUBP、FSUBR、FSUBRP、FISUB、FISUBR 相同,只是減法改成除法。

改變符號:FCHS

這個指令會改變 ST 的正負值,如果原先 ST 為正值,執行後變為負值;原先為負值,執行後為正值。

絕對值:FABS

把 ST 之值取出,取其絕對值後再存回去。

平方根:FSQRT

將 ST 之值取出,開根號後再存回去。

FSCALE 指令

這個指令是計算 ST*2ST(1)之值,再把結果存入 ST 埵 ST(1) 之值不變。ST(1) 必須是在 -32768 到 32768 (-215 到 215 )之間的整數,如果超過這個範圍計算結果無法確定,如果不是整數 ST(1) 會先向零捨入成整數再計算。所以為安全起見,最好是由字組整數載入到 ST(1) 堙C

FRNDINT 指令

這個指令是把 ST 的數值捨入成整數,FPU 提供四種捨入方式,由 FPU 的控制字組(control word)中的 RC 兩個位元決定,如下表:

RC捨入控制 說明例子
00四捨五入 向最近的整數
逢四捨去,遇五進位
4.8 → 5.0  -4.8 →-5.0
4.2 → 4.0  -4.2 →-4.0
01向負無窮大捨入 正值捨去小數部分
負值捨去小數部分後再減一
4.8 → 4.0  -4.8 →-5.0
4.2 → 4.0  -4.2 →-5.0
10向正無窮大捨入 正值捨去小數部分後再加一
負值捨去小數部分
4.8 → 5.0  -4.8 →-4.0
4.2 → 5.0  -4.2 →-4.0
11向零捨去 不論正負值均捨去小數部分 4.8 → 4.0  -4.8 →-4.0
4.2 → 4.0  -4.2 →-4.0

FPREM 指令

這個指令是求部份餘數(partial remaimder),較簡略的說法是將 ST 除以 ST(1) 後的餘數存回 ST,ST(1) 則不變。這個指令實際運作時,是以連續減法的方式求出餘數,詳細情形在三角函數時說明。

FXTRACT 指令

這個指令稱為抽取指數與有效數(extract exponent and significand),是把 ST 內的數值改成 X*2Y,然後把 Y 存回 ST 堙A再把 X 推入堆疊,所以最後 ST 為有效數,ST(1) 為以 2 為底的指數。FXTRACT 與 FSCALE 恰好成相反運算。

整理

講了這麼多的算數指令,在此做個整理。x87 的指令可分為 6 大類:資料傳輸 ( data transfer ) 指令、算術指令、超越函數 ( transcendental ) 指令、常數 ( constant ) 指令、比較 ( comparison ) 指令、處理機控制 ( processor control ) 指令。

針對算術指令,x87 提供了 18 個有關四則運算的指令以及三個較常用的指令。這 18 個四則運算的指令基本格式都是像下面這樣

指令    目的運算元, 來源運算元

其操作過程都是把目的運算元和來源運算元做加、減、乘、除後再存回目的運算元,其中加法與乘法目的運算元和來源運算元互換並不影響結果,但減法與除法則結果會不同,所以又分為兩種,標準減法是目的運算元減去來源運算元後再存回目的運算元,而『反』減法則是來源運算元減去目的運算元後再存回目的運算元,除法也和減法相同,不再贅述。

這些四則運算指令依目的運算元及來源運算元的格式又可分為三種,:


註一:事實上,FLD 也可以載入整數,但該整數的定義,和 FILD 的不同。假如用 FILD 載入,則必須用 DW、DD、DQ 三種方式宣告,並且其後的資料必須是沒有小數點或 E。( E 表示 10 的幾次方,這幾次方寫在 E 的後面 )。假如用 FLD 載入,則用 DD、DQ 方式宣告,而其後的資料必須包含小數點或是 E。例如:

num1    dd      123456
num2    dd      123456.0
        fild    num1
        fld     num2

雖然 num1、num2 都是十二萬三千四百五十六的整數,但是經由 MASM 編碼後之結果不同,num1 被看成是十六進位整數,編碼成 01E240,num2 被看成是 IEEE 754 浮點格式,編碼成 00 20 F1 47,因此載入方法不同。

註二:事實上,浮點數的編碼方式有兩種,一種是 IEEE 754 格式,另一種是微軟自訂的『Microsoft 二進位格式』。在 MASM 第 5.0 版及其以後版本的浮點數,MASM 會自動編碼成 IEEE 754 格式;而在 MASM 4.0 及其以前的版本會自動使用『Microsoft 二進位格式』。要使用那一種編碼方式,可以在原始程式的第一行或第一個區段定義前面加上『.8087』或『.MSFLOAT』指示元,前者表示使用 IEEE 754 編碼,後者使用微軟二進位格式編碼。

換句話說,如果您是使用 MASM 5.0 及其以後的版本,不加『.8087』或者加入『.8087』,都會被編碼成 IEEE 754 格式;只有加上『.MSFLOAT』指示元後,才會使用『微軟二進位格式』。在 MASM 4.0 及其前版的組譯程式,要使用 IEEE 754 格式編碼,就一定要加上『.8087』指示元;否則會使用微軟二進位格式。

另外還有『.80287』指示元是用來使用 80287 新增加的指令。

註三:短實數和長實數有效數的最高位元必為一,為什麼會這樣呢?我們知道如果把記憶體或暫存器中的二進位數向左移一個位元代表乘以 2;反之,向右移一個位元表示除以 2。所以如果最高的幾個位元是零的話,那麼乾脆就直接把有效數向左移位,直到最高位元是一,再把指數部份減掉向左移動的位數,所得結果會跟原數相同,但是這麼一來,可以使有效位數的精密度多一點。因此短實數和長實數有效數的最高位元必為一,像這樣的編碼方式稱為正規形式 ( Normal )。又因為最高位元必為一,如果再把它省略掉,不就又可以增加一有效位數嗎?的確如此,所以短實數和長實數有效數的最高位元代表的是二分之一,接下來是四分之一、八分之一、十六分之一……。暫時實數雖然也是以正規形式的方式編碼,但是其有效數的最高位元的一並不省略,所以第 63 位元必為一,而第 62 位元則表示二分之一、接著就是四分之一、八分之一……。至於暫時實數的引導位元為何不省略,小木偶並不清楚。

由剛才所說的,可以得知短實數與長實數有效數最高位元代表二分之一,可能為零或一;而暫時實數有效數的最高位元代表一,而且必為一。那麼暫時實數有效數的最高位元可不可以為零呢?答案是可以的,不過這時候這個數並不是一般的實數,我們稱為反常數 ( denormals ) 或異常值 ( unnormals )。請看註四。

註四:x87 所能接受的資料形態共有七種,分別是四種整數:字組整數、短整數、長整數、聚集 BCD 數;以及三種浮點數:短實數、長實數、暫時實數。

除了聚集 BCD 數外,整數均以十六進位來表示,只是長度不同而已,字組整數長 16 位元 ( 2 個位元組 ),用『DW』定義,在 MASM 6.x 堣]可以用『WORD』定義。短整數長 32 位元 ( 4 個位元組 ),用『DD』定義 ( 定義雙字組之意,define di-word,在 MASM 6.x 堣]可以用『DWORD』定義 )。長整數長 64 位元 ( 8 個位元組 ),用『DQ』定義 ( 定義四字組之意,define quart-word,在 MASM 6.x 堣]可以用『QWORD』定義 )。

至於實數的表示方式,可分為短實數、長實數和暫時實數,分別以『DD』、『DQ』、『DT』定義,例如下面的例子:

N0      DD      6.02E23     ;定義亞佛加厥常數
h       DT      6.626E-34   ;定義蒲朗克常數
Z       DQ      -100.0      ;定義負 100

這三種實數的資料形態,不管是那一種,都可以分成三部份,分別是符號 ( sign )、有效數 ( significand )、指數 ( exponent ),如下式。至短實數、長實數、暫時實數的差別在於有效數與指數所佔用的位元範圍及指數的基準值不同而已。

實數 = (-1)sign×significand×2exponent

短實數的說明,已經在前文說明,在此不說明了。長實數以『DQ』定義 ( MASM 6.x 堣]可以用『QWORD』定義長實數),正負位元在第 63 位元,指數部份在第 52 到 62 位元,其餘為有效數部份,基準值為 1023。暫時實數以『DT』定義 ( 在 MASM 6.x 堣]可以用『TBYTE』定義 ),正負位元在第 79 位元,指數部份在第 78 到 64 位元,其餘為有效數部份,基準值為 16383。暫時實數與長、短實數的有效數有一點不同,暫時實數的有效數是在第 63 位元,此位元是由 1 開始表示、下一個 ( 第 62 位元 ) 表示 1/2……;而長、短實數則分別在第 51、22 位元,此位元由 1/2 開始、下一個位元則是表示 1/4……。在 FPU 堆疊暫存器媕x存的數都是暫時實數,即使用整數載入 FPU 也會將他轉換成暫時實數。底下是它們的說明圖:

8087 資料形態圖解

底下是它們的列表整理:

資料型態佔用
位元組
指數
基準數
十進位時
有效數位數
能表示範圍在記憶體中表示 -127
字組整數 2-5 -32768 到 32767 的整數 FF 81
短整數4- 10 -2147483648 到 2147483647 的整數 FF FF FF 81
長整數 8-18 -9.223×1018 到 9.223×1018的整數
(-9223372036854775808 到 9223372036854775807 )
FF FF FF FF FF FF FF 81
聚集
BCD 整數
10-18 -999999999999999999 到
999999999999999999 的整數
80 00 00 00 00 00 00 00 01 27
短實數 4127
7FH
6 或 7 8.43×10-37 到 3.37×1038
或 -8.43×10-37 到 -3.37×1038
C2 FE 00 00
長實數 81023
3FFH
15 或 16 4.19×10-307 到 1.67×10308
或 -4.19×10-307 到 -1.67×10308
C0 5F C0 00 00 00 00 00
暫時實數 1016383
3FFFH
19 3.3621×10-4932 到 1.1897×104932
或-3.3621×10-4932 到 -1.1897×104932
C0 05 FE 00 00 00 00 00 00 00

FPU 除了能表示『正常』的實數 ( 正負實數 ) 之外,還可以表示幾種特殊的數值,還包含 0 以及非數值 ( NaN,Not a Number ),底下是暫時實數的編碼方式,至於長實數、短實數的編碼方式參閱英特爾的 Pentium® Processor Family Developer’s Manual Volume 3: Architecture and Programming Manual。

範圍意義範圍意義
符號指數有效數 符號指數有效數
000000000 0000 0000 0000正零 100000000 0000 0000 0000負零
000000000 0000 0000 0001

7FFF FFFF FFFF FFFF
Denormals 100000000 0000 0000 0001

7FFF FFFF FFFF FFFF
Denormals
000008000 0000 0000 0000

FFFF FFFF FFFF FFFF
Pseudo-
denormals
100008000 0000 0000 0000

FFFF FFFF FFFF FFFF
Pseudo-
denormals
00001

7FFE
0000 0000 0000 0000

7FFF FFFF FFFF FFFF
Unnormals 10001

7FFE
0000 0000 0000 0000

7FFF FFFF FFFF FFFF
Unnormals
00001

7FFE
8000 0000 0000 0000

FFFF FFFF FFFF FFFF
Normals 10001

7FFE
8000 0000 0000 0000

FFFF FFFF FFFF FFFF
Normals
07FFF0000 0000 0000 0000Pseudo-
infinity
17FFF0000 0000 0000 0000Pseudo-
infinity
07FFF0000 0000 0000 0001

3FFF FFFF FFFF FFFF
Pseudo-
Signaling
NaNs
17FFF0000 0000 0000 0001

3FFF FFFF FFFF FFFF
Pseudo-
Signaling
NaNs
07FFF0400 0000 0000 0000

7FFF FFFF FFFF FFFF
Pseudo-
Quiet NaNs
17FFF0400 0000 0000 0000

7FFF FFFF FFFF FFFF
Pseudo-
Quiet NaNs
07FFF8000 0000 0000 0000正無窮大 17FFF8000 0000 0000 0000負無窮大
07FFF8000 0000 0000 0001

BFFF FFFF FFFF FFFF
Signaling
NaNs
17FFF8000 0000 0000 0001

BFFF FFFF FFFF FFFF
Signaling
NaNs
07FFFC000 0000 0000 0000

FFFF FFFF FFFF FFFF
Quiet NaNs 17FFFC000 0000 0000 0000

FFFF FFFF FFFF FFFF
Quiet NaNs

上表中左側是正數 ( 底色為棕色 ),右側是負數 ( 底色為深藍色 ),至於底色為紫色的部份是沒有被 FPU 所支援的範圍。

在上表中的 FPU 編碼堙A零可以分為正零、負零,但實際應用上可視為相等,FXAM 能用來檢查正零或負零。

而一般實數 ( normals ) 的指數部份從 1 到 7FFE,而有效數部份從 8000 0000 0000 0000 到 FFFF FFFF FFFF FFFF,請注意它的有效數的最高位元 ( 也就是第 63 位元 ) 必定是 1。

反常數 ( denormals ) 是指數為零,有效數從 1 到 7FFF FFFF FFFF FFFF 之間的數,其實在數學上它代表很小的數。例如記憶體中如果有個暫時實數是 00 00 40 00 00 00 00 00 00 00,那麼這個數的指數是 0-16386,有效數由二分之一開始,後面也沒有了,所以這個數是 2-16383×二分之一,約等於 8.40528×10-4933。像這種情形已經比 x87 所能儲存的一般實數還小, 一般稱為低於下限,underflow 。FPU 並不會把它設為零,然後提出錯誤訊號給 CPU;它會儘量把正確但是有效位數不那麼多的數值保存在堆疊暫存器,並且也能繼續運算。反常數如果由記憶體載入至 FPU 堆疊暫存器堙A或在運算中產生反常數,那麼它會被 FPU 轉換成異常數 ( unnormals )。

異常數 ( unnormals ) 的指數部份是從 1 到 7FFE,而且有效數部份的最高位元為零,異常數事實上可以經由位元移位而使之變成為一般實數。如果異常數參與運算,可能會得到一般實數,但也有可能得到一個異常數,同樣的,FPU 會儘量的保持最大的精密度。

正負無窮大的指數部份為 7FFF,有效數部份為 8000 0000 0000 0000,正無窮大的符號位元為零;負無窮大的符號位元為一。無窮大可在 FPU 運算中當做運算元,例如 10 除以正無窮大時會得到正零;反過來 10 除以零會得到正無窮大。總之,FPU 儘量保持運算的結果正確。

非數值 ( NAN ) 是指除了無窮大以外,所有指數部份為 7FFF 的數值。非數值可分為兩種:Signaling NaNs、Quiet NaNs。有效數的最高位元為 0,稱為「Signaling NaNs」;為 1,則稱為「Quiet NaNs」。對暫時實數而言,有效數的最高位元是第 62 位元 ( 暫時實數有 80 位元長,從 0∼79 );對長實數而言,是第 51 位元;對短實數而言,是第 22 位元。x87 只會產生「Quiet NaNs」,不產生「Signaling NaNs」。當 x87 對負數開根號、零除以零、對負數取對數、對兩個堆疊暫存器相加卻有一暫存器是空的、對大於 1 的數做反正弦運算等,都會產生「Quiet NaNs」。


回到首頁到第二十一章到第二十三章