Ch 23 FPU (2) 一元二次方程式

這一章堣p木偶將延續上一章的指令,寫一個計算一元二次方程式的程式,ROOT.ASM。


一元二次方程式之根

底下小木偶就利用以上所提的 8087 指令求得一元二次方程式之根。根據國中所學的,假如有一個一元二次方程式 ax2+bx+c=0,則此方程式 x 之解為

一元二次方程式根之公式

底下這個程式是求 x2-25=0 方程式之解,如果您要求其他方程式的解,必須修改原始程式之的 lr_a、lr_b、lr_c 三係數 (在 MASM 6.x 裡,c 是保留字,所以為了使 MASM 5.x 與 6.x 都能順利組譯,我在係數之前加上 lr,表示長實數之意)。這個程式也只能由 SYMDEB.EXE 觀看結果,小木偶將於稍後再撰寫直接顯示 ST 之十進位數值於螢幕上的副程式。

;***************************************
code    segment
        assume  cs:code,ds:code
        org     100h
;---------------------------------------
start:  finit             ;---st--;-st(1)-;-st(2)-;-st(3)-;06
        fld     lr_b      ;   b   ;
        fmul    lr_b      ;  b2   ;       ;       ;       ;08
        fld     lr_a      ;   a   ;  b2   ;
        fmul    lr_c      ;  ac   ;  b2   ;       ;       ;10
        fimul   const     ;  4ac  ;  b2   ;
        fsubp   st(1),st  ;D=bb-4ac       ;       ;       ;12
        shr     const,1
        fsqrt             ; SQR(D);       ;       ;       ;14
        fld     lr_b      ;   b   ; SQR(D)
        fchs              ;  -b   ; SQR(D);       ;       ;16
        fld     st        ;  -b   ;  -b   ; SQR(D)
        fadd    st,st(2)  ;-b+SQ(D)  -b   ; SQR(D);       ;18
        fild    const     ;   2   ;-b+SQ(D)  -b   ; SQR(D)
        fmul    lr_a      ;  2a   ;-b+SQ(D)  -b   ; SQR(D);20
        fdiv    st(1),st  ;  2a   ;  x1   ;  -b   ; SQR(D)
        fxch    st(1)     ;  x1   ;  2a   ;  -b   ; SQR(D);22
        fstp    x1        ;  2a   ;  -b   ; SQR(D)
        fxch    st(1)     ;  -b   ;  2a   ; SQR(D);       ;24
        fsubrp  st(2),st  ;  2a   ;-b-SQ(D)
        fdivp   st(1),st  ;  x2   ;       ;       ;       ;26
        fstp    x2
        int     20h     ;28
        org     160h    ;29
lr_a    dq      1.00    ;30
lr_b    dq      0.00	;31 
lr_c    dq      -25.0	;32
x1      dq      ?       ;33 其中之一根
x2      dq      ?       ;34 其中之一根
const   dw      4       ;35
;---------------------------------------
code    ends
;***************************************
        end     start

先來看看執行結果。

h:\homepage\source>e:symdeb root.com [Enter]
Microsoft (R) Symbolic Debug Utility  Version 4.00
Copyright (C) Microsoft Corp 1984, 1985.  All rights reserved.

Processor is [80286]
-G [Enter]

Program terminated normally (0)
-DL 160 L5 [Enter]
2117:0160  00 00 00 00 00 00 F0 3F  +0.1E+1
2117:0168  00 00 00 00 00 00 00 00  +0.0E+0
2117:0170  00 00 00 00 00 00 39 C0  -0.25E+2
2117:0178  00 00 00 00 00 00 14 40  +0.5E+1
2117:0180  00 00 00 00 00 00 14 C0  -0.5E+1
-Q [Enter]

