天天看点

visual c++6.0对浮点数处理器的初始化

     <C++反汇编与逆向分析>的作者在书中P21页列写了一段代码:

int main()
{
    int nInt0;
    scanf("%f",&nInt);
}      

并简短的提到,运行上面这段程序并输入小数,将会导致程序崩溃,这是由于在浮点寄存器没有初始化前进行浮点操作,

将无法转换小数部分。解决方案是在代码中任意位置定义一个浮点型变量(附注,并初始化),即可对浮点寄存器进行初始化。

    出于好奇,我验证了作者所说并得到了如下的错误提示:

visual c++6.0对浮点数处理器的初始化

    嗯,vs的调试功能实在太有限(也可能是我不会用),于是我换用windbg调试,并在异常处得到下列堆栈回溯:

(714.220): Break instruction exception - code 80000003 (first chance)
eax=00000001 ebx=7ffdf000 ecx=77f5168d edx=00140608 esi=02009b8c edi=0012ff80
eip=00404863 esp=0012fad8 ebp=0012fc94 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
floatex!_NMSG_WRITE+0x73:
00404863 cc              int     3
0:000> kb
ChildEBP RetAddr  Args to Child              
0012fc94 004047e4 000000ff 0012fca8 00401291 floatex!_NMSG_WRITE+0x73 [crt0msg.c @ 221]
0012fca0 00401291 0012fcb4 0040baba 00000002 floatex!_FF_MSGBANNER+0x44 [crt0msg.c @ 169]
0012fca8 0040baba 00000002 0012fef8 0040264e floatex!_amsg_exit+0x11 [crt0.c @ 255]
0012fcb4 0040264e 00000000 0012ff7c 0012fd28 floatex!_fptrap+0xa [crt0fp.c @ 47]
0012fef8 004010ca 0042da38 0042b01d 0012ff30 floatex!_input+0x113e [input.c @ 836]
0012ff20 0040103d 0042b01c 0012ff7c 00160014 floatex!scanf+0x5a [scanf.c @ 56]
0012ff80 0040122c 00000001 00440ed0 00440e20 floatex!main+0x2d [c:\studio\floatex\float.cpp @ 7]
0012ffc0 77e5eb69 00160014 02009b8c 7ffdf000 floatex!mainCRTStartup+0xfc [crt0.c @ 206]
WARNING: Stack unwind information not available. Following frames may be wrong.
0012fff0 00000000 00401130 00000000 78746341 kernel32!CreateProcessInternalW+0x1177      

注意上面每行栈回溯的最后都标有(crt库)文件及行数,这倒提醒我一件事:如果装过SDK,就能在编译器目录下找到crt库的源码,如vc++6.0对应的crt源文件可能位于:

C:\Program Files\Microsoft Visual Studio\VC98\CRT\SRC      

sdk提供这样的功能为我定位和分析这种"Runtime Errot"提供了极大的便利。接下来,重启windbg并添加源文件路径,再次重现错误:

0:000> .srcpath C:\Program Files\Microsoft Visual Studio\VC98\CRT\SRC ;设置crt库源文件搜索路径
Source search path is: C:\Program Files\Microsoft Visual Studio\VC98\CRT\SRC
0:000> g
ModLoad: 77d10000 77d9d000   C:\WINDOWS\system32\user32.dll
ModLoad: 77c40000 77c80000   C:\WINDOWS\system32\GDI32.dll
;...此处删去部分模块加载的信息
(2dc.658): Break instruction exception - code 80000003 (first chance) 
;为scanf输入值后将触发Runtime error。点击Retry后,程序会进入调试状态并触发断点
floatex!_NMSG_WRITE+0x73:
00404863 cc              int     3
0:000> kb
ChildEBP RetAddr  Args to Child              
0012fc94 004047e4 000000ff 0012fca8 00401291 floatex!_NMSG_WRITE+0x73 [crt0msg.c @ 221]
0012fca0 00401291 0012fcb4 0040baba 00000002 floatex!_FF_MSGBANNER+0x44 [crt0msg.c @ 169]
0012fca8 0040baba 00000002 0012fef8 0040264e floatex!_amsg_exit+0x11 [crt0.c @ 255] ;<--弹Runtime error对话框的代码
0012fcb4 0040264e 00000000 0012ff7c 0012fd28 floatex!_fptrap+0xa [crt0fp.c @ 47]
0012fef8 004010ca 0042da38 0042b01d 0012ff30 floatex!_input+0x113e [input.c @ 836]
0012ff20 0040103d 0042b01c 0012ff7c 00160014 floatex!scanf+0x5a [scanf.c @ 56]
0012ff80 0040122c 00000001 00440ed0 00440e20 floatex!main+0x2d [c:\studio\floatex\float.cpp @ 7]
0:000> .frame 3 ;_amsg_exit(位于frame 2)本身不值得研究,所以从frame 3着手
03 0012fcb4 0040264e floatex!_fptrap+0xa [crt0fp.c @ 47]      

