在
C/C++
語言中,函數(shù)是如何被調(diào)用的呢?本文就實(shí)際的例子,走進(jìn)匯編代碼來看下函數(shù)調(diào)用的過程。
首先看一個(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.?
沒有參數(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?
0040112A
? push??????? esi?
0040112B? push??????? edi?
0040112C
? lea???????? edi,[ebp-0CCh]
00401132? mov???????? ecx,33h
00401137? mov?? ??????eax,0CCCCCCCCh
0040113C
? rep stos??? dword ptr [edi]
??? int i =0;
0040113E? mov???????? dword ptr [i],0 //
直接將數(shù)據(jù)
0
放到指定地址中
??? test1();
00401145? call??????? test1 (401030h)
???
??? test(10);
0040114A
? push??????? 0Ah?
0040114C
? call??????? test (401000h)
00401151? add???????? esp,4
???
??? test3(1,2,3);
00401154? push??????? 3???
00401156? push??????? 2???
00401158? push??????? 1???
0040115A
? call??????? test3 (401090h)
0040115F
? add???????? esp,0Ch
?
??? i=test2();
00401162? call??????? test2 (401060h)
00401167? mov???????? dword ptr [i],eax
?
??? test4();
0040116A
? call??????? test4 (4010C0h)
???
??? test5();
0040116F
? call ???????test5 (4010F0h)
?
??? return 0;
00401174? xor???????? eax,eax
}
00401176? pop???????? edi?
00401177? pop???????? esi?
00401178? pop???????? ebx?
00401179? add???????? esp,0CCh
0040117F
? cmp???????? ebp,esp
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
0040112A
? push??????? esi? //
保存
esi
0040112B? push??????? edi? //
保存
edi
0040112C
? lea???????? edi,[ebp-0CCh] //
將
edp-0CC
地址送
EAX
00401132? mov???????? ecx,33h //CC/4
得到的
00401137? mov???????? eax,0CCCCCCCCh //
初始化為
0XCCCCCCCCH
0040113C
? rep stos??? dword ptr [edi]//
復(fù)制
這寫匯編是編譯器為我們生成的函數(shù)入口部分,基本的含義是為臨時(shí)變量分配空間,并且初始化臨時(shí)變量。
這里需要說明幾點(diǎn):
1.?
函數(shù)調(diào)用是通過堆棧來完成的。
2.?
函數(shù)入口的地方必須為臨時(shí)變量分配一定空間;實(shí)際上如果沒有臨時(shí)變量,也要留出
C0
個(gè)字節(jié)。
3.?
堆棧棧頂指針隨數(shù)據(jù)的進(jìn)入逐漸減小。因此
sub esp
,
0CCh
實(shí)際上是留出了
CC
個(gè)自己的堆棧空間。
我們看到實(shí)現(xiàn)將棧頂指針保存在
ebp
中,然后對(duì)該段空間設(shè)置初始值。而
0XCCCCCCH
是由堆棧的性質(zhì)決定,可以看
MSDN
。
如果開始的時(shí)候假設(shè)
ESP
等于
0X12FEE0
,那么在保存
EBP
之后,
ESP
變成
0X12FEDC
,那么后來
EBP
中的值就是這個(gè)值,在保存的空間(從
0X12FE10
到
0X12FEDC
)上將所有的內(nèi)存都初始化為
0XCC
。而
i
被分配在
0X12FED4
處,也就是第一個(gè)預(yù)留的位置)。
?
?
call??????? test1 (401030h)
由于已經(jīng)知道
i
的地址了,對(duì)
i
的賦值就很簡(jiǎn)單了。這里看調(diào)用第一個(gè)沒有參數(shù)沒有返回值的
test1
函數(shù);僅僅一條語句,將
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>
0040114A
,實(shí)際上是
test1
的下條指令。
因此我們說
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,0C0h
00401039? push??????? ebx?
0040103A
? push??????? esi?
0040103B? push??????? edi?
0040103C
? lea???????? edi,[ebp-0C0h]
00401042? mov??? ?????ecx,30h
00401047? mov???????? eax,0CCCCCCCCh
0040104C
? rep stos??? dword ptr [edi]
?
}
0040104E? pop???????? edi?
0040104F
? pop???????? esi?
00401050? pop???????? ebx?
00401051? mov???????? esp,ebp
00401053? pop???????? ebp?
00401054? ret????? ??????
上面的命令基本相同,主要區(qū)別在于
test1
內(nèi)部沒有臨時(shí)變量,因此這里只保留了
C0
個(gè)自己的空間。
?
繼續(xù)回到主程序:
??? test(10);
0040114A
? push??????? 0Ah?
0040114C
? call??????? test (401000h)
00401151? add???????? esp,4
由于
test
函數(shù)有一個(gè)參數(shù),因此需要首先將參數(shù)壓入堆棧中,然后執(zhí)行與前面相似的操作。
這里有一點(diǎn)需要注意:函數(shù)返回之后需要將壓入的參數(shù)彈出;可以使用
pop
命令,也可以使用
add
命令來執(zhí)行。
?
對(duì)于
test3
的調(diào)用:
??? test3(1,2,3);
00401154? push??????? 3???
00401156? push??????? 2???
00401158? push??????? 1???
0040115A
? call??????? test3 (401090h)
0040115F
? add???????? esp,0Ch
?
由于它需要三個(gè)參數(shù),因此都必須壓入棧,返回的時(shí)候一次性彈出。
?
下面看如何調(diào)用帶有返回值的參數(shù):
??? i=test2();
00401162? call??????? test2 (401060h)
00401167? mov???????? dword ptr [i],eax
其他的相同,但重要的一點(diǎn)是函數(shù)的返回值是通過
eax
寄存器來返回的。
?
其他幾個(gè)函數(shù)的調(diào)用不同的是臨時(shí)變量數(shù)目的不同,僅僅在初始化預(yù)留空間的時(shí)候不同,基本上是每增加一個(gè)變量多出
12
個(gè)字節(jié)的堆棧空間。
?
而
mian
函數(shù)的返回值,有點(diǎn)特別:
??? return 0;
00401174? xor???????? eax,eax
特別的不在于通過
eax
返回,而是自己和自己異或,大部分返回
0
的函數(shù)都這么做。
?
在
mian
函數(shù)退出的時(shí)候有這段代碼:
00401176? pop???????? edi?
00401177? pop???????? esi?
00401178? pop???????? ebx?
00401179? add???????? esp,0CCh
0040117F
? cmp???????? ebp,esp
00401181? call??????? _RTC_CheckEsp (4011E0h)
00401186? mov???????? esp,ebp
00401188? pop???????? ebp?
00401189? ret?????????????
前面幾行是將寄存器的值恢復(fù),而
add esp
,
0CCh
是將保留的堆棧空間釋放,同時(shí)比較
ebp
是否與
esp
相等,如果不相等就提示相應(yīng)的錯(cuò)誤,說明有內(nèi)存泄露等。最后將
ebp
彈出然后返回。
?
從上面的分析我們可以看到編譯器為我們做了很多事情,包括:堆棧空間分配和釋放、寄存器狀態(tài)保存、參數(shù)傳遞等。當(dāng)然這些事情也可以完全由我們自己來完成,那么需要做的是使用關(guān)鍵字
naked
來聲明函數(shù)。