了解反匯編的一些小知識對于我們在開發(fā)軟件時進行
編程與調試大有好處,下面以VS2008環(huán)境下的VC++簡單介紹一下反匯編的一些小東西!如果有些解釋有問題的地方,希望大家能夠指出。
1、新建簡單的VC控制臺應用
程序(對此熟悉的同學可以略過)
A、打開Microsoft Visual Studio 2008,選擇主菜單“File”
B、選擇子菜單“New”下面的“Project”,打開“New Project”對話框。
C、左邊選擇Visual C++下的win32,右邊選擇Win32 Console Application,然后輸入一個工程名,點擊“OK”即可,在出現(xiàn)的向導中,一切默認,點擊Finish即可。
D、在出現(xiàn)的編輯區(qū)域內會出現(xiàn)以你設定的工程名命名的CPP文件。內容如下:
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv)
{
return 0;
}
2、VS查看匯編代碼
A、VC處于調試狀態(tài)才能看到匯編指令窗口。因此,可以在 return 0 上設置一個斷點:把光標移到 return 0 那一行上,然后按下F9鍵設置一個斷點。
B、按下F5鍵進入調試狀態(tài),當程序停在 return 0 這一行上時,打開菜單“Debug”下的“Windows”子菜單,選擇“Disassembly”。這樣,出現(xiàn)一個反匯編的窗口,顯示下面的信息:
--- d:/my documents/visual
studio 2008/projects/casmtest/casmtest/casmtest_main.cpp
// CAsmTest.cpp : 定義控制臺應用
程序的入口點。
//
#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()的反匯編的相關信息,相信學過匯編語言的肯定就能夠了解它所做的操作了。
3、簡單了解一下常見的匯編指令
為了照顧到沒學過匯
編程序的同志們,這里簡單介紹一下常見的幾種匯編指令。
A、add:加法指令,第一個是目標操作數(shù),第二個是源操作數(shù),格式為:目標操作數(shù) = 目標操作數(shù) + 源操作數(shù)。
B、sub:減法指令,格式同 add。
C、call:調用函數(shù),一般函數(shù)的參數(shù)放在寄存器中。
D、ret:跳轉會調用函數(shù)的地方。對應于call,返回到對應的call調用的下一條指令,若有返回值,則放入eax中。
E、push:把一個32位的操作數(shù)壓入堆棧中,這個操作在32位機中會使得esp被減4(字節(jié)),esp通常是指向棧頂?shù)模ㄟ@里要指出的是:學過單片機的同學請注意單片機種的堆棧與Windows下的堆棧是不同的,請參考相應資料),這里頂部是地址小的區(qū)域,那么,壓入堆棧的數(shù)據(jù)越多,esp也就越來越小。
F、pop:與push相反,esp每次加4(字節(jié)),一個數(shù)據(jù)出棧。pop的參數(shù)一般是一個寄存器,棧頂?shù)臄?shù)據(jù)被彈出到這個
寄存器中。
一般不會把sub、add這樣的算術指令,以及call、ret這樣的跳轉指令歸入堆棧相關指令中。但是實際上在函數(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ù)所指向的內容,然而lea提取內容的地址,這樣就實現(xiàn)了把(ebx-0ch)放入到了edi中,但是mov指令是不支持第二個操作數(shù)是一個
寄存器減去一個數(shù)值的。
J、stos:串行存儲指令,它實現(xiàn)把eax中的數(shù)據(jù)放入到edi所指的地址中,同時edi后移4個字節(jié),這里的stos實際上對應的是stosd,其他的還有stosb,stosw分別對應1,2個字節(jié)。
K、jmp:無條件跳轉指令,對應于大量的條件跳轉指令。
L、jg:條件跳轉,大于時成立,進行跳轉,通常條件跳轉之前會有一條比較指令(用于設置標志位)。
M、jl:小于時跳轉。
N、jge:大于等于時跳轉。
O、
cmp:比較大小指令,結果用來設置標志位。
4、函數(shù)參數(shù)傳遞方式
函數(shù)調用規(guī)則指的是調用者和被調用函數(shù)間傳遞參數(shù)及返回參數(shù)的方法,在Windows上,常用的有Pascal方式、WINAPI方式(_stdcall)、C方式(_cdecl)。
A、_cdecl C調用規(guī)則:
(a)參數(shù)從右到左進入堆棧;
(b)在函數(shù)返回后,調用者要負責清除堆棧,這種調用方式通常會生成較大的可執(zhí)行
程序。
B、_stdcall又稱為WINAPI,調用規(guī)則如下:
(a)參數(shù)從右到左進入堆棧;
(b)被調用的函數(shù)在返回前自行清理堆棧,這種方式生成的代碼比cdecl小。
C、Pascal調用規(guī)則(主要用于Win16函數(shù)庫中,現(xiàn)在基本不用):
(a)參數(shù)從左到右進入堆棧;
(b)被調用的函數(shù)在返回前自行清理堆棧。
(c)不支持可變參數(shù)的函數(shù)調用。
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。大家在學習反匯編的過程中會發(fā)現(xiàn),其實
編譯器會根據(jù)情況把相應長度的這樣一段作為局部變量的空間,而這里把局部變量區(qū)域全都初始化成0CCCCCCCCh也是有其用意的,做VC
編程的工作者,特別是初學者可能不會對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 ); //調用庫函數(shù)printf輸出數(shù)據(jù)
delete pTest; //刪除這個指針
return 0;
}
這里僅僅看下在new與delete進行空間管理時進行反匯編時可能出現(xiàn)的一些情況,我們把上面
源代碼稱為源代碼(1),我們按照前面講解的查看VS下反匯編的方法可以看到對應于上面代碼的反匯編代碼如下:
--- 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ù)預處理部分
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變量中
;調用printf函數(shù)進行數(shù)據(jù)輸出
printf( "*pTest = %d/r/n", *pTest ); //調用庫函數(shù)printf輸出數(shù)據(jù)
00411424 mov esi,esp ;用于調用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 ;調用函數(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 ; 中放入的是分配的地址,下面幾條指令轉悠一圈
00411444 mov dword ptr ,eax ;就是要把要清理的地址送入堆棧,然后調用delete函數(shù)
0041144A mov ecx,dword ptr
00411450 push ecx
00411451 call operator delete (411091h)
00411456 add esp,4 ;對堆棧進行處理,同new與printf函數(shù)
;函數(shù)結束后,進行最終的清理工作
return 0;
00411459 xor eax,eax ;做相應的清理工作,堆棧中保存的變量送回原
寄存器}
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)非法訪問提示。當然,你應該可以想到,那個提示是可以被屏蔽掉的,你可以把這部分區(qū)域填充上數(shù)據(jù)或者修改 iint 3 調用的中斷程序。
從以上反匯編程序,我們可以發(fā)現(xiàn)幾點:
A、一些內部的庫函數(shù)是不會對堆棧進行出棧管理的,所以若要對反匯
編程序進行操作時,一點要注意這一點
B、編譯器會自動的加上一些對棧頂?shù)臋z查工作,這個是我們在做VC調試時經常遇到的一個問題,就是堆棧錯誤
當然以上只是對debug版本下的
程序進行反匯編,如果為release 版本,代碼就會進行大量的優(yōu)化,在理解時會有一定的難度,有興趣朋友可以試著反匯編一下,推薦大家有IDA返回工具,感覺挺好用的。