浮點數也稱做實型資料(實數),形式上就是數學中的小數。浮點型資料有兩種表達方式: 一種是用數字和小數點表示的,如123.456; 另一種是用指數方式表示,如1.2e-6 或1.2E-6(1.2*10-6)。
在計算機中實數是如何存儲的呢?主要分為定點實數存儲方式和浮點實數存儲方式這兩種。所謂定點實數,就是約定整數位和小數位的長度,比如用4位元組存儲實數,我們可以讓高兩個位元組存放整數部分,低兩個位元組存儲小數部分。這樣的好處是計算的效率高,缺點是如果我們想存儲65536.5,由于整數的表達範圍超過了兩個位元組,用定點存儲的方式就無法存儲了。
對應地,也有浮點實數存儲方式,就是用一部分二進制位存放小數點的位置資訊,我們可以稱之為”指數域”,其他的資料位用來存儲沒有小數點的資料和符号,我們可以稱之為“資料域”、“符号域”。在通路時取得指數域,與資料域運算後得到真值,如67.625,利用浮點實數存儲方式,資料域可以記錄為67625,小數點的位置可以記錄為10的-3次方。後來引進了浮點協處理器(FPU),專門負責對浮點數的處理,使得對處理實數的效率大大提高,于是浮點實數存儲方式也就普及開來,成為了現在主流的實數存儲方式。
在C/C++中,使用浮點方式存儲實數,用兩種資料類型來儲存浮點數:float(單精度)和double(雙精度)。Float在記憶體中占用4位元組空間,double在記憶體中占用8個位元組空間。Double類型比float類型精度更高。這兩種資料在記憶體中都是以十六進制方式存儲,但與整型資料有所不同。
整型資料是将十進制直接轉換成二進制儲存在記憶體中,以十六進制方式顯示。而浮點類型不是将一個浮點小數直接轉換成二進制儲存,而是将浮點小數轉換成二進制碼 後 重新編碼,再進行存儲。C/c++的浮點數是有符号的。
在C/C++中,将浮點數強制轉換成整數時,不會采用數學上的四舍五入方式,而是舍棄掉小數部分。
浮點數的操作不會用到通用寄存器,而是用浮點協處理器的浮點寄存器。
浮點數的編碼方式
浮點編碼轉換采用的是IEEE規定的編碼标準,float和double這兩種類型資料的轉換原理相同,但由于表示的範圍不一樣,編碼方式有些差別。IEEE規定的浮點數編碼會将一個浮點數轉換為二進制數。以科學計數法劃分,将浮點數拆分為3個部分:符号、指數和尾數。
1、float類型的IEEE編碼
Float類型在記憶體中占4個位元組(32位)。最高位表示符号:在剩餘的31位中,從右到左取8位 用于表示指數,其餘用于表示尾數。如圖2-2所示:

