程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 關於__stdcall和__cdecl調用方式的理解

關於__stdcall和__cdecl調用方式的理解

編輯:C++入門知識

 __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此類支持可變參數的函數,由於不知道調用者會傳遞多少個參數,也不知道會壓多少個參數入棧,因此函數本身內部不可能清理堆棧,只能由調用者清理了。


 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved