先来看一段简单的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位类型带来的好处也不仅仅是精度上的增加,处理器的速度上也有相应的优势!理解到这一点儿,对代码质量也是有一定的提高的!