float類型的IEEE編碼
1)在進行二進制轉換前,需要對單精度(float)浮點數進行科學計數法轉換。例如,将float類型的12.25f(f表示為float單精度類型)轉換為IEEE編碼,需要将12.25f轉換成對應的二進制數1100.01,整數部分為1100,小數部分為01。小數點向左移動,每移動1次指數加1,移動到除了符号位的最高位1處,停止移動。這裡移動3次。對12.25f進行科學記數法轉換後的二進制部分為1.10001,指數部分為3。在IEEE編碼中,由于在二進制情況下,最高位始終為1,為一個恒定值,故将其忽略不計。這裡是一個正數,是以符号位添0。
12.25f經過IEEE轉換後各位的情況:
符号位:0
指數為:十進制 3+127,轉換為二進制10000010
尾數位:10001 000000000000000000(當不足23位時,低位補0填充)
由于尾數位中最高位1是恒定值,故省略不計,隻要在轉換回十進制時加1即可。為什麼指數位要加127呢?由于指數可能出現負數,十進制127 可表示二進制數 01111111。IEEE編碼方式規定,當指數域小于0111111時為一個負數,反之為正數,是以 指數域加上十進制數 127 表示正數。
12.25f轉換後的IEEE編碼按二進制拼接為 0 10000010 10001000000000000000000。轉換後成十六進制數 0x41440000,記憶體中以小端進行存儲,故為 00 00 44 41。分析結果如圖所示:
2)上面示範了符号位為正,指數為也為正的情況。那麼什麼情況下指數位可以為負呢?根據科學記數法,小數點向整數部分移動時,指數做加法。相反,小數點向小數部分移動時,指數需要以0起始做減法。浮點數 -0.125f轉換成IEEE編碼後,将會是一個符号位為1,指數部分為負的小數。-0.125f經轉換後二進制部分為0.001,用科學記數法為1.0,指數為-3。
-0.125f 經過IEEE轉碼後各位的情況為:
符号位:1
指數位:十進制127+(-3),轉換為二進制是 01111100,如果不足8位,則高位補0
尾數位:0000000000000000000000000
-0.125f經轉換後的IEEE編碼二進制拼接為 1 01111100 0000000000000000000000000。轉換後成十六進制為 0xBE000000,記憶體中顯示為 00 00 00 BE。分析結果如圖所示:
3)上面的兩個浮點小數部分轉換為二進制時都是有窮的,如果小數部分轉換為二進制時得到一個無窮值,則會根據尾數部分的長度舍棄多餘的部分。單精度浮點數1.3f,小數部分轉換為二進制就會産生無窮值,依次轉換為0.3、0.6、1.2、0.4、0.8、1.6、1.2、0.4、0.8...,轉換後得到的二進制數位1.01001100110011001100110,到第23為時終止,尾數部分無法再儲存。
1.3f經過IEEE轉換後各位的情況:
符号位:0
指數位:十進制0+127,轉換二進制01111111
尾數位:01001100110011001100110
1.3f 轉換後的IEEE編碼二進制拼接為 0 01111111 01001100110011001100110。轉換成十六進制數位 0x3fa66666,記憶體中顯示為 66 66 a6 3f。由于在轉換二進制過程中産生了無窮值,舍棄了部分位數,是以進行IEEE編碼轉換後得到的是一個近似值,存在一定的誤差。再将這個IEEE編碼值轉換成十進制小數,得到的值為1.2516582,四舍五入後為1.3.這也解釋了為什麼C++ 在比較浮點數值是否為0時,要做一個區間而不是直接進行等值比較。如:
float fTemp = 0.0001f; // 精确範圍
if (fFloat >= -fTemp && fFloat <= fTemp)
{
fTemp等于0
}
2.double類型的IEEE編碼
前文講解了單精度浮點類型的IEEE編碼。Double類型和float類型大同小異,隻是double類型表示的範圍更大,占用空間更多,精度更準。
Double 類型占8位元組的記憶體空間,同樣最高位也用于表示符号,指數位占11位,剩餘的52位用于表示尾數。
在float中,指數位範圍用8位表示,加127後用于判斷指數符号。在double中,由于擴大了精度,是以指數範圍使用11位正數來表示,加上1023來用于指數符号判斷。
Double 類型的IEEE編碼轉換過程和float一樣。
3.浮點數指令
浮點數的操作指令和普通資料類型不同,浮點數操作是通過浮點寄存器來實作的,而普通資料使用的是通用寄存器,如eax、edx、ebx等。
浮點寄存器是通過棧結構來實作的,由ST(0)~ST(7)共8個棧空間組成,每個浮點寄存器占8個位元組。每次使用浮點寄存器都是先使用St(0),而不能越過ST(0)直接使用ST(1)。浮點寄存器的使用就是壓棧、出棧的過程。當ST(0)存在資料時,執行壓棧操作,ST(0)中的資料将進入到ST(1)中,如無出棧操作,将順序地向下壓棧,直到将浮點寄存器占滿。常用浮點數指令如下所示:IN 表示操作數 入棧。OUT表示操作數出棧。
常用浮點數指令
其他運算指令和普通指令類似,隻需在前面加F就行,如 FSUB和FSUBP等。
在使用浮點指令時,都要先利 用ST(0)進行運算。當ST(0)中有值時,便會将ST(0)中的資料順序向下存放到ST(1)中,然後再将資料放入ST(0)中。如果再次操作ST(0),則會先将ST(1)中的資料放入ST(2)中,然後将ST(0)中的資料放入到ST(1)中,最後才将新的資料存放到ST(0)。以此類推,在八個浮點寄存器都有值的情況下繼續向ST(0)存放資料,這時會丢棄ST(7)中的資料資訊。
1)下面通過一個簡單的例子來了解各個指令的使用流程:
// 浮點數使用
float fFloat = (float)argc;
00401028 fild dword ptr [ebp+8]
//将ebp+8處的整型資料轉換成浮點型,并放入ST(0)中,對應變量 argc
0040102B fst dword ptr [ebp-4]
//從ST(0) 中取出資料以浮點編碼的方式放入位址ebp-4 中,對應變量 fFloat
printf("%f", fFloat);
0040102E sub esp,8
//這裡對esp減 8 操作是由于浮點數作為變參函數的參數時需要轉換成雙精度浮點值,
//這步操作是 提前準備8位元組的棧空間,以便存放double資料。
00401031 fstp qword ptr [esp]
//将ST(0) 中的資料傳入esp中,并彈出ST(0)。
00401034 push offset string "%f" (00426020)
00401039 call printf (00401420)
0040103E add esp,0Ch
argc = (int)fFloat;
//将 float類型資料轉換成int型
00401041 fld dword ptr [ebp-4]
//将ebp-4處的資料以浮點型壓入ST(0)中。
00401044 call __ftol (00401588)
//調用函數 __ftol 進行浮點數轉換, __ftol的實作見下文。
00401049 mov dword ptr [ebp+8],eax
printf("%d", argc);
0040104C mov eax,dword ptr [ebp+8]
0040104F push eax
00401050 push offset string "%d" (0042601c)
00401055 call printf (00401420)
0040105A add esp,8
從上面示例中可以發現,float類型的浮點數雖然占4個位元組,但都是以8個位元組(qword)方式進行處理。當浮點數作為參數時,并不能直接壓棧。Push 指令隻能傳入4位元組資料到棧中,這樣會丢失4位元組資料。這就是為什麼使用printf函數以整型方式輸出浮點數會産生錯誤的原因。Printf以整數方式輸出時,将對應參數作為4位元組資料,按補碼方式解釋。而真正壓入的參數為浮點類型時,資料長度為8位元組,需要按浮點編碼解釋。
2)浮點數作為傳回值的情況也是如此,同樣需要傳遞8位元組資料,代碼如下所示:
float fFloat;
fFloat = GetFloat();
00401058 call @ILT+5(_GetFloat) (0040100a)
//調用GetFloat函數
0040105D fst dword ptr [ebp-4]
//由于浮點數需要特殊處理,浮點數占8個位元組,無法使用EAX進行傳遞
//是以使用 浮點寄存器 ST(0) 作為傳回值
printf("%f", fFloat);
00401060 sub esp,8
00401063 fstp qword ptr [esp]
00401066 push offset string "%f" (00426020)
0040106B call printf (00401420)
00401070 add esp,0Ch
//GetFloat 函數
float GetFloat()
{
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,40h
00401016 push ebx
00401017 push esi
00401018 push edi
00401019 lea edi,[ebp-40h]
0040101C mov ecx,10h
00401021 mov eax,0CCCCCCCCh
00401026 rep stos dword ptr [edi]
return 12.25f;
00401028 fld dword ptr [string "%d" (0042601c)]
//将浮點數儲存在 ST(0)中,在傳回值為浮點數的情況下,無法使用EAX
//使用ST(0)作為傳回值進行傳遞。
}
0040102E pop edi
0040102F pop esi
00401030 pop ebx
00401031 mov esp,ebp
00401033 pop ebp
00401034 ret
3)在上面代碼中,float型資料被強制轉換為int型,編譯器通過了__ftol函數實作了轉換過程,如下面所示:
__ftol:
00401588 push ebp
00401589 mov ebp,esp
0040158B add esp,0FFFFFFF4h
//儲存環境,預留語句變量空間
0040158E wait
0040158F fnstcw word ptr [ebp-2]
00401592 wait
00401593 mov ax,word ptr [ebp-2]
00401597 or ah,0Ch
0040159A mov word ptr [ebp-4],ax
0040159E fldcw word ptr [ebp-4]
//浮點異常檢查、CPU與FPU的同步工作
004015A1 fistp qword ptr [ebp-0Ch]
//從ST(0)中取出8位元組資料轉換成整型并存入到ebp-0ch中
//從ST(0)中彈出
004015A4 fldcw word ptr [ebp-2]
004015A7 mov eax,dword ptr [ebp-0Ch]
//使用eax儲存整型資料的低4位元組,用于傳回
004015AA mov edx,dword ptr [ebp-8]
//使用edx儲存整型資料的高4位元組,用于傳回
004015AD leave
//釋放棧空間
004015AE ret
004015AF int 3
————————摘自《C++反彙編與逆向分析技術揭秘》