天天看點

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數組中的各個函數指針,為程式提供不同浮點數處理功能。