呼叫慣例定義了函式呼叫時參數傳遞順序、堆疊清理責任與返回值處理方式。Microsoft Visual C++ 支援五種慣例:__cdecl、__stdcall、__fastcall、thiscall 與 naked。所有慣例均以 EAX 傳回 32 位元值,64 位元值則用 EDX:EAX 配對。naked 極具彈性,此處不贅述。以下聚焦前四種核心慣例。
| 關鍵字 | 堆疊清理者 | 參數壓入順序 |
|---|---|---|
__cdecl | 呼叫者 | 從右至左 |
__stdcall | 被呼叫者 | 從右至左 |
__fastcall | 被呼叫者 | 從右至左,前兩個參數分別置於 ECX、EDX |
thiscall | 視可變參數而定 | 從右至左 |
__cdecl(C/C++ 預設)printf)。ADD ESP, n 指令,增加程式碼體積。__stdcall(Win32 API 標準)RET n 指令中自動清理堆疊,呼叫端無額外指令。__fastcall(核心層高效呼叫)ECX、EDX 傳遞,減少記憶體存取,效率最高。thiscall(C++ 成員函式預設)this 指標置於 ECX,參數入堆疊,被呼叫者清理。__cdecl 風格,this 最後壓入堆疊,呼叫者清理。DWORD ThreadFunc(LPVOID lpParam) { /* ... */ }
CreateThread(..., (LPTHREAD_START_ROUTINE)ThreadFunc, ...);
若編譯選項未指定 /Gz(強制 __stdcall),編譯器預設產生 __cdecl 版本的 ThreadFunc。CreateThread 期望 __stdcall,執行緒啟動時可能正常,但結束時因堆疊未正確清理(呼叫者以為被呼叫者會清理,實則未清理),導致存取違規或非法指令錯誤。
修正:在函式定義前加上 __stdcall 或專案設定 /Gz。

圖 1:執行緒堆疊運行狀態示意
呼叫前後 ESP 差值區間包含參數表與返回位址。以 int __cdecl func(void* p) 為例,呼叫端組合語言:
PUSH p
CALL func
ADD ESP, 4 ; __cdecl 呼叫者清理
若為 __stdcall,則無 ADD ESP, 4,func 內部以 RET 4 清理。
唯有 __cdecl 支援可變參數,因呼叫者知曉參數個數與大小,方能正確清理堆疊。CRT 於 varargs.h 定義核心巨集:
typedef char *va_list;
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#define va_start(ap) (ap = (va_list)&va_alist)
#define va_arg(ap, t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
#define va_end(ap) (ap = (va_list)0)
va_list 實為 char*,大小等於 CPU 位址寬度(32 位元為 4 位元組)。_INTSIZEOF 依型別大小對齊至 int 倍數。va_start 取得最後一個固定參數在堆疊上的位址。va_arg 先移動指標至下一參數,再解參考取值。va_end 歸零指標,結束取參流程。本質上即:定位堆疊起始位置 → 依型別大小逐個取出參數值。
理解呼叫慣例與堆疊操作,是避免跨模組、跨語言、多執行緒介面錯誤的關鍵。結合實例與 CRT 原始碼分析,可見可變參數表實際上就是對堆疊記憶體的有序走訪與解讀。