- C++反匯編與逆向分析技術揭秘(第2版)
- 錢林松 張延清
- 5247字
- 2021-09-27 17:05:10
2.2 浮點數類型
計算機也需要運算和存儲數學中的實數。在計算機的發展過程中,曾產生過多種存儲實數的方式,有的現在已經很少使用了。不管如何存儲,都可以將其劃分為定點實數存儲方式和浮點實數存儲方式兩種。所謂定點實數,就是約定整數位和小數位的長度,比如用4字節存儲實數,我們可以約定兩個高字節存放整數部分,兩個低字節存儲小數部分。
這樣做的好處是計算效率高,缺點也顯而易見:存儲不靈活,比如我們想存儲65536.5,由于整數的表達范圍超過了2字節,就無法用定點實數存儲方式了。對應地,也有浮點實數存儲方式,道理很簡單,就是用一部分二進制位存放小數點的位置信息,我們可以稱之為“指數域”,其他的數據位用來存儲沒有小數點時的數據和符號,我們可以稱之為“數據域”“符號域”。在訪問時取得指數域,與數據域運算后得到真值,如67.625,利用浮點實數存儲方式,數據域可以記錄為67625,小數點的位置可以記為10的-3次方,對該數進行訪問時計算一下即可。
浮點實數存儲方式的優缺點和定點實數存儲方式的正好相反。在80286之前,程序員常常為實數的計算傷腦筋,而后來出現的浮點協處理器,可以協助主處理器分擔浮點運算,程序員計算實數的效率因此得到提升,于是浮點實數存儲方式也就普及開來,成為現在主流的實數存儲方式。但是,在一些條件惡劣的嵌入式開發場合,仍可看到定點實數的存儲和使用。
在C/C++中,使用浮點方式存儲實數,用兩種數據類型來保存浮點數:float(單精度)、double(雙精度)。float在內存中占4字節,double在內存中占8字節。由于占用空間大,double可描述的精度更高。這兩種數據類型在內存中同樣以十六進制方式存儲,但與整型類型有所不同。
整型類型是將十進制轉換成二進制保存在內存中,以十六進制方式顯示。浮點類型并不是將一個浮點小數直接轉換成二進制數保存,而是將浮點小數轉換成的二進制碼重新編碼,再進行存儲。C/C++的浮點數是有符號的。
在C/C++中,將浮點數強制轉換為整數時,不會采用數學上四舍五入的方式,而是舍棄掉小數部分(第4章會提到的“向零取整”),不會進位。
浮點數的操作不會用到通用寄存器,而是會使用浮點協處理器的浮點寄存器,專門對浮點數進行運算處理。
2.2.1 浮點數的編碼方式
浮點數編碼轉換采用的是IEEE規定的編碼標準,float和double這兩種類型數據的轉換原理相同,但由于表示的范圍不一樣,編碼方式有些許區別。IEEE規定的浮點數編碼會將一個浮點數轉換為二進制數。以科學記數法劃分,將浮點數拆分為3部分:符號、指數、尾數。
1. float類型的IEEE編碼
float類型在內存中占4字節(32位)。最高位用于表示符號,在剩余的31位中,從左向右取8位表示指數,其余表示尾數,如圖2-2所示。

圖2-2 float類型的二進制表示說明
在進行二進制轉換前,需要對單精度浮點數進行科學記數法轉換。例如,將float類型的12.25f轉換為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=130,轉換為二進制為10000010。
- 尾數位:10001 000000000000000000(當不足23位時,低位補0填充)。
由于尾數位中最高位1是固定值,故忽略不計,只要在轉換回十進制數時加1即可。為什么指數位要加127呢?這是因為指數可能出現負數,十進制數127可表示為二進制數01111111,IEEE編碼方式規定,當指數小于0111111時為一個負數,反之為正數,因此01111111為0。
將示例中轉換后的符號位、指數位和尾數位按二進制拼接在一起,就成為一個完整的IEEE浮點編碼:01000001010001000000000000000000。轉換成十六進制數為0x41440000,內存中以小尾方式進行排列,故為00 00 44 41,分析結果如圖2-3所示。

