VS2008反彙編小解
瞭解反彙編的一些小知識對於我們在開發軟體時進行程式設計與除錯大有好處,下面以VS2008環境下的VC++簡單介紹一下反彙編的一些小東西!如果有些解釋有問題的地方,希望大家能夠指出。
1、新建簡單的VC控制檯應用程式(對此熟悉的同學可以略過)
A、開啟Microsoft Visual Studio 2008,選擇主選單“File”
B、選擇子選單“New”下面的“Project”,開啟“New Project”對話方塊。
C、左邊選擇Visual C++下的win32,右邊選擇Win32 Console Application,然後輸入一個工程名,點選“OK”即可,在出現的嚮導中,一切預設,點選Finish即可。
D、在出現的編輯區域內會出現以你設定的工程名命名的CPP檔案。內容如下:
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
return 0;
}
2、VS檢視彙編程式碼
A、VC處於除錯狀態才能看到彙編指令視窗。因此,可以在 return 0 上設定一個斷點:把游標移到 return 0 那一行上,然後按下F9鍵設定一個斷點。
B、按下F5鍵進入除錯狀態,當程式停在 return 0 這一行上時,開啟選單“Debug”下的“Windows”子選單,選擇“Disassembly”。這樣,出現一個反彙編的視窗,顯示下面的資訊:
--- 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,[ebp-0C0h] 00411382 mov ecx,30h 00411387 mov eax,0CCCCCCCCh 0041138C rep stos dword ptr es:[edi] return 0; 0041138E xor eax,eax } 00411390 pop edi 00411391 pop esi 00411392 pop ebx 00411393 mov esp,ebp 00411395 pop ebp 00411396 ret
上面就是系統生成的main函式原型,確切的說是_tmain()的反彙編的相關資訊,相信學過組合語言的肯定就能夠了解它所做的操作了。
3、簡單瞭解一下常見的彙編指令
為了照顧到沒學過彙編程式的同志們,這裡簡單介紹一下常見的幾種彙編指令。
A、add:加法指令,第一個是目標運算元,第二個是源運算元,格式為:目標運算元 = 目標運算元 + 源運算元。
B、sub:減法指令,格式同 add。
C、call:呼叫函式,一般函式的引數放在暫存器中。
D、ret:跳轉會呼叫函式的地方。對應於call,返回到對應的call呼叫的下一條指令,若有返回值,則放入eax中。
E、push:把一個32位的運算元壓入堆疊中,這個操作在32位機中會使得esp被減4(位元組),esp通常是指向棧頂的(這裡要指出的是:學過微控制器的同學請注意單片機種的堆疊與Windows下的堆疊是不同的,請參考相應資料),這裡頂部是地址小的區域,那麼,壓入堆疊的資料越多,esp也就越來越小。
F、pop:與push相反,esp每次加4(位元組),一個數據出棧。pop的引數一般是一個暫存器,棧頂的資料被彈出到這個暫存器中。
一般不會把sub、add這樣的算術指令,以及call、ret這樣的跳轉指令歸入堆疊相關指令中。但是實際上在函式引數傳遞過程中,sub和add最常用來操作堆疊;call和ret對堆疊也有影響。
G、mov:資料傳送。第一個引數是目的運算元,第二個引數是源運算元,就是把源運算元拷貝到目的一份。
H、xor:異或指令,這本身是一個邏輯運算指令,但在彙編指令中通常會見到它被用來實現清零功能。用 xor eax,eax這種操作來實現 mov eax,0,可以使速度更快,佔用位元組數更少。
I、lea:取得第二個引數地址後放入到前面的暫存器(第一個引數)中。
然而lea也同樣可以實現mov的操作,例如:
lea edi,[ebx-0ch]
方括號表示儲存單元,也就是提取方括號中的資料所指向的內容,然而lea提取內容的地址,這樣就實現了把(ebx-0ch)放入到了edi中,但是mov指令是不支援第二個運算元是一個暫存器減去一個數值的。
J、stos:序列儲存指令,它實現把eax中的資料放入到edi所指的地址中,同時edi後移4個位元組,這裡的stos實際上對應的是stosd,其他的還有stosb,stosw分別對應1,2個位元組。
K、jmp:無條件跳轉指令,對應於大量的條件跳轉指令。
L、jg:條件跳轉,大於時成立,進行跳轉,通常條件跳轉之前會有一條比較指令(用於設定標誌位)。
M、jl:小於時跳轉。
N、jge:大於等於時跳轉。
O、cmp:比較大小指令,結果用來設定標誌位。
4、函式引數傳遞方式
函式呼叫規則指的是呼叫者和被呼叫函式間傳遞引數及返回引數的方法,在Windows上,常用的有Pascal方式、WINAPI方式(_stdcall)、C方式(_cdecl)。
A、_cdecl C呼叫規則:
(a)引數從右到左進入堆疊;
(b)在函式返回後,呼叫者要負責清除堆疊,這種呼叫方式通常會生成較大的可執行程式。
B、_stdcall又稱為WINAPI,呼叫規則如下:
(a)引數從右到左進入堆疊;
(b)被呼叫的函式在返回前自行清理堆疊,這種方式生成的程式碼比cdecl小。
C、Pascal呼叫規則(主要用於Win16函式庫中,現在基本不用):
(a)引數從左到右進入堆疊;
(b)被呼叫的函式在返回前自行清理堆疊。
(c)不支援可變引數的函式呼叫。
5、VC中訪問無效變量出錯原因
我們看上面主函式反彙編後的其中一段程式碼如下:
0041137C lea edi,[ebp-0C0h]
00411382 mov ecx,30h
00411387 mov eax,0CCCCCCCCh
0041138C rep stos dword ptr es:[edi]
從程式碼的表面上看,它是實現把從ebp-0C0h開始的30h個字的空間寫入0CCCCCCCCh。其中eax為四位的資料,這樣可以計算:
0C0h = 30h * 4
也就是把從ebp-0C0h 到ebp之間的空間初始化為0CCCCCCCCh。大家在學習反彙編的過程中會發現,其實編譯器會根據情況把相應長度的這樣一段作為區域性變數的空間,而這裡把區域性變數區域全都初始化成0CCCCCCCCh也是有其用意的,做VC程式設計的工作者,特別是初學者可能不會對0CCCCCCCCh這個常量陌生。0cch實際上是int 3指令的機器碼,這是一個斷點中斷指令(在反編譯出的資訊中大家會看到int 3),因為區域性變數不可被執行,或者如果在沒有初始化的時候進行了訪問,則就會出現訪問失敗錯誤。這個在VC編譯Debug版本中才能看到提示這個錯誤,在Release版本中,會以另外一種錯誤形式體現。下面,我們修改主程式看下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 ); //呼叫庫函式printf輸出資料
delete []pTest; //刪除這個指標
return 0;
}
這裡僅僅看下在new與delete進行空間管理時進行反彙編時可能出現的一些情況,我們把上面原始碼稱為原始碼(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)函式預處理部分
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,[ebp-0E8h] ;提出要置為0CCCCCCCCh 的空間起始地址
004113D2 mov ecx,3Ah ;要置為0CCCCCCCCh 的個數,每個佔4個位元組
004113D7 mov eax,0CCCCCCCCh ;於是3Ah * 4 = 0E8h
004113DC rep stos dword ptr es:[edi] ;進行置為0CCCCCCCCh操作
;(2)定義一個int 型指標,分配空間後,並初始化為 3 ,
int *pTest = new int(3); //定義一個整型指標,並初始化為 3
004113DE push 4 ;要分配的空間長度,會根據定義的資料型別而不同
004113E0 call operator new (411186h) ;分配空間,並把分配空間的起始地址放入eax中
004113E5 add esp,4 ;由於new與delete函式本身沒有對棧進行彈出操作,所以,要編寫者自己處理
004113E8 mov dword ptr [ebp-0E0h],eax ;比較分配的空間是否為0,如果為0
004113EE cmp dword ptr [ebp-0E0h],0
004113F5 je wmain+51h (411411h)
004113F7 mov eax,dword ptr [ebp-0E0h] ;對於分配的地址分配空間進行賦值為:3
004113FD mov dword ptr [eax],3
00411403 mov ecx,dword ptr [ebp-0E0h]
00411409 mov dword ptr [ebp-0E8h],ecx ;似乎用[ebp - 0E0h]和[ebp - 0E8h]作為了中間儲存單元
0041140F jmp wmain+5Bh (41141Bh)
00411411 mov dword ptr [ebp-0E8h],0 ;上面分配空間失敗是的操作
0041141B mov edx,dword ptr [ebp-0E8h]
00411421 mov dword ptr [pTest],edx ;資料最後送入pTest變數中
;呼叫printf函式進行資料輸出
printf( "*pTest = %d/r/n", *pTest ); //呼叫庫函式printf輸出資料
00411424 mov esi,esp ;用於呼叫printf後的Esp檢測,不明白編譯器為什麼這樣做
00411426 mov eax,dword ptr [pTest] ;提取要列印的資料,先是地址,下面一條是提取具體資料
00411429 mov ecx,dword ptr [eax]
0041142B push ecx ;兩個引數入棧
0041142C push offset string "*pTest = %d/r/n" (41573Ch)
00411431 call dword ptr [__imp__printf (4182C4h)] ;呼叫函式
00411437 add esp,8 ;由於庫函式無出棧管理操作,同new與delete,所以要加 8,進行堆疊處理
0041143A cmp esi,esp ;對堆疊的棧頂進行測試
0041143C call @ILT+325(__RTC_CheckEsp) (41114Ah)
;進行指標變數的清理工作
delete []pTest; //刪除這個指標
00411441 mov eax,dword ptr [pTest] ;[pTest] 中放入的是分配的地址,下面幾條指令轉悠一圈
00411444 mov dword ptr [ebp-0D4h],eax ;就是要把要清理的地址送入堆疊,然後呼叫delete函式
0041144A mov ecx,dword ptr [ebp-0D4h]
00411450 push ecx
00411451 call operator delete (411091h)
00411456 add esp,4 ;對堆疊進行處理,同new與printf函式
;函式結束後,進行最終的清理工作
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 -------------------------------------------------------------;後面不再是原始碼
在列出反彙編程式時把反彙編程式碼的上下的分解註釋也列了出來,親手去檢視的朋友可能會發現在這段程式碼的之外的其他部分會有大量的int 3彙編中的中斷指令,這個是與上面的所說的0CCCCCCCCh具有一致性,這些區域是無效區域,但程式碼訪問這些區域時就會出現非法訪問提示。當然,你應該可以想到,那個提示是可以被遮蔽掉的,你可以把這部分割槽域填充上資料或者修改 iint 3 呼叫的中斷程式。
從以上反彙編程式,我們可以發現幾點:
A、一些內部的庫函式是不會對堆疊進行出棧管理的,所以若要對反彙編程式進行操作時,一點要注意這一點
B、編譯器會自動的加上一些對棧頂的檢查工作,這個是我們在做VC除錯時經常遇到的一個問題,就是堆疊錯誤
當然以上只是對debug版本下的程式進行反彙編,如果為release 版本,程式碼就會進行大量的優化,在理解時會有一定的難度,有興趣朋友可以試著反彙編一下,推薦大家有IDA返回工具,感覺挺好用的。