__stdcall和__cdecl都是函數調用約定關鍵字,先給出這兩者的區別,然後舉實例分析:
__stdcall:參數由右向左壓入堆棧;堆棧由函數本身清理。
__cdecl:參數也是由右向左壓入堆棧;但堆棧由調用者清理。
另外,這兩者在同一名字修飾約定下,編譯過後變量和函數的名字也不一樣,具體見另一博文:名字修飾約定extern "C"與extern "C++"淺析
下面給出實例分析:
[cpp]
#include "stdio.h"
#include <iostream>
#include <Windows.h>
#include <conio.h>
using namespace std;
int __stdcall Func_stdcall(int nParam1, int nParam2)
{
return 1;
}
int __cdecl Func_cdecl(int nParam1, int nParam2)
{
return 1;
}
int main()
{
int a = Func_stdcall(1, 2);
a = Func_cdecl(1, 2);
return 0;
}
#include "stdio.h"
#include <iostream>
#include <Windows.h>
#include <conio.h>
using namespace std;
int __stdcall Func_stdcall(int nParam1, int nParam2)
{
return 1;
}
int __cdecl Func_cdecl(int nParam1, int nParam2)
{
return 1;
}
int main()
{
int a = Func_stdcall(1, 2);
a = Func_cdecl(1, 2);
return 0;
}
以上代碼在XP + VC++6.0 SP6環境下編譯,編譯後的匯編代碼如下:
首先要明確上圖匯編代碼中幾個指令的作用:
1.call:將call下一條指令的EIP壓入堆棧,然後跳到@後標號地址處執行;
2.ret:將堆棧的當前數據彈出給EIP,然後繼續執行;
3.ret n:n表示一個整數,將堆棧的當前數據彈出給EIP,再將ESP的值加上n,然後繼續執行。
我們再看匯編代碼,調用Func_stdcall和Func_cdecl時,都是由調用者(main函數)將參數壓入堆棧,注意地址0x00401127、0x00401129和0x00401133、0x00401135都是先壓入2,再壓入1,這個順序就是函數參數由右向左的順序。
再注意地址0x0040110F,這是調用Func_stdcall時的出口指令,"ret 8"先把EIP的值彈出,然後再將ESP的值加8,相當於執行兩次出棧的操作。因為編譯環境是32位的,調用Func_stdcall時壓入的2和1,其實是壓入的兩個32位整數值,剛好占8個字節。然後再繼續執行EIP處的指令,此時EIP的值應為0x00401130,為call指令的下一條指令,這條指令是將返回的值賦給變量a。可見,堆棧的清理是由Func_stdcall內部處理的,外部調用者並不處理。
然後再來看看__cdecl修飾的Func_cdecl,注意地址0x0040111B,只有一個指令“ret”,只將堆棧當前的值彈出給EIP,然後繼續執行。但是在調用前已經壓入了兩個32位的整數值,堆棧還沒有被清理。我們再來看看繼續執行的指令,地址0x0040113C處的指令為繼續執行的指令,指令為“add esp,8“,這個很好理解了,直接將esp的值加上8,也相當於執行兩次出棧操作。但這是由調用者(main參數)進行的,因此堆棧是由調用者進行清理的。
__stdcall通常用於Windows API中,可見如下代碼:
[cpp]
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
#define cdecl _cdecl
#ifndef CDECL
#define CDECL _cdecl
#endif
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
#define cdecl _cdecl
#ifndef CDECL
#define CDECL _cdecl
#endif
而C和C++程序的缺省調用方式則為__cdecl,下圖為VC++6.0的默認設置,因此在不顯式寫明調用約定的情況下,一般都是采用__cdecl方式,而在與Windows API打交道的場景下,通常都是顯式的寫明使用__stdcall,才能與Windows API保持一致。
另外,還要注意的是,如printf此類支持可變參數的函數,由於不知道調用者會傳遞多少個參數,也不知道會壓多少個參數入棧,因此函數本身內部不可能清理堆棧,只能由調用者清理了。