天天看點

c++二進制轉十進制_C/C++中浮點數的編碼存儲

浮點數也稱做實型資料(實數),形式上就是數學中的小數。浮點型資料有兩種表達方式: 一種是用數字和小數點表示的,如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所示:

c++二進制轉十進制_C/C++中浮點數的編碼存儲

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。分析結果如圖所示:

c++二進制轉十進制_C/C++中浮點數的編碼存儲

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。分析結果如圖所示:

c++二進制轉十進制_C/C++中浮點數的編碼存儲

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表示操作數出棧。

c++二進制轉十進制_C/C++中浮點數的編碼存儲

常用浮點數指令

其他運算指令和普通指令類似,隻需在前面加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++反彙編與逆向分析技術揭秘》