Ch 24 FPU (3) 指數

這一章,小木偶將介紹 8087 的重軸戲,超越指令,控制字組,最後發展出幾個有關計算指數的副程式。

超越指令

所謂超越指令是指複雜的函數運算的指令。8087 有五個超越指令,一個用於指數,兩個用於自然對數,另外兩個用於三角函數的計算。對於 8087 的超越指令所輸入的引數和數學上的引數不見得相同,例如求 2 的 X 次方,在數學上 X 可以是任意值,但用 8087 計算時 X 卻只能在 0 到 0.5 之間,如果要求 2 的任意數次方得用程式來計算。假如在 8087 程式中的引數超過這個範圍,會引起錯誤但是 8087 卻沒有檢查機制,所以要小心使用。

F2XM1 指令

這個指令是用來計算 2ST-1 然後將結果存回 ST 堙CF2XM1 堛 X 表示 ST 暫存器,M 表示減法之意,它的語法就是

F2XM1

不含任何運算元。這個指令運算之前還有一個限制,那就是 X 必須是在 0 到 0.5 之間的實數才行,可以等於 0 或 0.5;在 Pentium 等級及其以上 ( 註一 ),X 可以擴充到在 0 到 1 之間。這個指令之所以要減一的目的是如果 X 很小,則 2X 會很接近一,減去一可增加有效數。

FYL2X 指令

這個指令是用來計算 ST(1)*Log2 ST,這個指令會先彈出 ST 然後以計算的結果取代 ST 暫存器。這個指令的限制是 ST 必須為正數。

FYL2XP1 指令

這個指令是計算 ST(1)*Log2( ST+1 ),計算完成之後,會先彈出 ST,然後再以計算的結果取代 ST 暫存器。ST 必須是大於零且小於二分之根號二的數。這個指令在 ST 之值很接近 0 時,比 FYL2X 有較好的準確度。

FPTAN 指令

這個指令用來計算 tan ST 之結果,而計算結果是以分數 Y/X 的形式存入堆疊,計算後先把 tan ST 之值推入堆疊(當作 Y 值),再把 1 (當作 X 值)推入堆疊,換句話說最後的結果是 ST(1) 為 tan ST,ST 為 1。在 8087 等級的 FPU 運算時,計算前 ST 必須是 0 到四分之圓周率的徑度;如果在 Pentium 等級及其以上的 CPU,除了計算前 ST 須以徑度表示外,似乎沒有範圍限制。

FPATAN 指令

這個指令是用來計算 arctan ( ST(1)/ST ) 的,然後把計算結果以徑度表示存入 ST。整個計算過程是先彈出堆疊頂當做分母(X),再彈出新的堆疊頂( 也就是原來的 ST(1) )當做分子,計算 Y/X 之反正切函數,再把計算結果存回堆疊頂。如果是在 8087 FPU 上運算,計算前 ST 與 ST(1) 必須為正值,而在 Pentium 及其以上的 CPU 則無此限制。

在 80387 等級及其以上的 FPU 還提供的更多的超越函數,小木偶在下面介紹。

FSIN 指令

這是用來計算堆疊頂的正弦函數 (sin),再把結果推入堆疊頂,計算前堆疊頂沒有範圍的限制,但要使用徑度,80387 等級及其以上的 FPU 才提供這個指令。

FCOS 指令

這是用來計算堆疊頂的餘弦函數 (cos),再把結果推入堆疊頂,計算前堆疊頂沒有範圍的限制,但要使用徑度,80387 等級及其以上的 FPU 才提供這個指令。

FSINCOS 指令

這個指令只有在 80387 等級及其以上的 FPU 才提供,它會彈出堆疊頂然後計算 sin ST 與 cos ST 之值,然後把 sin ST 之結果推入堆疊暫存器,再把 con ST 之結果推入堆疊暫存器,所以堆疊頂為餘弦值, ST(1) 為正弦值。


用 8087 計算 2x 的副程式

原理

8087 指令埵釣潃茼傢 2x 的指令,FSCALE 與 F2XM1,但是前者只能用在 x 值是在 -32768 和 32768 之間的整數,而後者只能用在 x 是在 0 和 0.5 之間的實數,所以假如要計算 2 的任意次方,必須另寫一個副程式才行,而且還要利用到數學上的原理:

2a+b=2a*2b

應用上述數學原理,我們可以把任意實數分成整數部份(a)與小數部份(b),但是考慮到 F2XM1 只能接受在 0 和 0.5 之間的實數為指數,以及指數為負數時,可以分成下面四種情形:

但是實際上撰寫程式時,如果用 FRNDINT 向負無窮大方向捨入,求出整數部分,再用指數減去整數部分得到小數部分,那第一種情形與第三種情形是一樣的,第二種情形與第四種情形是一樣的,所以實際上只要考慮小數部分是否超過 0.5 兩種情形就可以了。完整的程式如下,我將它取名為 TWO_PX0X.ASM( 0 表示 8087 以上可使用,第二個 X 表示只能用於 EXE 檔):

;目的:求 2 的次方數,此指數可以是整數、負數、浮點數
;輸入:ST(0):指數
;輸出:ST(0):2的次方數
;限制:8087以上均可使用且只能用於 EXE 檔
;此副程式用到堆疊暫存器深度為 ST(3)
;原理是利用 2a+b=2a*2b,因為 8087 指令 FSCALE 只能計算 2 的整
;數次方,F2XM1 只能計算 2 的小數次方,此小數必須在 0 和 0.5 之間
        .8087
