組合語言學習筆記(十二)-浮點指令
浮點數如何儲存
浮點數的運算完全不同於整數,從暫存器到指令,都有一套獨特的處理流程,浮點單元也稱作x87 FPU。
現在看浮點數的表示方式,我們所知道的,計算機使用二進位制儲存資料,所表示的數字都具有確定性,那是如何表示浮點這種具有近似效果的資料呢,答案是通過科學計數,科學計數由符號,尾數和指數表示,這三部分都是一個整數值,具體來看一下IEEE二進位制浮點標準:
格式 | 說明 |
---|---|
單精度 | 32位:符號佔1位,指數佔8位,尾數中的小數部分佔23位 |
雙精度 | 64位:符號佔1位,指數佔11位,尾數中的小數部分佔52位 |
擴充套件精度 | 80位:符號佔1位,指數佔16位,尾數中的小數部分佔63位 |
以單精度為例,在記憶體中的儲存格式如下(左邊為高位):
| 1位符號 | 8位指數 | 23位尾數 |
其中符號位1表示負數,0表示正數,這與整數形式的符號位意義相同; 科學計數法表示形式如 m * (b ^ e),m為尾數,b為基數,e是指數,再二進位制中,基數毫無疑問是2,對單精度,指數為中間8位二進位制表示的數字,其中的尾數是形如1.1101 小數點後面的整數值。
關於指數,由於需要表示正負兩種資料,IEEE標準規定單精度指數以127為分割線,實際儲存的資料是指數加127所得結果,127為高位為零,後7位為1所得,其他雙精度也以此方式計算。
為了解釋記憶體中浮點數的儲存方式,舉一個浮點數的例子說明:
float test = 123.456;
int main()
{
return 0;
}
例子再簡單不過了,僅僅定義了一個全域性的float型別,我們通過gcc -S test.c
來生成彙編,看看123.456
是如何儲存的,開啟反彙編後的檔案,看到符號_test
後定義的數字是 1123477881
(這裡gcc定義成了long型別,不過沒有關係,因為都是四位元組數字,具體的型別還得看如何使用)。可以使用計算器把十進位制數字轉化為二進位制:0
10000101 11101101110100101111001
,這裡根據單精度的劃分方式把32位劃分成三部分,符號位為0,為正數,指數為 133,減去127得6,尾數加上1.,形式為1.11101101110100101111001
,擴大2
^ 23次方為111101101110100101111001
,十進位制16181625
,後除以2
^ (23 – 6) = 131072
,結果為123.45600128173828125
,與我們所定義的浮點數正好相符。
浮點暫存器
這裡介紹了浮點數的二進位制表示,前面說過浮點單元計算使用獨立的暫存器,在暫存器那篇也稍有提及,這裡詳細說明一下浮點單元的暫存器設施。
FPU有 8 個獨立定址的80位暫存器,名稱分別為r0, r1, …, r7,他們以堆疊形式組織在一起,統稱為暫存器棧,編寫浮點指令時棧頂也寫為st(0),最後一個暫存器寫作st(7)。
FPU另有3個16位的暫存器,分別為控制暫存器、狀態暫存器、標記暫存器,現一一詳細說明此三個暫存器的作用:
狀態暫存器,為使用者記錄浮點計算過程中的狀態,其中各位的含義如下:
0 —— 非法操作異常
1 —— 非規格化運算元異常
2 —— 除數為0異常
3 —— 溢位標誌異常
4 —— 下溢標誌異常
5 —— 精度異常標誌
6 —— 堆疊錯誤
7 —— 錯誤彙總狀態
8 —— 條件程式碼位0(c0)
9 —— 條件程式碼位1(c1)
10 —— 條件程式碼位2 (c2)
11-13 —— 堆疊頂指標
14 —— 條件程式碼位3(c3)
15 —— 繁忙標誌
其中讀取狀態暫存器內容可使用 fstsw %ax
控制暫存器的位含義如下:
0 —— 非法操作異常掩碼
1 —— 非法格式化異常掩碼
2 —— 除數為0異常掩碼
3 —— 溢位異常掩碼
4 —— 下溢異常掩碼
5 —— 精度異常亞曼
6-7 —— 保留
8-9 —— 精度控制(00單精度,01未使用,10雙精度,11擴充套件精度)
10-11 —— 舍入控制(00舍入到最近,01向下舍入,10向上舍入,11向0舍入)
12 —— 無窮大控制
13–15 —— 保留
其中讀取控制暫存器和設定控制暫存器的指令如下:
# 載入到記憶體
fstcw control
# 載入到控制器
fldcw control
最後的標誌暫存器最為簡單,分別0-15位分別標誌r0-r7共8個暫存器,每個暫存器佔2位,這兩位的含義如下:
11 —— 合法擴充套件精度
01 —— 零
10 —— 特殊浮點
11 —— 無內容
另外對浮點暫存器的一些控制指令如下:
# 初始化fpu,控制、狀態設為預設值,但不改變fpu的資料
finit
# 恢復儲存環境
fldenv buffer
fstenv buffer
#清空浮點異常
fnclex
#fpu狀態儲存
fssave
fstenv 儲存控制暫存器、狀態暫存器、標記暫存器、FPU指令指標偏移量、FPU資料指標,FPU最後執行的操作碼到記憶體中。
浮點數指令
接下來將要詳細說明其計算過程,要計算資料首先得看如何從記憶體中載入資料到暫存器,同時把結果從暫存器取出到記憶體,除了載入記憶體中的浮點資料指令,另外還有一些常量的載入,現列舉如下:
指令 | 說明 |
---|---|
finit | 初始化控制和狀態暫存器,不改變fpu資料暫存器 |
fstcw control | 將控制暫存器內容放到記憶體control處 |
fstsw status | 將狀態暫存器內容放到記憶體status處 |
flds value | 載入記憶體中的單精浮點到fpu暫存器堆疊 |
fldl value | 載入記憶體中的雙精浮點到fpu暫存器堆疊 |
fldt value | 載入記憶體中的擴充套件精度點到fpu暫存器堆疊 |
fld %st(i) | 將%st(i)暫存器資料壓入fpu暫存器堆疊 |
fsts value | 單精度資料儲存到value,不出棧 |
fstl value | 雙精度資料儲存到value,不出棧 |
fstt value | 擴充套件精度資料儲存到value,不出棧 |
fstps value | 單精度資料儲存到value,出棧 |
fstpl value | 雙精度資料儲存到value,出棧 |
fstpt value | 擴充套件精度資料儲存到value,出棧 |
fxch %st(i) | 交換%st(0)和%st(i) |
fld1 | 把 +1.0 壓入 FPU 堆疊中 |
fldl2t | 把 10 的對數(底數2)壓入 FPU 堆疊中 |
fldl2e | 把 e 的對數(底數2)壓入 FPU 堆疊中 |
fldpi | 把 pi 的值壓入 FPU 堆疊中 |
fldlg2 | 把 2 的對數(底數10)壓入 FPU 堆疊中 |
fldln2 | 把 2 的對數(底數e) 壓入堆疊中 |
fldz | 把 +0.0 壓入壓入堆疊中 |
以上指令雖多,但是還是很有規律,字首f表示fpu操作,ld載入,st儲存設定,p字尾彈出堆疊,s、l、t字尾表示單精度,雙精度,擴充套件精度,c字尾表 示控制暫存器,s字尾表示狀態暫存器。當然這僅僅是對AT&T語法而言,對MASM語法沒有s,l,t之分,需要使用type ptr來指明精度,即記憶體大小。
學會靈活的載入彈出資料堆疊後,接下來就要看一些基本的計算:
fadd 浮點加法
fdiv 浮點除法
fdivr 反向浮點除法
fmul 浮點乘法
fsub 浮點減法
fsubr 反向浮點減法
對於以上的每種指令,有幾種指令格式,以fadd為例,列舉如下:
# 內從中的32位或者64位值和%st(0)相加
fadd source
# 把%st(x)和%st(0)相加,結果存入%st(0)
fadd %st(x), %st(0)
# 把%st(0)和%st(x)相加,結果存入%st(x)
fadd %st(0), %st(x)
# 把%st(0)和%st(x)相加,結果存入%st(x),彈出%st(0)
faddp %st(0), %st(x)
# 把%st(0)和%st(1)相加,結果存入%st(1),彈出%st(0)
faddp
# 把16位或32位整數與%st(0)相加,結果存入%st(0)
fiadd source
這僅僅是對AT&T語法而言,對MASM源運算元與目的運算元相反!另外,對AT&T,與記憶體相關指令可加s、l指定記憶體精度。其中反向加法和反向除法是計算過程中目的與源反向計算。
浮點計算例子
接下來舉一個AT&T語法的例子,來計算表示式的值 ( 12.34 * 13 ) + 334.75 ) / 17.8 :
# ( 12.34 * 13 ) + 334.75 ) / 17.8
.section .data
values: .float 12.34, 13, 334.75, 17.8
result: .double 0.0
outstring: .asciz "result is %f\n"
.section .text
.globl _main
_main:
leal values, %ebx
flds 12(%ebx)
flds 8(%ebx)
flds 4(%ebx)
flds (%ebx)
fmulp
faddp
fdivp %st(0), %st(1)
fstl result
leal result, %ebx
pushl 4(%ebx)
pushl (%ebx)
pushl $outstring
call _printf
end:
pushl $0
call _exit
前四個flds載入所有的資料到暫存器堆疊,可以單步執行並是用gdb的print $st0列印堆疊暫存器的值,可以看到為什麼是堆疊暫存器。需要說明的是由於printf的%f是double型別的輸出,所以最後要把一個8位元組浮點放 到棧中傳遞,最終結果為27.818541,可以看到與計算器計算的結果近似相等。
浮點高階運算
除了基本的浮點計算,x87還提供了一些諸如餘弦運算等高階計算功能:
指令 | 說明 |
---|---|
f2xm1 | 計算2的乘方(次數為st0中的值,減去1 |
fabs | 計算st0中的絕對值 |
fchs | 改變st0中的值的符號 |
fcos | 計算st0中的值的餘弦 |
fpatan | 計算st0中的值的部分反正切 |
fprem | 計算st0中的值除以st1的值的部分餘數 |
fprem1 | 計算st0中的值除以st1的值的IEEE部分餘弦 |
fptan | 計算st0中的值的部分正切 |
frndint | 把st0中的值舍入到最近的整數 |
fscale | 計算st0乘以2的st1次方 |
fsin | 計算st0中的值的正弦 |
fsincos | 計算st0中的值的正弦和餘弦 |
fsqrt | 計算st0中的值的平方根 |
fyl2x | 計算st1*log st0 以2為底 |
fyl2xp1 | 計算st1*log (st0 + 1) 以2為底 |
下面來看一下浮點條件分支,浮點數的比較不像整數,可以容易的使用cmp指令比較,判斷eflags的值,關於浮點數比較,fpu提供獨立的比較機制和指令,現對這組比較指令進行說明:
指令 | 說明 |
---|---|
fcom | 比較st0和st1暫存器的值 |
fcom %st(x) | 比較st0和stx暫存器的值 |
fcom source | 比較st0和32/64位記憶體值 |
fcomp | 比較st0和st1暫存器的值,並彈出堆疊 |
fcomp %st(x) | 比較st0和stx暫存器的值,並彈出堆疊 |
fcomp source | 比較st0和32/64位記憶體值,並彈出堆疊 |
fcompp | 比較st0和st1暫存器的值,並兩次彈出堆疊 |
ftst | 比較st0和0.0 |
浮點數比較的結果放入狀態暫存器的c0,c2,c3條件程式碼位中,其值如下:
結果 | c3 | c2 | c0 |
---|---|---|---|
st0 > source | 0 | 0 | 0 |
st0 < source | 0 | 0 | 1 |
st0 = source | 1 | 0 | 0 |
如此倘若直接判斷c0,c2,c3的值比較繁瑣,所以可以使用一些技巧,首先使用fstsw指令獲得fpu狀態暫存器的值並存入ax,再使用sahf指令把 ah暫存器中的值載入到eflags暫存器中,sahf指令把ah暫存器的第0、2、4、6、7分別傳送至進位、奇偶、對準、零、符號位,不影響其他標 志,ah暫存器中這些位剛好包含fpu狀態暫存器的條件程式碼值,所以通過fstsw和sahf指令組合,可以傳送如下值:
把c0位傳送到eflags的進位標誌
把c2位傳送到eflags的奇偶校驗標誌
把c3位傳送到eflags的零標誌
傳送完畢後,可以用條件跳轉使用不同的結果值,另外需要說明的是浮點數相等判斷,因為浮點數本身儲存結構決定了它僅僅是一個近似值,所以不能直接判斷是否相 等,這樣可能與自己預期的結果不同,應該判斷兩個浮點數之差是否在一個很小的誤差範圍內,來決定這兩個浮點數是否相等。
根據上面的技巧,使用fstsw和fpu指令組合,可以方便的使用浮點判斷結果,這對我們是一種便利,而intel的工程師又為我們設計了一個組合指令,fcomi指令執行浮點比較結果並把結果存放到eflags暫存器的進位,奇偶,和零標誌。
指令 | 說明 |
---|---|
fcomi | 比較st0和stx暫存器的值 |
fcomip | 比較st0和stx暫存器,並彈出堆疊 |
fucomi | 比較之前檢查無序值 |
fucomip | 比較之前檢查無序值,之後彈出堆疊 |
判斷結束後eflags的標誌設定如下:
結果 | ZF | PF | CF |
---|---|---|---|
st0 > st(x) | 0 | 0 | 0 |
st0 < st(x) | 0 | 0 | 1 |
st0 = st(x) | 1 | 0 | 0 |
CMOV移動指令
最後介紹的是類似cmov的指令,根據判斷結果決定是否需要移動資料,其AT&T格式為 fcmovxx source, destination,其中source是st(x)暫存器,destination是st(0)暫存器。
指令 | 說明 |
---|---|
fcmovb | 如果st(0)小於st(x),則進行傳送 |
fcmove | 如果st(0)等於st(x),則進行傳送 |
fcmovbe | 如果st(0)小於或等於st(x),則進行傳送 |
fcmovu | 如果st(0)無序,則進行傳送 |
fcmovnb | 如果st(0)不小於st(x),則進行傳送 |
fcmovne | 如果st(0)不等於st(x),則進行傳送 |
fcmovnbe | 如果st(0)不小於或等於st(x),則進行傳送 |
fcmovnu | 如果st(0)非無序,則進行傳送 |
以上可以看出,無論從暫存器的操作,還是計算過程,都比整數運算要繁瑣的多,而且看似很簡單的一個表示式,轉化成浮點彙編需要做很多工作,由於其複雜性,同 一個表示式可以有多種運算過程,當然其中的效率相差很大,這依賴於對浮點彙編的理解程度,好在有高階語言處理相關工作,編寫浮點指令的情況比較少見。
相關文章
- 組合語言學習筆記組合語言筆記
- 《組合語言》王爽,學習筆記(子程式呼叫)組合語言筆記
- 組合語言-學習記錄(二)組合語言
- 組合語言零基礎入門學習筆記(一)組合語言筆記
- 組合語言學習筆記03——暫存器(CPU工作原理)組合語言筆記
- 【R語言學習筆記】探索ggplot的排列組合(一)R語言筆記
- 《組合語言》王爽,學習筆記(標誌暫存器下)組合語言筆記
- 組合語言指令大全組合語言
- 【學習筆記】組合數學筆記
- 組合數學學習筆記筆記
- Go語言核心36講(Go語言進階技術十二)--學習筆記Go筆記
- 【R語言學習筆記】探索ggplot的排列組合:線圖(一)R語言筆記
- 讀書寫筆記-王爽《組合語言》筆記組合語言
- 組合語言學習記錄--輸入輸出字串組合語言字串
- Solidity語言學習筆記————32、建立合約Solid筆記
- Go語言核心36講(Go語言實戰與應用十二)--學習筆記Go筆記
- ARM組合語言最常用指令組合語言
- 組合語言-CALL和RET指令組合語言
- C 語言學習筆記筆記
- C語言學習筆記C語言筆記
- 組合語言學習記錄--第一個彙編程式組合語言
- 組合最佳化 學習筆記筆記
- Go語言核心36講(Go語言實戰與應用二十二)--學習筆記Go筆記
- js學習筆記(十二)——語法速查表JS筆記
- 讀書筆記:組合語言(王爽)實驗七筆記組合語言
- Solidity語言學習筆記————35、抽象合約和介面Solid筆記抽象
- 《組合語言》第十二章 內中斷組合語言
- 【C++學習筆記】型別組合C++筆記型別
- Solidity語言學習筆記————13、固定大小位元組陣列Solid筆記陣列
- Solidity語言學習筆記————14、動態位元組陣列Solid筆記陣列
- 組合數學筆記-排列與組合筆記
- 組合語言1 - 什麼是組合語言?組合語言
- 熱更新語言--lua學習筆記筆記
- c語言學習筆記===函式C語言筆記函式
- 《JavaScript語言精粹》學習筆記二JavaScript筆記
- 《JavaScript語言精粹》學習筆記一JavaScript筆記
- R語言學習筆記之一R語言筆記
- Go 基礎語言學習筆記Go筆記