為了能夠方便觀察係數與根,所以小木偶在程式第 29 行加入『org 160h』,使得所有長實數變數能夠由位址 160H 開始,前三個長實數依次是二次方程式的係數,第四、五個長實數是兩根。在 SYMDEB 堙A『E』是表示 10 的冪方的意思,例如,位址 160H 的長實數是 +0.1E+1 就是表示 +0.1 乘以 10 的一次方,也就是 1 的意思。

至於為何我選在位址 160H 開始存放變數呢?其實 160H 這個位址的獲得經過假設再精確求得。該位址的限制是不能覆蓋程式碼,因程式不大,小木偶先假設位址為 1C0H,組譯連結後由 SYMDEB 觀察得知最後一行『INT 20H』在位址 157H,所以我再修改為 160H。

ROOT.ASM 程式有一個缺陷,假如 b2-4ac < 0,其平方根會產生錯誤,這時應使用 FPU 的比較指令進而產生條件跳躍,使程式轉向。

使 FPU 能條件跳躍

所謂條件跳躍就是比較兩數的大小,如果某數較大,就執行一段程式,若較小則執行另一段程式,類似這樣的跳躍稱為條件跳躍。在 FPU 的指令集並沒有能使 FPU 跳躍的指令,事實上 FPU 也無法改變 CPU 暫存器之值 (還記得要改變程式執行位址必須改變 CS:IP 之值),所以要產生 FPU 條件跳躍必須用間接方法。其步驟有四個:

  1. 先用 FPU 的比較指令改變 FPU 的狀態字組暫存器。
  2. 再用 FPU 的指令 FSTSW 把狀態字組存入一個記憶體變數堙C
  3. 將此記憶體變數存入 AH 暫存器,再用 CPU 指令 SAHF 指令存入 CPU 的旗標暫存器堙C此步驟使旗標暫存器之值等於狀態字組。
  4. 再用 CPU 指令 JL/JG/JE/JA/JB 來跳躍至正確處執行。

狀態字組

FPU 有五類暫存器,前章已介紹過最常用的堆疊暫存器,這裡將介紹狀態字組(status word)。顧名思義,狀態字組是一個 16 位元的暫存器,表示 FPU 的狀態,所謂狀態是指 FPU 是不是在忙碌中、是否除以零、是否無效運算、現在堆疊頂端是那一個堆疊暫存器等等,在此處我們要注意的是四個狀態碼位元,C0、C1、C2、C3。這些位元分布如下圖:

x87 狀態字組

比較指令:FCOM、FCOMP、FCOMPP、FICOM、FICOMP

標題所見的這些比較指令都是以堆疊頂為目的運算元,而來源運算元可以是記憶體變數或是其他的堆疊暫存器,例如

FCOM    ST,ST(1)
FCOM    ST,x

上述第一例是比較 ST 和 ST(1) 之數值,第二例是比較 ST 和記憶體變數 x 之數值,而這記憶體變數的形態可以是短實數和長實數。因為 FCOM 等指令均以 ST 為目的運算元,所以 ST 是可以省略不寫的,例如上述兩例,可以寫成下面的樣子,

FCOM    ST(1)
FCOM    x

結果是一樣的。假如 ST 的比較對象是 ST(1) 的話( 來源運算元是 ST(1) ),連 ST(1) 也可以省略。至於 FCOMP 和 FCOMPP 分別是比較後彈出一次和彈出兩次,這裡彈出的數會消失不見並沒有存入記憶體中,這點和 FSTP 不同。而 FICOM 和 FICOMP 是用來比較整數的,而這來源運算元整數形態可以是字組整數和短整數。

FPU 的比較指令會改變狀態字組的 C3 和 C0 位元,C3、C0 位元是在狀態字組的第八、14 位元,如上圖。比較後依 ST 和來源運算元的大小,C3 和 C0 設定方式如下表:

  比較結果      C3  C0
----------------------------
ST>來源運算元   0   0
ST<來源運算元   0   1
 ST=來源運算元   1   0

FSTSW 指令

FSTSW (store status word) 這個指令的功用是用來把狀態字組取出並存入AX暫存器或 16 位元長的記憶體變數堙A其語法是

