__RTC_CheckEsp 等 C Rumtime問題

來源:互聯網
上載者:User

昨天我在調試 板卡的DEMO的時候發現編譯不成功.出現了__RTC等的運行庫問題。

原來是他們給的DEMO是用VS2008寫的,然後又只給了VC6的工程,並且未給VC6下的庫,只給了VC9下的庫!!!

不過通過這次問題,以後出現C Rumtime問題我就有了一定的瞭解。現轉載兩篇文章如下!在VC6中沒有找到C Rumtime的設定選項,只好在VS2005上調試,發現灰常之不習慣!!!!!!!

 

 

VC7/VC8開發的庫在VC6中的使用問題--轉載 

        現 在,微軟一些新的SDK基本上都是用VC7/VC8(即VS .NET 2003/VS 2005)來開發的,當我們用VC6使用這些庫的Debug版本時就會發生連結錯誤,對於我們自己用VC7/VC8開發靜態庫或動態庫也存在同樣的問題, 這主要是由於VC7/VC8使用了不同的調試資訊格式以及增加了一些安全檢測機製造成的。

我們可以在VC7/VC8中修改一下工程的配置資訊使其能夠被VC6使用,具體操作如下:

 

        1. 開啟工程設定介面,選擇C/C++屬性頁面,將“常規(General) -> 調試資訊格式(Debug Information Format)” 改為“禁用(Disabled)”。

如果不進行此處修改,VC6在連結時將出現如下錯誤:

fatal error LNK1103: debugging information corrupt; recompile module 

        2. 將“代碼產生(Code Generation) -> 基本運行時檢查(Basic Runtime Checks)”改為“預設(Default)”。

如果不進行此處修改,VC6在連結時將出現如下錯誤:

error LNK2001: unresolved external symbol __RTC_Shutdown
error LNK2001: unresolved external symbol __RTC_InitBase
error LNK2001: unresolved external symbol __RTC_CheckEsp
error LNK2001: unresolved external symbol @_RTC_CheckStackVars@8
error LNK2001: unresolved external symbol __RTC_UninitUse 

        3. 將“代碼產生(Code Generation) -> 緩衝區安全檢查(Buffer Security Check)”改為“否(No)”。

如果不進行此處修改,VC6在連結時將出現如下錯誤:

error LNK2001: unresolved external symbol ___security_cookie
error LNK2001: unresolved external symbol @__security_check_cookie@4

經過上述修改後,實際上產生的Debug版本已經不含調試資訊了,因此我們也可以讓VC6下的Debug版直接使用VC7/VC8編譯的Release版,不過要注意修改Release版的運行期庫類型,使其與VC6一致。

 

我在上篇文章舉了一個簡單的C++程式非常簡略的解釋C++代碼和彙編代碼的對應關係,在後面的文章中我將按照不同的Topic來仔細介紹更多相關的細節。雖然我很想一開始的時候就開始直接介紹C++和彙編代碼的對應關係,不過由於VC編譯器會在代碼中插入各種檢查,SEH,C++異常等代碼,因此我覺得有必要先寫一下一些在閱讀VC產生的彙編代碼的時候常見的一些東西,然後再開始具體的分析C++代碼的反組譯碼。這篇文章會首先涉及到運行時檢查(Runtime Checking)

Runtime Checking

運行時檢查是VC編譯器提供了運行時刻的對程式正確性/安全性的一種動態檢查,可以在項目的C++選項中開啟Small Type Check和Basic Runtime Checks來啟用Runtime Check。

同時,也可以使用/RTC開關來開啟檢查,/RTC後面跟c, u, s代表啟用不同類型的檢查。Smaller Type Check對應/RTCc, Basic Runtime Checks對應/RTCs和/RTCu。

/RTCc開關

RTCc開關可以用來檢查在進行類型轉換的保證沒有不希望的截斷(Truncation)發生。以下面的代碼為例:

    char ch = 0;

    short s = 0x101;

    ch = s;

當VC執行到ch = s的時候會報告如下錯誤:

原因是0x101已經超過了char的表示範圍。

之前會導致錯誤地的代碼對應的彙編代碼如下所示:

; 42   :     char ch = 0;

 

      mov   BYTE PTR _ch$[ebp], 0

 

; 43   :     short s = 0x101;

 

      mov   WORD PTR _s$[ebp], 257              ; 00000101H

 

; 44   :     ch = s;

 

      mov   cx, WORD PTR _s$[ebp]

      call  @_RTC_Check_2_to_1@4

      mov   BYTE PTR _ch$[ebp], al

可以看到,賦值的時候,VC編譯器先將s的值放到cx寄存器中,然後調用_RTC_Check_2_to_1@4函數來檢查是否有資料截斷的問題,結果放在al中,最後將al放到ch之中。_RTC_Check_2_to_1@4顧名思義是檢查2個byte的資料被轉換成1個byte的資料(short是2個byte,char是一個byte),代碼如下:

_RTC_Check_2_to_1:

00411900  push        ebp 

00411901  mov         ebp,esp

00411903  push        ebx 