;***************************************
data    segment byte    public  'data'  ;10
half    dd      0.5             ;11 短實數 0.5
cw      dw      ?               ;12 控制字組
sw      dw      ?               ;13 狀態字組
data    ends
;***************************************
code    segment byte    public  'code'  ;16
        assume  cs:code,ds:data ;17
        public  two_p_x_0x      ;18 p表示求指數次方、x表指數、
;-----------------------------------0表示8087以上可使用、x表示用EXE執行檔
two_p_x_0x      proc    near
        fstcw   cw              ;21 取得控制字組
        fwait                   ;22 等待 8087 儲存完畢
        push    cw              ;23 保存原控制字組
        and     cw,0f3ffh       ;24 使控制字組變成向負無窮大捨入,欲達此目
        or      cw,00400h       ;25 的必須使控制字組第 10、11 位元變為 01
        fldcw   cw              ;26 載入新的控制字組
        fld     st      ;   x   ;   x           ;27
        frndint         ;i=int x;   x           ;28 向負無窮大捨入
        pop     cw                              ;29 取回舊的控制字組
        fldcw   cw      ;   i   ;   x           ;30 載入舊的控制字組
        fsub    st(1),st;   i   ; f=x-i         ;31 ST(1)為小數部分f
        fxch            ;   f   ;   i
        fld     half    ;  0.5  ;   f   ;   i   ;33 載入 0.5
        fxch            ;   f   ;  0.5  ;   i   ;34 調整小數部
        fprem           ; adj f ;  0.5  ;   i   ;35 分是否超0.5
        fstsw   sw                              ;36 取得狀態字組
        fstp    st(1)   ;   f   ;   i           ;37 去掉 0.5
        f2xm1           ; 2f-1  ;   i           ;38 求 2 的小數部分次方-1
        fld1            ;   1   ; 2f-1  ;   i   ;39 載入 1
        faddp   st(1),st;  2f   ;   i           ;40 完成 2 的小數部分次方

        test    sw,200h         ;41 比較小數部分是否小於 0.5
        jz      less_half       ;42 小於

        fld1            ;   1   ;  2f  ;   i   ;45 大於等於時小數部
        fadd    st,st   ;   2   ;  2f  ;   i   ;46 分還得乘上根號二
        fsqrt           ; SQ(.5);  2f  ;   i   ;47 ST 為根號二
        fmulp   st(1),st;SQ()2f;   i           ;48 完成 2 的小數部分次方
less_half:
        fscale          ;  2x  ;   i           ;50 已求得 2x
        fstp    st(1)   ;  2x                  ;51 去掉整數部分
        ret             ;52 返回主程式
two_p_x_0x      endp
;---------------------------------------
code    ends
;***************************************
        end     two_p_x_0x

您可以將這個副程式加入自己的程式庫,這個副程式含有兩個區段,所以只能用於 EXE 格式的執行檔,當您由主程式呼叫這個副程式時,主程式的程式碼區段應宣告為『code segment public 'code'』,資料區段應宣告為『data segment public 'data'』,這樣 LINK.EXE 就能使主程式的資料區段與 two_p_x_0x 副程式的資料區段合而為一,主程式的程式碼區段與 two_p_x_0x 副程式的程式碼區段合而為一,請參考第十一章

觀察

小木偶來示範如何使用這個副程式。底下這個程式將計算 2 的 3.8 次方,小木偶把它命名為 TST2P.ASM,TST 是 test 之意。

        .8087
;***************************************
stack   segment stack
        dw      40h dup (?)
stack   ends
;***************************************
data    segment public  'data'
power   dq      3.8
answer  dq      ?
data    ends
;***************************************
code    segment public  'code'
        assume  cs:code,ds:data
        extrn   two_p_x_0x:near
;---------------------------------------
main    proc    far
start:  push    ds
        sub     ax,ax
        push    ax
        mov     ax,data
        mov     ds,ax
		
        finit
        fld     power
        call    two_p_x_0x
        fstp    answer
        ret
main    endp
;---------------------------------------
code    ends
;***************************************
        end     start

這個程式很簡單,所以小木偶沒有加上什麼註解,只在區段宣告處要注意的地方以白色標出來而已。

H:\HomePage\SOURCE>path h:\homepage\masm50;%path% [Enter]
                      → 設定路徑,以後免輸入『..\masm50\』
H:\HomePage\SOURCE>masm tst2p; [Enter]
Microsoft (R) Macro Assembler Version 5.00
Copyright (C) Microsoft Corp 1981-1985, 1987.  All rights reserved.


  51576 + 365352 Bytes symbol space free

      0 Warning Errors
      0 Severe  Errors

H:\HomePage\SOURCE>link tst2p [Enter]

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

Run File [TST2P.EXE]:[Enter]
List File [NUL.MAP]:[Enter]
Libraries [.LIB]:myasmlib [Enter]

上面是組譯以及連結的步驟,底下用 SYMDEB.EXE 載入 TST2P.EXE 來觀察看看。

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

