昨天我在調試 板卡的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. 在調用其他外部函數的時候:
假設我們有下面的代碼:
對應的彙編代碼如下:
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
轉載請註明出處