我在上篇文章舉了一個簡單的 C++ 程序非常簡略的解釋 C++ 代碼和匯編代碼的對應關系,在后面的文章中我將按照不同的 Topic 來仔細介紹更多相關的細節。雖然我很想一開始的時候就開始直接介紹 C++ 和匯編代碼的對應關系,不過由于 VC 編譯器會在代碼中插入各種檢查, SEH , C++ 異常等代碼,因此我覺得有必要先寫一下一些在閱讀 VC 生成的匯編代碼的時候常見的一些東西,然后再開始具體的分析 C++ 代碼的反匯編。這篇文章會首先涉及到運行時檢查( Runtime Checking )
Runtime Checking
運行時檢查是 VC 編譯器提供了運行時刻的對程序正確性 / 安全性的一種動態檢查,可以在項目的 C++ 選項中打開 Small Type Check 和 Basic Runtime Checks 來啟用 Runtime Check 。
<shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f></formulas><path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></path><lock aspectratio="t" v:ext="edit"></lock></shapetype><shape id="Picture_x0020_4" style="VISIBILITY: visible; WIDTH: 453pt; HEIGHT: 28.5pt; mso-wrap-style: square" type="#_x0000_t75" o:spid="_x0000_i1027"><imagedata o:title="" src="file:///D:%5Ctmp%5Cmsohtmlclip1%5C01%5Cclip_image001.png"></imagedata></shape>
同時,也可以使用 /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 的時候會報告如下錯誤:
<shape id="Picture_x0020_1" style="VISIBILITY: visible; WIDTH: 327pt; HEIGHT: 144.75pt; mso-wrap-style: square" type="#_x0000_t75" o:spid="_x0000_i1026"><imagedata o:title="" src="file:///D:%5Ctmp%5Cmsohtmlclip1%5C01%5Cclip_image003.png"></imagedata></shape>
原因是 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 或者 的正數,沒有超過范圍,直接跳轉到 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 ,否則出錯
發表評論
- 瀏覽: 1292026 次
-
性別:
- 來自: 杭州
-
最新評論
-
netkongjian
: 不錯的軟件知識,感謝分享!
軟件加密方式 -
norce
: 效果不錯~
JS實現圖片幻燈片效果 -
zxbear
: 鏈接已失效
《jQuery基礎教程:第2版》PDF -
架構師
: 在技術領域方面Java還是世界上最好的,而且有很多第三方控件的 ...
專訪:Ruby能否成為第二個Java -
freddie
: 如何拖動表格邊框調整行高和列寬?
可編輯的表格(JavaScript)
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

評論