Processor is [80286]
-t [Enter]
AX=0000  BX=0000  CX=0127  DX=0000  SP=007E  BP=0000  SI=0000  DI=0000
DS=2201  ES=2201  SS=2211  CS=221B  IP=0001   NV UP EI PL NZ NA PO NC
221B:0001 2BC0           SUB    AX,AX
-t [Enter]
AX=0000  BX=0000  CX=0127  DX=0000  SP=007E  BP=0000  SI=0000  DI=0000
DS=2201  ES=2201  SS=2211  CS=221B  IP=0003   NV UP EI PL ZR NA PE NC
221B:0003 50             PUSH   AX
-t [Enter]
AX=0000  BX=0000  CX=0127  DX=0000  SP=007C  BP=0000  SI=0000  DI=0000
DS=2201  ES=2201  SS=2211  CS=221B  IP=0004   NV UP EI PL ZR NA PE NC
221B:0004 B81922         MOV    AX,2219  →資料區段的區段位址
-t [Enter]
AX=2219  BX=0000  CX=0127  DX=0000  SP=007C  BP=0000  SI=0000  DI=0000
DS=2201  ES=2201  SS=2211  CS=221B  IP=0007   NV UP EI PL ZR NA PE NC
221B:0007 8ED8           MOV    DS,AX
-t [Enter]
AX=2219  BX=0000  CX=0127  DX=0000  SP=007C  BP=0000  SI=0000  DI=0000
DS=2219  ES=2201  SS=2211  CS=221B  IP=0009   NV UP EI PL ZR NA PE NC
221B:0009 9B             WAIT
-dl 0 l2 [Enter]
2219:0000  66 66 66 66 66 66 0E 40  +0.38E+1  → 2 的 3.8 次方
2219:0008  00 00 00 00 00 00 00 00  +0.0E+0   → answer 位址
-ds 10 l1 [Enter]
2219:0010  00 00 00 3F  +0.5E+0 →在副程式 two_p_x_0x 所定義的變數 half
-dw ds:14 L2 [Enter]
2219:0014  0000 0000 →在副程式 two_p_x_0x 所定義的變數 cw 和 sw
-u cs:0 [Enter]
221B:0000 1E             PUSH   DS
221B:0001 2BC0           SUB    AX,AX
221B:0003 50             PUSH   AX
221B:0004 B81922         MOV    AX,2219
221B:0007 8ED8           MOV    DS,AX
221B:0009 9B             WAIT
221B:000A DBE3           FINIT
221B:000C 9B             WAIT
-db ds:0 L30 [Enter]
2219:0000  66 66 66 66 66 66 0E 40-00 00 00 00 00 00 00 00  ffffff.@........
2219:0010  00 00 00 3F 00 00 00 00-00 00 00 00 00 00 00 00  ...?............
2219:0020  1E 2B C0 50 B8 19 22 8E-D8 9B DB E3 9B DD 06 00  .+@P8.".X.[c.]..

仔細觀察整個程式的資料區段是在 2219:0000 到 2219:0017 處,最前面的 8 個位元組(白色)是 3.8,再來的 8 個位元組(紅色)是 answer 變數,這兩個都在主程式中宣告的;接下來的 4 個位元組(藍色)是 0.5,再來的一個字組(橘色)是 cw,再來的一個位元組(紫色)是 sw,這三個變數是在 two_p_x_0x 中宣告,和在主程式中的資料結合成一個資料區段。

接下來是程式碼區段,也就是命名為『code』的區段,資料區段後還有 8 個位元組沒用到,但是 code 區段卻從 221B:0000 處開始,這是因為沒有特別指明 MASM 會把區段設定從每一節(para)處開始,所謂一節是指 10H 個位元組,請參考第 11 章。221B:0000 這個位址事實上和 2219:0020 這個位址是同一個位址,不信?您看看它們的內容都是 1E 2B C0 ……。最後再來看看 LINK.EXE 是如何把主程式的程式碼與 two_p_x_0x 副程式的程式碼連在一起。

-u cs:9 36 [Enter]
221B:0009 9B             WAIT
221B:000A DBE3           FINIT
221B:000C 9B             WAIT
221B:000D DD060000       FLD    QWord Ptr [0000]
221B:0011 E80600         CALL   001A
221B:0014 9B             WAIT
221B:0015 DD1E0800       FSTP   QWord Ptr [0008]
221B:0019 CB             RETF
221B:001A 9B             WAIT
221B:001B D93E1400       FSTCW  [0014]
221B:001F 9B             WAIT
221B:0020 FF361400       PUSH   [0014]
221B:0024 81261400FFF3   AND    Word Ptr [0014],F3FF
221B:002A 810E14000004   OR     Word Ptr [0014],0400
221B:0030 9B             WAIT
221B:0031 D92E1400       FLDCW  [0014]
221B:0035 9B             WAIT
221B:0036 D9C0           FLD    ST(0)

很明顯的,藍色部份就是主程式,橘色部份是 two_p_x_0x 副程式,這是因為在副程式中,假指令『segment』用了『byte』選項,所以副程式的程式碼區段可以由任意位址開始,因此 LINK.EXE 將副程式緊密的接在主程式後面。請參考第 11 章以求融會貫通。

-g [Enter]

