函數調用約定和堆棧

函數調用約定和堆棧

1 什麼是堆棧

編譯器通常使用堆棧實現函數調用。堆棧是存儲器的一個區域,嵌入式環境有時須要程序員本身定義一個數組做爲堆棧。Windows爲每一個線程自動維護一個堆棧,堆棧的大小能夠設置。編譯器使用堆棧來堆放每一個函數的參數、局部變量等信息。程序員

函數調用常常是嵌套的,在同一時刻,堆棧中會有多個函數的信息,每一個函數佔用一個連續的區域。一個函數佔用的區域被稱做幀(frame)。數組

編譯器從高地址開始使用堆棧。 假設咱們定義一個數組a[1024]做爲堆棧空間,一開始棧頂指針指向a[1023]。若是棧裏有兩個函數a和b,且a調用了b,棧頂指針會指向函數b的 幀。若是函數b返回。棧頂指針就指向函數a的幀。若是在棧裏放了太多東西形成溢出,破壞的是a[0]上面的東西。多線程

在多線程(任務)環境,CPU的堆棧指針指向的存儲器區域就是當前使用的堆棧。切換線程的一個重要工做,就是將堆棧指針設爲當前線程的堆棧棧頂地址。函數

不一樣CPU,不一樣編譯器的堆棧佈局、函數調用方法均可能不一樣,但堆棧的基本概念是同樣的。佈局

2 函數調用約定

函數調用約定包括傳遞參數的順序,誰負責清理參數佔用的堆棧等,例如 :線程

  參數傳遞順序 誰負責清理參數佔用的堆棧
__pascal 從左到右 調用者
__stdcall 從右到左 被調函數
__cdecl 從右到左 調用者

調用函數的代碼和被調函數必須採用相同的函數的調用約定,程序才能正常運行。在Windows上,__cdecl是C/C++程序的缺省函數調用約定。指針

在有的cpu上,編譯器會用寄存器傳遞參數,函數使用的堆棧由被調函數分配和釋放。這種調用約定在行爲上和__cdecl有一個共同點:實參和形參數目不符不會致使堆棧錯誤。調試

不過,即便用寄存器傳遞參數,編譯器在進入函數時,仍是會將寄存器裏的參數存入堆棧指定位置。參數和局部變量同樣應該在堆棧中有一席之地。參數能夠被理解爲由調用函數指定初值的局部變量。blog

3 例子:__cdecl和__stdcall

不一樣的CPU,不一樣的編譯器,堆棧的佈局多是不一樣的。本文以x86,VC++的編譯器爲例。編譯器

VC++編譯器的已經再也不支持__pascal, __fortran, __syscall等函數調用約定。目前只支持__cdecl和__stdcall。

採用__cdecl或__stdcall調用方式的程序,在剛進入子函數時,堆棧內容是同樣的。esp指向的棧頂是返回地址。這是被call指令壓入堆棧的。下面是參數,左邊參數在上,右邊參數在下(先入棧)。

如前表所示,__cdecl和__stdcall的區別是:__cdecl是調用者清理參數佔用的堆棧,__stdcall是被調函數清理參數佔用的堆棧。

因爲__stdcall的被調函數在編譯時就必須知道傳入參數的準確數目(被調函數要清理堆棧),因此不能支持變參數函數,例如printf。並且若是調用者使用了不正確的參數數目,會致使堆棧錯誤。

經過查看彙編代碼,__cdecl函數調用在call語句後會有一個堆棧調整語句,例如:

      a = 0x1234;

 

      b = 0x5678;

 

    c = add(a, b);

對應x86彙編:

      mov dword ptr [ebp-4],1234h

 

      mov dword ptr [ebp-8],5678h

 

      mov eax,dword ptr [ebp-8]

 

      push eax

 

      mov ecx,dword ptr [ebp-4]

 

      push ecx

 

      call 0040100a


add esp,8

    mov dword ptr [ebp-0Ch],eax


__stdcall的函數調用則不須要調整堆棧:

      call 00401005

 

    mov dword ptr [ebp-0Ch],eax

函數

      int __cdecl add(int a, int b)

 

      {

 

      return a+b;

 

    }

產生如下彙編代碼(Debug版本):

      push ebp

 

      mov ebp,esp

 

      sub esp,40h

 

      push ebx

 

      push esi

 

      push edi

 

      lea edi,[ebp-40h]

 

      mov ecx,10h

 

      mov eax,0CCCCCCCCh

 

      rep stos dword ptr [edi]

 

      mov eax,dword ptr [ebp+8]

 

      add eax,dword ptr [ebp+0Ch]

 

      pop edi

 

      pop esi

 

      pop ebx

 

      mov esp,ebp

 

      pop ebp


ret // 跳轉到esp所指地址,並將esp+4,使esp指向進入函數時的第一個參數

再查看__stdcall函數的實現,會發現與__cdecl函數只有最後一行不一樣:

    ret 8 // 執行ret並清理參數佔用的堆棧

對於調試版本,VC++編譯器在「直接調用地址」時會增長檢查esp的代碼,例如:

      ta = (TAdd)add; // TAdd定義:typedef int (__cdecl *TAdd)(int a, int b);

 

    c = ta(a, b);

產生如下彙編代碼:

      mov [ebp-10h],0040100a

 

      mov esi,esp

 

      mov ecx,dword ptr [ebp-8]

 

      push ecx

 

      mov edx,dword ptr [ebp-4]

 

      push edx


call dword ptr [ebp-10h]
add esp,8

      cmp esi,esp


call __chkesp (004011e0)

    mov dword ptr [ebp-0Ch],eax

__chkesp 代碼以下。若是esp不等於函數調用前保存的值,就會轉到錯誤處理代碼。

      004011E0 jne __chkesp+3 (004011e3)

 

      004011E2 ret

 

    004011E3 ;錯誤處理代碼

__chkesp的錯誤處理會彈出對話框,報告函數調用形成esp值不正確。 Release版本的彙編代碼要簡潔得多。也不會增長 __chkesp。若是發生esp錯誤,程序會繼續運行,直到「遇到問題須要關閉」。

3 補充說明

函數調用約定只是「調用函數的代碼」和被調用函數之間的關係。

假設函數A是__stdcall,函數B調用函數A。你必須經過函數聲明告訴編譯器,函數A是__stdcall。編譯器天然會產生正確的調用代碼。

若是函數A是__stdcall。但在引用函數A的地方,你卻告訴編譯器,函數A是__cdecl方式,編譯器產生__cdecl方式的代碼,與函數A的調用約定不一致,就會發生錯誤。

以delphi調用VC函數爲例,delphi的函數缺省採用__pascal約定,VC的函數缺省採用__cdecl約定。咱們通常將VC的函數設爲__stdcall,例如:

    int __stdcall add(int a, int b);

在delphi中將這個函數也聲明爲__stdcall,就能夠調用了:

      function add(a: Integer; b: Integer): Integer;

 

    stdcall; external 'a.dll';

由於考慮到可能被其它語言的程序調用,很多API採用__stdcall的調用約定。

相關文章
相關標籤/搜索