首先本文關于函數調用約定部分來自轉載和整理,參考文章:
C/C++函數調用約定
函數調用約定解析
一:函數調用約定;
函數調用約定是函數調用者和被調用的函數體之間關于參數傳遞、傳回值傳遞、堆棧清除、寄存器使用的一種約定;
它是需要二進制級别相容的強約定,函數調用者和函數體如果使用不同的調用約定,将可能造成程式執行錯誤,必須把它看作是函數聲明的一部分;
二:常見的函數調用約定;
VC6中的函數調用約定;
調用約定 堆棧清除 參數傳遞
__cdecl 調用者 從右到左,通過堆棧傳遞
__stdcall 函數體 從右到左,通過堆棧傳遞
__fastcall 函數體 從右到左,優先使用寄存器(ECX,EDX),然後使用堆棧
thiscall 函數體 this指針預設通過ECX傳遞,其它參數從右到左入棧
__cdecl是C/C++的預設調用約定; VC的調用約定中并沒有thiscall這個關鍵字,它是類成員函數預設調用約定;(後來的VC 版本是可以使用這個關鍵字的)
C/C++中的main(或wmain)函數的調用約定必須是__cdecl,不允許更改;
預設調用約定一般能夠通過編譯器設定進行更改,如果你的代碼依賴于調用約定,請明确指出需要使用的調用約定;
Delphi6中的函數調用約定;
調用約定 堆棧清除 參數傳遞
register 函數體 從左到右,優先使用寄存器(EAX,EDX,ECX),然後使用堆棧
pascal 函數體 從左到右,通過堆棧傳遞
cdecl 調用者 從右到左,通過堆棧傳遞(與C/C++預設調用約定相容)
stdcall 函數體 從右到左,通過堆棧傳遞(與VC中的__stdcall相容)
safecall 函數體 從右到左,通過堆棧傳遞(同stdcall)
Delphi中的預設調用約定是register,它也是我認為最有效率的一種調用方式,而cdecl是我認為綜合效率最差的一種調用方式;
VC中的__fastcall調用約定一般比register效率稍差一些;
C++Builder6中的函數調用約定;
調用約定 堆棧清除 參數傳遞
__fastcall 函數體 從左到右,優先使用寄存器(EAX,EDX,ECX),然後使用堆棧 (相容Delphi的register)
(register與__fastcall等同)
__pascal 函數體 從左到右,通過堆棧傳遞
__cdecl 調用者 從右到左,通過堆棧傳遞(與C/C++預設調用約定相容)
__stdcall 函數體 從右到左,通過堆棧傳遞(與VC中的__stdcall相容)
__msfastcall 函數體 從右到左,優先使用寄存器(ECX,EDX),然後使用堆棧(相容VC的__fastcall)
常見的函數調用約定中,隻有cdecl約定需要調用者來清除堆棧;
C/C++中的函數支援參數數目不定的參數清單,比如printf函數;由于函數體不知道調用者在堆棧中壓入了多少參數,
是以函數體不能友善的知道應該怎樣清除堆棧,那麼最好的辦法就是把清除堆棧的責任交給調用者;
這應該就是cdecl調用約定存在的原因吧;
VB一般使用的是stdcall調用約定;(ps:有更強的保證嗎)
Windows的API中,一般使用的是stdcall約定;(ps: 有更強的保證嗎)
建議在不同語言間的調用中(如DLL)最好采用stdcall調用約定,因為它在語言間相容性支援最好;
三:函數傳回值傳遞方式
其實,傳回值的傳遞從處理上也可以想象為函數調用的一個out形參數; 函數傳回值傳遞方式也是函數調用約定的一部分;
有傳回值的函數傳回時:一般int、指針等32bit資料值(包括32bit結構)通過eax傳遞,(bool,char通過al傳遞,short通過ax傳遞),特别的__int64等64bit結構(struct) 通過edx,eax兩個寄存器來傳遞(同理:32bit整形在16bit環境中通過dx,ax傳遞); 其他大小的結構(struct)傳回時把其位址通過eax傳回;(是以傳回值類型不是1,2,4,8byte時,效率可能比較差)
參數和傳回值傳遞中,引用方式的類型可以看作與傳遞指針方式相同;
float/double(包括Delphi中的extended)都是通過浮點寄存器st(0)傳回;
========================================================================
本文環境使用 VC 2013 release win32 編譯,禁用優化,非特别說明都是這個環境。
debug release,是否優化,win32 和 x64 選項都對彙編有影響,一定要注意。
先看整個代碼:
int __stdcall FunStdcall(int a, int b, int c)
{
return a + b + c;
}
int __cdecl FunCdecl(int a, int b, int c)
{
return a + b + c;
}
int __fastcall FunFastcall(int a, int b, int c)
{
return a + b + c;
}
int __vectorcall FunVectorcall(int a, int b, int c)
{
return a + b + c;
}
int _tmain(int argc, _TCHAR* argv[])
{
if (1)
{
int a = FunStdcall(1, 2, 3);
int b = FunCdecl(1, 2, 3);
int c = FunFastcall(1, 2, 3);
int d = FunVectorcall(1, 2, 3);
if (a < b)
{
return 1;
}
}
}
在我的VC 版本上發現了這四種約定, thiscall 不讨論。
StdCall
首先看 stdcall 調用部分,也就是 A函數調用B函數中的A。
首先參數按照從右到左的順序依次 push 壓入棧中,然後調用函數,調用結束後将 eax 的值拷貝給指針 ebp-4 處。可以猜測 ebp - 4 就是變量 a 的位址。
不妨就在這裡直接看一下,在螢幕裡面看看:
顯然,ebp-4 就是 a 的位址,那麼誰能保證函數執行完的時候他們依然相等呢?
可以說,這就是函數調用約定的一部分。隻有約定俗成,達成規範,才能保證這一點。
上圖中也可以看到 esp 的值是 0xa30(前面部分不關心,除非剛需要關心)。執行三次 push看看:
發現 esp 變成了 0xa24,比 0xa30 減小了0xC(注意是16進制)。也正是三個 int 的大小。
也就是棧的變化是從大到小,壓棧會導緻棧頂位址減小。記住現在的大小 0xa24
接下來就是 call 了。
對比之前的圖,發現 a esp 都變化了 ebp 沒變。
a 變化的原因是這裡的a 是函數裡面,也就是參數 a,而不是之前的a了。同時會發現這裡的a 的位址就是之前提到的 esp !
因為 push c push b puch a 三個指令執行後,當然 a 位址就是 esp。(這裡 abc 說的是三個參數)。
是以這裡解釋了為什麼 a會變化并且恰好就是之前的 esp。
至于為什麼 esp 會變化?參考:
http://www.360doc.com/content/15/0602/00/12129652_474998519.shtml
也就是說,如果當次函數調用是段内偏移,Call Func 等價于:
push eip
jump Func
是以導緻 esp 變化,減小了 4。現在驗證一下,也就是這時候棧頂存的應該就是 eip。
可以看到 eip = 0xeb38a0。
esp = 0x 5dfa20。跳轉到這個記憶體位址看看:
看到内容是 0xeb38fa 和 0xeb38a0 很像,為什麼不一樣???再仔細想一想,往回看一下:
可以看到 位址 0xeb38a0 其實是被調用函數的位址,而 0xeb38fa 是 call 之後代碼的位址!而打斷點的時候已經進入函數内部了,這時候的 eip (指令指針)已經變化了,随意他們不一緻。
是以這裡代碼可以猜測一下應該是:
push 3;
push 2;
push 1;
push eip;// eip = 0xeb38fa
jmp 0xeb38a0;
// 執行函數
…
// 将 eax 的值給 a
mov dword ptr [ebp-4],eax
進入函數後執行兩條指令:
這裡是把 ebp 壓棧儲存,然後将最新的 esp 當作 ebp。
接下來三條指令就是做加法了,等效于: eax = a + b + c。
等等,這個 abc 哪裡來的?之前 push 3 push 2 push 1 有啥用? 上面兩條有關 ebp 的指令有啥用?
看圖:
可以看到這裡有個選項,再右上角,有個顯示符号名。也就是 abc 其實是友善我們檢視的,并不是本身就是 abc。如果不勾選:變成了 ebp + 8 (0ch/10h)了。回顧整理之前的代碼:
push 3;
push 2;
push 1;
push eip;// eip = 0xeb38fa
jmp 0xeb38a0;
// 執行函數
push ebp;
mov ebp, esp;
eax = a + b + c;// 這裡是用意思代表實際操作
pop ebp;
ret 0ch;
// 将 eax 的值給 a
mov dword ptr [ebp-4],eax
可以看到目前的棧頂 esp 被 ebp儲存了。從棧頂往下的資料分别是:
原來ebp的值 原來eip的值 1 2 3。
是以 ebp + 8/0ch/10h 分别就是 1 2 3。
而 ebp 的一個作用也正是友善去使用參數。
接下來看隻有2 條指令:
pop ebp;
ret 0ch;
參考之前的入棧操作:
push 3;
push 2;
push 1;
push eip;// eip = 0xeb38fa
push ebp;
除了做了一條 ebp 的逆操作,無法還原棧!
顯然重點就在 ret 0Ch這裡了。
看幾張圖:
可以看到 pop ebp 前後導緻 esp 增加了 4。下一句指令就是 ret 0ch了,先記住目前 esp 的值:0x5dfa20
執行語句後:
可以看到 esp 變成l 0x5dfa30。剛好恢複到了 push 3 之前的狀态。
也就是這一條指令做了很多事情,參考:
http://www.360doc.com/content/15/0602/00/12129652_474998519.shtml
其實就是:
pop eip;
pop 3;
pop 2;
pop 1;
// 注意這裡的 pop 3 隻是一種邏輯意義,也就是彈出之前壓入的三個參數。
ret 0ch。稍微準确點是:
pop eip;
add esp 0ch;
看目前狀态:
執行一條語句:
可以看到 确實把 eax 的值給 a了,變成了6。本函數完成!
有了上面的一個詳細的分析,可以大概看一下調用約定的一些差異了:
圖中框出來的是調用方對四次調用的相關代碼,可以看到 push 參數 3 2 1。順序都是一樣的。
也就是最開始說的從右到左的順序。
push 完之後都是一句 call 。再款選的最後都是 mov xxx eax;其實就是對應的四條指派語句。
同時看到 stdcall cdecl 都是 push 3 2 1。三個參數直接入棧。
而 fastcall vectorcall 是把第1 2 個參數分别放到了 ecx edx,第三個參數入棧。
同時看到 cdecl 是調用者做的平衡堆棧:
add esp, 0ch。
猜想:其他都是被調用者做的:
可以看到 cdecl 函數裡面是一句 ret。而其他的是 ret 0ch ret 4(這後面的資料是根據入棧參數個數而定的)。同時發現沒有 vectorcall 對應的函數彙編!因為他和 fastcall 是一個東西,至少這裡是這樣的:
接下來對比一下,用一個 FunStdcall(double, double, double)測試一下:
可以看到執行了三次圖中方框中的類似操作,不難猜測,這其實就是三次:
push double。至少 double 不能直接 push 而已。為例驗證,我們看到最後一次是邏輯意義:
push ds:[4108F8h]。也就是 push 第一個參數 push 1.0f。
觀察監視發現:
*(double*)0x4108F8 确實就是 1.0f
同樣*(double*)esp 是 1.0f。因為剛好把 1.0f 入棧了。
本文隻粗略提供一些思路,也是備忘。後面再深入學習了。