彙編中的調用約定

調用棧

棧這個概念在數據結構中有詳細的講解,就不羅嗦了。

列出一些要點:

1. 先入先出。

2. 永遠只能從棧的最上方存或取數據。

 

在x86處理器中,壓棧的指令時PUSH。將一個item壓入棧頂會導致棧頂指針減小4個字節。棧頂指針用寄存器ESP來存儲, 相應的,這個寄存器的名字是Stack Pointer的縮寫。

 

壓棧

壓棧時,會依次發生下面的事:

1. 棧頂指針ESP減小4個字節。

2. 要壓入棧中的數據被拷貝到ESP指向的地址中。

可以看出,棧是向低地址端增長的,也就是說棧裏的內容越多,棧頂指針就越小。

2009-11-1 17-27-02

 

出棧

出棧時,會一次發生下面的操作:

1. 棧頂指針(ESP)指向的棧中的數據被取回。

2. 棧頂指針增加4個字節。

這樣,棧頂指針ESP就總是指向下一個棧中可用的數據了。

 

下圖中,我們依次向棧中壓入A,B,C三個數據,棧中情況如圖所示。

4

 

線程棧

在Win32中,每當創建一個線程,就有一兆字節(一百萬個,一千個一千,別拍我,我不囉嗦了)的虛擬內存被當做棧空間而保留,供這個線程使用。ESP寄存器指向這塊保留內存的最頂部(地址值最大的那一端),這樣棧就被初始化完畢了。要檢驗當前棧指針的值,可以在debugger中使用‘r’命令。ESP寄存器的值,就是棧指針的值。

 

嵌套函數調用

程序很少是一個巨長無比的單個函數。一般都是不同的函數負責不同的功能,在需要的時候會被主函數調用到。在前面的彙編語言基礎之五的例子中,很好的表現了這一情況。那麼改程序是如何做到調用一個函數,然後回來繼續執行的呢?文章中已經做了詳細的分析和代碼註釋。

 

調用約定

如果你寫的代碼使用的是同一種語言,並且使用同一種編譯器,那麼調用約定這個說法對於你來說沒啥用處。如果是你用多種語言,不同的編譯器,那麼處理不同語言的標準的問題就出現了。爲了讓程序員能夠處理多語言混合編程的情況,牛人們建立了一些用來指定如何調用函數和傳遞參數的規則。這些規則叫做調用約定。

比如,一個程序員用Pascal語言寫了一個名爲Sqrt的函數。該函數帶有一個浮點型的參數,返回參數的平方根。

function Sqrt(NumArgument: real) : real;

C語言的程序員需要知道Pascal程序員期望如何傳遞那個NumArgument參數給Sqrt函數。要處理這樣的混合語言編程,Pascal語言定義了Pascal調用約定。這個約定就是我們經常看到的STDCALL,而術語PASCAL已經過時了,不再使用了。

 

開始和結束代碼都被編譯器自動添加,用以保存寄存器,設置棧底,和在調用結束時恢復棧的狀態。這兩部分代碼跟CPU架構和編譯器相關。理解這部分代碼對於理解如何debug非常重要。下面的例子展現了使用STDCALL調用約定時,自動產生出來的開始和結束代碼。

典型被調用函數的開始代碼(prolog)

push

ebp

保存棧底(Frame Pointer)

mov

ebp, esp

修改棧底爲當前棧頂的位置

sub

esp, 8

棧頂向上移動八個字節,用來保存局部變量

push

push

push

ebx

ecx

edx

保存寄存器中的值

典型的被調用者的結束代碼(epilog)

pop 
pop 
pop
edx 
ecx 
ebx
恢復寄存器的值

mov

esp, ebp

恢復棧頂的指針,讓它指向棧底。這樣局部變量佔用的空間就被釋放了。

pop

ebp

恢復EBP(Frame Pointer)

ret

0x8

棧頂向下移動8個字節,來釋放棧中壓入的函數參數。(結合下面ret指令的說明再想想)

 

有些函數需要在調用另一個函數時先保存一些寄存器的值,以便恢復調用之後繼續使用它們。棧是個暫時保存寄存器的好地方,所以開始代碼會做一些額外的壓棧操作來保存一些以後需要用到的寄存器的值。

 

2009-11-2 10-57-012009-11-2 11-01-55上面的圖已經很清楚了,結合這個圖再仔細的看一下RET指令的說明,就會更清楚RET指令的每一個含義了。

