當 C/C++ 程式與作業系統 API、第三方 DLL、組合語言、不同編譯器產出的模組互動時,呼叫雙方必須對參數傳遞、堆疊清理、名稱修飾達成共識。呼叫慣例違配會導致堆疊損毀、存取違規、連結錯誤等難以診斷的問題。本文系統整理主流呼叫慣例機制、名稱修飾規則與實務應用。
| 慣例關鍵字 | 參數壓堆順序 | 堆疊清理者 | 適用場景 | 編譯器命令列 | 名稱修飾 (C 模式) |
|---|---|---|---|---|---|
| __cdecl | 右至左 | 呼叫者 | C/C++ 預設、可變參數函式 | /Gd | _funcname |
| __stdcall | 右至左 | 被呼叫者 | Win32 API、COM、回呼函式 | /Gz | _funcname@N (N=參數總位元組數) |
| __fastcall | 前兩個 DWORD 用 ECX/EDX,其餘右至左壓堆 | 被呼叫者 | 效能關鍵路徑、Borland/Intel 編譯器預設 | /Gr | @funcname@N |
| thiscall | this 指標放 ECX,參數右至左壓堆 | 被呼叫者 | C++ 非靜態成員函式 (MSVC) | 無 (隱含) | C++ 名稱修飾 (見下) |
| naked | 無序曲 (程式設計師全權負責) | 程式設計師 | 裸機程式、中斷服務、手寫組語包裝 | 需配合 __declspec(naked) | 無修飾 |
printf 等),因為只有呼叫者知道實際傳了多少參數。_FunctionName。RET N 指令中清理堆疊 → 固定參數個數,不支援可變參數。WNDPROC) 均採用此慣例。WINAPI 宏定義為 __stdcall,寫可移植程式碼請用 WINAPI。_FunctionName@ParamBytes,例如 _MyFunc@12 表示 3 個 32-bit 參數。ECX, EDX,其餘右至左壓堆。EAX, EDX, ECX (視型別而定)。@FunctionName@ParamBytes;Borland 為 @FunctionName@ParamBytes (類似但細節不同)。this 指標固定放入 ECX (MSVC x86)。RET。C++ 支援函式重載、命名空間、類別成員,連結器需要唯一符號,因此編碼了完整簽章。
基本格式:?修飾名@@編碼簽章@Z
範例:
int __stdcall Foo(char*, unsigned long)
→ ?Foo@@YGHPADK@Z
?? 標識 C++ 修飾名
Foo 函式名
@@YG __stdcall 靜態/全域函式標識
H 回傳型別 int
PAD char* (PA=指標, D=char)
K unsigned long
@Z 結束標記
void __cdecl Bar()
→ ?Bar@@YAXXZ
@@YA __cdecl 標識
X void 回傳
X 無參數
Z 結束
__fastcall 使用 @@YI
成員函式:?MethodName@@QAE... (Q=this 指標, A=非 const, E=const 等)
| 代碼 | 型別 |
|---|---|
| X | void |
| D | char |
| E | unsigned char |
| F | short |
| H | int |
| I | unsigned int |
| J | long |
| K | unsigned long |
| M | float |
| N | double |
| _N | bool |
| P + 型別代碼 | 指標 |
| R + 型別代碼 | 參考 |
| A + 型別代碼 | 陣列 |
| 0 | 重複前一型別 (壓縮) |
| 慣例 | 修飾前綴/後綴 | 參數順序 | 備註 |
|---|---|---|---|
| __cdecl (預設) | 前綴 _,名稱不變 | 右至左壓堆 | 可寫 _cdecl 或 cdecl |
| __fastcall | 前綴 @,後綴 @N | 左至右壓堆 (寄存器優先) | BCB 預設優化慣例 |
| __pascal | 全大寫,無修飾 | 左至右壓堆 | Pascal 相容 |
| __stdcall | 名稱不變 | 右至左壓堆 | Win32 API 相容 |
// MyDll.h
#ifdef MYDLL_EXPORTS
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif
// 強制使用 __stdcall,確保跨語言/編譯器相容
extern "C" MYDLL_API int __stdcall Add(int a, int b);
// 定義檔 .def 替代方案 (不需修飾名)
/*
LIBRARY MyDll
EXPORTS
Add = _Add@8 ; 若用 __stdcall 需指定修飾名
*/
// Win32 回呼必須用 __stdcall (WINAPI)
typedef BOOL (WINAPI *ENUMWINDOWSPROC)(HWND, LPARAM);
// 錯誤範例:預設 __cdecl 會導致堆疊不平衡
// BOOL CALLBACK MyEnumProc(HWND, LPARAM); // 缺少 WINAPI/__stdcall
extern "C" 是否遺漏。/RTCs (執行時堆疊檢查) 可早期發現。dumpbin /exports 或 depends.exe 檢視 DLL 實際匯出修飾名,對照標頭檔宣告。MSVC x64 僅支援 **單一呼叫慣例** (類似 __fastcall):
RCX, RDX, R8, R9XMM0~XMM3__cdecl, __stdcall, __fastcall 關鍵字被忽略 (僅相容原始碼)__stdcall (或 WINAPI) + extern "C"。thiscall),勿手動干預。__fastcall 或內聯,但需實測驗證。dumpbin /symbols, undname, Depends, /RTCs。