編譯器通常使用堆棧實現函數調用。堆棧是存儲器的一個區域,嵌入式環境有時須要程序員本身定義一個數組做爲堆棧。Windows爲每一個線程自動維護一個堆棧,堆棧的大小能夠設置。編譯器使用堆棧來堆放每一個函數的參數、局部變量等信息。程序員
函數調用常常是嵌套的,在同一時刻,堆棧中會有多個函數的信息,每一個函數佔用一個連續的區域。一個函數佔用的區域被稱做幀(frame)。數組
編譯器從高地址開始使用堆棧。 假設咱們定義一個數組a[1024]做爲堆棧空間,一開始棧頂指針指向a[1023]。若是棧裏有兩個函數a和b,且a調用了b,棧頂指針會指向函數b的 幀。若是函數b返回。棧頂指針就指向函數a的幀。若是在棧裏放了太多東西形成溢出,破壞的是a[0]上面的東西。多線程
在多線程(任務)環境,CPU的堆棧指針指向的存儲器區域就是當前使用的堆棧。切換線程的一個重要工做,就是將堆棧指針設爲當前線程的堆棧棧頂地址。函數
不一樣CPU,不一樣編譯器的堆棧佈局、函數調用方法均可能不一樣,但堆棧的基本概念是同樣的。佈局
函數調用約定包括傳遞參數的順序,誰負責清理參數佔用的堆棧等,例如 :線程
參數傳遞順序 | 誰負責清理參數佔用的堆棧 | |
__pascal | 從左到右 | 調用者 |
__stdcall | 從右到左 | 被調函數 |
__cdecl | 從右到左 | 調用者 |
調用函數的代碼和被調函數必須採用相同的函數的調用約定,程序才能正常運行。在Windows上,__cdecl是C/C++程序的缺省函數調用約定。指針
在有的cpu上,編譯器會用寄存器傳遞參數,函數使用的堆棧由被調函數分配和釋放。這種調用約定在行爲上和__cdecl有一個共同點:實參和形參數目不符不會致使堆棧錯誤。調試
不過,即便用寄存器傳遞參數,編譯器在進入函數時,仍是會將寄存器裏的參數存入堆棧指定位置。參數和局部變量同樣應該在堆棧中有一席之地。參數能夠被理解爲由調用函數指定初值的局部變量。blog
不一樣的CPU,不一樣的編譯器,堆棧的佈局多是不一樣的。本文以x86,VC++的編譯器爲例。編譯器
VC++編譯器的已經再也不支持__pascal, __fortran, __syscall等函數調用約定。目前只支持__cdecl和__stdcall。
採用__cdecl或__stdcall調用方式的程序,在剛進入子函數時,堆棧內容是同樣的。esp指向的棧頂是返回地址。這是被call指令壓入堆棧的。下面是參數,左邊參數在上,右邊參數在下(先入棧)。
如前表所示,__cdecl和__stdcall的區別是:__cdecl是調用者清理參數佔用的堆棧,__stdcall是被調函數清理參數佔用的堆棧。
因爲__stdcall的被調函數在編譯時就必須知道傳入參數的準確數目(被調函數要清理堆棧),因此不能支持變參數函數,例如printf。並且若是調用者使用了不正確的參數數目,會致使堆棧錯誤。
經過查看彙編代碼,__cdecl函數調用在call語句後會有一個堆棧調整語句,例如:
對應x86彙編:
add esp,8
__stdcall的函數調用則不須要調整堆棧:
函數
產生如下彙編代碼(Debug版本):
ret // 跳轉到esp所指地址,並將esp+4,使esp指向進入函數時的第一個參數
再查看__stdcall函數的實現,會發現與__cdecl函數只有最後一行不一樣:
對於調試版本,VC++編譯器在「直接調用地址」時會增長檢查esp的代碼,例如:
產生如下彙編代碼:
call dword ptr [ebp-10h]
add esp,8
call __chkesp (004011e0)
__chkesp 代碼以下。若是esp不等於函數調用前保存的值,就會轉到錯誤處理代碼。
__chkesp的錯誤處理會彈出對話框,報告函數調用形成esp值不正確。 Release版本的彙編代碼要簡潔得多。也不會增長 __chkesp。若是發生esp錯誤,程序會繼續運行,直到「遇到問題須要關閉」。
函數調用約定只是「調用函數的代碼」和被調用函數之間的關係。
假設函數A是__stdcall,函數B調用函數A。你必須經過函數聲明告訴編譯器,函數A是__stdcall。編譯器天然會產生正確的調用代碼。
若是函數A是__stdcall。但在引用函數A的地方,你卻告訴編譯器,函數A是__cdecl方式,編譯器產生__cdecl方式的代碼,與函數A的調用約定不一致,就會發生錯誤。
以delphi調用VC函數爲例,delphi的函數缺省採用__pascal約定,VC的函數缺省採用__cdecl約定。咱們通常將VC的函數設爲__stdcall,例如:
在delphi中將這個函數也聲明爲__stdcall,就能夠調用了:
由於考慮到可能被其它語言的程序調用,很多API採用__stdcall的調用約定。