本文檔譯自 www.codeproject.com 的文章 "Calling Conventions Demystified",作者 Nemanja Trifunovic,原文參見此處 引言 - Introduction 在學習 Windows 編程的漫長、艱難而美妙的旅途中,你可能會對函數聲明前出 ...
本文檔譯自 www.codeproject.com 的文章 "Calling Conventions Demystified",作者 Nemanja Trifunovic,原文參見此處
引言 - Introduction
在學習 Windows 編程的漫長、艱難而美妙的旅途中,你可能會對函數聲明前出現的奇怪說明符感到好奇,比如 __cdecl
、__stdcall
、__fastcall
、WINAPI
等等。在閱讀過 MSDN 或其他參考資料之後,你可能知道了這些說明符是用來為函數指定一種叫“調用約定”的東西。在這篇文章中,我會使用 Visual C++ 來向你解釋不同的調用約定。我要強調的是,上面提到的說明符是微軟特有的,如果你想編寫可移植代碼,就不應該使用它們。
那麼,調用約定究竟是什麼呢?當我們調用函數時,通常會將參數傳遞給它,並獲得返回值。而調用約定就描述了參數是如何傳遞、值是如何從函數返回的。它還指定了函數名稱的修飾方式。不過,編寫優秀的 C/C++ 程式真的一定要瞭解調用約定嗎?並不是。但是,它可能有助於調試。此外,如果要把 C/C++ 與彙編代碼鏈接,那麼這也有幫助。
要理解本文,你需要具備彙編編程的一些非常基本的知識。
無論使用哪種調用約定,都會發生以下情況:
- 所有參數都被擴展到 4 位元組(除非特別說明,預設在 Win32 上),並放入記憶體的適當位置,這些位置通常在棧上。不過它們也可能被放在寄存器中,這便是通過調用約定指定的。
- 程式執行流會跳轉到被調用函數的地址。
- 在函數內部,寄存器 ESI、EDI、EBX 和 EBP 的值被保存在棧上。執行這些操作的代碼部分稱為 function prolog,通常由編譯器生成。
- 執行函數代碼,並將返回值放入 EAX 寄存器中。
- 寄存器 ESI、EDI、EBX 和 EBP 的值從棧中恢復。執行此操作的代碼段稱為 function epilog,與 function prolog 一樣,在大多數情況下,它由編譯器生成。
- 參數從棧中移除。此操作稱為清棧(stack cleanup),可以在被調用函數的內部執行,也可以由調用方執行,具體取決於所使用的調用約定。
作為調用約定的例子(不考慮 this
),我們將使用一個簡單的函數:
int sumExample (int a, int b)
{
return a + b;
}
對這個函數的調用看起來像這樣:
int c = sum (2, 3);
對於使用 __cdecl
、__stdcall
、__fastcall
的例子,我會把示例代碼編譯成 C 代碼。本文後面提到的函數名修飾用的是 C 的修飾方法。C++ 的名稱修飾方法超出了本文的討論範圍。
C 調用約定 - C calling convention (__cdecl)
這個約定是 C/C++ 的預設調用約定。如果項目被設置成使用其他的調用約定,我們也可以通過顯式聲明 __cdecl
來為某個函數指定:
int __cdecl sumExample (int a, int b);
__cdecl
調用約定的主要特點是:
- 參數將從右到左依次壓入棧中。
- 由調用者執行清棧。
- 函數名用下劃線字元
_
作為首碼進行修飾。
現在,示例函數的調用看起來像這樣:
; // 參數從右到左依次壓入棧中
push 3
push 2
; // 調用函數
call _sumExample
; // 增加參數的總大小到 ESP 寄存器(向高位移動棧指針),以此來清理堆棧
add esp,8
; // 將 EAX 的返回值複製到局部變數 (int c)
mov dword ptr [c],eax
被調用函數 sumExample
的內部如下所示:
; // function prolog
push ebp
mov ebp,esp
sub esp,0C0h
push ebx
push esi
push edi
lea edi,[ebp-0C0h]
mov ecx,30h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
; // return a + b;
mov eax,dword ptr [a]
add eax,dword ptr [b]
; // function epilog
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
標準調用約定 - Standard calling convention (__stdcall)
這個調用約定常常用在 Win32 API 的函數上。事實上,WINAPI
只是 __stdcall
的另一個名稱。
#define WINAPI __stdcall
同樣,可以為一個函數顯式指定標準調用約定:
int __stdcall sumExample (int a, int b);
我們也可以使用編譯器選項 /Gz
來給所有未顯式聲明約定的函數指定 __stdcall
。
__stdcall
調用約定的主要特點是:
- 參數將從右到左依次壓入棧中。
- 由被調用的函數執行清棧。
- 函數名通過添加下劃線
_
和@
字元和所需的堆棧空間位元組數來修飾。
調用示例如下:
; // 參數從右到左依次壓入棧中
push 3
push 2
; // 調用函數
call _sumExample@8
; // 將 EAX 的返回值複製到局部變數 (int c)
mov dword ptr [c],eax
函數如下所示:
; // 此處是 function prolog (和 __cdecl 的例子一樣,略過)
; // return a + b;
mov eax,dword ptr [a]
add eax,dword ptr [b]
; // 此處是 function epilog (和 __cdecl 的例子一樣,略過)
; // 清棧並返回控制流
ret 8
因為棧由被調用的函數清理,所以通常 __stdcall
調用約定創建的可執行文件比 __cdecl
要小。因為在 __cdecl
中,必須為每個函數調用生成清棧的代碼。另一方面,參數數量可變的函數(如 printf()
)必須使用 __cdecl
,因為只有調用者知道函數調用中的參數數量;所以,也只有調用方纔能執行清棧。
Fast 調用約定 - Fast calling convention (__fastcall)
__fastcall
指出,只要有可能,參數就應該放在寄存器中,而不是棧中。這減少了函數調用的成本,因為使用寄存器的操作比使用堆棧的操作要快。
我們可以顯式聲明 __fastcall
來使用約定,如下所示:
int __fastcall sumExample (int a, int b);
我們也可以使用編譯器選項 /Gr
來給所有未顯式聲明約定的函數指定 __fastcall
。
__fastcall
的主要特點是:
- 需要 32 位大小(及以下)的前兩個函數參數被放入寄存器 ECX 和 EDX。其餘的從右向左壓入堆棧。
- 被調用的函數負責從堆棧中彈出參數。
- 函數名通過在開頭添加
@
字元並附加@
和參數所需的位元組數(十進位)來修飾。
註意:Microsoft 保留在未來的編譯器版本中更改傳遞參數的寄存器的權利。
調用例子如下:
; // 將參數放入寄存器 EDX 和 ECX 中
mov edx,3
mov ecx,2
; // 調用函數
call @fastcallSum@8
; // 從寄存器 EAX 拷貝返回值到局部變數 (int c)
mov dword ptr [c],eax
函數內部:
; // function prolog
push ebp
mov ebp,esp
sub esp,0D8h
push ebx
push esi
push edi
push ecx
lea edi,[ebp-0D8h]
mov ecx,36h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
pop ecx
mov dword ptr [ebp-14h],edx
mov dword ptr [ebp-8],ecx
; // return a + b;
mov eax,dword ptr [a]
add eax,dword ptr [b]
;// function epilog
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
這個調用約定究竟和 __cdecl
、__stdcall
相比有多快呢?你可以自己尋找答案。通過聲明不同的約定,再比較執行時間看看吧。我沒有發現 __fastcall
比其他調用約定更快,不過你可能會得出不同的結論。
Thiscall - Thiscall
Thiscall
是調用 C++ 類成員函數的預設調用約定(參數數量可變的除外)。
這種約定的主要特點是:
- 參數將從右到左依次壓入棧中。
this
被放在 ECX 寄存器中。 - 由被調用的函數執行清棧。
這個調用約定的例子有點不同。首先,代碼被編譯為 C++,而不是 C。其次,我們用一個帶有成員函數的結構體,而不是用自由函數。
struct CSum
{
int sum ( int a, int b) {return a+b;}
};
函數調用的彙編代碼如下所示:
push 3
push 2
lea ecx,[sumObj]
call ?sum@CSum@@QAEHHH@Z ; CSum::sum
mov dword ptr [s4],eax
函數內部如下所示:
push ebp
mov ebp,esp
sub esp,0CCh
push ebx
push esi
push edi
push ecx
lea edi,[ebp-0CCh]
mov ecx,33h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
pop ecx
mov dword ptr [ebp-8],ecx
mov eax,dword ptr [a]
add eax,dword ptr [b]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8
如果我們有一個成員函數使用可變數量參數會發生什麼?在這種情況下,會使用 __cdecl
,this
最後被壓入棧。
總結 - Conclusion
長話短說,我們總結調用約定之間的主要區別:
__cdecl
是 C 和 C++ 程式的預設調用約定。這種調用約定的優點是,它允許使用具有可變數量參數的函數。缺點是它會創建更大的可執行文件。__stdcall
多用於 Win32 API 函數。它不允許函數具有可變數量的參數。__fastcall
嘗試將參數放在寄存器中,而不是堆棧中,從而使函數調用更快。Thscall
調用約定是不使用可變參數的 C++ 成員函數使用的預設調用約定。
在大多數情況下,這就是你需要瞭解的關於調用約定的全部內容。