高級語言函數調用編譯為機器碼時,CPU 不知道參數個數、類型,無硬件自動保存參數。雙方需約定:
這兩點定義了「調用約定」。本文以 x86 32 位為例深度剖析五大主流約定。
ESP:棧頂指針。EBP:幂幂基址指針 (幂幂框架指針)。PUSH:ESP-=4; [ESP]=val。POP:val=[ESP]; ESP+=4。CALL:隱式 PUSH EIP (返回地址) 並跳轉。RET n:POP EIP; ESP+=n (清理參數)。RET n)。_FuncName@ParamBytes (如 _function@8)。// C/C++ 聲明
int __stdcall Add(int a, int b);
// 調用端彙編
push 2 ; 右參先入
push 1 ; 左參後入
call Add
; 被調用端彙編
Add:
push ebp
mov ebp, esp
mov eax, [ebp+8] ; 參數 a (EBP+8: 舊EBP, 返回地址, a, b)
add eax, [ebp+0Ch] ; 參數 b
pop ebp
ret 8 ; 清理 8 字節參數
ADD ESP, n)。_FuncName (僅前導下劃線,MSVC)。printf)。因調用者知曉參數總大小,可動態清理。; 調用端
push 2
push 1
call _Add
add esp, 8 ; 調用者清理
; 被調用端
_Add:
push ebp
mov ebp, esp
mov eax, [ebp+8]
add eax, [ebp+0Ch]
pop ebp
ret ; 無立即數,不清理棧
ECX、EDX 傳遞,其餘右→左入棧。RET n)。@FuncName@ParamBytes。this 指針放 ECX,參數右→左入棧,被調用者清理 (類 stdcall)。this 最後入棧 (所有參數之後),調用者清理 (類 cdecl)。class A { int f1(int a, int b); int f2(int a, ...); };
A a; a.f1(1,2); a.f2(3,1,2,3);
; f1 調用 (固定參數)
push 2
push 1
lea ecx, [a] ; this 在 ECX
call f1
; f2 調用 (可變參數)
push 3 ; 參數 3
push 2
push 1
push 3 ; 參數 a (計數)
lea eax, [a]
push eax ; this 最後入棧
call f2
add esp, 14h ; 調用者清理
PUSH EBP; MOV EBP,ESP 等)、不允許 return 返回值。RET n。__declspec(naked) int __stdcall Add(int a, int b) {
__asm mov eax, a
__asm add eax, b
__asm ret 8 ; 必須顯式清理棧
}
cdecl,.cpp 為 stdcall → Link 階段可能通過 (名字修飾不同導致 LNK2019),若強制轉型調用則運行時棧崩潰。cdecl,調用端用 WINAPI (stdcall) 定義函數指針 → 參數清理雙方各執一詞,ESP 錯位,後續調用全部崩潰。// DLL 導出 (cdecl)
__declspec(dllexport) int Func(int a, int b);
// 應用層錯誤聲明 (stdcall)
typedef int (WINAPI *DLLFUNC)(int, int); // WINAPI = __stdcall
DLLFUNC p = (DLLFUNC)GetProcAddress(hLib, "Func");
p(1,2); // 災難發生!
| 約定 | 參數順序 | 清理者 | 可變參 | 典型用途 |
|---|---|---|---|---|
cdecl | 右→左 | 調用者 | ✅ | C/C++ 默認、CRT 函數、跨模塊通用 |
stdcall | 右→左 | 被調用者 | ❌ | Win32 API、COM、固定參數 DLL |
fastcall | 寄存器+棧 | 被調用者 | ❌ | 高頻調用熱點、編譯器優化 |
thiscall | ECX+棧 | 視參數定 | ✅(特殊) | C++ 非靜態成員函數 |
naked | 完全手動 | 手動 | 手動 | 驅動、Bootloader、極致優化 |