Program terminated normally (0)
-DL 221B:0 L2 [Enter]
221B:0000  66 66 66 66 66 66 0E 40  +0.38E+1
221B:0008  1F E1 DB DA 8C DB 2B 40  +0.1392880901273798E+2 →23.8

執行看看,果然已經計算出 23.8 了。


用 Pentium 計算 2x 的副程式

假如使用 Pentium 來計算 2x 的話,情形就沒有這麼複雜,因為 Pentium 等級以上的 CPU (或 NPU) 其 F2XM1 的指數可以是在 0 到 1 之間,故不用再考慮指數的小數部分是否超過 0.5,完整的副程式如下,小木偶將它的原始檔案取名為 TWO_PX5X.ASM:

       .8087
;***************************************
data    segment byte    public  'data'
cw      dw      ?               ;04 控制字組
data    ends
;***************************************
code    segment byte    public  'code'
        assume  cs:code,ds:data
        public  two_p_x_5x
;---------------------------------------
;目的:求 2 的次方數,此指數可以是整數、負數、浮點數
;輸入:ST(0):指數
;輸出:ST(0):2的次方數
;限制:Pentium 以上均可使用且只能用於 EXE 檔
;此副程式用到堆疊暫存器深度為 ST(3)
;備註:1.此副程式可以用在 pentium 及其以上等級的 FPU。
;      2.此副程式原理是利用 2a+b=2a*2b,a 表示整數部分,b表示小數部分
two_p_x_5x      proc    near
        fstcw   cw       ;19 取得控制字組
        fwait            ;20 等待 pentium 儲存完畢
        push    cw       ;21 保存原控制字組
        and     cw,0f3ffh;22 使控制字組變成向負無窮大捨入,欲達此目
        or      cw,00400h;23 的必須使控制字組第 10、11 位元變為 01
        fldcw   cw       ;24 載入新的控制字組
        fld     st       ;   x   ;   x   ;25
        frndint          ;i=int x;   x   ;26 向負無窮大捨入
        pop     cw                       ;27 取回舊的控制字組
        fldcw   cw       ;   i   ;   x   ;28 載入舊的控制字組
        fsub    st(1),st ;   i   ; f=x-i ;29 ST(1)為小數部分,f
        fxch             ;   f   ;   i   ;30 交換
        f2xm1            ; 2f-1 ;   i           ;31 求 2 的小數部分次方
        fld1             ;   1   ; 2f-1 ;   i   ;32 載入 1
        faddp   st(1),st ;  2f  ;   i           ;33 完成 2 的小數部分次方
        fscale           ;  2x  ;   i           ;34 使 2
        fstp    st(1)    ;  2x                  ;35 去掉整數部分
        ret              ;36 返回主程式
two_p_x_5x      endp
;---------------------------------------
code    ends
;***************************************
        end     two_p_x_5x

這個副程式也可以加入我們的程式庫中,現在的電腦等級都在 Pentium !!! 以上,因此這個副程式應該要比 two_p_x_0x 還常用才對。


求 XY

雖然 FPU 並沒有提供直接計算 XY 的指令,但是有了求 2X 的副程式,很容易就能利用數學公式寫出求 XY 的副程式。

XY = 2log2XY = 2Ylog2X

根據上面的公式,我們只要用指令 FYL2X 求出 Ylog2X 即可 ( 但有限制,請參考註二 ),再把這個數值存在 FPU 的堆疊頂,呼叫 two_p_x_5x 即可求出 XY,程式如下:

        .8087
;***************************************
data    segment byte    public  'data'
sw      dw      ?       ;04 狀態字組
data    ends
;***************************************
code    segment byte    public  'code'
        assume  cs:code,ds:data
;---------------------------------------
;計算 XY 之值,原理:XY=2log2XY=2Ylog2X
;輸入:ST - 底數,X
;      ST1- 指數,Y
;輸出:若錯誤(零的零次方或底數為負值),則進位旗標被設定;
;      若沒錯誤,則進位旗標被清除,且 ST 為 X 的 Y 次方,XY,堆疊深度減一
;限制:這個副程式只能用在 Pentium 以上,組譯成 EXE 檔            
        public  x_p_y_5x
        extrn   two_p_x_5x:near
x_p_y_5x        proc    near
        ftst
        push    ax
        fstsw   sw
        fwait
        mov     ax,sw           ;23 把狀態字組移入 AX
        sahf                    ;24 把狀態字組的高位元部份移入旗標
        jz      zero            ;25 底數為0
        jc      err1            ;--st0--;--st1--;26 底數為負數
                                ;   X   ;   Y
        fyl2x                   ; Ylog2X;
        call    two_p_x_5x      ;   XY  ;
exit:   clc             ;30 清除進位旗標
        pop     ax
        ret

zero:   fcomp           ;34 底數為零,彈出底數
        ftst            ;35 檢查指數是否為零
        fstsw   sw
        mov     ax,sw
        sahf
        jz      err2
        fcomp           ;40 底數為零,指數不為零
        fldz            ;41 彈出底數(第34行)再彈出
        jmp     exit    ;42 指數(上一行),再載入零

err1:   fcomp           ;44
err2:   fcomp           ;45 指數與底數均為零或底數為負數
        stc             ;46 設定進位旗標
        pop     ax
        ret
