在上一篇博客中小覽call stack(調用棧) (一)中,我展示了如何在windbg中觀察調用棧的相關信息:函數的返回地址,參數,返回值。這些信息都按照一定的規則存儲在固定的地方。這個規則就是調用約定(calling convention)。
?
調用約定在計算機界不是什么新鮮的概念,已經有許多相關的文獻給予詳細的介紹。比較全面的介紹可以參見wikipedia上的相關頁面。然而,如果你和我一樣,在第一次接觸調用約定的時候,覺得這個概念是個高深神秘的冬冬,那么就請跟隨我一起,在這篇博客中看看他的由來,他的范疇以及他的用途。
?
為什么需要調用約定?
在具體介紹調用約定的定義之前,我們先來看看為什么我們需要一個稱之為調用約定的冬冬。如果各位了解匯編語言(不了解的話,看下面的這段會稍微有些費力,不過我盡可能把匯編的相關知識解釋的清楚一些),那么回憶一下我們是怎么來做一個函數調用的。
?
匯編語言提供了一條指令,call ptr,其功能是把CS:IP (指令段:指令指針,決定著下一條執行指令的地址)壓棧,并且修改CPU的指令指針,作一個跳轉。在函數結束的地方,我們使用另一條指令,ret,其功能是把棧中的返回地址取出,并且跳轉到那條指令。
?
在這里匯編語言只提供了指令跳轉的命令,作為函數調用另一個重要組成部分的參數傳遞,其方式就很靈活,你可以通過寄存器傳值,可以通過調用棧傳值,可以通過某一塊具體的內存傳值(類似全局變量)。然后在被調用函數中,從寄存器,棧或者是內存中讀取這些信息。想象一下如果被調用函數是某一個程序員所編寫的,調用者是另一個程序員,那么他倆之間對于參數的傳遞方式就有了一個約定。
?
高級語言的出現,把這個問題隱藏了起來。我們在編寫一般的c++程序的時候,通常不需要顧慮參數傳遞的底層實現,但是,這并不意味著這一問題不再出現——我們只是把責任推給了編譯器。編譯器作為一個計算機程序,總是遵照一定的規則工作,每一個規則對應了一種調用約定。
?
久而久之,那些經典的規則所產生的調用約定,就成了耳熟能詳的冬冬:
?
耳熟能詳的調用約定
在介紹這些調用規范之前,我想先說明的是,下面所涉及的調用規范是在32位x86處理器windows平臺上的。把范疇限定在32位處理器的原因是:16位處理器已經退出CPU的歷史舞臺,64微處理器無論是IA64還是AMD64都只有一個調用規范——只有32位處理器呈現百家成名,百花齊放的景象。(對了,你當然明白調用規范是綁定在處理器架構上的概念,因為它涉及太多的諸如寄存器之類的處理器架構細節。)聚焦于windows則是因為我現在的工作只涉及這一平臺。
下表的出處來自于The Old New Thing以及張羿的csdn專欄,并作了適當修改。
首先來看所有的調用規范都遵循的規定:返回值存儲在EDX:EAX中,EDI,ESI,EBP,EBX是保留的存儲器。(即函數可以任意使用這些寄存器,無需擔心破壞了調用者的寄存器狀態)
調用約定名稱
?清理堆棧
?參數壓棧順序
?備注
?
cdecl
?調用者 (Caller)
?從右往左?
?因為是調用者清理Stack,因此允許變參 (如printf)
?
stdcall
?被調用者 (Callee)
?從右往左?
?一般在Windows API和COM中使用,也是.NET和Native代碼調用的缺省Calling Convention。
順便提一下,Windows中API的Calling Convention所使用到的WINAPI宏在PC機上是__stdcall,而在WinCE上則是__cdecl,并非一成不變。
?
Thiscall (Microsoft)
?被調用者 (Callee)
?從右往左
?基本上等價stdcall, 除了this指針用ECX傳遞
?
Fastcall (Microsoft)
?被調用者 (Callee)
?從右往左
?和Stdcall類似,但是會選擇兩個從左往右數最先可以放在寄存器里面的參數放在ECX和EDX中
?
大家可能對清理堆棧,參數壓棧順序這些概念不是很清楚,在這里我會通過一個具體的例子來說明。下面列出了一小段程序和它的匯編代碼:
view plaincopy to clipboardprint?
#include <stdio.h>??
int __stdcall Test(int a, char b, short c)??
{??
??? printf("%d %c %d", a, b, c);??
??? return a+c;??
}??
void main()??
{??
??? int a = Test(5, 'a', 10);??
}?
#include <stdio.h>
int __stdcall Test(int a, char b, short c)
{
??? printf("%d %c %d", a, b, c);
??? return a+c;
}
void main()
{
??? int a = Test(5, 'a', 10);
}
在main中對Test的調用對應了如下的匯編代碼:
view plaincopy to clipboardprint?
00412004 6a0a??????????? push??? 0Ah??
00412006 6a61??????????? push??? 61h??
00412008 6a05??????????? push??? 5??
0041200a e800f0feff????? call??? test!ILT+10(?TestYGHHDFZ) (0040100f)??
0041200f 8945fc????????? mov???? dword ptr [ebp-4],eax ss:002b:001?
00412004 6a0a??????????? push??? 0Ah
00412006 6a61??????????? push??? 61h
00412008 6a05??????????? push??? 5
0041200a e800f0feff????? call??? test!ILT+10(?TestYGHHDFZ) (0040100f)
0041200f 8945fc????????? mov???? dword ptr [ebp-4],eax ss:002b:001
?
在這個例子中,我們可以觀察到如下信息:
1. 壓棧順序:棧中首先壓入的是0A(十進制中的10),是最后一個參數,其次是’a’,最后是5,所以說__stdcall的壓棧順序是從右向左。
2. 返回值存放在eax中:在call指令之后,把eax的值存入到[ebp-4]中,對應了c++代碼中對a的賦值,可見eax是返回值的存放之所。
3. 被調用函數清理棧:在call指令和mov指令沒有額外的其他指令,可見之前放到棧里的參數,都已經被函數Test清理了(Test的最后一條指令是ret 0c),把棧的指針調整了三個變量的位置。
4. 函數更名:細心的讀者會發現call指令后面跟的是如同亂碼般的test!ILT+10(?TestYGHHDFZ),這是編譯器做的手腳(name mangling),不同的調用規范下,編譯器會按照不同的規則對函數進行更名。我不想細究的原因在于:一方便,函數更名的規則本身就在變化,我目前使用的編譯器,會按照以前__thiscall的規則來更名__stdcall的函數。另一方面,許多debuger比如windbg,會自動的把命名調整回來。
如何指定調用約定
通常,我們真正需要考慮到調用約定的場景,是對一些外部類庫的使用。舉例來說,如果我們要調用的函數由另外一個類庫提供,那么,我們需要根據這個函數所聲明的調用約定來使用這個函數。也就是說,我們要告訴編譯器,請按照這個調用約定,生成相關的代碼,來使用那個來自于類庫的函數。對于MSVC的編譯器來說,有下面的這些開關:
編譯器開關
?調用規范
?
/Gd
?__cdecl
?
/Gr
?__fastcall
?
/Gz
?__stdcall
?
其中/Gz是c++的默認選項。
?
另外一個例子是,提供給別人的回調函數,需要根據調用者的要求,聲明調用約定,舉一個例子來說,在windows中開始一個新的線程。
這時候,可以在函數聲明的語句中,在返回值類型后面插入相關的調用規范,如前面的例子中所示。
view plaincopy to clipboardprint?
int __stdcall Test(int a, char b, short c)?
int __stdcall Test(int a, char b, short c)?
如果你是一個.NET用戶(終于,我可以談及一些我們的產品了),那么你在P/Invoke的時候仍然需要調用約定。DllImportAttibute中,有一個字段CallingConvention,就是對應這個需求生成的。
?
view plaincopy to clipboardprint?
[DllImport("ole32.dll", EntryPoint="CoCreateInstance", CallingConvention=CallingConvention.StdCall)]??
public static extern? int CoCreateInstance(ref Guid rclsid, IntPtr pUnkOuter, uint dwClsContext, ref Guid riid, ref System.IntPtr ppv) ;?
[DllImport("ole32.dll", EntryPoint="CoCreateInstance", CallingConvention=CallingConvention.StdCall)]
public static extern? int CoCreateInstance(ref Guid rclsid, IntPtr pUnkOuter, uint dwClsContext, ref Guid riid, ref System.IntPtr ppv) ;?
調用約定的用武之地
看了上面的介紹之后,你可能會想,我們只需要根據文檔上聲明的調用約定,在自己的代碼中指定相應的調用約定就可以了。那么,了解清楚每一個調用約定的具體內容對我們有什么幫助呢?
我認為,了解調用約定首先可以幫助我們深入了解函數調用部分的匯編代碼的原理。有很多時候,錯誤的使用了調用規范是一個很難察覺的bug。
其次,了解調用約定在只擁有公共符號(public symbol)進行調試的時候對我們幫助很大,公共符號通常只能讓我們觀察到調用棧信息。那么了解了調用約定之后,我們至少能利用調用棧找到函數參數,函數返回值等信息。
?
總結以及下期預告
今天我花費了蠻多筆墨講解調用規范,對于這一系列的主題“調用棧”來說,調用規范是一個息息相關的概念。下一次,我將通過一個windbg調試腳本來觀察遵循stdcall的調用棧,作為這一系列的收尾,敬請期待。
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/mountaintaiII/archive/2009/03/12/3985729.aspx