C的呼叫慣例

來源:互聯網
上載者:User

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

int function(int a,int b)

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

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

函數調用時,調用者依次把參數壓棧,然後調用函數,函數被調用以後,在堆棧中取得資料,並進行計算。Function Compute結束以後,或者調用者、或者函數本身修改堆棧,使堆棧恢複原裝。在參數傳遞中,有兩個很重要的問題必須得到明確說明:

  1) 當參數個數多於一個時,按照什麼順序把參數壓入堆棧;2) 函數調用後,由誰來把堆棧恢複原裝。

在進階語言中,通過函數呼叫慣例來說明這兩個問題。常見的呼叫慣例有:

stdcallcdeclfastcallthiscallnaked call
stdcall呼叫慣例

stdcall很多時候被稱為pascal呼叫慣例,因為pascal是早期很常見的一種教學用電腦程式設計語言,其文法嚴謹,使用的函數呼叫慣例就是stdcall。在Microsoft C++系列的C/C++編譯器中,常常用PASCAL宏來聲明這個呼叫慣例,類似的宏還有WINAPI和CALLBACK。

stdcall呼叫慣例聲明的文法為(以前文的那個函數為例):

int __stdcall function(int a,int b)

stdcall的呼叫慣例意味著:

  1)參數從右向左壓入堆棧;2)函數自身修改堆棧;3)函數名自動加前置的底線,後面緊跟一個@符號,其後緊跟著參數的尺寸。

以上述這個函數為例,參數b首先被壓棧,然後是參數a,函數調用function(1,2)調用處翻譯成組合語言將變成:

  push 2          第二個參數入棧  push 1          第一個參數入棧  call function   調用參數,注意此時自動把cs:eip入棧

而對於函數自身,則可以翻譯為:

  push  ebp               儲存ebp寄存器,該寄存器將用來儲存堆棧的棧頂指標,可以在函數退出時恢複  mov   ebp,esp           儲存堆棧指標  mov   eax,[ebp + 8H]    堆棧中ebp指向位置之前依次儲存有ebp,cs:eip,a,b,ebp +8指向a  add   eax,[ebp + 0CH]   堆棧中ebp + 12處儲存了b  mov   esp,ebp           恢複esp  pop   ebp  ret   8

而在編譯時間,這個函數的名字被翻譯成_function@8

注意不同編譯器會插入自己的彙編代碼以提供編譯的通用性,但是大體代碼如此。其中在函數開始處保留esp到ebp中,在函數結束恢複是編譯器常用的方法。

從函數調用看,2和1依次被push進堆棧,而在函數中又通過相對於ebp(即剛進函數時的堆棧指標)的位移量存取參數。函數結束後,ret 8表示清理8個位元組的堆棧,函數自己恢複了堆棧。

cdecl呼叫慣例

cdecl呼叫慣例又稱為C呼叫慣例,是C語言預設的呼叫慣例,它的定義文法是:

  int function (int a ,int b)           // 不加修飾就是C呼叫慣例  int __cdecl function(int a,int b)     // 明確指出C呼叫慣例

在寫本文時,出乎我的意料,發現cdecl呼叫慣例的參數壓棧順序是和stdcall是一樣的,參數首先由有向左壓入堆棧。所不同的是,函數本身不清理堆棧,調用者負責清理堆棧。由於這種變化,C呼叫慣例允許函數的參數的個數是不固定的,這也是C語言的一大特色。對於前面的function函數,使用cdecl後的彙編碼變成:

  調用處  push   1  push   2  call   function  add    esp,8              注意:這裡調用者在恢複堆棧  被調用函數_function處  push   ebp                儲存ebp寄存器,該寄存器將用來儲存堆棧的棧頂指標,可以在函數退出時恢複  mov    ebp,esp            儲存堆棧指標  mov    eax,[ebp + 8H]     堆棧中ebp指向位置之前依次儲存有ebp,cs:eip,a,b,ebp +8指向a  add    eax,[ebp + 0CH]    堆棧中ebp + 12處儲存了b  mov    esp,ebp            恢複esp  pop    ebp  ret                       注意,這裡沒有修改堆棧