x_p_y_5x        endp
;---------------------------------------
code    ends
;***************************************
        end     x_p_y_5x

這個副程式沒什麼新的觀念了,只是要注意的是在數學上,零的零次方是無意義的,因此小木偶加上了一段程式檢查指數與底數是否同時為零,如果是這樣的話那就設定進位旗標傳回主程式,使主程式知道這是輸入錯誤的引數。

如果要把 x_p_y_5x 製作成程式庫,供給其他程式呼叫,還有一點瑕疵。試看 x_p_y_5x 用去兩個堆疊暫存器,而 two_p_x_5x 用去四個,如果主程式本身已在 FPU 堥洏峇T個堆疊暫存器,顯然就超過了。因此,最好是有方法能夠把 FPU 的狀態保存起來,等 x_p_y_5x 要返回主程式之前,再回存。幸好 FPU 的研發團隊早已設計了一系列指令,可以把 FPU 的狀態存入記憶體堙A請參考註三


供 COM 程式庫使用的副程式

變數位址錯誤的問題

前面所建立的兩個副程式:two_p_x_5x 以及 y_p_x_5x 因為有兩個區段,因此只能製作成 EXE 檔,如果要製作成 COM 檔所能使用的副程式會必須克服另一個問題,那就是副程式的資料與程式碼是混在同一區段堙A當存取副程式的變數時,CPU 所得到的變數位址是錯誤的。

以 two_p_x_5x 為例,如果您以為只要把 cw 移到副程式的程式碼內,並刪去 data 區段,變成下面這樣,存成 TWO_PX5C.ASM:

       .8087
;***************************************
code    segment byte
        assume  cs:code,ds:code
        public  two_p_x_5c
;---------------------------------------
two_p_x_5c      proc    near
        fstcw   cw
        fwait
        push    cw
        and     cw,0f3ffh
        or      cw,00400h
        fldcw   cw
        fld     st       ;   x   ;   x   ;
        frndint          ;i=int x;   x   ;
        pop     cw                       ;
        fldcw   cw       ;   i   ;   x   ;
        fsub    st(1),st ;   i   ; f=x-i ;
        fxch             ;   f   ;   i   ;
        f2xm1            ; 2f-1 ;   i           ;
        fld1             ;   1   ; 2f-1 ;   i   ;
        faddp   st(1),st ;  2f  ;   i           ;
        fscale           ;  2x  ;   i           ;
        fstp    st(1)    ;  2x                  ;
        ret              ;返回主程式
cw      dw      ?        ;cw 變數
two_p_x_5c      endp
;---------------------------------------
code    ends
;***************************************
        end     two_p_x_5c

如果上述副程式加入程式庫,再以下面的主程式連結:

        .8087
;***************************************
code    segment public  'code'
        assume  cs:code,ds:code
        extrn   two_p_x_5c:near
        org     100h
;---------------------------------------
main    proc    far
start:  jmp     short begin
p1      dq      0.5     ;0.5 次方
ans1    dq      ?       ;求 2 的 0.5 次方答案處
p2      dq      -2.2    ;-2.2 次方
ans2    dq      ?       ;求 2 的 -2.2 次方
begin:  finit
        fld     p1
        call    two_p_x_5c
        fstp    n1
        fld     p2
        call    two_p_x_5c
        fstp    n2
        ret
main    endp
;---------------------------------------
code    ends
;***************************************
        end     start

結果,cw 變數會在 40H 處,不信的話,請看 SYMDEB.EXE 載入的情形:

Microsoft (R) Symbolic Debug Utility  Version 4.00
Copyright (C) Microsoft Corp 1984, 1985.  All rights reserved.

Processor is [80286]
-u [Enter]
220E:0100 EB20           JMP    0122 →跳過資料區
220E:0102 0000           ADD    [BX+SI],AL
220E:0104 0000           ADD    [BX+SI],AL
220E:0106 0000           ADD    [BX+SI],AL
220E:0108 E03F           LOOPNZ 0149
220E:010A 0000           ADD    [BX+SI],AL
220E:010C 0000           ADD    [BX+SI],AL
220E:010E 0000           ADD    [BX+SI],AL
-u 122 [Enter]
220E:0122 9B             WAIT
220E:0123 DBE3           FINIT
220E:0125 9B             WAIT
220E:0126 DD060201       FLD    QWord Ptr [0102]
220E:012A E81300         CALL   0140 →呼叫 two_p_x_5c 副程式
220E:012D 9B             WAIT
220E:012E DD1E0A01       FSTP   QWord Ptr [010A]
220E:0132 9B             WAIT
-u 140 [Enter]
220E:0140 9B             WAIT
220E:0141 D93E4000       FSTCW  [0040] →把控制字組存入 CW 變數
220E:0145 9B             WAIT            但 CW 位址是錯的
220E:0146 FF364000       PUSH   [0040]
220E:014A 81264000FFF3   AND    Word Ptr [0040],F3FF
220E:0150 810E40000004   OR     Word Ptr [0040],0400
220E:0156 9B             WAIT
220E:0157 D92E4000       FLDCW  [0040]
-u 179 [Enter]
220E:0179 9B             WAIT
220E:017A D9FD           FSCALE
220E:017C 9B             WAIT
220E:017D DDD9           FSTP   ST(1)
220E:017F C3             RET
220E:0180 0000           ADD    [BX+SI],AL →正確的 CW 變數所在處