FSTSW   mem16
FSTSW   AX

注意!FSTSW 之後所接的運算原可以是 AX 或 16 位元長的記憶體變數,前者只能用在 387 等級以上的 FPU;至於 16 位元長的記憶體變數則是 8087 就可以使用了。為什麼只能接 AX 而不可接其他 16 位元的暫存器呢?主要是為了下面 SAHF 指令能很方便的把 AH 暫存器內的資料移到旗標暫存器堙A否則 FPU 的運算元是不能使用 CPU 的暫存器。

SAHF 指令

SAHF 是 8088 指令集中的一個指令,並非 FPU 指令。這個指令是把 AH 暫存器中的值移到旗標暫存器的較低的 8 個位元,這 8 個位元只有第七、六、四、二、零位元有用,第五、三、一位元沒有使用,請參考附錄二8088 的旗標暫存器

所以假如 AH 為 0100 0000B,則執行 SAHF 指令後,零旗標會被設為一,您可以預測看看下列程式片段將會有何結果?

        mov     ah,01000000b
        sahf
        jz      zf_set
        mov     dx,offset mes1
        jmp     short print
zf_set: mov     dx,offset mes2
print:  mov     ah,9
        int     21h
mes2    db      '零旗標已設定$'
mes1    db      '零旗標未設定$'

這程式片段會顯示出『零旗標已設定』。所以事實上,跳躍指令其實是僅僅看旗標暫存器的設定值決定如何跳到那裡去執行,至於旗標暫存器如何設定,在跳躍指令執行時是不須理會的

FPU 的狀態暫存器堛 C3 和 C0 位元,經 FSTSW 指令傳到 16 位元記憶體變數,再接著移到 AX 暫存器堮氶AAH 的第 6、0 位元就是表示 C3 和 C0 位元,您可以對照上面 8087 狀態字組與 8088 旗標暫存器兩張圖,發現 C3 恰好對應 ZF,C0 恰好對應 CF。之後再由 SAHF 指令把 AH 移到旗標暫存器,所以 C3、C0 就移到零旗標與進位旗標了,只要檢查這兩個旗標就可以比較 ST 與來源運算元的大小,而決定跳躍方向。

修改 ROOT.ASM 程式

前面所撰寫的求一元二次方程式兩根的程式 ROOT.ASM 有缺陷,在這裡小木偶將他修改如果判別式小於零,程式能轉向執行。修改之後程式如下:

;***************************************
code    segment
        assume  cs:code,ds:code
        org     100h
;---------------------------------------
start:  finit             ;---st--;-st(1)-;-st(2)-;-st(3)-;06
        fld     lr_b      ;   b   ;
        fmul    lr_b      ;  b^2  ;       ;       ;       ;08
        fld     lr_a      ;   a   ;  b^2  ;
        fmul    lr_c      ;  ac   ;  b^2  ;       ;       ;10
        fimul   const     ;  4ac  ;  b^2  ;
        fsubp   st(1),st  ;D=bb-4ac       ;       ;       ;12
        shr     const,1
        fcom    zero      ;判別式是否小於 0                14
        fstsw   status    ;結果存於 status 變數          15
        fwait             ;等待儲存完畢                    16
        mov     ax,status ;比較結果移入 AX                 17
        sahf              ;比較結果移入旗標暫存器          18
        jb      d_less_0  ;若小於則跳躍到 d_less_0 處      19
        fsqrt             ; SQR(D);       ;       ;       ;20
        fld     lr_b      ;   b   ; SQR(D)
        fchs              ;  -b   ; SQR(D);       ;       ;22
        fld     st        ;  -b   ;  -b   ; SQR(D)
        fadd    st,st(2)  ;-b+SQ(D)  -b   ; SQR(D);       ;24
        fild    const     ;   2   ;-b+SQ(D)  -b   ; SQR(D)
        fmul    lr_a      ;  2a   ;-b+SQ(D)  -b   ; SQR(D);26
        fdiv    st(1),st  ;  2a   ;  x1   ;  -b   ; SQR(D)
        fxch    st(1)     ;  x1   ;  2a   ;  -b   ; SQR(D);28
        fstp    x1        ;  2a   ;  -b   ; SQR(D)
        fxch    st(1)     ;  -b   ;  2a   ; SQR(D);       ;30
        fsubrp  st(2),st  ;  2a   ;-b-SQ(D)
        fdivp   st(1),st  ;  x2   ;       ;       ;       ;32
        fstp    x2
        int     20h               ;34 結束程式
