理解 __cdecl/__stdcall/__fastcall 的棧操作差異,掌握 C++ 名稱修飾規則,實現跨語言無縫互操作
調用約定規定了函數調用時的參數入棧順序、棧清理責任方、返回值傳遞方式、名稱修飾規則。不同語言/編譯器默認約定不同,跨模塊調用必須統一。
測試代碼:
int __cdecl Add_cdecl(int a, int b);
int __stdcall Add_stdcall(int a, int b);
int __fastcall Add_fastcall(int a, int b, int c, int d); // 前兩個進寄存器
__cdecl (C/C++ 默認)
; 調用端
push 2 ; 參數右→左入棧
push 1
call Add_cdecl
add esp, 8 ; 調用者清理棧 (2 參數 × 4 字節)
; 函數體 (簡化)
Add_cdecl:
push ebp
mov ebp, esp
mov eax, [ebp+8] ; a
add eax, [ebp+12] ; b
pop ebp
ret ; 僅返回,不清理棧
特點:支持可變參數 (printf)、調用者清棧、修飾名 _FuncName。
__stdcall (Win32 API / COM 標準)
; 調用端
push 2
push 1
call Add_stdcall
; 無 add esp, 8 — 由被調用者清理
; 函數體
Add_stdcall:
push ebp
mov ebp, esp
mov eax, [ebp+8]
add eax, [ebp+12]
pop ebp
ret 8 ; 返回時清理 8 字節參數棧
特點:固定參數數量、被調用者清棧 (ret n)、修飾名 _FuncName@n (n=參數字節數)。
__fastcall (高性能內部調用)
; 調用端 Add_fastcall(1, 2.0, 3, 4)
push 4 ; 第 4 參數入棧
mov edx, 3 ; 第 3 參數 → EDX
push 40000000h ; 第 2 參數 (double 2.0) 高 32 位
push 0 ; 第 2 參數低 32 位
mov ecx, 1 ; 第 1 參數 → ECX
call Add_fastcall
; 函數體
Add_fastcall:
push ebp
mov ebp, esp
push ecx ; 保存第 1 參數
mov [ebp-8], edx ; 保存第 3 參數
; 第 2 參數已在棧上 [ebp+8]/[ebp+12] (double 佔 8 字節)
; 計算...
pop ecx
mov esp, ebp
pop ebp
ret 0Ch ; 清理棧上 12 字節 (參數 3,4)
特點:前兩個 ≤4 字節參數進 ECX/EDX、其餘進棧、被調用者清棧、修飾名 @FuncName@n。
為支持重載、命名空間、類成員,C++ 編譯器將函數簽名編碼進符號名。MSVC 修飾規則(簡化):
// 例子
void Func(int); // ?Func@@YAXH@Z
void Func(int, double); // ?Func@@YAXHN@Z
class A { void Func(); }; // ?Func@A@@QAEXXZ
namespace N { void Func(); } // ?Func@N@@YAXXZ
關鍵點:
extern "C" 禁用 C++ 修飾,生成 C 風格名 (_Func@n 或 Func),跨語言導出必加。
// MyDLL.h
#ifdef MYDLL_EXPORTS
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif
// 導出 C 接口:extern "C" + __stdcall + DLL_API
extern "C" DLL_API int __stdcall Add(int a, int b);
extern "C" DLL_API int __stdcall Sub(int a, int b);
// 導出 C++ 類:類級別 dllexport/dllimport
#ifdef MYDLL_EXPORTS
#define DLL_CLASS __declspec(dllexport)
#else
#define DLL_CLASS __declspec(dllimport)
#endif
class DLL_CLASS Math {
public:
static int Mul(int a, int b); // 靜態成員也需類導出
int AddMember(int a, int b);
};
| 現象 | 原因 | 解決 |
|---|---|---|
| Run-Time Check Failure #0 (棧指針損壞) | 調用約定不匹配 (__cdecl vs __stdcall) | 雙方統一聲明 __stdcall,函數指針類型含 __stdcall |
| Linker LNK2019: 無法解析的外部符號 _Func@12 | 調用方期望 __stdcall,DLL 導出 __cdecl | DLL 加 __stdcall,或調用方改 __cdecl |
| GetProcAddress 返回 NULL | 名稱修飾導致符號不匹配 | 用 extern "C" + .def 文件固定導出名 |
| C# DllImport EntryPointNotFound | C# 默認 CallingConvention.StdCall,C++ 導出 __cdecl | C++ 加 __stdcall,或 C# 指定 CallingConvention.Cdecl |
dumpbin /exports MyDLL.dll (VS 命令提示符)Depends.exe 圖形化查看undname 解碼修飾名:undname ?Func@@YAXH@Z下一節將講解:.def 文件高級用法、導出變量/類的二進制佈局、跨編譯器 ABI 兼容策略。