切换帧栈后,定位到_fptrap函数中,很可惜,这几乎是一个空函数:

void __cdecl _fptrap(void)
{
        _amsg_exit(_RT_FLOAT);
}      

基于这样的现实,我猜测引起“Runtime Error”的成因还在上一个函数中,因此只能不厌其烦的再次切换到上一层堆栈,进入input函数。但是,我仔细核对了2次input的代码,发现input函数通过_fassign函数指针来调用_fptrap函数。在调用_fassign前仅仅检测格式化字符串的内容,并没有对浮点数输入有特殊的处理(具体代码在座的各位也可以一起分析一下,限于篇幅的原因就不再贴出来了)。更重要的一点,根据我的分析,就算换成了浮点数,依然会调用_fassign,如下图: 

int main()
{
  float val=0;
  scanf("%f",&val);
}      
visual c++6.0对浮点数处理器的初始化

不过,这次从函数指针_fassig进去后,就会发现无法显示源码了!起初,我怀疑windbg出错了,就查看反汇编代码,发现浮点数寄存器初始化与否,_fassign前后2次指向的代码不同:

1).如果未初始化,则指向_fptrap,反汇编代码行肯定寥寥无几:

0:000> uf _fptrap
floatex!_fptrap [crt0fp.c @ 46]:
   46 0040a4b0 55              push    ebp
   46 0040a4b1 8bec            mov     ebp,esp
   47 0040a4b3 6a02            push    2
   47 0040a4b5 e8766effff      call    floatex!_amsg_exit (00401330)
   47 0040a4ba 83c404          add     esp,4
   48 0040a4bd 5d              pop     ebp
   48 0040a4be c3              ret      

2).如果经过初始化,_fassign指向一段未公开的代码,但其反汇编代码也不是很复杂:

0:000> uf .
floatex!_fassign:
00403500 55              push    ebp
00403501 8bec            mov     ebp,esp
00403503 83ec0c          sub     esp,0Ch
00403506 837d0800        cmp     dword ptr [ebp+8],0
0040350a 7420            je      floatex!_fassign+0x2c (0040352c)

floatex!_fassign+0xc:
0040350c 8b4510          mov     eax,dword ptr [ebp+10h]
0040350f 50              push    eax
00403510 8d4df8          lea     ecx,[ebp-8]
00403513 51              push    ecx
00403514 e8a7680000      call    floatex!_atodbl (00409dc0)
00403519 83c408          add     esp,8
0040351c 8b550c          mov     edx,dword ptr [ebp+0Ch]
0040351f 8b45f8          mov     eax,dword ptr [ebp-8]
00403522 8902            mov     dword ptr [edx],eax
00403524 8b4dfc          mov     ecx,dword ptr [ebp-4]
00403527 894a04          mov     dword ptr [edx+4],ecx
0040352a eb18            jmp     floatex!_fassign+0x44 (00403544)

floatex!_fassign+0x2c:
0040352c 8b5510          mov     edx,dword ptr [ebp+10h]
0040352f 52              push    edx
00403530 8d45f4          lea     eax,[ebp-0Ch]
00403533 50              push    eax
00403534 e807690000      call    floatex!_atoflt (00409e40)
00403539 83c408          add     esp,8
0040353c 8b4d0c          mov     ecx,dword ptr [ebp+0Ch]
0040353f 8b55f4          mov     edx,dword ptr [ebp-0Ch]
00403542 8911            mov     dword ptr [ecx],edx

floatex!_fassign+0x44:
00403544 8be5            mov     esp,ebp
00403546 5d              pop     ebp
00403547 c3              ret      

