【原创】C运行时函数对浮点参数的处理
先来看一段简单的Win32汇编代码:
.386 .model flat, stdcall option casemap:none include windows.inc include kernel32.inc includelib kernel32.lib include msvcrt.inc includelib msvcrt.lib .data fNum1 REAL4 999.999 dwNum2 DWORD 3 fNum3 REAL4 ? .const szFormat BYTE "%.3f",0Dh, 0Ah, 0 .code start: finit fld fNum1 fild dwNum2 fdiv fstp fNum3 invoke crt_printf, addr szFormat, fNum3 invoke crt__getch invoke ExitProcess, 0 end start
代码很简单,没必要注释。简单说说,程序定义了三个变量,fNum1、fNum3为32位实型(对应C语言中的float型),dwNum2为32位双字型(对应C语言中的int型),然后用浮点指令进行 fNum3 = fNum1 / dwNum2 运算。这确实再简单不过,初看程序也没什么问题,运算结果用C运行时函数printf按照浮点格式输出,格式化字符串指定为"%.3f",也就是按照三个小数的浮点格式输出,预期的输出结果应该是:333.333。编译运行一下,输出结果如下:
38326911894457280000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000.000
诧异了,这完全是风马牛不相及的一个结果!那么此时,再看看代码,想想问题出在哪里?
在还没有找出问题之前,让我们再来看一段具有相同功能的C语言代码:
#include <stdio.h> int main( void ) { float fNum1 = 999.999; int iNum2 = 3; float fNum3; fNum3 = fNum1 / iNum2; printf( "%.3f\n", fNum3 ); return 0; }
这段C代码实现的功能同上面的汇编代码是完全一样的,编译运行,程序正确的输出了:333.333
思考之后,再来分析一下问题:
回想一下Intel处理器对浮点数据的处理过程:内存操作数被载入到FPU的寄存器栈中,然后按照后缀格式进行数据计算处理;FPU的寄存器栈由8个可独立寻址的80位寄存器构成,因此,所有的内存浮点操作数载入到浮点寄存器中都扩展到了80位;也就是说汇编中的REAL4、REAL8、REAL10,C语言中的float、[long] double这些浮点类型参与运算的时候被载入浮点寄存器中后都按IEEE浮点格式扩展到了80位。在运算完成之后,又按照浮点格式减缩到内存操作数的位长存储到内存中。这也是为什么C、C++中整形和浮点型数据混合运算的时候整形被提升到了浮点型的原因,因为浮点运算是由FPU完成的。
程序的汇编版本输出的数据是不正确的。那么,是不是浮点运算完成之后没有正确的保存到正确的内存位置呢? 我们可以通过调试器观察一下相关数据位是否正确吧!
00401011 |. DBE3 FINIT 00401013 |. D905 00404000 FLD DWORD PTR DS:[fNum1] 00401019 |. DB05 04404000 FILD DWORD PTR DS:[dwNum2] 0040101F |. DEF9 FDIVP ST(1),ST 00401021 |. D91D 08404000 FSTP DWORD PTR DS:[fNum3] 00401027 |. FF35 08404000 PUSH DWORD PTR DS:[fNum3] ; /<%.3f> = 333.3330 0040102D |. 68 54304000 PUSH OFFSET FloatTes.szFormat ; |format = "%.3f" 00401032 |. FF15 D0504000 CALL DWORD PTR DS:[<&msvcrt.printf>] ; \printf 00401038 |. 83C4 08 ADD ESP,8 0040103B |. FF15 D4504000 CALL DWORD PTR DS:[<&msvcrt._getch>] ; [_getch 00401041 |. 6A 00 PUSH 0 ; /ExitCode = 0 00401043 \. E8 0E000000 CALL FloatTes._ExitProcess@4 ; \ExitProcess
EFL 00000246 (NO,NB,E,BE,NS,PE,GE,LE) ST0 valid 333.33300781250000000 ST1 empty 0.0 ST2 empty 0.0 ST3 empty 0.0 ST4 empty 0.0 ST5 empty 0.0 ST6 empty 0.0 ST7 empty 3.0000000000000000000 3 2 1 0 E S P U O Z D I FST 3800 Cond 0 0 0 0 Err 0 0 0 0 0 0 0 0 (GT) FCW 037F Prec NEAR,64 掩码 1 1 1 1 1 1
上面是程序汇编版本在OD中的运行情况,通过观察浮点堆栈寄存器中的值可以看出,程序的运算结果没有出错,也将值正确的存储到了fNum3中,接下来就是printf函数的调用。压入的参数中fNum3的值是正确的,接下来的一切时候还是没有什么问题,可是输出结果就是不对!
再转过来看看VC中的反汇编情况:
5: float fNum1 = 999.999; 011C354E fld dword ptr [string "%f" (11C5748h)] 011C3554 fstp dword ptr [fNum1] 6: int iNum2 = 3; 011C3557 mov dword ptr [iNum2],3 7: float fNum3; 8: 9: fNum3 = fNum1 / iNum2; 011C355E fild dword ptr [iNum2] 011C3561 fdivr dword ptr [fNum1] 011C3564 fstp dword ptr [fNum3] 10: printf( "%.3f\n", fNum3 ); 011C3567 fld dword ptr [fNum3] 011C356A mov esi,esp 011C356C sub esp,8 011C356F fstp qword ptr [esp] 011C3572 push offset string "%.3f\n" (11C57B0h) 011C3577 call dword ptr [__imp__printf (11C82BCh)] 011C357D add esp,0Ch 011C3580 cmp esi,esp 011C3582 call @ILT+310(__RTC_CheckEsp) (11C113Bh) 11: return 0; 011C3587 xor eax,eax 12: }
上面是对应程序C版本在Visual Studio 2008中的反汇编情况。可以看出,赋值的指令跟我们汇编程序中的没什么区别,运算完成之后结果存储到了fNum3中,接下来是printf函数的调用。慢着!问题出现了!!这里在压入printf函数参数的时候对fNum3变量进行了一次转换!!分析一下这段代码:
00F03567 fld dword ptr [fNum3] ; 这里将fNum3载入了浮点寄存器 00F0356A mov esi,esp ; 这两句在堆栈中预留了8个字节(64)位的空间 00F0356C sub esp,8 00F0356F fstp qword ptr [esp] ; fNum3从浮点寄存器中存储到了预留的8个字节的堆栈中 00F03572 push offset string "%.3f\n" (0F057B0h) 00F03577 call dword ptr [__imp__printf (0F082BCh)]
结果似乎明了了!让我们总结一下吧!!
程序的C版本中,printf的参数在压栈的时候扩展成了double类型,这是正确的,因为printf的格式控制字符串中%f要求的类型就是double,f也并不是float的表示,不清楚可以参考相关手册(如MSDN)。从float扩展到double的这个过程,是C编译器义不容辞的工作。但在MASM中,汇编器不知道%f要求的是64位的浮点格式,你也就不能强求他帮你完成这样的转换!那么,剩下的就交给我们自己吧!
修正程序数据段的数据类型:
.data fNum1 REAL8 999.999 dwNum2 DWORD 3 fNum3 REAL8 ?
编译运行,程序正确输出!
这里提醒一下,平时编程过程中浮点数据尽量用64位类型(double、Real8),64位浮点类型比起float、Real4等32位类型带来的好处也不仅仅是精度上的增加,处理器的速度上也有相应的优势!理解到这一点儿,对代码质量也是有一定的提高的!
--克劳德曼
2012-4-18 20:16:01