圖2-3 單精度浮點數12.25f轉換為IEEE編碼
上面演示了符號位為正、指數位也為正的情況。那么什么情況下指數位可以為負呢?根據科學記數法,小數點向整數部分移動時,指數做加法。相反,小數點向小數部分移動時,指數需要以0起始做減法。浮點數-0.125f轉換IEEE編碼后,將會是一個符號位為1、指數部分為負的小數。-0.125f經轉換后二進制部分為0.001,用科學記數法表示為1.0,指數為-3。
-0.125fIEEE轉換后各位的情況如下。
- 符號位:1。
- 指數位:十進制127+(-3),轉換為二進制是01111100,如果不足8位,則高位補0。
- 尾數位:00000000000000000000000。
-0.125f轉換后的IEEE編碼二進制拼接為10111110000000000000000000000000。轉換成十六進制數為0xBE000000,內存中顯示為00 00 00 BE,分析結果如圖2-4所示。

圖2-4 單精度浮點數-0.125f轉換為IEEE編碼
上面的兩個浮點數小數部分轉換為二進制時都是有窮的,如果小數部分轉換為二進制時得到一個無窮值,則會根據尾數部分的長度舍棄多余的部分。如單精度浮點數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編碼二進制拼接為00111111101001100110011001100110。轉換成十六進制數為0x3FA66666,在內存中顯示為66 66 A6 3F。由于在轉換二進制過程中產生了無窮值,舍棄了部分位數,所以進行IEEE編碼轉換后得到的是一個近似值,存在一定的誤差。再次將這個IEEE編碼值轉換成十進制小數,得到的值為1.2516582,四舍五入保留一位小數之后為1.3。這就解釋了為什么C++在比較浮點數值是否為0時,要做一個區間比較而不是直接進行等值比較。正確浮點數比較的代碼見代碼清單2-1。
代碼清單2-1 正確浮點數比較
float f1 = 0.0001f; // 精確范圍 if (f2 >= -f1 && f2 <= f1) { // f1等于0 }
2. double類型的IEEE編碼
前文講解了單精度浮點類型的IEEE編碼。double類型和float類型大同小異,只是double類型表示的范圍更大,占用空間更多,是float類型所占空間的兩倍。當然,精準度也會更高。
double類型占8字節的內存空間,同樣,最高位也用于表示符號,指數位占11位,剩余52位表示位數。
在float類型中,指數位范圍用8位表示,加127后用于判斷指數符號。在double類型中,由于擴大了精度,因此指數范圍使用11位正數表示,加1023后可用于指數符號判斷。
double類型的IEEE編碼轉換過程與float類型一樣,讀者可根據float類型的轉換流程來轉換double類型,此處不再贅述。
2.2.2 基本的浮點數指令
前面介紹了浮點數的編碼方式,下面我們來學習浮點數指令。浮點數的操作指令與普通數據類型不同,浮點數操作是通過浮點寄存器實現的,而普通數據類型使用的是通用寄存器,它們分別使用兩套不同的指令。
在早期CPU中,浮點寄存器是通過棧結構實現的,由ST(0)~ST(7)共8個棧空間組成,每個浮點寄存器占8字節。每次使用浮點寄存器都是率先使用ST(0),而不能越過ST(0)直接使用ST(1)。浮點寄存器的使用就是壓棧、出棧的過程。當ST(0)中存在數據時,執行壓棧操作后,ST(0)中的數據將裝入ST(1)中,如無出棧操作,將按順序向下壓棧,直到將浮點寄存器占滿為止。常用浮點數指令的介紹如表2-1所示,其中,IN表示操作數入棧,OUT表示操作數出棧。
表2-1 常用浮點數指令表