上下两段代码在长度上有着明显的差别,我粗略的看了一下第二段代码的实现是将用户输入到屏幕上的数字字符串转换成浮点数,而不是弹出"Runtime Error"----令人不悦的对话框!那么,问题来了,是什么造成如此的差别?别急,下面慢慢分析。

    网上粗略的浏览了一下,大多数汇编代码在进行浮点运算前都会初始化浮点控制器,进而可以推测,win32程序也少不了这个步骤,而且在进入main函数之前,浮点控制器已经完成就绪。很明显,上述的初始化工作一定是在c++启动代码中完成的。让我们借助IDA移步到mainCRTStartup函数中。

    在__cinit中,我发现一处可疑的调用call __FPinit

__cinit      __cinit         proc near               ; CODE XREF: _mainCRTStartup+D2p
__cinit                      push    ebp
__cinit+1                    mov     ebp, esp
__cinit+3                    cmp     __FPinit, 0
__cinit+A                    jz      short loc_403A82
__cinit+C                    call    __FPinit
__cinit+12
__cinit+12   loc_403A82:                             ; CODE XREF: __cinit+Aj
__cinit+12                   push    offset ___xi_z
__cinit+17                   push    offset ___xi_a
__cinit+1C                   call    _initterm
__cinit+21                   add     esp, 8
__cinit+24                   push    offset ___xc_z
__cinit+29                   push    offset ___xc_a
__cinit+2E                   call    _initterm
__cinit+33                   add     esp, 8
__cinit+36                   pop     ebp
__cinit+37                   retn
__cinit+37   __cinit         endp      

本想着查看__FPinit的定义,然而,它不过是一个函数指针,指向__fpmath

.data:00431A38 __FPinit        dd offset __fpmath      ; DATA XREF: __cinit+3r      

继续跟进__fpmath,我们会发现正是它完成了初始化浮点控制器的任务。顺带说一下,在__fpmath中会调用__cfltcvt_init,下面是调用流程图:

visual c++6.0对浮点数处理器的初始化

__cfltcvt_init会使用__cfltcvt_tab函数地址数组设置诺干回调函数,包括前文提到的__fassign:

__cfltcvt_init      __cfltcvt_init  proc near               ; CODE XREF: __fpmath+6p
__cfltcvt_init                      push    ebp
__cfltcvt_init+1                    mov     ebp, esp
__cfltcvt_init+3                    mov     __cfltcvt_tab, offset __cfltcvt
__cfltcvt_init+D                    mov     off_431D08, offset __cropzeros
__cfltcvt_init+17                   mov     off_431D0C, offset __fassign  <---设置__fassign函数指针的值
__cfltcvt_init+21                   mov     off_431D10, offset __forcdecpt
__cfltcvt_init+2B                   mov     off_431D14, offset __positive
__cfltcvt_init+35                   mov     off_431D18, offset __cfltcvt
__cfltcvt_init+3F                   pop     ebp
__cfltcvt_init+40                   retn
__cfltcvt_init+40   __cfltcvt_init  endp      

这是__cfltcvt_tab数组的定义,共定义了6个函数地址,供__cfltcvt_init设置。通过__cfltcvt_tab数组来设置浮点处理函数的目的可能是为了兼容其他编译选项,如:MultiThread Debug\MultiThread等选项,这些不同的选项使用的浮点数处理函数略有差别

.data:00431D04 __cfltcvt_tab   dd offset __fptrap      ; DATA XREF: __cfltcvt_init+3w
.data:00431D04                                         ; __output+6ADr
.data:00431D08 off_431D08      dd offset __fptrap      ; DATA XREF: __cfltcvt_init+Dw
.data:00431D08                                         ; __output+6F1r
.data:00431D0C off_431D0C      dd offset __fptrap      ; DATA XREF: __cfltcvt_init+17w
.data:00431D0C                                         ; __input+1138r
.data:00431D10 off_431D10      dd offset __fptrap      ; DATA XREF: __cfltcvt_init+21w
.data:00431D10                                         ; __output+6CFr
.data:00431D14 off_431D14      dd offset __fptrap      ; DATA XREF: __cfltcvt_init+2Bw
.data:00431D18 off_431D18      dd offset __fptrap      ; DATA XREF: __cfltcvt_init+35w      

    最后,我猜测,在编译阶段,如果,IDE发现程序将进行浮点运算,则使__Fpinit指向__fpmath,初始化浮点数控制器;否则保持__Fpinit为空。同时,按编译选项填写__cfltcvt_tab数组中的各个函数指针,为程序提供不同浮点数处理功能。