RET∶ 指令助記符——返回。 
一、段內返回。先將棧頂的字送入IP,然後SP增2 。若帶立即數,SP再加立即數(丟棄一些在執行CALL之前入棧的參數)。 
二、段間返回。棧頂的字送入IP後(SP增 2),再將棧頂的字送入CS,SP再增2 。若帶立即數,則SP再加立即數。

 

在本系列文章的第一篇就說明了Win32中使用平面尋址,用不着段寄存器的。不過出於完整性考慮,還是將過時的段寄存器表列在下面。

1, 代碼段寄存器CS:存放當前正在運行的程序代碼所在段的段基值,表示當前使用的指令代碼可以從該段寄存器指定的存儲器段中取得,相應的偏移值則由IP提供。

2, 數據段寄存器DS:指出當前程序使用的數據所存放段的最低地址,即存放數據段的段基值。

3, 堆棧段寄存器SS:指出當前堆棧的底部地址,即存放堆棧段的段基值。

4, 附加段寄存器ES:指出當前程序使用附加數據段的段基址,該段是串操作指令中目的串所在的段。

 

STDCALL調用約定

該調用約定有如下的特點:

*參數自右向左入棧

*被調用者負責清理棧

*函數命名用下劃線開頭

*函數命名之後加一個@符號,再緊跟參數的尺寸

*函數的命名過程沒有大小寫的轉換

不能處理帶有可變參數的函數

 

調用者職責

      先壓棧最右邊的參數

      再壓下一個

      最後壓最左邊的參數

      調用函數functionX

被調用函數的職責

      push ebp

      mov ebp, esp

      sub esp, local_size

      …

      mov esp, ebp

      pop ebp

      ret 參數的字節數

 

CDECL調用約定

該調用約定有如下的特點:

*參數自右向左傳遞

*調用者負責清理棧

*函數命名用下劃線開頭

*不進行大小寫轉換。

         所以,名爲FunctionCall的函數,在符號表中被記錄爲_FunctionCall

*可以處理可變數目參數的函數。

*是C和C++程序默認的調用約定。

 

         這個說法正確麼?

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

         分析如下:

         如果僅僅是因爲壓棧順序的原因,那麼stdcall也是從右向左壓棧呀,爲什麼stdcall就不能處理可變參數呢?stdcall和cdecl最重要的區別就是誰去清理棧中參數。stdcall是由被調用函數清除參數的,而cdecl是由調用者清除的。所謂調用約定,就是調用者和被調用者之間約定好的對函數調用棧的一種職責分配。

        MSDN解釋cdecl可以處理可變參數的函數調用就一句話:Because the stack is cleaned up by the caller, it can do vararg functions. 看來cdecl能實現可變參數並不是由於參數壓棧的順序。

        我們先來弄清一個問題,實現可變參數的難度在哪裏?是如何得到參數的類型和個數麼?

        調用者肯定知道有多少個參數要傳,確切的說是知道要傳的參數的尺寸是多大,調用者將他們依次壓入了棧。被調用者接手控制的時候,如何知道參數的尺寸和個數呢?正如上面所說,第一個參數可以得到,並且知道類型,可以訪問。如同printf函數,第一個參數中有%d,%f之類的後繼參數指示符。通過它們,就可以得到後面的參數的類型和個數了,參數的類型和個數都知道了,所有參數的尺寸也可以知道了。得到參數的類型和個數取決於函數的實現。stdcall和cdecl的被調用者總是能得到參數的尺寸和個數的。這與誰去清理棧無關。注意編譯後的函數是沒有代碼去計算傳遞給它的參數的尺寸的。

        還是沒有回答那個問題,實現可變參數的難度在哪裏?

        假設在stdcall的情況下,含有可變參數的被調用函數順利的得到了所有的參數,完成了操作,該返回了可是並沒有去計算,也沒有一個地方記錄了它接受的棧的空間是多大。他不知道該如何去清理棧。而調用者又不負責清理棧。所以產生了麻煩。

        cdecl的調用者負責清理棧,調用者是知道它傳了多大的參數的,它可以順利的清理棧,讓程序繼續運行下去。

本段參考文章:http://blog.csdn.net/ZhouHM/archive/2004/04/07/14721.aspx

http://hi.baidu.com/dtzw/blog/item/cc17ba119eb39374cb80c4eb.html

 

