天天看點

C語言的函數調用約定(stdcall+cdecl+thiscall+fastcall)

在C語言中,假設我們有這樣的一個函數:

int function(int a,int b)
           

調用時隻要用result = function(1,2)這樣的方式就可以使用這個函數。

但是,當進階語言被編譯成計算機可以識别的機器碼時,有一個問題就凸現出來:在CPU中,計算機沒有辦法知道一個函數調用需要多少個、什麼樣的參數,也沒有硬體可以儲存這些參數。也就是說,計算機不知道怎麼給這個函數傳遞參數,傳遞參數的工作必須由函數調用者和函數本身來協調。為此,計算機提供了一種被稱為棧的資料結構來支援參數傳遞。

棧的簡述:是一種先進後出的資料結構,棧有一個存儲區、一個棧頂指針。棧頂指針指向堆棧中第一個可用的資料項(被稱為棧頂)。使用者可以在棧頂上方向棧中加入資料,這個操作被稱為壓棧(Push),壓棧以後,棧頂自動變成新加入資料項的位置,棧頂指針也随之修改。使用者也可以從堆棧中取走棧頂,稱為彈出棧(pop),彈出棧後,棧頂下的一個元素變成棧頂,棧頂指針随之修改。

函數調用時,調用者依次把參數壓棧,然後調用函數,函數被調用以後,在堆棧中取得資料,并進行計算。函數計算結束以後,或者調用者、或者函數本身修改堆棧,使堆棧恢複原裝。

在參數傳遞中,有兩個很重要的問題必須得到明确說明:

  • 當參數個數多于一個時,按照什麼順序把參數壓入堆棧
  • 函數調用後,由誰來把堆棧恢複原裝

在進階語言中,通過函數調用約定來說明這兩個問題。

函數調用約定

是函數調用者和被調用的函數體之間關于參數傳遞、傳回值傳遞、堆棧清除、寄存器使用的一種約定。

常見的調用約定有:

  • stdcall(pascal)
  • cdecl
  • fastcall
  • thiscall
  • naked call

簡述:

  1. stdcall(pascal)–Standard Call的縮寫,C++的标準調用方式

    在Microsoft C++系列的C/C++編譯器中,常常用PASCAL宏來聲明這個調用約定,類似的宏還有WINAPI和CALLBACK。

    一般WIN32的函數都是__stdcall

    聲明的文法為:

    int __stdcall function(int a,int b)
               
    stdcall的調用約定意味着:
    • 參數從右向左壓入堆棧
    • 函數自身清理堆棧
    • 函數名自動加前導的下劃線,後面緊跟一個@符号,其後緊跟着參數的尺寸
  2. cdecl–C Declaration的縮寫,C語言預設的調用約定

    聲明的文法為:

    int function (int a ,int b) //不加修飾就是C調用約定 
    int __cdecl function(int a,int b)//明确指出C調用約定
               
    cdecl調用約定意味着:
    • 參數從右向左壓入堆棧
    • 調用者負責清理堆棧
    • C調用約定允許函數的參數的個數是不固定的,這也是C語言的一大特色。
    • 僅在函數名前加上一個下劃線字首,格式為_functionname。
  3. fastcall

    聲明的文法為:

    int fastcall function(int a,int b)
               
    fastcall調用約定意味着:
    • 函數的第一個和第二個DWORD參數(或者尺寸更小的)通過ecx和edx傳遞,其他參數通過從右向左的順序壓棧
    • 函數自身清理堆棧
    • 函數名修改規則同stdcall:函數名自動加前導的下劃線,後面緊跟一個@符号,其後緊跟着參數的尺寸
  4. thiscall–C++類成員函數預設的調用約定。

    thiscall是唯一一個不能明确指明的函數修飾,因為thiscall不是關鍵字。由于成員函數調用還有一個this指針,是以必須特殊處理,thiscall意味着:

    • 參數從右向左入棧
    • 如果參數個數不确定,this指針在所有參數壓棧後被壓入堆棧。如果參數個數确定,this指針通過ecx傳遞給被調用者。
    • 如果參數個數不确定,調用者清理堆棧,否則函數自己清理堆棧
    對于參數個數固定情況下,類似于stdcall,不定時則類似cdecl
  5. naked call

    一個很少見的調用約定,一般用于實模式驅動程式設計

C++編譯時函數名修飾約定規則:

  • __stdcall調用約定:
    • 以“?”辨別函數名的開始,後跟函數名;
    • 函數名後面以“@@YG”辨別參數表的開始,後跟參數表;
    • 參數表以代号表示:
      1. X–void ,
      2. D–char,
      3. E–unsigned char,
      4. F–short,
      5. H–int,
      6. I–unsigned int,
      7. J–long,
      8. K–unsigned long,
      9. M–float,
      10. N–double,
      11. _N–bool,
      12. PA–表示指針,後面的代号表明指針類型,如果相同類型的指針連續出現,以“0”代替,一個“0”代表一次重複

    • 參數表的第一項為該函數的傳回值類型,其後依次為參數的資料類型,指針辨別在其所指資料類型前;
    • 參數表後以“@Z”辨別整個名字的結束,如果該函數無參數,則以“Z”辨別結束。
    例如:
    int Test1(char *var1,unsigned long)-----“[email protected]@YGHPADK@Z” 
    void Test2() -----“[email protected]@YGXXZ” 
               
  • __cdecl調用約定:

    規則同上面的_stdcall調用約定,隻是參數表的開始辨別由上面的“@@YG”變為“@@YA”。

  • __fastcall調用約定:

    規則同上面的_stdcall調用約定,隻是參數表的開始辨別由上面的“@@YG”變為“@@YI”。

函數調用約定導緻的常見問題

如果定義的約定和使用的約定不一緻,則将導緻堆棧被破壞,導緻嚴重問題,下面是兩種常見的問題:

  1. 函數原型聲明和函數體定義不一緻
  2. DLL導入函數時聲明了不同的函數約定

調用函數的代碼和被調函數必須采用相同的函數的調用約定,程式才能正常運作。

繼續閱讀