2023拉

C运行时函数对浮点参数的处理(转)

上一篇 / 下一篇  2012-04-19 09:45:18 / 个人分类:软件开发相关

   

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


TAG:

 

评分:0

我来说两句

Open Toolbar