d_less_0:                         ;35 判別式小於零,無實數解
        mov     dx,offset message ;36 印出無實數解
        mov     ah,9
        int     21h
        int     20h               ;39 結束程式

        org     1e0h    ;41
lr_a    dq      1.00    ;42
lr_b    dq      0.00    ;43
lr_c    dq      25.0    ;44
x1      dq      ?       ;45 其中之一根
x2      dq      ?       ;46 其中之一根
zero    dq      0       ;47
const   dw      4       ;48
status  dw      ?       ;49 狀態字組
message db      '無實數解$'
;---------------------------------------
code    ends
;***************************************
        end     start

將他存為 ROOT2.ASM,並組譯成 ROOT2.COM 檔,在 SYMDEB.EXE 堸鶡璅蟻[察結果。如果判別式為負值,則停止運算而印出『無實數解』後終止程式。

修改後的程式增加了幾行,第 14 到第 19 行,也就是在做開平方根運算前先檢查 ST 是否小於零,此時 ST 內的數值就是所謂的判別式 (b2-4ac)。值得注意的是第 16 行,小木偶加上了 FWAIT 指令,確保 FPU 已經把狀態字組存入 status 變數後,CPU 才將 status 之值存入 AX 堙C小木偶也試過如果把這行拿掉,似乎也能正確執行。


常數指令

FPU 埵 7 個指令是用來把常用的常數推入堆疊頂稱為『常數指令』。這些常數都是內建在 FPU 堛獐ぉ,都屬於暫時實數,其有效位數達 19 位數,當程式需要用到時可以很快的載入,節省時間與記憶體。這 7 個常數指令介紹如下:

FLD1

載入 1.0。( 把 1.0 推入堆疊頂 )

FLDZ

載入零。( 把 0.0 推入堆疊頂 )

FLDPI

載入圓周率,3.141592653589793239。( 把 π 推入堆疊頂 )

FLDL2T

載入 Log210。( 把 Log210 推入堆疊頂 )

FLDL2E

載入 Log2e,e 是自然對數的底數。( 把 Log2e 推入堆疊頂 )

FLDLG2

載入 Log102。( 把 Log102 推入堆疊頂 )

FLDLN2

載入 Loge2,事實上 Loge2 等於 Ln 2。( 把 Loge2 推入堆疊頂 )


註一:事實上有另一個指令可以直接比較 ST 和零,這個指令是 FTST (可以記成 TeST)。

FTST 指令

FTST 後不須接任何運算元,比較後狀態字組的設定方式和 FCOM 相同:

比較結果   C3  C0
----------------------------
ST>0       0   0
ST<0       0   1
 ST=0       1   0

FXAM

除了 FTST 可以檢查堆疊頂是否為 0 之外,FXAM 也可以。FXAM 的能力不僅如此,還可以檢查堆疊頂的數值屬於哪一類型,它會把結果記錄在狀態字組的 C0、C1、C2、C3 四個位元堙A如下表。C1 位元是 ST(0) 的符號,如果是正數,此位元為 0;如果是負數,此位元為 1。

C3C2C0分類
000不支援
001非數值 ( NaN,Not a Number )
010正常數值
011無窮大
100
101ST 是空的
110反常值 ( denormal )

回到首頁到第二十二章到第二十四章