MSDN中說,該修飾自動在函數名前加前置的底線,因此函數名在符號表中被記錄為_function,但是我在編譯時間似乎沒有看到這種變化。

由於參數按照從右向左順序壓棧,因此最開始的參數在最接近棧頂的位置,因此當採用不定個數參數時,第一個參數在棧中的位置肯定能知道,只要不定的參數個數能夠根據第一個後者後續的明確的參數確定下來,就可以使用不定參數,例如對於CRT中的sprintf函數,定義為:

int sprintf(char* buffer,const char* format,...)

由於所有的不定參數都可以通過format確定,因此使用不定個數的參數是沒有問題的。

fastcall

fastcall呼叫慣例和stdcall類似,它意味著:

  1) 函數的第一個和第二個DWORD參數(或者尺寸更小的)通過ecx和edx傳遞,其他參數通過從右向左的順序壓棧;2) 被調用函數清理堆棧;3) 函數名修改規則同stdcall。

其聲明文法為:int fastcall function(int a,int b)

thiscall

thiscall是唯一一個不能明確指明的函數修飾,因為thiscall不是關鍵字。它是C++類成員函數預設的呼叫慣例。由於成員函數調用還有一個this指標,因此必須特殊處理,thiscall意味著:

  1) 參數從右向左入棧;2) 如果參數個數確定,this指標通過ecx傳遞給被調用者;如果參數個數不確定,this指標在所有參數壓棧後被壓入堆棧;3) 對參數個數不定的,調用者清理堆棧,否則函數自己清理堆棧。

為了說明這個呼叫慣例,定義如下類和使用代碼:

class A{public:int function1(int a,int b);int function2(int a,...);};int A::function1 (int a,int b){return a+b;}int A::function2(int a,...){va_list ap;va_start(ap,a);int i;int result = 0;for(i = 0 ; i < a ; i ++){result += va_arg(ap,int);}return result;}void callee(){A a;a.function1(1,2);a.function2(3,1,2,3);}

callee函數被翻譯成彙編後就變成:

  // 函數function1調用  0401C1D    push        2  00401C1F   push        1  00401C21   lea         ecx,[ebp-8]  00401C24   call   function1             注意,這裡this沒有被入棧  // 函數function2調用  00401C29   push        3  00401C2B   push        2  00401C2D   push        1  00401C2F   push        3  00401C31   lea         eax,[ebp-8]      這裡引入this指標  00401C34   push        eax  00401C35   call   function2  00401C3A   add         esp,14h

可見,對於參數個數固定情況下,它類似於stdcall,不定時則類似cdecl

naked call

這是一個很少見的呼叫慣例,一般程式設計者建議不要使用。編譯器不會給這種函數增加初始化和清理代碼,更特殊的是,你不能用return返回傳回值,只能用插入彙編返回結果。這一般用於實模式驅動程式設計,假設定義一個求和的加法程式,可以定義為:

   __declspec(naked) int  add(int a,int b)   {       __asm mov eax,a       __asm add eax,b       __asm ret   }

注意,這個函數沒有顯式的return傳回值,返回通過修改eax寄存器實現,而且連退出函數的ret指令都必須顯式插入。上面代碼被翻譯成彙編以後變成:

   mov    eax,[ebp+8]   add    eax,[ebp+12]   ret    8

注意這個修飾是和__stdcall及cdecl結合使用的,前面是它和cdecl結合使用的代碼,對於和stdcall結合的代碼,則變成:

   __declspec(naked) int __stdcall function(int a,int b)   {       __asm mov eax,a       __asm add eax,b       __asm ret 8        //注意後面的8   }

至於這種函數被調用,則和普通的cdecl及stdcall調用函數一致。

函數呼叫慣例導致的常見問題

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

1) 函數原型聲明和函數體定義不一致2) DLL匯入函數時聲明了不同的函數約定