00411904  mov         ebx,ecx

00411906  mov         eax,ebx

00411908  and         eax,0FF00h

0041190D  je          _RTC_Check_2_to_1+24h (411924h)

0041190F  cmp         eax,0FF00h

00411914  je          _RTC_Check_2_to_1+24h (411924h)

00411916  mov         eax,dword ptr [ebp+4]

00411919  push        1   

0041191B  push        eax 

0041191C  call        _RTC_Failure (411195h)

00411921  add         esp,8

00411924  mov         al,bl

00411926  pop         ebx 

00411927  pop         ebp 

00411928  ret             

1.     00411904~00411906:ecx儲存著s的值,然後又被轉移到eax中。

2.     00411908~0041190D:檢查eax和0xff00相與,並檢查是否結果為0,如果結果為0,說明這個short值是0或者<128的正數,沒有超過範圍,直接跳轉到00411924獲得結果並返回

3.     0041190F~00411914:檢查eax是否等於0xff00,如果相等,說明這個short值是負數,並且>=-128,在char的表示範圍之內,可以接受,跳轉到00411924

4.     如果上面檢查都沒有通過,說明這個值已經超過了範圍,調用_RTC_Failure函數報錯

要解決這個問題,很簡單,把代碼改為下面這樣就可以了:

    char ch = 0;

    short s = 0x101;

    ch = s & 0xff;

 

/RTCu開關

這個開關的作用是開啟對未初始設定變數的檢查,比靜態警告要有用一些。考慮下面的代碼:

    int a;

    char ch;

    scanf("%c", &ch);

 

    if( ch = 'y' ) a = 10;

 

    printf("%d", a);

編譯器無從通過Flow Analysis知道a在printf之前是否被正確初始化,因為a = 10這個分支是由外部條件決定的,所以只有動態監測方法才可以知道到底程式有沒有Bug(當然從這裡我們可以很明顯的看出這個程式必然是有Bug的)。顯然把變數的值和一個具體值來比較是無法知道變數是否被初始化的,所以編譯器需要通過一個額外的BYTE來跟蹤此變數是否被初始化:

函數的開始代碼如下:

      push  ebp

      mov   ebp, esp

      sub   esp, 228                      ; 000000e4H

      push  ebx

      push  esi

      push  edi

      lea   edi, DWORD PTR [ebp-228]

      mov   ecx, 57                             ; 00000039H

      mov   eax, -858993460                     ; ccccccccH

      rep stosd

      mov   BYTE PTR $T5147[ebp], 0

最後一句很關鍵,把$T5147變數的值設定為0,表示並沒有初始化a這個變數。

當ch = ‘y’的時候,編譯器除了執行a=10之外還會將$T5147設定為1

      mov   BYTE PTR $T5147[ebp], 1

      mov   DWORD PTR _a$[ebp], 10              ; 0000000aH

之後,在printf之前,編譯器會檢查$T5147這個變數的值,如果為0,說明沒有初始化,執行__RTC_UninitUse報告錯誤,否則跳轉到相應代碼執行printf語句:

      cmp   BYTE PTR $T5147[ebp], 0

      jne   SHORT $LN4@wmain

      push  OFFSET $LN5@wmain

      call  __RTC_UninitUse

      add   esp, 4

$LN4@wmain:

      mov   esi, esp

      mov   eax, DWORD PTR _a$[ebp]

      push  eax

      push  OFFSET ??_C@_02DPKJAMEF@?$CFd?$AA@

      call  DWORD PTR __imp__printf

      add   esp, 8

      cmp   esi, esp

      call  __RTC_CheckEsp

 

/RTCs開關

這個開關是用來檢查和Stack相關的問題:

1.     Debug模式下把Stack上的變數初始化為0xcc,檢查未初始化的問題

2.     檢查陣列變數的Overrun

3.     檢查ESP是否被毀壞

Debug模式下初始設定變數為0xcc

假設我們有下面的代碼:

void func()

{

    int a;

    int b;

    int c;

}

對應的彙編代碼如下:

?func@@YAXXZ PROC                         ; func, COMDAT

 

; 38   : {

 

      push  ebp

      mov   ebp, esp

      sub   esp, 228                      ; 000000e4H

      push  ebx

      push  esi

      push  edi

      lea   edi, DWORD PTR [ebp-228]

      mov   ecx, 57                             ; 00000039H

      mov   eax, -858993460                     ; ccccccccH

      rep stosd

 

; 39   :     int a;

; 40   :     int b;

; 41   :     int c;

; 42   :

; 43   : }

 

      pop   edi

      pop   esi

      pop   ebx

      mov   esp, ebp

      pop   ebp

      ret   0

?func@@YAXXZ ENDP

1.     sub esp, 228:s編譯器為棧分配了228個byte

2.     接著3個push指令儲存寄存器

3.     Lea edi, DWORD PTR [ebp-228]一直到repstosd指令是初始化從ebp-228開始寫57個0xcccccccc,也就是57*4=228個0xcc,正好填滿之前sub esp, 228所分配的空間。這段代碼會把所有的變數初始化為0xcc。

