在C/C++語(yǔ)言中,函數(shù)是如何被調(diào)用的呢?本文就實(shí)際的例子,走進(jìn)匯編代碼來(lái)看下函數(shù)調(diào)用的過(guò)程。
首先看一個(gè)簡(jiǎn)單的代碼例子:
void test(int i)
{
int j = i;
}
void test1()
{
}
int test2()
{
return 1;
}
void test3(int a,int b,int c)
{
}
void test4()
{
int i,j;
}
void test5()
{
int i,j,k,l;
}
int main()
{
int i =0;
test1();
test(10);
test3(1,2,3);
i=test2();
test4();
test5();
return 0;
}
這段代碼很簡(jiǎn)單,mian函數(shù)調(diào)用幾個(gè)被測(cè)試的函數(shù),分別是:
1. 沒(méi)有參數(shù)
2. 有一個(gè)參數(shù)
3. 有3個(gè)參數(shù)
4. 有返回值
5. 有兩個(gè)臨時(shí)變量
6. 有多個(gè)臨時(shí)變量
在VC7中,我們將斷點(diǎn)設(shè)置到main函數(shù)入口的地方;然后F5運(yùn)行程序。再按ALT+8反匯編,我們看到下面的代碼:
Main函數(shù)變成這樣了:
int main()
{
00401120 push ebp
00401121 mov ebp,esp
00401123 sub esp,0CCh
00401129 push ebx
0040112B push edi
00401132 mov ecx,33h
00401137 mov eax,0CCCCCCCCh
int i =0;
0040113E mov dword ptr [i],0 //直接將數(shù)據(jù)0放到指定地址中
test1();
00401145 call test1 (401030h)
test(10);
00401151 add esp,4
test3(1,2,3);
00401154 push 3
00401156 push 2
00401158 push 1
i=test2();
00401162 call test2 (401060h)
00401167 mov dword ptr [i],eax
test4();
test5();
return 0;
00401174 xor eax,eax
}
00401176 pop edi
00401177 pop esi
00401178 pop ebx
00401179 add esp,0CCh
00401181 call _RTC_CheckEsp (4011E0h)
00401186 mov esp,ebp
00401188 pop ebp
00401189 ret
函數(shù)入口部分:
00401120 push ebp //保存ebp的值
00401121 mov ebp,esp //將當(dāng)前棧頂指針?biāo)偷?/span>ebp
00401123 sub esp,0CCh //將棧頂指針下移0XCC個(gè)字節(jié),為臨時(shí)變量留出空間
00401129 push ebx //保存ebx
0040112B push edi //保存edi
00401132 mov ecx,33h //CC/4得到的
00401137 mov eax,0CCCCCCCCh //初始化為0XCCCCCCCCH
這寫(xiě)匯編是編譯器為我們生成的函數(shù)入口部分,基本的含義是為臨時(shí)變量分配空間,并且初始化臨時(shí)變量。
這里需要說(shuō)明幾點(diǎn):
1. 函數(shù)調(diào)用是通過(guò)堆棧來(lái)完成的。
2. 函數(shù)入口的地方必須為臨時(shí)變量分配一定空間;實(shí)際上如果沒(méi)有臨時(shí)變量,也要留出C0個(gè)字節(jié)。
3. 堆棧棧頂指針隨數(shù)據(jù)的進(jìn)入逐漸減小。因此sub esp,0CCh實(shí)際上是留出了CC個(gè)自己的堆??臻g。
我們看到實(shí)現(xiàn)將棧頂指針保存在ebp中,然后對(duì)該段空間設(shè)置初始值。而0XCCCCCCH是由堆棧的性質(zhì)決定,可以看MSDN。
如果開(kāi)始的時(shí)候假設(shè)ESP等于0X12FEE0,那么在保存EBP之后,ESP變成0X12FEDC,那么后來(lái)EBP中的值就是這個(gè)值,在保存的空間(從0X12FE10到0X12FEDC)上將所有的內(nèi)存都初始化為0XCC。而i被分配在0X12FED4處,也就是第一個(gè)預(yù)留的位置)。
call test1 (401030h)
由于已經(jīng)知道i的地址了,對(duì)i的賦值就很簡(jiǎn)單了。這里看調(diào)用第一個(gè)沒(méi)有參數(shù)沒(méi)有返回值的test1函數(shù);僅僅一條語(yǔ)句,將test1的函數(shù)地址給call指令。
EAX = CCCCCCCC EBX = 7FFDE000 ECX = 00000000 EDX = 00000001
ESI = 00000040 EDI = 0012FEDC EIP = 00401145 ESP = 0012FE04
EBP = 0012FEDC EFL = 00000202
上面是Call指令調(diào)用前各寄存器的值;下面是調(diào)用后的值:
EAX = CCCCCCCC EBX = 7FFD7000 ECX = 00000000 EDX = 00000001
ESI = 00000040 EDI = 0012FEDC EIP = 00401030 ESP = 0012FE00
EBP = 0012FEDC EFL = 00000202
主要變化在于EIP和ESP;前者是指令指針寄存器,而后者是堆棧指針寄存器。調(diào)用前指令的位置在00401145位置,而call指定將EIP改為test1的地址;同時(shí)將返回地址入棧;可以看到當(dāng)前棧頂?shù)闹凳?/span>
因此我們說(shuō)Call指定做了兩件事情:
1. 將EIP從當(dāng)前值改為被調(diào)用函數(shù)的值。
2. 將返回地址,也就是當(dāng)前地址的下條指令放入堆棧。
現(xiàn)在進(jìn)入test1中看個(gè)究竟。
void test1()
{
00401030 push ebp
00401031 mov ebp,esp
00401033 sub esp,
00401039 push ebx
0040103B push edi
00401042 mov ecx,30h
00401047 mov eax,0CCCCCCCCh
}
0040104E pop edi
00401050 pop ebx
00401051 mov esp,ebp
00401053 pop ebp
00401054 ret
上面的命令基本相同,主要區(qū)別在于test1內(nèi)部沒(méi)有臨時(shí)變量,因此這里只保留了C0個(gè)自己的空間。
繼續(xù)回到主程序:
test(10);
00401151 add esp,4
由于test函數(shù)有一個(gè)參數(shù),因此需要首先將參數(shù)壓入堆棧中,然后執(zhí)行與前面相似的操作。
這里有一點(diǎn)需要注意:函數(shù)返回之后需要將壓入的參數(shù)彈出;可以使用pop命令,也可以使用add命令來(lái)執(zhí)行。
對(duì)于test3的調(diào)用:
test3(1,2,3);
00401154 push 3
00401156 push 2
00401158 push 1
由于它需要三個(gè)參數(shù),因此都必須壓入棧,返回的時(shí)候一次性彈出。
下面看如何調(diào)用帶有返回值的參數(shù):
i=test2();
00401162 call test2 (401060h)
00401167 mov dword ptr [i],eax
其他的相同,但重要的一點(diǎn)是函數(shù)的返回值是通過(guò)eax寄存器來(lái)返回的。
其他幾個(gè)函數(shù)的調(diào)用不同的是臨時(shí)變量數(shù)目的不同,僅僅在初始化預(yù)留空間的時(shí)候不同,基本上是每增加一個(gè)變量多出12個(gè)字節(jié)的堆??臻g。
而mian函數(shù)的返回值,有點(diǎn)特別:
return 0;
00401174 xor eax,eax
特別的不在于通過(guò)eax返回,而是自己和自己異或,大部分返回0的函數(shù)都這么做。
在mian函數(shù)退出的時(shí)候有這段代碼:
00401176 pop edi
00401177 pop esi
00401178 pop ebx
00401179 add esp,0CCh
00401181 call _RTC_CheckEsp (4011E0h)
00401186 mov esp,ebp
00401188 pop ebp
00401189 ret
前面幾行是將寄存器的值恢復(fù),而add esp,0CCh是將保留的堆??臻g釋放,同時(shí)比較ebp是否與esp相等,如果不相等就提示相應(yīng)的錯(cuò)誤,說(shuō)明有內(nèi)存泄露等。最后將ebp彈出然后返回。
從上面的分析我們可以看到編譯器為我們做了很多事情,包括:堆??臻g分配和釋放、寄存器狀態(tài)保存、參數(shù)傳遞等。當(dāng)然這些事情也可以完全由我們自己來(lái)完成,那么需要做的是使用關(guān)鍵字naked來(lái)聲明函數(shù)。
聯(lián)系客服