以後者為例,假設我們在dll中聲明了一種函數為:

   __declspec(dllexport) int func(int a,int b);    //注意,這裡沒有stdcall,使用的是cdecl

使用時代碼為:

   ...   typedef int (*WINAPI DLLFUNC)func(int a,int b);   hLib = LoadLibrary(...);   DLLFUNC func = (DLLFUNC)GetProcAddress(...);   //這裡修改了呼叫慣例   result = func(1,2);  //導致錯誤   ...

由於調用者沒有理解WINAPI的含義錯誤的增加了這個修飾,上述代碼必然導致堆棧被破壞,MFC在編譯時間插入的checkesp函數將告訴你,堆棧被破壞了。

關於WINAPI和CALLBACK兩個宏:

WINAPI 
·Use in place of FAR PASCAL in API declarations. If you are writing a DLL with exported API entry points, you can use this for your own APIs. 

CALLBACK
·Use in place of FAR PASCAL in application callback routines such as window procedures and dialog procedures. 

再看看到底這兩個宏的內容是什麼吧

VC:WINDEF.h
#define CALLBACK    PASCAL //=_pascal,VC已經不支援直接使用_pascal了
#define WINAPI      CDECL //=_cdecl

BCB:windef.h
#define CALLBACK    __stdcall
#define WINAPI      __stdcall

引出了cdecl stdcall等一些可能很少見的關鍵字

那麼cdecl、pascal、stdcall、fastcall等修飾符號到底什麼意思呢?
非常簡單,就是關於堆棧的一些說明,首先是函數參數壓棧順序,其次是
壓入堆棧的內容由誰來清除,調用者還是函數自己?
這些開關用來告訴編譯器產生什麼樣的彙編代碼。

下面把區別列表如下:

Directive Parameter order  Clean-up Passes parameters in registers?
 register  Left-to-right   Routine   Yes
 pascal   Left-to-right   Routine   No
 cdecl   Right-to-left   Caller    No
 stdcall   Right-to-left   Routine   No
 safecall  Right-to-left   Routine   No

簡單說明:

__cdecl是C/C++和MFC程式預設使用的呼叫慣例,也可以在函式宣告時加上__cdecl關鍵字來手工指定。採用__cdecl約定時,函數參數按照從右至左的順序入棧,並且由調用函數者把參數彈出棧以清理堆棧。因此,實現可變參數的函數只能使用該呼叫慣例。由於每一個使用__cdecl約定的函數都要包含清理堆棧的代碼,所以產生的可執行檔大小會比較大。__cdecl可以寫成_cdecl。
        __stdcall呼叫慣例用於調用Win32 API函數。採用__stdcal約定時,函數參數按照從右至左的順序入棧,被調用的函數在返回前清理傳送參數的棧,函數參數個數固定。由於函數體本身知道傳進來的參數個數,因此被調用的函數可以在返回前用一條ret n指令直接清理傳遞參數的堆棧。__stdcall可以寫成_stdcall。
        __fastcall約定用於對效能要求非常高的場合。__fastcall約定將函數的從左邊開始的兩個大小不大於4個位元組(DWORD)的參數分別放在ECX和EDX寄存器,其餘的參數仍舊自右向左壓棧傳送,被調用的函數在返回前清理傳送參數的堆棧。__fastcall可以寫成_fastcall。 

·特別說明
1. 在預設情況下,採用__cdecl方式,因此可以省略.
2. WINAPI一般用於修飾動態連結程式庫中匯出函數
3. CALLBACK僅用於修飾回呼函數
4. 你可能已經發現,VC下和BCB下對WINAPI的定義不同,那麼你至少理解了
   為什麼不能直接從BCB下調用VC的dll的一個原因了。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.