函數是如何被調用的?-探索代碼背后的故事
在 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?
0040112B? push??????? edi?
00401132? mov???????? ecx,33h
00401137? mov?? ??????eax,0CCCCCCCCh
??? int i =0;
0040113E? mov???????? dword ptr [i],0 //
直接將數據
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?????????????
函數入口部分:
00401120? push??????? ebp? //
保存
ebp
的值
00401121? mov???????? ebp,esp //
將當前棧頂指針送到
ebp
00401123? sub???????? esp,0CCh //
將棧頂指針下移
0XCC
個字節,為臨時變量留出空間
00401129? push??????? ebx? //
保存
ebx
0040112B? push??????? edi? //
保存
edi
00401132? mov???????? ecx,33h //CC/4
得到的
00401137? mov???????? eax,0CCCCCCCCh //
初始化為
0XCCCCCCCCH
這寫匯編是編譯器為我們生成的函數入口部分,基本的含義是為臨時變量分配空間,并且初始化臨時變量。
這里需要說明幾點:
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
的地址;同時將返回地址入棧;可以看到當前棧頂的值是
因此我們說
Call
指定做了兩件事情:
1.?
將
EIP
從當前值改為被調用函數的值。
2.? 將返回地址,也就是當前地址的下條指令放入堆棧。
現在進入
test1
中看個究竟。
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????? ??????
上面的命令基本相同,主要區別在于 test1 內部沒有臨時變量,因此這里只保留了 C0 個自己的空間。
繼續回到主程序:
??? test(10);
00401151? add???????? esp,4
由于 test 函數有一個參數,因此需要首先將參數壓入堆棧中,然后執行與前面相似的操作。
這里有一點需要注意:函數返回之后需要將壓入的參數彈出;可以使用 pop 命令,也可以使用 add 命令來執行。
對于 test3 的調用:
??? 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
其他的相同,但重要的一點是函數的返回值是通過 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
00401181? call??????? _RTC_CheckEsp (4011E0h)
00401186? mov???????? esp,ebp
00401188? pop???????? ebp?
00401189? ret?????????????
前面幾行是將寄存器的值恢復,而 add esp , 0CCh 是將保留的堆棧空間釋放,同時比較 ebp 是否與 esp 相等,如果不相等就提示相應的錯誤,說明有內存泄露等。最后將 ebp 彈出然后返回。
從上面的分析我們可以看到編譯器為我們做了很多事情,包括:堆棧空間分配和釋放、寄存器狀態保存、參數傳遞等。當然這些事情也可以完全由我們自己來完成,那么需要做的是使用關鍵字 naked 來聲明函數。
posted on 2007-01-18 15:08 笨笨 閱讀(2517) 評論(3) 編輯 收藏 引用 所屬分類: 編碼

