FPU (3) 指數
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' ;10half dd 0.5 ;11 短實數 0.5cw 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.8answer dq ?data ends;***************************************code segment public 'code' assume cs:code,ds:data extrn two_p_x_0x:near;---------------------------------------main proc farstart: push ds sub ax,ax push ax mov ax,data mov ds,ax finit fld power call two_p_x_0x fstp answer retmain 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.00Copyright (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.40Copyright (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.00Copyright (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=0000DS=2201 ES=2201 SS=2211 CS=221B IP=0001 NV UP EI PL NZ NA PO NC221B:0001 2BC0 SUB AX,AX-t [Enter]AX=0000 BX=0000 CX=0127 DX=0000 SP=007E BP=0000 SI=0000 DI=0000DS=2201 ES=2201 SS=2211 CS=221B IP=0003 NV UP EI PL ZR NA PE NC221B:0003 50 PUSH AX-t [Enter]AX=0000 BX=0000 CX=0127 DX=0000 SP=007C BP=0000 SI=0000 DI=0000DS=2201 ES=2201 SS=2211 CS=221B IP=0004 NV UP EI PL ZR NA PE NC221B:0004 B81922 MOV AX,2219 →資料區段的區段位址-t [Enter]AX=2219 BX=0000 CX=0127 DX=0000 SP=007C BP=0000 SI=0000 DI=0000DS=2201 ES=2201 SS=2211 CS=221B IP=0007 NV UP EI PL ZR NA PE NC221B:0007 8ED8 MOV DS,AX-t [Enter]AX=2219 BX=0000 CX=0127 DX=0000 SP=007C BP=0000 SI=0000 DI=0000DS=2219 ES=2201 SS=2211 CS=221B IP=0009 NV UP EI PL ZR NA PE NC221B: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 DS221B:0001 2BC0 SUB AX,AX221B:0003 50 PUSH AX221B:0004 B81922 MOV AX,2219221B:0007 8ED8 MOV DS,AX221B:0009 9B WAIT221B:000A DBE3 FINIT221B: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 WAIT221B:000A DBE3 FINIT221B:000C 9B WAIT221B:000D DD060000 FLD QWord Ptr [0000]221B:0011 E80600 CALL 001A221B:0014 9B WAIT221B:0015 DD1E0800 FSTP QWord Ptr [0008]221B:0019 CB RETF221B:001A 9B WAIT221B:001B D93E1400 FSTCW [0014]221B:001F 9B WAIT221B:0020 FF361400 PUSH [0014]221B:0024 81261400FFF3 AND Word Ptr [0014],F3FF221B:002A 810E14000004 OR Word Ptr [0014],0400221B:0030 9B WAIT221B:0031 D92E1400 FLDCW [0014]221B:0035 9B WAIT221B: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+1221B: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:nearx_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 ;44err2: fcomp ;45 指數與底數均為零或底數為負數 stc ;46 設定進位旗標 pop ax retx_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 farstart: jmp short beginp1 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 retmain endp;---------------------------------------code ends;*************************************** end start
結果,cw 變數會在 40H 處,不信的話,請看 SYMDEB.EXE 載入的情形:
Microsoft (R) Symbolic Debug Utility Version 4.00Copyright (C) Microsoft Corp 1984, 1985. All rights reserved. Processor is [80286]-u [Enter]220E:0100 EB20 JMP 0122 →跳過資料區220E:0102 0000 ADD [BX+SI],AL220E:0104 0000 ADD [BX+SI],AL220E:0106 0000 ADD [BX+SI],AL220E:0108 E03F LOOPNZ 0149220E:010A 0000 ADD [BX+SI],AL220E:010C 0000 ADD [BX+SI],AL220E:010E 0000 ADD [BX+SI],AL-u 122 [Enter]220E:0122 9B WAIT220E:0123 DBE3 FINIT220E:0125 9B WAIT220E:0126 DD060201 FLD QWord Ptr [0102]220E:012A E81300 CALL 0140 →呼叫 two_p_x_5c 副程式220E:012D 9B WAIT220E:012E DD1E0A01 FSTP QWord Ptr [010A]220E:0132 9B WAIT-u 140 [Enter]220E:0140 9B WAIT220E:0141 D93E4000 FSTCW [0040] →把控制字組存入 CW 變數220E:0145 9B WAIT 但 CW 位址是錯的220E:0146 FF364000 PUSH [0040]220E:014A 81264000FFF3 AND Word Ptr [0040],F3FF220E:0150 810E40000004 OR Word Ptr [0040],0400220E:0156 9B WAIT220E:0157 D92E4000 FLDCW [0040]-u 179 [Enter]220E:0179 9B WAIT220E:017A D9FD FSCALE220E:017C 9B WAIT220E:017D DDD9 FSTP ST(1)220E:017F C3 RET220E: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.00Copyright (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 裏,所以只要取得 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,cwfstcw 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_adaddr 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 BX2118:0141 E80000 CALL 0144 2118:0144 5B POP BX2118:0145 81EB0400 SUB BX,00042118:0149 9B WAIT2118:014A 2ED9BF5100 FSTCW CS:[BX+0051]2118:014F 9B WAIT2118:0150 2EFFB75100 PUSH CS:[BX+0051]-g 140 [Enter]AX=0000 BX=0000 CX=0093 DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000DS=2118 ES=2118 SS=2118 CS=2118 IP=0140 NV UP EI PL NZ NA PO NC2118:0140 53 PUSH BX →副程式起始於 140H-t [Enter]AX=0000 BX=0000 CX=0093 DX=0000 SP=FFFA BP=0000 SI=0000 DI=0000DS=2118 ES=2118 SS=2118 CS=2118 IP=0141 NV UP EI PL NZ NA PO NC2118:0141 E80000 CALL 0144 → 假裝呼叫副程式 rel_ad-t [Enter]AX=0000 BX=0000 CX=0093 DX=0000 SP=FFF8 BP=0000 SI=0000 DI=0000DS=2118 ES=2118 SS=2118 CS=2118 IP=0144 NV UP EI PL NZ NA PO NC2118:0144 5B POP BX →由堆疊取得呼叫後返回位址-t [Enter]AX=0000 BX=0144 CX=0093 DX=0000 SP=FFFA BP=0000 SI=0000 DI=0000DS=2118 ES=2118 SS=2118 CS=2118 IP=0145 NV UP EI PL NZ NA PO NC2118:0145 81EB0400 SUB BX,0004 → addr 距副程式起始處 4 個位元組-t [Enter]AX=0000 BX=0140 CX=0093 DX=0000 SP=FFFA BP=0000 SI=0000 DI=0000DS=2118 ES=2118 SS=2118 CS=2118 IP=0149 NV UP EI PL NZ NA PO NC2118:0149 9B WAIT-t [Enter]AX=0000 BX=0140 CX=0093 DX=0000 SP=FFFA BP=0000 SI=0000 DI=0000DS=2118 ES=2118 SS=2118 CS=2118 IP=014A NV UP EI PL NZ NA PO NC2118:014A 2ED9BF5100 FSTCW CS:[BX+0051] CS:0191=0000-u 187 [Enter] ﹂→基底相對定址法2118:0187 DEC1 FADDP ST(1),ST2118:0189 9B WAIT2118:018A D9FD FSCALE2118:018C 9B WAIT2118:018D DDD9 FSTP ST(1)2118:018F 5B POP BX2118:0190 C3 RET2118: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 的引數必須為正值。
回到首頁, 到第二十三章, 到第二十五章
FPU (3) 指數相关推荐
- 上海天氣情況及空氣質量指數
天氣數據覆蓋範圍 之前一直關注的是 Twitter 上面 美帝上海領館的 空氣檢測數據 覺得不是很方便,畢竟只是一條 Tweet ,於是用了 aqicn 的 widget 做了一個 頁面 方便查看. ...
- 信用指數旁的星星表示什麼?
信用指數達 10 分的 eBay 會員就會獲得信用評價星星.了解更多如何爭取更高信用指數. 在大部分情況下,會員會收到: +1 分(每次得到正面的評語和指數時) 0 分(每次得到中性的評語和指數時) ...
- 孫丕恕:計算力已成為數字經濟先行指數 決定未來發展潛力
中國網財經9月20日訊(記者暢帥帥)2018夏季達沃斯論壇9月18日至20日在天津召開."創新型社會"."智慧城市"等成為本屆夏季達沃斯論壇熱議的話題,浪潮集團 ...
- FPU (1) 简介
FPU 简介 FPU 是什麼 FPU 稱為浮點運算器是 floating-point processor unit 的縮寫它是一個處理數學運算的晶片.早在 1979 年英特爾就為了搭配 8086/80 ...
- matlab shortest函数,MATLAB函數graphallshortestpaths不返回對稱矩陣
我正在使用MATLAB函數graphallshortestpaths來計算無向網絡頂點之間的最短路徑.無向網絡作爲加權邊緣列表文件給出,您可以在其中找到here.MATLAB函數graphallsho ...
- Big Data應用:以玩家意見之數據分析來探討何謂健康型線上遊戲(上)
首先,所有資料都可以從網路上找到,只是我做了一些分析與整理而已.純粹分享心得~~ 最近再做研究的時候我跟我的同事K先生在某次偶然的討論中發現了一件有趣的事情. [疑~~~~~~~新楓之谷的玩家人氣指數 ...
- MATLAB保存数据为dat格式,將matlab中數據保存為txt或dat格式
將matlab中數據保存為txt或dat格式[轉] 具體的命令是:用save *.txt -ascii x x為變量*.txt為文件名,該文件存儲於當前工作目錄下,再打開就可以打開后,數據有可能是以指 ...
- ASP与數据庫,文本文件鏈接精髓
ASP与數据庫,文本文件鏈接精髓<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office&quo ...
- 【機器學習2021】預測本頻道觀看人數 (下) - 深度學習基本概念簡介
机器学习 2021 - Introduction of Machine / DeepLearning \qquadLinear 的 Model 也許太過簡單了,怎麼說它太過簡單呢?我們可以想像說 x1 ...
最新文章
- 4G EPS 中的 FDD/TDD 无线帧
- 网络工程师学习资料:路由器配置案例分析
- 每日程序C语言47-找到年龄最大的人并输出
- 深度学习导论(6)误差计算
- java部署平台_开源Java自动化部署平台JDeploy
- Ubuntu16.04下codeblocks16.01安装,适用于不同的架构
- html 中加载字体太慢,前端解决中文字体加载慢的问题
- OpenShift 4 - DevSecOps (2) - 修复 RHACS 发现的安全隐患
- Mysql更新计数器_MySQL实现计数器如何在高并发场景下更新并保持数据正确性
- endnote大客户版_Endnote软件的使用,有图有干货!
- oracle 归档日志激增,一次归档日志激增的分析.
- raid卡 4k 设置 linux,硬盘“大户”看过来,手把手教你组建 RAID 磁盘阵列
- JAVA超市综合管理信息区块链系统毕设论文
- Win10删除微软拼音输入法
- Plugin: Memory Dump by aeon update 12.10
- html5摄像头 在线演示,基于HTML5实现的超酷摄像头(HTML5 webcam)摄
- 350个运动摄影lr调色预设(含lr预设导入教程)
- 吉他调音软件AP Guitar Tuner使用经验
- linux+gunzip解压命令,Linux中的Gunzip命令详解
- 生活随记-四十年流水线
热门文章
- 01 导论【计量经济学及stata应用】
- Curl windows下载地址
- 基于混沌映射的自适应樽海鞘群算法-附代码
- 如何创建网站:2021年看这份指南就够了
- .NET Framework 介绍
- 浪潮孙丕恕:“云+数”赋能 共享数字经济
- Java并发技术要点梳理
- 最详细的教程,教你如何彻底关闭Chrome浏览器自动更新
- 字节跳动接力反内卷:宣布取消大小周,直接降薪 15%?
- 解决AttributeError: ‘NoneType‘ object has no attribute ‘val‘ if left.val!=right.val:Line 17 问题