1)用VS2010新建Win32 Console Application,工程名爲ACECore,工程建立完成後得到打開文件ACECore.cpp,代碼如下:
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
return 0;
}
2)用VS2010查看彙編代碼的方法:
1. VC必須處於debug狀態才能看到彙編指令窗口。因此在上面代碼return 0一句上設置斷點。
2.按下F5鍵調試程序,當程序停在斷點處時,打開菜單“Debug”下的“Windows”子菜單,選擇“Disassembly”。這樣就出現反彙編窗口,顯示彙編代碼:
--- g:/acecore/acecore/acecore.cpp ---------------------------------------------
// ACECore.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
00411350 push ebp
00411351 mov ebp,esp
00411353 sub esp,0C0h
00411359 push ebx
0041135A push esi
0041135B push edi
0041135C lea edi,[ebp-0C0h]
00411362 mov ecx,30h
00411367 mov eax,0CCCCCCCCh
0041136C rep stos dword ptr es:[edi]
return 0;
0041136E xor eax,eax
}
3)相關彙編指令:
push:把一個32位的操作數壓入堆棧中,這個操作導致esp被減4。esp被稱爲棧頂,壓入堆棧的數據越多,這個堆棧也就越堆越高,esp地址就越來越小。在32位平臺上,esp每次減少4(字節)。
pop:esp被加4,一個數據出棧。pop的參數一般是一個寄存器,棧頂數據被彈出到這個寄存器中。
某些指令會“自動”地操作堆棧:call指令會把它的下一條指令的地址壓入堆棧中,然後跳轉到它調用的函數的開頭處;而單純的jmp是不會這樣做的。同時,ret會自動彈出返回地址。
call的本質相當於push+jmp,ret的本質相當於pop+jmp。
不但push、pop、call和ret會操作堆棧,sub和add也可以用於操作堆棧。如果要一次在堆棧中分配4個4字節長整型的空間,那麼沒有必要4次調用push,很簡單地把esp減去4*4=16即可。當然,也可以同樣地用add指令來恢復它。
lea:取得地址(第二個參數)後放入到前面的寄存器(第一個參數)中。實際上,有時候lea用來做和mov同樣的事情,比如賦值:
lea edi,[ebp – 0cch]
其中,方括弧表示存儲器,也就是ebp-0cch這個地址所指的存儲器內容。但是lea語法要求取[ebp-0cch]的地址,這個地址就是ebp-0cch,把這個地址放到edi中,也就是說,這等同於:
mov edi,ebp-0cch
但以上的mov指令時錯誤的,因爲mov不支持後一個操作數寫成寄存器減去數字。而lea支持,因此可以用lea來代替它。
Stos指令:
mov ecx, 30h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
stos是串存儲指令,它的功能是將eax中的數據放入edi所指的地址中,同時,edi會增加4(字節數)。rep時指令重複執行ecx中填寫的次數。方括弧表示存儲器,這個地址實際上就是edi的內容所指向的地址。這裏的stos其實對應的是stosd,其他還有stosb、stosw,分別對應於處理4、1、2個字節。
上面代碼中對堆棧30h*4(=0c0h)個字節初始化爲0CCh(也就是int3指令的機器碼),這樣發生意外時執行堆棧裏面的內容會引發調試中斷。
==============lost的分割線===============
C函數的參數傳遞過程:
1)C語言程序通過堆棧把參數從函數外部傳入到函數內部,同時,在堆棧中劃分區域來容納函數的內部變量。對於C語言默認的調用方式,函數調用方把參數反序(從右到左)地壓入堆棧中,被調用方把堆棧復原。這些參數對齊到機器字長,16位、32位、64位CPU下分別對齊到2、4、8個字節。
2)函數調用規則指的是調用者和被調用者函數間傳遞參數及返回參數的方法,在Windows上,常用的有Pascal方式、WINAPI方式(_stdcall)、C方式(_cdecl)。
_cdecl調用規則:
(1)參數從右到左進入堆棧;
(2)在函數返回後,調用者要負責清除堆棧,所以這個調用常會生成較大的可執行文件。
_stdcall又稱爲WINAPI,其調用規則:
(1)參數從右到左入棧;
(2)被調用的函數在返回前自行清理堆棧,所以生成的代碼比cdecl下。
Pascal調用規則:
(1)參數從左到右入棧;
(2)被調用參數在返回前自行清理堆棧;
(3)不支持可變參數的函數調用。
此外,在Windows內核中還常見有快速調用方式(_fastcall);在C++編譯的代碼中有this call方式(_thiscall)。
3)以如下函數作爲例子分析:
void ACEFunction(int a, int b)
{
int c = a + b;
}
int _tmain(int argc, _TCHAR* argv[])
{
int a = 1;
int b = 2;
ACEFunction(a, b);
return 0;
}
標準的C函數調用方式(_cdecl):
(1)調用者把參數反序地壓入堆棧中;
(2)調用函數;
(3)調用者負責把堆棧清理復原。
注意:在Windows中,不管哪種調用方式都是返回值放在eax中,然後返回。外部從eax中得到返回值。
_cdecl方式下被調用函數需要做以下的事情:
(1)保存ebp。ebp總是被我們用來保存這個函數執行之前的esp的值,執行完畢後,我們用ebp恢復esp;同時,調用此函數的上層函數也用ebp做同樣的事情,所以先把ebp壓入堆棧,函數返回之前彈出,避免ebp被我們改動。
(2)保存esp到ebp中。
上面兩步的代碼如下:
;保存ebp,並把esp放入ebp中,此時ebp和esp同
;都是這次函數調用時的棧頂
00411360 push ebp
00411361 mov ebp,esp
(3)在堆棧中騰出一個區域用來保存局部變量,這就是常說的所謂局部變量時保存在棧空間中的。方法是:把esp減少一個數值,這樣就等於壓入了一堆變量。恢復時,只有把esp恢復成ebp中保存的數據就行。
(4)保存ebx、esi、edi到堆棧中,函數調用完後恢復。
上面兩步對應代碼如下:
;把esp往下移動一個範圍,等於在堆棧中放出一片新
;的空間來保存局部變量
00411363 sub esp,0CCh
00411369 push ebx
0041136A push esi
0041136B push edi
(5)把局部變量區域初始化成全0CCCCCCCCh。0CCh實際上是int3指令的機器碼,這是一個斷點中斷指令。因爲局部變量不可能被執行,如果執行了,必然程序出錯,這時發生中斷來提示開發者。這時VC編譯Debug版本的特有操作:
0041136C lea edi,[ebp-0CCh]
00411372 mov ecx,33h
00411377 mov eax,0CCCCCCCCh
0041137C rep stos dword ptr es:[edi]
(6)然後做函數裏應該做的事情。參數的獲取是ebp+12字節爲第二個參數,ebp+8字節爲第一個參數(反序入棧),依次增加。最後ebp+4字節處是要返回的地址。
(7)恢復ebx、esi、edi、esp、ebp,最後返回:
00411387 pop edi
00411388 pop esi
00411389 pop ebx
0041138A mov esp,ebp
0041138C pop ebp
0041138D ret
用VS2010編譯Debug版本,完整的反彙編代碼如下:
--- g:/acecore/acecore/acecore.cpp ---------------------------------------------
// ACECore.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
void ACEFunction(int a, int b)
{
00411360 push ebp ;保存ebp,並把esp放入ebp中。此時ebp與esp相同
00411361 mov ebp,esp ;都是這次函數調用時的棧頂
00411363 sub esp,0CCh ;把esp往上移動一個範圍,等於在堆棧中放出一片新
;的空間用來存儲局部變量
00411369 push ebx ;下面保存三個寄存器:ebx、esi、edi
0041136A push esi
0041136B push edi
0041136C lea edi,[ebp-0CCh] ;原本是想使用“mov edi, ebp-0CCh”,但是mov不支持
;“-”操作,所以先對ebp-0CCh取內容(即[ebp-0CCh]),
;再利用lea把[ebp-0CCh]的地址也就是ebp-0CCh放到edi中
;目的是把保存局部變量的區域(即ebp-0CCh開始的區域)
;初始化成全部爲0CCCCCCCCh。
00411372 mov ecx,33h
00411377 mov eax,0CCCCCCCCh
0041137C rep stos dword ptr es:[edi] ;寫入0CCh指令(中斷)
int c = a + b;
0041137E mov eax,dword ptr [a] ;加法操作,從堆棧中取得從外部傳入的參數。
00411381 add eax,dword ptr [b] ;通過ida反彙編可以看到,其實這兩天指令是:
;mov eax, [ebp+8]
;add eax, [ebp+0Ch]
;參數是通過ebp從堆棧中取得的。這裏看到的是
;VC調試器的顯示結果,是爲了方便閱讀,
;直接加上了參數名
00411384 mov dword ptr [c],eax
}
00411387 pop edi ;恢復edi、dsi、ebx
00411388 pop esi
00411389 pop ebx
0041138A mov esp,ebp ;恢復原來的ebp和esp,讓上一級調用的
0041138C pop ebp ; 函數可以正常使用
0041138D ret
主程序中對這個函數的調用方式是:
004123DC mov eax,dword ptr [b] ;把b, a兩個參數壓入堆棧
004123DF push eax
004123E0 mov ecx,dword ptr [a]
004123E3 push ecx
004123E4 call ACEFunction (411014h) ;調用函數ACEFunction
004123E9 add esp,8 ;恢復堆棧