您會發現,CW 變數變成在 40H 的位址了,這當然是不對的,COM 檔的程式是由 100H 處開始,100H 之前是 PSP。之所以會這樣是因為 MASM 把 dw 等假指令都看成直接接在程式碼的 CPU 指令,MASM 把這個記憶體位址保留給 CW 使用。當用 LINK 連結時,也是把這個記憶體位址接在程式碼後面,並不是接在資料後面,所以存取 CW 變數時,CW 表示『該變數距離副程式起始位址多少位元組』,但不是連結後的正確位址。您可以在副程式組譯時,製作列表檔觀察證明這一點:

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

Object filename [two_px5c.OBJ]:[Enter]
Source listing  [NUL.LST]: two_px5c[Enter] →製作列表檔
Cross-reference [NUL.CRF]: [Enter]

  50904 + 366040 Bytes symbol space free

      0 Warning Errors
      0 Severe  Errors

觀察 TWO_PX5C.LST 您可以看到以下片段:

                N a m e         	Type	 Value	 Attr

CW . . . . . . . . . . . . . . .  	L WORD	0040	CODE

TWO_P_X_5C . . . . . . . . . . .  	N PROC	0000	CODE	Global	Length = 0042

@FILENAME  . . . . . . . . . . .  	TEXT  two_px5c		

CW 變數在 0040H 的地方,且在副程式最後面,長度為字組,長一個字組,所以整個 TWO_P_X_5C 長 42H 個位元組。

程式碼內嵌變數時,取得正確位址之方法

知道錯誤的原因了,接下來要如何解決呢?我們現在已經知道當程式庫內的副程式內嵌資料或變數時,這些資料或變數所表示的位址並不是連結後真正位址,而是距離該副程式起始位址多少個位元組,所以變數真正位址應該是『該變數距離副程式起始位址多少個位元組』加上『副程式起始位址』,而『該變數距離副程式起始位址多少個位元組』其實就是上述的錯誤位址,也就是該變數所代表的位址。

至於『副程式起始位址』應為『副程式某一個正在執行之指令位址』減去『該指令距離副程式起始位址多少位元組』。其實『副程式某一個正在執行之指令位址』就存在 CS:IP 堙A所以只要取得 IP 之值即可,可惜翻遍所有 80X86 指令都沒有這個指令,不過我們有變通的方法。當程式呼叫副程式時,會把要執行的位址推入堆疊,因此我們只要『假裝』呼叫副程式,再到堆疊取出堆疊頂就得到正執行的位址了。而『該指令距離副程式起始位址多少位元組』可以用一個運算子,THIS,來取得(稍後介紹 THIS)。

看完上面說明,不被搞得頭昏眼花才怪,請參考下圖、上文、底下的 two_p_x_5c 副程式原始碼以及用 SYMDEB 載入的情形,用力想想看吧。或者假如您還有更好的方法說明這段複雜的位址關係,請來信指導,小木偶在此謝謝。

取得程式碼內嵌變數正確位址說明圖

THIS 運算子

這個運算子是用來取得 THIS 所在位址距程式起始位址多少個位元組。它必須在後面接上形態 (type),形態可以是 BYTE、WORD、DWORD、QWORD、TBYTE,如果是用在標記,其形態可以是 NEAR、FAR、PROC。

由堆疊中取得的『副程式某一個正在執行之指令位址』減去由 THIS 運算子取得的『該指令距離副程式起始位址多少位元組』就是『副程式起始位址』,把它存入 BX 暫存器。

基底相對定址法

BX 之值再加上『該變數距離副程式起始位址多少個位元組』就是變數正確位址,這時必須用『基底相對定址法』來取得變數正確位址:

[BX+相對值]
[BP+相對值]

相對值可以是常數或變數,如果是變數的話,也可以寫成

變數[BX]
變數[BP]

以這個程式為例,cw[bx] 的意思是會到 BX 暫存器與 cw 相加後所得的位址去取得該位址的數值,如果沒有特別用『凌越區段』則會取得 DS:BX+cw 位址之值。如果暫存器是 BP 的話,則會取得 SS:BP+相對值所指的位址之數值。

凌越區段

取得變數位址後,還有一個問題有待克服,那就是我們所取得的位址其實只是偏移位址,而變數完整的位址是包含區段位址,可是這個變數包含在副程式內,副程式會被 LINK.EXE 安排在程式碼的區段,並非在資料區段,因此在存取該變數時,必須指定在程式碼區段,請參考原始程式第 17 行以及第 19 到第 22 行的寫法,也起參考第 9 章暫存器間接定址的那一段落。

整理

綜合上述,底下的 two_p_x_5c 副程式的第 17 行

fstcw   cs:cw[bx]

其實相當於

add     bx,cw
fstcw   cs:[bx]

two_p_x_5c 原始程式

小木偶把 two_p_x_5x 副程式稍稍改寫變成 two_p_x_5c,如下:

;***************************************
code    segment byte
        assume  cs:code,ds:code
        public  two_p_x_5c
