了解反匯編的一些小知識對于我們在開發(fā)軟件時進行編程與調(diào)試大有好處,下面以VS2008環(huán)境下的VC++簡單介紹一下反匯編的一些小東西!如果有些解釋有問題的地方,希望大家能夠指出。 1、新建簡單的VC控制臺應(yīng)用程序(對此熟悉的同學(xué)可以略過) A、打開Microsoft Visual Studio 2008,選擇主菜單“File” B、選擇子菜單“New”下面的“Project”,打開“New Project”對話框。 C、左邊選擇Visual C++下的win32,右邊選擇Win32 Console Application,然后輸入一個工程名,點擊“OK”即可,在出現(xiàn)的向?qū)е?,一切默認,點擊Finish即可。 D、在出現(xiàn)的編輯區(qū)域內(nèi)會出現(xiàn)以你設(shè)定的工程名命名的CPP文件。內(nèi)容如下: #include "stdafx.h" int _tmain(int argc, _TCHAR* argv) { return 0; } 2、VS查看匯編代碼 A、VC處于調(diào)試狀態(tài)才能看到匯編指令窗口。因此,可以在 return 0 上設(shè)置一個斷點:把光標(biāo)移到 return 0 那一行上,然后按下F9鍵設(shè)置一個斷點。 B、按下F5鍵進入調(diào)試狀態(tài),當(dāng)程序停在 return 0 這一行上時,打開菜單“Debug”下的“Windows”子菜單,選擇“Disassembly”。這樣,出現(xiàn)一個反匯編的窗口,顯示下面的信息: --- d:/my documents/visual studio 2008/projects/casmtest/casmtest/casmtest_main.cpp // CAsmTest.cpp : 定義控制臺應(yīng)用程序的入口點。 // #include "stdafx.h" int _tmain(int argc, _TCHAR* argv) { 00411370 push ebp 00411371 mov ebp,esp 00411373 sub esp,0C0h 00411379 push ebx 0041137A push esi 0041137B push edi 0041137C lea edi, 00411382 mov ecx,30h 00411387 mov eax,0CCCCCCCCh 0041138C rep stos dword ptr es: return 0; 0041138E xor eax,eax } 00411390 pop edi 00411391 pop esi 00411392 pop ebx 00411393 mov esp,ebp 00411395 pop ebp 00411396 ret 上面就是系統(tǒng)生成的main函數(shù)原型,確切的說是_tmain()的反匯編的相關(guān)信息,相信學(xué)過匯編語言的肯定就能夠了解它所做的操作了。 3、簡單了解一下常見的匯編指令 為了照顧到?jīng)]學(xué)過匯編程序的同志們,這里簡單介紹一下常見的幾種匯編指令。 A、add:加法指令,第一個是目標(biāo)操作數(shù),第二個是源操作數(shù),格式為:目標(biāo)操作數(shù) = 目標(biāo)操作數(shù) + 源操作數(shù)。 B、sub:減法指令,格式同 add。 C、call:調(diào)用函數(shù),一般函數(shù)的參數(shù)放在寄存器中。 D、ret:跳轉(zhuǎn)會調(diào)用函數(shù)的地方。對應(yīng)于call,返回到對應(yīng)的call調(diào)用的下一條指令,若有返回值,則放入eax中。 E、push:把一個32位的操作數(shù)壓入堆棧中,這個操作在32位機中會使得esp被減4(字節(jié)),esp通常是指向棧頂?shù)模ㄟ@里要指出的是:學(xué)過單片機的同學(xué)請注意單片機種的堆棧與Windows下的堆棧是不同的,請參考相應(yīng)資料),這里頂部是地址小的區(qū)域,那么,壓入堆棧的數(shù)據(jù)越多,esp也就越來越小。 F、pop:與push相反,esp每次加4(字節(jié)),一個數(shù)據(jù)出棧。pop的參數(shù)一般是一個寄存器,棧頂?shù)臄?shù)據(jù)被彈出到這個寄存器中。 一般不會把sub、add這樣的算術(shù)指令,以及call、ret這樣的跳轉(zhuǎn)指令歸入堆棧相關(guān)指令中。但是實際上在函數(shù)參數(shù)傳遞過程中,sub和add最常用來操作堆棧;call和ret對堆棧也有影響。 G、mov:數(shù)據(jù)傳送。第一個參數(shù)是目的操作數(shù),第二個參數(shù)是源操作數(shù),就是把源操作數(shù)拷貝到目的一份。 H、xor:異或指令,這本身是一個邏輯運算指令,但在匯編指令中通常會見到它被用來實現(xiàn)清零功能。用 xor eax,eax這種操作來實現(xiàn) mov eax,0,可以使速度更快,占用字節(jié)數(shù)更少。 I、lea:取得第二個參數(shù)地址后放入到前面的寄存器(第一個參數(shù))中。 然而lea也同樣可以實現(xiàn)mov的操作,例如: lea edi, 方括號表示存儲單元,也就是提取方括號中的數(shù)據(jù)所指向的內(nèi)容,然而lea提取內(nèi)容的地址,這樣就實現(xiàn)了把(ebx-0ch)放入到了edi中,但是mov指令是不支持第二個操作數(shù)是一個寄存器減去一個數(shù)值的。 J、stos:串行存儲指令,它實現(xiàn)把eax中的數(shù)據(jù)放入到edi所指的地址中,同時edi后移4個字節(jié),這里的stos實際上對應(yīng)的是stosd,其他的還有stosb,stosw分別對應(yīng)1,2個字節(jié)。 K、jmp:無條件跳轉(zhuǎn)指令,對應(yīng)于大量的條件跳轉(zhuǎn)指令。 L、jg:條件跳轉(zhuǎn),大于時成立,進行跳轉(zhuǎn),通常條件跳轉(zhuǎn)之前會有一條比較指令(用于設(shè)置標(biāo)志位)。 M、jl:小于時跳轉(zhuǎn)。 N、jge:大于等于時跳轉(zhuǎn)。 O、cmp:比較大小指令,結(jié)果用來設(shè)置標(biāo)志位。 4、函數(shù)參數(shù)傳遞方式 函數(shù)調(diào)用規(guī)則指的是調(diào)用者和被調(diào)用函數(shù)間傳遞參數(shù)及返回參數(shù)的方法,在Windows上,常用的有Pascal方式、WINAPI方式(_stdcall)、C方式(_cdecl)。 A、_cdecl C調(diào)用規(guī)則: (a)參數(shù)從右到左進入堆棧; (b)在函數(shù)返回后,調(diào)用者要負責(zé)清除堆棧,這種調(diào)用方式通常會生成較大的可執(zhí)行程序。 B、_stdcall又稱為WINAPI,調(diào)用規(guī)則如下: (a)參數(shù)從右到左進入堆棧; (b)被調(diào)用的函數(shù)在返回前自行清理堆棧,這種方式生成的代碼比cdecl小。 C、Pascal調(diào)用規(guī)則(主要用于Win16函數(shù)庫中,現(xiàn)在基本不用): (a)參數(shù)從左到右進入堆棧; (b)被調(diào)用的函數(shù)在返回前自行清理堆棧。 (c)不支持可變參數(shù)的函數(shù)調(diào)用。 5、VC中訪問無效變量出錯原因 我們看上面主函數(shù)反匯編后的其中一段代碼如下: 0041137C lea edi, 00411382 mov ecx,30h 00411387 mov eax,0CCCCCCCCh 0041138C rep stos dword ptr es: 從代碼的表面上看,它是實現(xiàn)把從ebp-0C0h開始的30h個字的空間寫入0CCCCCCCCh。其中eax為四位的數(shù)據(jù),這樣可以計算: 0C0h = 30h * 4 也就是把從ebp-0C0h 到ebp之間的空間初始化為0CCCCCCCCh。大家在學(xué)習(xí)反匯編的過程中會發(fā)現(xiàn),其實編譯器會根據(jù)情況把相應(yīng)長度的這樣一段作為局部變量的空間,而這里把局部變量區(qū)域全都初始化成0CCCCCCCCh也是有其用意的,做VC編程的工作者,特別是初學(xué)者可能不會對0CCCCCCCCh這個常量陌生。0cch實際上是int 3指令的機器碼,這是一個斷點中斷指令(在反編譯出的信息中大家會看到int 3),因為局部變量不可被執(zhí)行,或者如果在沒有初始化的時候進行了訪問,則就會出現(xiàn)訪問失敗錯誤。這個在VC編譯Debug版本中才能看到提示這個錯誤,在Release版本中,會以另外一種錯誤形式體現(xiàn)。下面,我們修改主程序看下new與delete的反匯編的效果(注釋直接加到反匯編的代碼中了)。 VC生成工程,寫入源代碼如下: (1)情況1 // ASM_Test.cpp : Defines the entry point for the console application. ( 源代碼1 ) // #include "stdafx.h" #include "stdlib.h" int _tmain(int argc, _TCHAR* argv) { int *pTest = new int(3); //定義一個整型指針,并初始化為 3 printf( "*pTest = %d/r/n", *pTest ); //調(diào)用庫函數(shù)printf輸出數(shù)據(jù) delete pTest; //刪除這個指針 return 0; } 這里僅僅看下在new與delete進行空間管理時進行反匯編時可能出現(xiàn)的一些情況,我們把上面源代碼稱為源代碼(1),我們按照前面講解的查看VS下反匯編的方法可以看到對應(yīng)于上面代碼的反匯編代碼如下: --- f:/mysource/asm_test/asm_test/asm_test.cpp --------------------------------- ( 反匯編代碼 1) // ASM_Test.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include "stdlib.h" int _tmain(int argc, _TCHAR* argv) { ;(1)函數(shù)預(yù)處理部分 004113C0 push ebp 004113C1 mov ebp,esp ;保存堆棧的棧頂位置 004113C3 sub esp,0E8h ;要置為0CCCCCCCCh 保留變量空間長度 004113C9 push ebx ;保存寄存器ebx、esi、edi 004113CA push esi 004113CB push edi 004113CC lea edi, ;提出要置為0CCCCCCCCh 的空間起始地址 004113D2 mov ecx,3Ah ;要置為0CCCCCCCCh 的個數(shù),每個占4個字節(jié) 004113D7 mov eax,0CCCCCCCCh ;于是3Ah * 4 = 0E8h 004113DC rep stos dword ptr es: ;進行置為0CCCCCCCCh操作 ;(2)定義一個int 型指針,分配空間后,并初始化為 3 , int *pTest = new int(3); //定義一個整型指針,并初始化為 3 004113DE push 4 ;要分配的空間長度,會根據(jù)定義的數(shù)據(jù)類型而不同 004113E0 call operator new (411186h) ;分配空間,并把分配空間的起始地址放入eax中 004113E5 add esp,4 ;由于new與delete函數(shù)本身沒有對棧進行彈出操作,所以,要編寫者自己處理 004113E8 mov dword ptr ,eax ;比較分配的空間是否為0,如果為0 004113EE cmp dword ptr ,0 004113F5 je wmain+51h (411411h) 004113F7 mov eax,dword ptr ;對于分配的地址分配空間進行賦值為:3 004113FD mov dword ptr ,3 00411403 mov ecx,dword ptr 00411409 mov dword ptr ,ecx ;似乎用和作為了中間存儲單元 0041140F jmp wmain+5Bh (41141Bh) 00411411 mov dword ptr ,0 ;上面分配空間失敗是的操作 0041141B mov edx,dword ptr 00411421 mov dword ptr ,edx ;數(shù)據(jù)最后送入pTest變量中 ;調(diào)用printf函數(shù)進行數(shù)據(jù)輸出 printf( "*pTest = %d/r/n", *pTest ); //調(diào)用庫函數(shù)printf輸出數(shù)據(jù) 00411424 mov esi,esp ;用于調(diào)用printf后的Esp檢測,不明白編譯器為什么這樣做 00411426 mov eax,dword ptr ;提取要打印的數(shù)據(jù),先是地址,下面一條是提取具體數(shù)據(jù) 00411429 mov ecx,dword ptr 0041142B push ecx ;兩個參數(shù)入棧 0041142C push offset string "*pTest = %d/r/n" (41573Ch) 00411431 call dword ptr ;調(diào)用函數(shù) 00411437 add esp,8 ;由于庫函數(shù)無出棧管理操作,同new與delete,所以要加 8,進行堆棧處理 0041143A cmp esi,esp ;對堆棧的棧頂進行測試 0041143C call @ILT+325(__RTC_CheckEsp) (41114Ah) ;進行指針變量的清理工作 delete pTest; //刪除這個指針 00411441 mov eax,dword ptr ; 中放入的是分配的地址,下面幾條指令轉(zhuǎn)悠一圈 00411444 mov dword ptr ,eax ;就是要把要清理的地址送入堆棧,然后調(diào)用delete函數(shù) 0041144A mov ecx,dword ptr 00411450 push ecx 00411451 call operator delete (411091h) 00411456 add esp,4 ;對堆棧進行處理,同new與printf函數(shù) ;函數(shù)結(jié)束后,進行最終的清理工作 return 0; 00411459 xor eax,eax ;做相應(yīng)的清理工作,堆棧中保存的變量送回原寄存器 } 0041145B pop edi 0041145C pop esi 0041145D pop ebx 0041145E add esp,0E8h ;進行堆棧的棧頂判斷 00411464 cmp ebp,esp 00411466 call @ILT+325(__RTC_CheckEsp) (41114Ah) 0041146B mov esp,ebp 0041146D pop ebp 0041146E ret --- No source file -------------------------------------------------------------;后面不再是源代碼 在列出反匯編程序時把反匯編代碼的上下的分解注釋也列了出來,親手去查看的朋友可能會發(fā)現(xiàn)在這段代碼的之外的其他部分會有大量的int 3匯編中的中斷指令,這個是與上面的所說的0CCCCCCCCh具有一致性,這些區(qū)域是無效區(qū)域,但代碼訪問這些區(qū)域時就會出現(xiàn)非法訪問提示。當(dāng)然,你應(yīng)該可以想到,那個提示是可以被屏蔽掉的,你可以把這部分區(qū)域填充上數(shù)據(jù)或者修改 iint 3 調(diào)用的中斷程序。 從以上反匯編程序,我們可以發(fā)現(xiàn)幾點: A、一些內(nèi)部的庫函數(shù)是不會對堆棧進行出棧管理的,所以若要對反匯編程序進行操作時,一點要注意這一點 B、編譯器會自動的加上一些對棧頂?shù)臋z查工作,這個是我們在做VC調(diào)試時經(jīng)常遇到的一個問題,就是堆棧錯誤 當(dāng)然以上只是對debug版本下的程序進行反匯編,如果為release 版本,代碼就會進行大量的優(yōu)化,在理解時會有一定的難度,有興趣朋友可以試著反匯編一下,推薦大家有IDA返回工具,感覺挺好用的。 |
|