選擇0xcc是有一定理由的:

1.     0xcc不同於一般的初始化值,人們一般傾向於把變數初始化為0, 1, -1等比較簡單的值,而0xcc一般情況下足夠大,而且是負數,容易引起注意,而且一般變數的值很有可能不允許是0xcc,比較容易造成錯誤

2.     0xcc = int 3,如果作為代碼執行,則會引發斷點異常,比較容易引起注意

 

檢查陣列變數的Overrun

假設我們有下面的代碼:

void func

{

    char buf[104];

    scanf("%s", buf);

 

    return 0;

}

在scanf調用之後,會執行下面的代碼:

      mov   ecx, ebp

      push  eax

      lea   edx, DWORD PTR $LN5@wmain

      call  @_RTC_CheckStackVars@8

這段代碼會調用_RTC_CheckStackVars@8函數會在數組的開始和結束的地方檢查0xcccccccc有否被破壞,如果是,則報告錯誤。_RTC_CheckStackVars由於代碼過長這裡就不給出了,這個函數主要是利用編譯器儲存的數組位置和長度資訊,檢查數組的開頭和結尾:

$LN5@func:

      DD    1

      DD    $LN4@func

$LN4@func:

      DD    -112                          ; ffffff90H

      DD    104                           ; 00000068H

      DD    $LN3@func

$LN3@func:

      DB    98                            ; 00000062H

      DB    117                           ; 00000075H

      DB    102                           ; 00000066H

      DB    0

$LN5@func紀錄了數組的個數,而$LN4@func儲存了數組的位移量ebp - 112和數組的長度104,而$LN3@func則儲存了變數的名稱(0x62, 0x75, 0x66, 0 = “buf”)。

檢查ESP

ESP的錯誤很有可能是由調用協定的mistach造成,或者Stack本身沒有平衡。編譯器會在調用其他函數和在函數Prolog和Epilog(開始和結束代碼)的時候插入對ESP的檢查:

1.     在調用其他外部函數的時候:

假設我們有下面的代碼:

 

printf( "%d", 1 );

對應的彙編代碼如下:

      mov   esi, esp

      push  1

      push  OFFSET ??_C@_02DPKJAMEF@?$CFd?$AA@

      call  DWORD PTR __imp__printf

      add   esp, 8

      cmp   esi, esp

      call  __RTC_CheckEsp

可以看到檢查的代碼非常簡單直接,把ESP儲存在ESI之中,當調用printf,平衡堆棧之後,檢查esp和esi的是否一致,然後調用__RTC_CheckESP,__RTC_CheckESP代碼也很簡單:

_RTC_CheckEsp:

00412730  jne         esperror (412733h)

00412732  ret             

esperror:

……

00412744  call        _RTC_Failure (411195h)

……

00412754  ret 

 

如果不一致,跳轉到esperror標號報告錯誤。

 

2.     函數返回的時候:

以下面的代碼為例:

void func()

{

    __asm

    {

        push eax

    }

}

Func函數故意push eax來破壞堆棧的平衡性,對應的彙編代碼如下:

?func@@YAXXZ PROC                         ; func, COMDAT

 

; 38   : {

 

      push  ebp

      mov   ebp, esp

      sub   esp, 192                      ; 000000c0H

      push  ebx

      push  esi

      push  edi

      lea   edi, DWORD PTR [ebp-192]

      mov   ecx, 48                             ; 00000030H

      mov   eax, -858993460                     ; ccccccccH

      rep stosd

 

; 39   :     __asm

; 40   :     {

; 41   :         push eax

 

      push  eax

 

; 42   :     }

; 43   : }

 

      pop   edi

      pop   esi

      pop   ebx

      add   esp, 192                      ; 000000c0H

      cmp   ebp, esp

      call  __RTC_CheckEsp

      mov   esp, ebp

      pop   ebp

      ret   0

?func@@YAXXZ ENDP

 

在函數的初始化代碼中,func會將ebp儲存在Stack中,並且把當前esp儲存在ebp中。

?func@@YAXXZ PROC                         ; func, COMDAT

      push  ebp

      mov   ebp, esp

 

關鍵的檢查代碼在後面,當func函數恢複了堆棧之後,堆棧會恢複到之前剛儲存esp到ebp的那個狀態,這個時候ebp必然等於esp,否則出錯

      cmp   ebp, esp

      call  __RTC_CheckEsp

      mov   esp, ebp

      pop   ebp

      ret   0

?func@@YAXXZ ENDP

出錯的時候顯示的對話方塊如下:

OK,這次就寫到這裡。下面幾篇文章預定會寫到下面這些內容:

1.     /GS & Security Cookie

2.     Calling Conventions

3.     Name Mangling

4.     Structured Exception Handling

5.     Passing by Reference

6.     Member functions

7.     Object layout

8.     Virtual functions

9.     Virtual Inheritance

10.   C++ Exceptions

11.   Templates

敬請關注。

 

作者:      ATField
Blog:      http://blog.csdn.net/atfield
轉載請註明出處

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.