;---------------------------------------
;目的:計算 2 的任意次方
;輸入:ST--指數
;輸出:ST--2 的冪方數
;備註:1.此副程式可以用在 pentium 及其以上等級的 FPU 供給 COM、EXE 檔呼叫。
;      2.此副程式原理是利用 2a+b=2a*2b,a 表示整數部分,b表示小數部分
two_p_x_5c      proc    near
        push    bx              ;12 程式開始處
        call    rel_ad          ;13 『假裝』呼叫 rel_ad
addr    equ     this word       ;14 addr記錄『該指令距離副程式起始位址多少位元組』
rel_ad: pop     bx              ;15 『副程式某一個正在執行之指令位址』存於BX
        sub     bx,offset addr  ;16 BX 為『副程式起始位址』
        fstcw   cs:cw[bx]       ;17 取得控制字組
        fwait                   ;18 等待 pentium 儲存完畢
        push    cs:cw[bx]       ;19 保存原控制字組
        and     cs:cw[bx],0f3ffh;20 使控制字組變成向負無窮大捨入,欲達此目
        or      cs:cw[bx],00400h;21 的必須使控制字組第 10、11 位元變為 01
        fldcw   cs:cw[bx]       ;22 載入新的控制字組
        fld     st              ;   x   ;   x           ;23
        frndint                 ;i=int x;   x           ;24 向負無窮大捨入
        pop     cs:cw[bx]                               ;25 取回舊的控制字組
        fldcw   cs:cw[bx]       ;   i   ;   x           ;26 載入舊的控制字組
        fsub    st(1),st        ;   i   ; f=x-i         ;27 ST(1)為小數部分,f
        fxch                    ;   f   ;   i           ;28 交換
        f2xm1                   ; 2f-1 ;   i           ;29 求 2 的小數部分次方
        fld1                    ;   1   ; 2f-1 ;   i   ;30 載入 1
        faddp   st(1),st        ;  2f  ;   i           ;31 完成 2 的小數部分次方
        fscale                  ;  2x  ;   i           ;32 使 2
        fstp    st(1)           ;  2x                  ;33 去掉整數部分
        pop     bx              ;34 存回 BX
        ret                     ;35 返回主程式
cw      dw      ?               ;36 控制字組
two_p_x_5c      endp
;---------------------------------------
code    ends
;***************************************
        end     two_p_x_5c

小木偶把新修改後的 two_p_x_5c 加入程式庫再用上述主程式重新連結,用 SYMDEB 載入其 COM 檔觀察:

-u 140 [Enter] →直接到 two_p_x_5c 副程式起始處
2118:0140 53             PUSH   BX
2118:0141 E80000         CALL   0144 
2118:0144 5B             POP    BX
2118:0145 81EB0400       SUB    BX,0004
2118:0149 9B             WAIT
2118:014A 2ED9BF5100     FSTCW  CS:[BX+0051]
2118:014F 9B             WAIT
2118:0150 2EFFB75100     PUSH   CS:[BX+0051]
-g 140 [Enter]
AX=0000  BX=0000  CX=0093  DX=0000  SP=FFFC  BP=0000  SI=0000  DI=0000
DS=2118  ES=2118  SS=2118  CS=2118  IP=0140   NV UP EI PL NZ NA PO NC
2118:0140 53             PUSH   BX →副程式起始於 140H
-t [Enter]
AX=0000  BX=0000  CX=0093  DX=0000  SP=FFFA  BP=0000  SI=0000  DI=0000
DS=2118  ES=2118  SS=2118  CS=2118  IP=0141   NV UP EI PL NZ NA PO NC
2118:0141 E80000         CALL   0144 → 假裝呼叫副程式 rel_ad
-t [Enter]
AX=0000  BX=0000  CX=0093  DX=0000  SP=FFF8  BP=0000  SI=0000  DI=0000
DS=2118  ES=2118  SS=2118  CS=2118  IP=0144   NV UP EI PL NZ NA PO NC
2118:0144 5B             POP    BX →由堆疊取得呼叫後返回位址
-t [Enter]
AX=0000  BX=0144  CX=0093  DX=0000  SP=FFFA  BP=0000  SI=0000  DI=0000
DS=2118  ES=2118  SS=2118  CS=2118  IP=0145   NV UP EI PL NZ NA PO NC
2118:0145 81EB0400       SUB    BX,0004 → addr 距副程式起始處 4 個位元組
-t [Enter]
AX=0000  BX=0140  CX=0093  DX=0000  SP=FFFA  BP=0000  SI=0000  DI=0000
DS=2118  ES=2118  SS=2118  CS=2118  IP=0149   NV UP EI PL NZ NA PO NC
2118:0149 9B             WAIT
-t [Enter]
AX=0000  BX=0140  CX=0093  DX=0000  SP=FFFA  BP=0000  SI=0000  DI=0000
DS=2118  ES=2118  SS=2118  CS=2118  IP=014A   NV UP EI PL NZ NA PO NC
2118:014A 2ED9BF5100     FSTCW  CS:[BX+0051]                 CS:0191=0000
-u 187 [Enter]                         ﹂→基底相對定址法
2118:0187 DEC1           FADDP  ST(1),ST
2118:0189 9B             WAIT
2118:018A D9FD           FSCALE
2118:018C 9B             WAIT
2118:018D DDD9           FSTP   ST(1)
2118:018F 5B             POP    BX
2118:0190 C3             RET
2118:0191 0000           ADD    [BX+SI],AL → CW 真正位址