其他運算指令和普通指令類似,只須在前面加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)。以此類推,在8個浮點寄存器都有值的情況下繼續向ST(0)中的存放數據,這時會丟棄ST(7)中的數據信息。
1997年開始,Intel和AMD都引入了媒體指令(MMX),這些指令允許多個操作并行,允許對多個不同的數據并行執行同一操作。近年來,這些擴展有了長足的發展。名字經過了一系列的修改,從MMX到SSE(流SIMD擴展),以及最新的AVX(高級向量擴展)。每一代都有一些不同的版本。每個擴展都用來管理寄存器中的數據,這些寄存器在MMX中被稱為MM寄存器,在SSE中被稱為XMM寄存器,在AVX中被稱為YMM寄存器。MM寄存器是64位的,XMM是128位的,而YMM是256位的。每個YMM寄存器可以存放8個32位值或4個64位值,可以是整數,也可以是浮點數。YMM寄存器一共有16個(YMM0~YMM15),而XMM是YMM的低128位。常用SSE浮點數指令的介紹如表2-2所示。
表2-2 常用SSE浮點數指令表

下面通過一個簡單的示例介紹各指令的使用流程,幫助讀者熟悉浮點指令的使用方法,如代碼清單2-2所示。
代碼清單2-2 Debug版float指令練習
// C+源碼 #include <stdio.h> int main(int argc, char* argv[]) { float f = (float)argc; printf("%f", f); argc = (int)f; printf("%d", argc); return 0; } //x86_vs對應匯編代碼講解 00401000 push ebp 00401001 mov ebp, esp 00401003 push ecx 00401004 cvtsi2ss xmm0, dword ptr [ebp+8] 00401009 movss [ebp-4], xmm0 ;f = (float)argc; 0040100E cvtss2sd xmm0, dword ptr [ebp-4];xmm0=(double)f 00401013 sub esp, 8 00401016 movsd qword ptr [esp], xmm0 ;參數2 xmm0入棧 0040101B push offset asc_412160 ;參數1 "%f"入棧 00401020 call sub_401090 ;調用printf函數 00401025 add esp, 0Ch ;平衡棧 00401028 cvttss2si eax, dword ptr [ebp-4] 0040102D mov [ebp+8], eax ;argc=(int)f 00401030 mov ecx, [ebp+8] 00401033 push ecx ;參數2 argc入棧 00401034 push offset aD ;參數1 "%d"入棧 00401039 call sub_401090 ;調用printf函數 0040103E add esp, 8 ;平衡棧 00401041 xor eax, eax 00401043 mov esp, ebp 00401045 pop ebp 00401046 retn //x86_gcc對應匯編代碼講解 00401510 push ebp 00401511 mov ebp, esp 00401513 and esp, 0FFFFFFF0h ;棧對齊 00401516 sub esp, 30h 00401519 call ___main ;調用初始化函數 0040151E fild [ebp+8] ;argc轉換雙精度數入棧 00401521 fstp dword ptr [esp+2Ch] ;f=(float)argc 00401525 fld dword ptr [esp+2Ch] 00401529 fstp qword ptr [esp+4] ;參數2 (double)f入棧 0040152D mov dword ptr [esp], offset asc_404000 ;參數1 "%f"入棧 00401534 call _printf ;調用printf函數 00401539 fld dword ptr [esp+2Ch] ;f入棧st(0) 0040153D fnstcw word ptr [esp+1Eh] 00401541 movzx eax, word ptr [esp+1Eh] 00401546 or ah, 0Ch 00401549 mov [esp+1Ch], ax 0040154E fldcw word ptr [esp+1Ch] ;浮點異常檢查代碼 00401552 fistp [ebp+8] ;argc=(int)f 00401555 fldcw word ptr [esp+1Eh] 00401559 mov eax, [ebp+8] 0040155C mov [esp+4], eax ;參數2 argc入棧 00401560 mov dword ptr [esp], offset aD ;參數1 "%d"入棧 00401567 call _printf ;調用printf函數 0040156C mov eax, 0 00401571 leave 00401572 retn //x86_clang對應匯編代碼講解 00401000 push ebp 00401001 mov ebp, esp 00401003 sub esp, 24h 00401006 mov eax, [ebp+0Ch] ;eax=argv 00401009 mov ecx, [ebp+8] ;ecx=argc 0040100C mov dword ptr [ebp-4], 0 00401013 mov edx, [ebp+8] ;edx=argc 00401016 cvtsi2ss xmm0, edx ;xmm0=(int)argc 0040101A movss dword ptr [ebp-8], xmm0 ;f=(int)argc 0040101F movss xmm0, dword ptr [ebp-8] 00401024 cvtss2sd xmm0, xmm0 ;xmm0=(double)f 00401028 lea edx, asc_412160 ;edx="%f" 0040102E mov [esp], edx ;參數1 "%f"入棧 00401031 movsd qword ptr [esp+4], xmm0 ;參數2 xmm0入棧 00401037 mov [ebp-0Ch], eax 0040103A mov [ebp-10h], ecx 0040103D call sub_401070 ;調用printf函數 00401042 cvttss2si ecx, dword ptr [ebp-8] 00401047 mov [ebp+8], ecx ;argc=(int)f 0040104A mov ecx, [ebp+8] 0040104D lea edx, aD ;edx="%d" 00401053 mov [esp], edx ;參數1 "%d"入棧 00401056 mov [esp+4], ecx ;參數2 argc入棧 0040105A mov [ebp-14h], eax 0040105D call sub_401070 ;調用printf函數 00401062 xor ecx, ecx 00401064 mov [ebp-18h], eax 00401067 mov eax, ecx 00401069 add esp, 24h 0040106C pop ebp 0040106D retn //x64_vs對應匯編代碼講解 0000000140001000 mov [rsp+10h], rdx 0000000140001005 mov [rsp+8], ecx 0000000140001009 sub rsp, 38h 000000014000100D cvtsi2ss xmm0, dword ptr [rsp+40h] ;xmm0=(float)argc 0000000140001013 movss dword ptr [rsp+20h], xmm0 ;f=(float)argc 0000000140001019 cvtss2sd xmm0, dword ptr [rsp+20h] ;xmm0=(double)f 000000014000101F movaps xmm1, xmm0 0000000140001022 movq rdx, xmm1 ;參數2 (double)f 0000000140001027 lea rcx, asc_1400122C0 ;參數1 "%f" 000000014000102E call sub_1400010C0 ;調用printf函數 0000000140001033 cvttss2si eax, dword ptr [rsp+20h] ;eax=(int)f 0000000140001039 mov [rsp+40h], eax ;argc=(int)f 000000014000103D mov edx, [rsp+40h] ;參數2 argc 0000000140001041 lea rcx, aD ;參數1 "%d" 0000000140001048 call sub_1400010C0 ;調用printf函數 000000014000104D xor eax, eax 000000014000104F add rsp, 38h 0000000140001053 retn; //x64_gcc對應匯編代碼講解 0000000000401550 push rbp 0000000000401551 mov rbp, rsp 0000000000401554 sub rsp, 30h 0000000000401558 mov [rbp+10h], ecx 000000000040155B mov [rbp+18h], rdx 000000000040155F call __main ;調用初始化函數 0000000000401564 cvtsi2ss xmm0, dword ptr [rbp+10h] 0000000000401569 movss dword ptr [rbp-4], xmm0 ;f=(float)argc 000000000040156E cvtss2sd xmm0, dword ptr [rbp-4] ;xmm0=(double)f 0000000000401573 movq rax, xmm0 0000000000401578 mov rdx, rax 000000000040157B movq xmm1, rdx 0000000000401580 mov rdx, rax ;參數2 (double)f 0000000000401583 lea rcx, Format ;參數1 "%f" 000000000040158A call printf ;調用printf函數 000000000040158F movss xmm0, dword ptr [rbp-4] 0000000000401594 cvttss2si eax, xmm0 0000000000401598 mov [rbp+10h], eax ;argc=(int)f 000000000040159B mov edx, [rbp+10h] ;參數1 argc 000000000040159E lea rcx, aD ;參數2 "%d" 00000000004015A5 call printf ;調用printf函數 00000000004015AA mov eax, 0 00000000004015AF add rsp, 30h 00000000004015B3 pop rbp 00000000004015B4 retn //x64_clang對應匯編代碼講解 0000000140001000 sub rsp, 48h 0000000140001004 mov dword ptr [rsp+44h], 0 000000014000100C mov [rsp+38h], rdx ;保存argv 0000000140001011 mov [rsp+34h], ecx ;保存argc 0000000140001015 mov ecx, [rsp+34h] 0000000140001019 cvtsi2ss xmm0, ecx 000000014000101D movss dword ptr [rsp+30h], xmm0 ;f=(int)argc 0000000140001023 movss xmm0, dword ptr [rsp+30h] 0000000140001029 cvtss2sd xmm0, xmm0 ;xmm0=(double)f 000000014000102D lea rcx, asc_1400122C0 ;參數1 "%f" 0000000140001034 movaps xmm1, xmm0 0000000140001037 movq rdx, xmm0 ;參數2 (double)f 000000014000103C call sub_140001070 ;調用printf函數 0000000140001041 cvttss2si r8d, dword ptr [rsp+30h] 0000000140001048 mov [rsp+34h], r8d ;argc=(int)f 000000014000104D mov edx, [rsp+34h] ;參數2 argc 0000000140001051 lea rcx, aD ;參數1 "%d" 0000000140001058 mov [rsp+2Ch], eax 000000014000105C call sub_140001070 ;調用printf函數 0000000140001061 xor edx, edx 0000000140001063 mov [rsp+28h], eax 0000000140001067 mov eax, edx 0000000140001069 add rsp, 48h 000000014000106D retn
代碼清單2-2通過浮點數與整數、整數與浮點數間的互相轉換演示了數據傳送類型的浮點指令的使用方法。從示例中可以發現,float類型的浮點數雖然占4字節,但是使用浮點棧將以8字節方式進行處理,而使用媒體寄存器則以4字節處理。當浮點數作為參數時,并不能直接壓棧,PUSH指令只能傳入4字節數據到棧中,這樣會丟失4字節數據。這就是使用printf函數以整數方式輸出浮點數時會產生錯誤的原因。printf以整數方式輸出時,將對應參數作為4字節數據長度,按補碼方式解釋,而真正壓入的參數為浮點類型時,卻是8字節長度,需要按浮點編碼方式解釋。
浮點數作為返回值的情況也是如此,在32位程序中使用浮點棧st(0)作為返回值同樣需要傳遞8字節數據,64位程序中使用媒體寄存器xmm0作為返回值只需要傳遞4字節,如代碼清單2-3所示。
代碼清單2-3 浮點數作為返回值
// C++源碼 #include <stdio.h> float getFloat() { return 12.25f; } int main(int argc, char* argv[]) { float f = getFloat(); return 0; } //x86_vs對應匯編代碼講解 00401010 push ebp 00401011 mov ebp, es 00401013 push ecx ;參數1 argc入棧 00401014 call sub_401000 ;調用getFloat()函數 00401019 fstp dword ptr [ebp-4] ;f=getFloat()將st(0)的雙精度數轉換為單精度數 0040101C xor eax, eax 0040101E mov esp, ebp 00401020 pop ebp 00401021 retn 00401000 push ebp 00401001 mov ebp, esp 00401003 fld ds:flt_40D150 ;將返回值入棧到st(0)中,單精度數轉換為雙精度數入棧 00401009 pop ebp 0040100A retn ;getFloat()函數返回 //x86_gcc對應匯編代碼講解 00401517 push ebp 00401518 mov ebp, esp 0040151A and esp, 0FFFFFFF0h ;棧對齊 0040151D sub esp, 10h 00401520 call ___main ;調用初始化函數 00401525 call __Z8getFloatv ;調用getFloat()函數 0040152A fstp dword ptr [esp+0Ch] ;f=getFloat()將st(0)的雙精度數轉換為單精度數 0040152E mov eax, 0 00401533 leave 00401534 retn 00401510 fld ds:flt_404000 ;將返回值入棧到st(0)中,單精度數轉換為雙精度數入棧 00401516 retn ;getFloat()函數返回 //x86_clang對應匯編代碼講解 00401010 push ebp 00401011 mov ebp, esp 00401013 sub esp, 14h 00401016 mov eax, [ebp+0Ch] ;eax=argv 00401019 mov ecx, [ebp+8] ;ecx=argc 0040101C mov dword ptr [ebp-4], 0 00401023 mov [ebp-10h], eax 00401026 mov [ebp-14h], ecx 00401029 call sub_401000 ;調用getFloat()函數 0040102E fstp dword ptr [ebp-0Ch] ;f=getFloat()將st(0)的雙精度數轉換為單精度數 00401031 movss xmm0, dword ptr [ebp-0Ch] 00401036 xor eax, eax 00401038 movss dword ptr [ebp-8], xmm0 0040103D add esp, 14h 00401040 pop ebp 00401041 retn 00401000 push ebp 00401001 mov ebp, esp 00401003 fld ds:flt_40D150 ;將返回值入棧到st(0)中,單精度數轉換為雙精度數入棧 00401009 pop ebp 0040100A retn ;getFloat()函數返回 //x64_vs對應匯編代碼講解 0000000140001010 mov [rsp+10h], rdx 0000000140001015 mov [rsp+8], ecx 0000000140001019 sub rsp, 38h 000000014000101D call sub_140001000 ;調用getFloat()函數 0000000140001022 movss dword ptr [rsp+20h], xmm0;f=getFloat()從xmm0獲取返回值 0000000140001028 xor eax, eax 000000014000102A add rsp, 38h 000000014000102E retn 0000000140001000 movss xmm0, cs:dword_14000D2C0;xmm0=12.25f 0000000140001008 retn ;getFloat()函數返回 //x64_gcc對應匯編代碼講解 0000000000401559 push rbp 000000000040155A mov rbp, rsp 000000000040155D sub rsp, 30h 0000000000401561 mov [rbp+10h], ecx 0000000000401564 mov [rbp+18h], rdx 0000000000401568 call __main ;調用初始化函數 000000000040156D call _Z8getFloatv ;調用getFloat()函數 0000000000401572 movd eax, xmm0 0000000000401576 mov [rbp-4], eax ;f=getFloat()從xmm0獲取返回值 0000000000401579 mov eax, 0 000000000040157E add rsp, 30h 0000000000401582 pop rbp 0000000000401583 retn 0000000000401550 movss xmm0, cs:dword_404000;xmm0=12.25f 0000000000401558 retn ;getFloat()函數返回 //x64_clang對應匯編代碼講解 0000000140001010 sub rsp, 38h 0000000140001014 mov dword ptr [rsp+34h], 0 000000014000101C mov [rsp+28h], rdx 0000000140001021 mov [rsp+24h], ecx 0000000140001025 call sub_140001000 ; 調用getFloat()函數 000000014000102A xor eax, eax 000000014000102C movss dword ptr [rsp+20h], xmm0;f=getFloat()從xmm0獲取返回值 0000000140001032 add rsp, 38h 0000000140001036 retn 0000000140001000 movss xmm0, cs:dword_14000D2C0;xmm0=12.25f 0000000140001008 retn ;getFloat()函數返回
- 編程珠璣(續)
- DevOps入門與實踐
- MATLAB實用教程
- 征服RIA
- Python機器學習:手把手教你掌握150個精彩案例(微課視頻版)
- INSTANT Sinatra Starter
- AutoCAD 2009實訓指導
- iPhone應用開發從入門到精通
- 細說Python編程:從入門到科學計算
- Learning Hadoop 2
- 微信小程序開發實戰:設計·運營·變現(圖解案例版)
- Expert Cube Development with SSAS Multidimensional Models
- Learning jqPlot
- OpenStack Sahara Essentials
- Python深度學習:基于PyTorch