在
C/C++
語言中,函數是如何被調用的呢?本文就實際的例子,走進匯編代碼來看下函數調用的過程。
首先看一個簡單的代碼例子:
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;
}
?
這段代碼很簡單,
mian
函數調用幾個被測試的函數,分別是:
1.?
沒有參數
2.?
有一個參數
3.?
有
3
個參數
4.?
有返回值
5.?
有兩個臨時變量
6.?
有多個臨時變量
?
在
VC7
中,我們將斷點設置到
main
函數入口的地方;然后
F5
運行程序。再按
ALT+8
反匯編,我們看到下面的代碼:
Main
函數變成這樣了:
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 //
直接將數據
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?????????????
?
函數入口部分:
00401120? push??????? ebp? //
保存
ebp
的值
00401121? mov???????? ebp,esp //
將當前棧頂指針送到
ebp
00401123? sub???????? esp,0CCh //
將棧頂指針下移
0XCC
個字節,為臨時變量留出空間
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]//
復制
這寫匯編是編譯器為我們生成的函數入口部分,基本的含義是為臨時變量分配空間,并且初始化臨時變量。
這里需要說明幾點:
1.?
函數調用是通過堆棧來完成的。
2.?
函數入口的地方必須為臨時變量分配一定空間;實際上如果沒有臨時變量,也要留出
C0
個字節。
3.?
堆棧棧頂指針隨數據的進入逐漸減小。因此
sub esp
,
0CCh
實際上是留出了
CC
個自己的堆棧空間。
我們看到實現將棧頂指針保存在
ebp
中,然后對該段空間設置初始值。而
0XCCCCCCH
是由堆棧的性質決定,可以看
MSDN
。
如果開始的時候假設
ESP
等于
0X12FEE0
,那么在保存
EBP
之后,
ESP
變成
0X12FEDC
,那么后來
EBP
中的值就是這個值,在保存的空間(從
0X12FE10
到
0X12FEDC
)上將所有的內存都初始化為
0XCC
。而
i
被分配在
0X12FED4
處,也就是第一個預留的位置)。
?
?
call??????? test1 (401030h)
由于已經知道
i
的地址了,對
i
的賦值就很簡單了。這里看調用第一個沒有參數沒有返回值的
test1
函數;僅僅一條語句,將
test1
的函數地址給
call
指令。
EAX = CCCCCCCC EBX = 7FFDE000 ECX = 00000000 EDX = 00000001
ESI = 00000040 EDI = 0012FEDC EIP = 00401145 ESP = 0012FE04
EBP = 0012FEDC EFL = 00000202
上面是
Call
指令調用前各寄存器的值;下面是調用后的值:
EAX = CCCCCCCC EBX = 7FFD7000 ECX = 00000000 EDX = 00000001
ESI = 00000040 EDI = 0012FEDC EIP = 00401030 ESP = 0012FE00
EBP = 0012FEDC EFL = 00000202
主要變化在于
EIP
和
ESP
;前者是指令指針寄存器,而后者是堆棧指針寄存器。調用前指令的位置在
00401145
位置,而
call
指定將
EIP
改為
test1
的地址;同時將返回地址入棧;可以看到當前棧頂的值是
0040114A
,實際上是
test1
的下條指令。
因此我們說
Call
指定做了兩件事情:
1.?
將
EIP
從當前值改為被調用函數的值。
2.?
將返回地址,也就是當前地址的下條指令放入堆棧。
?
現在進入
test1
中看個究竟。
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????? ??????
上面的命令基本相同,主要區別在于
test1
內部沒有臨時變量,因此這里只保留了
C0
個自己的空間。
?
繼續回到主程序:
??? test(10);
0040114A
? push??????? 0Ah?
0040114C
? call??????? test (401000h)
00401151? add???????? esp,4
由于
test
函數有一個參數,因此需要首先將參數壓入堆棧中,然后執行與前面相似的操作。
這里有一點需要注意:函數返回之后需要將壓入的參數彈出;可以使用
pop
命令,也可以使用
add
命令來執行。
?
對于
test3
的調用:
??? 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
其他的相同,但重要的一點是函數的返回值是通過
eax
寄存器來返回的。
?
其他幾個函數的調用不同的是臨時變量數目的不同,僅僅在初始化預留空間的時候不同,基本上是每增加一個變量多出
12
個字節的堆棧空間。
?
而
mian
函數的返回值,有點特別:
??? return 0;
00401174? xor???????? eax,eax
特別的不在于通過
eax
返回,而是自己和自己異或,大部分返回
0
的函數都這么做。
?
在
mian
函數退出的時候有這段代碼:
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?????????????
前面幾行是將寄存器的值恢復,而
add esp
,
0CCh
是將保留的堆棧空間釋放,同時比較
ebp
是否與
esp
相等,如果不相等就提示相應的錯誤,說明有內存泄露等。最后將
ebp
彈出然后返回。
?
從上面的分析我們可以看到編譯器為我們做了很多事情,包括:堆棧空間分配和釋放、寄存器狀態保存、參數傳遞等。當然這些事情也可以完全由我們自己來完成,那么需要做的是使用關鍵字
naked
來聲明函數。