FPU (3) 指數

来源:互联网 发布:js radio 选中的值 编辑:程序博客网 时间:2024/06/04 20:05

 

Ch 24 FPU (3) 指數

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

超越指令

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

F2XM1 指令

這個指令是用來計算 2ST-1 然後將結果存回 ST 裏。F2XM1 裏的 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*Log2( ST(1)+1 ),這個指令會先彈出 ST 然後以計算的結果取代 ST 暫存器,ST(1) 必須是大於零且小於二分之根號二的數。這個指令在 Log 後的數很接近一時,比 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 之間的實數為指數,以及指數為負數時,可以分成下面四種情形:

  • 第一種最單純,指數為正數,且小數部分小於或等於 0.5,例如求 25.3 可以寫成 25*20.3

  • 第二種是指數為正數,且小數部分大於 0.5,這時再把小數部分分成 0.5 及超過 0.5 的部分,例如計算 25.7 應寫成 25*20.5*20.2

  • 第三種是指數為負數,且小數部分超過或等於 0.5,例如計算 2-3.8,這時可以寫成 2-4*20.2

  • 第四種是指數為負數,且小數部分不超過 0.5,例如計算 2-3.2,這時可以寫成 2-4*20.5*20.3

但是實際上撰寫程式時,如果用 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

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


供 COM 程式庫使用的副程式

變數位址錯誤的問題

前面所建立的兩個副程式:two_p_x_5x 以及 y_p_x_5x 因為有兩個區段,因此只能製作成 EXE 檔,如果要製作成 COM檔所能使用的副程式會必須克服另一個問題,那就是副程式的資料與程式碼是混在同一區段裏,當存取副程式的變數時,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 WORD0040CODE

TWO_P_X_5C . . . . . . . . . . . N PROC0000CODEGlobalLength = 0042

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

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

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

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

至於『副程式起始位址』應為『副程式某一個正在執行之指令位址』減去『該指令距離副程式起始位址多少位元組』。其實『副程式某一個正在執行之指令位址』就存在 CS:IP 裏,所以只要取得 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,取得堆疊頂端的數值,也就是 POPBX『某一個正在執行之指令位址』,下一行減去『POP BX這個指令距離副程式起始位址多少位元組』就得到『副程式起始位址』,此後只要存取變數,只需用『變數名[BX]』就能得到正確位址。


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

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


回到首頁,到第二十三章,到第二十五章
原创粉丝点击