- 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()函数返回