調用者職責

      先壓棧最右邊的參數

      再壓下一個

      最後壓最左邊的參數

      調用函數functionX

      增加esp,幅度爲參數的尺寸

 

被調用函數的職責(functionX)

      push ebp

      mov ebp, esp

      sub esp, local_size

      …

      mov esp, ebp

      pop ebp

      ret     

 

FASTCALL調用約定

這種調用約定表示:當可能的時候,參數會被放到寄存器中。因爲並不是所有的參數都有壓棧操作,所以要比stdcall和cdecl要快一些,所以才叫fastcall吧。呵呵。

該調用約定有如下的特點:

*參數從右至左傳遞

*被調用函數清理棧中的參數

*函數名前加@前綴,之後再緊跟一個@,在加上參數的字節數,格式爲@name@number

*函數名的大小寫不被轉換

*不能處理可變參數的函數

 

調用者職責

     將最右面的參數壓棧

     將下一個參數壓入EDX

     將第一參數壓入ECX

     調用FunctionX

被調用者指責(FunctionX)

     push ebp

     mov ebp, esp

     sub esp, local_size

     Do Function Processing...

     mov esp, ebp

     pop ebp

     ret <number_of_pushed_arguments>;注意,如果參數有兩個的時候,這裏的ret指令就不會帶立即數參數,因爲沒有參數被壓棧。

 

THISCALL調用約定

這是C++中擁有固定數目參數的成員函數的,默認的調用約定。該調用約定是不能被指定的,因爲THISCALL並不像STDCALL,CDECL一樣,它不是個關鍵字。THISCALL調用約定中,this指針通過ECX寄存器來傳遞給被調用的函數。

 

NAKED函數

聲明瞭naked屬性的函數,其中沒有prolog或者epilog代碼被插入。這樣,你就可以用inline assembler來寫你自己的prolog或者epilog指令序列了。Naked函數是以高級特性被提供的。他們允許你聲明這樣的函數- 函數處於非C或C++的上下文之中(不是C或C++的函數),而且參數放在什麼地方也沒有約定好,還有哪些寄存器被保留了也不清楚。總之,全靠程序員自己來處理了。

適用於寫一些與已經存在了的,使用彙編語言寫好的系統溝通的C語言程序。

什麼是inline assembler? 摘自wikipedia

In computer programming, the inline assembler is a feature of some compilers that allows very low level code written in assembly to be embedded in a high level language like C or Ada.

 

比較幾種調用約定

項目\調用方式 __stdcall(Win32) __cdecl __fastcall thiscall(Native C++) COM __declspec(naked)(__declspec是微軟的關鍵字,其他系統上可能沒有)
參數壓棧順序 從右至左 從右至左 從右至左, 
Arg1在ecx, 
Arg2在edx
從右至左, 
This指針在ecx
從右至左, 
最後壓入this指針
程序員自定義
參數位置 棧 + 寄存器 棧,寄存器ECX   程序員自定義
清除棧中參數的函數 被調用者 調用者 被調用者 被調用者 被調用者 程序員自定義
支持可變參數 程序員自定義
函數名字格式 _name@number _name @name@number     程序員自定義
函數名舉例 _NtWaitForSingleObject@12 _printf @AfpFsdDispCloseVol@4     程序員自定義
大小寫轉換 沒有 沒有 沒有 沒有   程序員自定義
C++編譯時函數名修飾約定的不同之處

函數名後面以"@@YG"標識參數表的開始,後跟參數表;

參數表的開始標識爲"@@YA"; 參數表的開始標識爲"@@YI"。     程序員自定義

 

練習:

1. 問:prologue和epilogue代碼都是做什麼的?

答:開場和收尾代碼是由編譯器自動產生的代碼,用於幫助程序在執行的過程中保存寄存器,建立棧框架,和在函數調用結束後恢復棧。

2. 問:說說看都有哪些種calling convention?

答:STDCALL, CDECL, FASTCALL, THISCALL, NAKEDFUNCTION。

3. 哪種調用約定依賴於被調用的函數來清理棧上的參數?

答:stdcall, fastcall, thiscall.

4. 用C++寫好的應用程序一般使用哪種調用約定?

答:cdecl和thiscall

5. Intel平臺上的THIS指針一般通過那個寄存器來傳送?

答:ECX。

發佈了13 篇原創文章 · 獲贊 0 · 訪問量 9920
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章