上述程式的第 15 行,POP BX 就是取得『某一個正在執行之指令位址』,而第 14 行的 addr equ this word 所代表的數值,就是『POP BX 這個指令距離副程式起始位址多少位元組』。rel_ad:和 POP BX 在記憶體堜狾的位址,在 MASM 組譯時並沒有完全確定,MASM 只是將它距離副程式的起始位址多少個位元組寫入 OBJ 檔,待連結時才看主程式的大小真正計算出來。addr 所佔的位址在 MASM 組譯時已經確定,連結時也不會更改。所以假如 two_p_x_5c 副程式不與其他程式連結(不被其他程式呼叫),單獨載入記憶體內,addr、rel_ad:、POP BX 這三個所佔的位址都是相同的,但是如果它與其它程式連結時,rel_ad:、POP BX 所佔的位址會隨主程式大小變動,但是 addr 仍然不變。

當程式第 13 行『假裝』呼叫副程式時,會把下一個要執行的指令 POP BX 位址推入堆疊,第 14 行是假指令,只是寫給組譯器看的,所以第 13 行執行完畢直接到第 15 行,POP BX,取得堆疊頂端的數值,也就是 POP BX『某一個正在執行之指令位址』,下一行減去『POP BX 這個指令距離副程式起始位址多少位元組』就得到『副程式起始位址』,此後只要存取變數,只需用『變數名[BX]』就能得到正確位址。


註一:80287、80387、80487 的 F2XM1 的引數部分是在 0 到 0.5 還是 0 到 1.0 之間,小木偶沒有更詳細的資料,所以無法確定。而 Pentium 等級的電腦,小木偶還有一台 Pentium-100 的筆記型電腦,所以可以測試確定在 0 到 1.0 之間。

註二:此限制主要是在 log 的引數必須為正值。

註三

FSAVE/FNSAVE 與 FRSTOR 指令

能夠把 x87 的狀態存入記憶體的指令有 FSAVE 及 FNSAVE,與之相對的指令是 FRSTOR。換句話說,FRSTOR 是把記憶體內容,存回 x87 相對應的暫存器堙C其語法是:

    FSAVE   m94/108byte
    FNSAVE  m94/108byte

FSAVE 可以把 x87 的控制字組、狀態字組、標籤字組、指令指標、運算元指標及 8 個堆疊暫存器存入記憶體內,然後再重置 x87。這些暫存器所存入的目標記憶體容量是 94 位元組或 108 位元組以及記憶體分配位址,需視當時 FPU 所處的環境決定,可以參考下圖:

下圖為 16 位元真實模式時,FSAVE 所需記憶體分配情況,需 94 個位元組:
16 位元真實模式時,FSAVE 所需記憶體分配圖

下圖為 16 位元保護模式時,FSAVE 所需記憶體分配情況,需 94 個位元組:
16 位元保護模式時,FSAVE 所需記憶體分配圖

下圖為 32 位元真實模式時,FSAVE 所需記憶體分配情況,需 108 個位元組:
32 位元真實模式時,FSAVE 所需記憶體分配圖

下圖為 32 位元保護模式時,FSAVE 所需記憶體分配情況,需 108 個位元組:
32 位元保護模式時,FSAVE 所需記憶體分配圖

由上面四張圖看來,大致上可以說,在 16 位元堙A需準備 94 個位元組,從第 14 個位元組開始存入堆疊暫存器;在 32 位元時,需準備 108 個位元組,從第 28 個位元組開始存入堆疊暫存器。至於 FNSAVE 的用法和 FSAVE 幾乎相同,差別是在於,執行 FSAVE 前,如果有例外發生,FSAVE 需等 x87 處理完例外的情形,再儲存資料;FNSAVE 則不等待例外處理好,就儲存資料。

如果要把 FSAVE/FNSAVE 存入記憶體的資料,再度存回 x87 堛漲U個暫存器堙A可以使用 FRSTOR 指令,FRSTOR 指令的語法為:

    FRSTOR  m94/108byte

跟 FSAVE/FNSAVE 一樣,FRSTOR 會把 94 個位元組或 108 個位元組的資料,載入到 x87 堿蛫奰釭獐存器堙A來源記憶體分配的情形,也跟 FASVE/FNSAVE 情形一樣。

另外,順帶一提,還有兩組指令:第一組是 FSTENV/FNSTENV 與 FLDENV;第二組是 FXSAVE 與 FXRSTOR。第一組指令中的 FSTENV 與 FNSTENV 是把 x87 的環境存入記憶體堙A然後遮罩所有例外位元;FLDENV 則是把其後所接的記憶體內容,存回 x87 環境堙C這三個指令的語法是:

    FSTENV  m14/28byte
    FNSTENV m14/28byte
    FLDENV  m14/28byte

FSTENV/FNSTENV 與 FLDENV 指令堛 x87 環境 ( environment ) 包含控制字組、狀態字組、標籤字組、指令指標、運算元指標和指令碼,也就是說,和 FSAVE/FNSAVE/FRSTOR 比較,少了 8 個堆疊暫存器。所接的目的記憶體大小,和 FSAVE/FNSAVE/FRSTOR 相比,也少了 8 個堆疊暫存器。


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