我在上篇文章舉了一個(gè)簡(jiǎn)單的 C++ 程序非常簡(jiǎn)略的解釋 C++ 代碼和匯編代碼的對(duì)應(yīng)關(guān)系,在后面的文章中我將按照不同的 Topic 來(lái)仔細(xì)介紹更多相關(guān)的細(xì)節(jié)。雖然我很想一開(kāi)始的時(shí)候就開(kāi)始直接介紹 C++ 和匯編代碼的對(duì)應(yīng)關(guān)系,不過(guò)由于 VC 編譯器會(huì)在代碼中插入各種檢查, SEH , C++ 異常等代碼,因此我覺(jué)得有必要先寫(xiě)一下一些在閱讀 VC 生成的匯編代碼的時(shí)候常見(jiàn)的一些東西,然后再開(kāi)始具體的分析 C++ 代碼的反匯編。這篇文章會(huì)首先涉及到運(yùn)行時(shí)檢查( Runtime Checking )
Runtime Checking
運(yùn)行時(shí)檢查是 VC 編譯器提供了運(yùn)行時(shí)刻的對(duì)程序正確性 / 安全性的一種動(dòng)態(tài)檢查,可以在項(xiàng)目的 C++ 選項(xiàng)中打開(kāi) Small Type Check 和 Basic Runtime Checks 來(lái)啟用 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>
同時(shí),也可以使用 /RTC 開(kāi)關(guān)來(lái)打開(kāi)檢查, /RTC 后面跟 c, u, s 代表啟用不同類(lèi)型的檢查。 Smaller Type Check 對(duì)應(yīng) /RTCc, Basic Runtime Checks 對(duì)應(yīng) /RTCs 和 /RTCu 。
/RTCc 開(kāi)關(guān)
RTCc 開(kāi)關(guān)可以用來(lái)檢查在進(jìn)行類(lèi)型轉(zhuǎn)換的保證沒(méi)有不希望的截?cái)啵? Truncation )發(fā)生。以下面的代碼為例:
char ch = 0;
short s = 0x101;
ch = s;
|
當(dāng) VC 執(zhí)行到 ch = s 的時(shí)候會(huì)報(bào)告如下錯(cuò)誤:
<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 已經(jīng)超過(guò)了 char 的表示范圍。
之前會(huì)導(dǎo)致錯(cuò)誤地的代碼對(duì)應(yīng)的匯編代碼如下所示:
; 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
|
可以看到,賦值的時(shí)候, VC 編譯器先將 s 的值放到 cx 寄存器中,然后調(diào)用 _RTC_Check_2_to_1@4 函數(shù)來(lái)檢查是否有數(shù)據(jù)截?cái)嗟膯?wèn)題,結(jié)果放在 al 中,最后將 al 放到 ch 之中。 _RTC_Check_2_to_1@4 顧名思義是檢查 2 個(gè) byte 的數(shù)據(jù)被轉(zhuǎn)換成 1 個(gè) byte 的數(shù)據(jù)( short 是 2 個(gè) byte , char 是一個(gè) 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 的值,然后又被轉(zhuǎn)移到 eax 中。
2. 00411908~0041190D :檢查 eax 和 0xff00 相與,并檢查是否結(jié)果為 0 ,如果結(jié)果為 0 ,說(shuō)明這個(gè) short 值是 0 或者 的正數(shù),沒(méi)有超過(guò)范圍,直接跳轉(zhuǎn)到 00411924 獲得結(jié)果并返回
3. 0041190F~00411914 :檢查 eax 是否等于 0xff00 ,如果相等,說(shuō)明這個(gè) short 值是負(fù)數(shù),并且 >=-128 ,在 char 的表示范圍之內(nèi),可以接受,跳轉(zhuǎn)到 00411924
4. 如果上面檢查都沒(méi)有通過(guò),說(shuō)明這個(gè)值已經(jīng)超過(guò)了范圍,調(diào)用 _RTC_Failure 函數(shù)報(bào)錯(cuò)
要解決這個(gè)問(wèn)題,很簡(jiǎn)單,把代碼改為下面這樣就可以了:
char ch = 0;
short s = 0x101;
ch = s & 0xff;
|
/RTCu 開(kāi)關(guān)
這個(gè)開(kāi)關(guān)的作用是打開(kāi)對(duì)未初始化變量的檢查,比靜態(tài)的警告要有用一些。考慮下面的代碼:
int a;
char ch;
scanf("%c", &ch);
if( ch = 'y' ) a = 10;
printf("%d", a);
|
編譯器無(wú)從通過(guò) Flow Analysis 知道 a 在 printf 之前是否被正確初始化,因?yàn)? a = 10 這個(gè)分支是由外部條件決定的,所以只有動(dòng)態(tài)的監(jiān)測(cè)方法才可以知道到底程序有沒(méi)有 Bug (當(dāng)然從這里我們可以很明顯的看出這個(gè)程序必然是有 Bug 的)。顯然把變量的值和一個(gè)具體值來(lái)比較是無(wú)法知道變量是否被初始化的,所以編譯器需要通過(guò)一個(gè)額外的 BYTE 來(lái)跟蹤此變量是否被初始化:
函數(shù)的開(kāi)始代碼如下:
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
|
最后一句很關(guān)鍵,把 $T5147 變量的值設(shè)置為 0 ,表示并沒(méi)有初始化 a 這個(gè)變量。
當(dāng) ch = ‘y’ 的時(shí)候,編譯器除了執(zhí)行 a=10 之外還會(huì)將 $T5147 設(shè)置為 1
mov BYTE PTR $T5147[ebp], 1
mov DWORD PTR _a$[ebp], 10 ; 0000000aH
|
之后,在 printf 之前,編譯器會(huì)檢查 $T5147 這個(gè)變量的值,如果為 0 ,說(shuō)明沒(méi)有初始化,執(zhí)行 __RTC_UninitUse 報(bào)告錯(cuò)誤,否則跳轉(zhuǎn)到相應(yīng)代碼執(zhí)行 printf 語(yǔ)句:
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 開(kāi)關(guān)
這個(gè)開(kāi)關(guān)是用來(lái)檢查和 Stack 相關(guān)的問(wèn)題:
1. Debug 模式下把 Stack 上的變量初始化為 0xcc ,檢查未初始化的問(wèn)題
2. 檢查數(shù)組變量的 Overrun
3. 檢查 ESP 是否被毀壞
Debug
模式下初始化變量為
0xcc
假設(shè)我們有下面的代碼:
void func()
{
int a;
int b;
int c;
}
|
對(duì)應(yīng)的匯編代碼如下:
?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 個(gè) byte
2. 接著 3 個(gè) push 指令保存寄存器
3. Lea edi, DWORD PTR [ebp-228] 一直到 repstosd 指令是初始化從 ebp-228 開(kāi)始寫(xiě) 57 個(gè) 0xcccccccc ,也就是 57*4=228 個(gè) 0xcc ,正好填滿(mǎn)之前 sub esp, 228 所分配的空間。這段代碼會(huì)把所有的變量初始化為 0xcc 。
選擇 0xcc 是有一定理由的 :
1. 0xcc 不同于一般的初始化值,人們一般傾向于把變量初始化為 0, 1, -1 等比較簡(jiǎn)單的值,而 0xcc 一般情況下足夠大,而且是負(fù)數(shù),容易引起注意,而且一般變量的值很有可能不允許是 0xcc ,比較容易造成錯(cuò)誤
2. 0xcc = int 3 ,如果作為代碼執(zhí)行,則會(huì)引發(fā)斷點(diǎn)異常,比較容易引起注意
檢查數(shù)組變量的
Overrun
假設(shè)我們有下面的代碼:
void func
{
char buf[104];
scanf("%s", buf);
return 0;
}
|
在 scanf 調(diào)用之后,會(huì)執(zhí)行下面的代碼:
mov ecx, ebp
push eax
lea edx, DWORD PTR $LN5@wmain
call @_RTC_CheckStackVars@8
|
這段代碼會(huì)調(diào)用 _RTC_CheckStackVars@8 函數(shù)會(huì)在數(shù)組的開(kāi)始和結(jié)束的地方檢查 0xcccccccc 有否被破壞,如果是,則報(bào)告錯(cuò)誤。 _RTC_CheckStackVars 由于代碼過(guò)長(zhǎng)這里就不給出了,這個(gè)函數(shù)主要是利用編譯器保存的數(shù)組位置和長(zhǎng)度信息,檢查數(shù)組的開(kāi)頭和結(jié)尾:
$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 紀(jì)錄了數(shù)組的個(gè)數(shù),而 $LN4@func 保存了數(shù)組的偏移量 ebp - 112 和數(shù)組的長(zhǎng)度 104 ,而 $LN3@func 則保存了變量的名稱(chēng)( 0x62, 0x75, 0x66, 0 = “buf” )。
檢查
ESP
ESP 的錯(cuò)誤很有可能是由調(diào)用協(xié)定的 mistach 造成,或者 Stack 本身沒(méi)有平衡。編譯器會(huì)在調(diào)用其他函數(shù)和在函數(shù) Prolog 和 Epilog (開(kāi)始和結(jié)束代碼)的時(shí)候插入對(duì) ESP 的檢查:
1. 在調(diào)用其他外部函數(shù)的時(shí)候:
假設(shè)我們有下面的代碼:
printf( "%d", 1 );
|
對(duì)應(yīng)的匯編代碼如下:
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
|
可以看到檢查的代碼非常簡(jiǎn)單直接,把 ESP 保存在 ESI 之中,當(dāng)調(diào)用 printf ,平衡堆棧之后,檢查 esp 和 esi 的是否一致,然后調(diào)用 __RTC_CheckESP , __RTC_CheckESP 代碼也很簡(jiǎn)單:
_RTC_CheckEsp:
00412730 jne esperror (412733h)
00412732 ret
esperror:
……
00412744 call _RTC_Failure (411195h)
……
00412754 ret
|
如果不一致,跳轉(zhuǎn)到 esperror 標(biāo)號(hào)報(bào)告錯(cuò)誤。
2. 函數(shù)返回的時(shí)候:
以下面的代碼為例:
void func()
{
__asm
{
push eax
}
}
|
Func 函數(shù)故意 push eax 來(lái)破壞堆棧的平衡性,對(duì)應(yīng)的匯編代碼如下:
?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
|
在函數(shù)的初始化代碼中, func 會(huì)將 ebp 保存在 Stack 中,并且把當(dāng)前 esp 保存在 ebp 中。
?func@@YAXXZ PROC ; func, COMDAT
push ebp
mov ebp, esp
|
關(guān)鍵的檢查代碼在后面,當(dāng) func 函數(shù)恢復(fù)了堆棧之后,堆棧會(huì)恢復(fù)到之前剛保存 esp 到 ebp 的那個(gè)狀態(tài),這個(gè)時(shí)候 ebp 必然等于 esp ,否則出錯(cuò)
- 2008-06-11 13:25
- 瀏覽 307
- 評(píng)論(0)
- 相關(guān)推薦
發(fā)表評(píng)論
- 瀏覽: 1292026 次
-
性別:
- 來(lái)自: 杭州
-
最新評(píng)論
-
netkongjian
: 不錯(cuò)的軟件知識(shí),感謝分享!
軟件加密方式 -
norce
: 效果不錯(cuò)~
JS實(shí)現(xiàn)圖片幻燈片效果 -
zxbear
: 鏈接已失效
《jQuery基礎(chǔ)教程:第2版》PDF -
架構(gòu)師
: 在技術(shù)領(lǐng)域方面Java還是世界上最好的,而且有很多第三方控件的 ...
專(zhuān)訪:Ruby能否成為第二個(gè)Java -
freddie
: 如何拖動(dòng)表格邊框調(diào)整行高和列寬?
可編輯的表格(JavaScript)
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

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

評(píng)論