小覽call stack(調(diào)用棧) (二)——調(diào)用約定
Posted on 2009-11-02 17:31 S.l.e!ep.¢% 閱讀(788) 評(píng)論(0) 編輯 收藏 引用 所屬分類(lèi): WinDbg在上一篇博客中小覽call stack(調(diào)用棧) (一)中,我展示了如何在windbg中觀(guān)察調(diào)用棧的相關(guān)信息:函數(shù)的返回地址,參數(shù),返回值。這些信息都按照一定的規(guī)則存儲(chǔ)在固定的地方。這個(gè)規(guī)則就是調(diào)用約定(calling convention)。
?
調(diào)用約定在計(jì)算機(jī)界不是什么新鮮的概念,已經(jīng)有許多相關(guān)的文獻(xiàn)給予詳細(xì)的介紹。比較全面的介紹可以參見(jiàn)wikipedia上的相關(guān)頁(yè)面。然而,如果你和我一樣,在第一次接觸調(diào)用約定的時(shí)候,覺(jué)得這個(gè)概念是個(gè)高深神秘的冬冬,那么就請(qǐng)跟隨我一起,在這篇博客中看看他的由來(lái),他的范疇以及他的用途。
?
為什么需要調(diào)用約定?
在具體介紹調(diào)用約定的定義之前,我們先來(lái)看看為什么我們需要一個(gè)稱(chēng)之為調(diào)用約定的冬冬。如果各位了解匯編語(yǔ)言(不了解的話(huà),看下面的這段會(huì)稍微有些費(fèi)力,不過(guò)我盡可能把匯編的相關(guān)知識(shí)解釋的清楚一些),那么回憶一下我們是怎么來(lái)做一個(gè)函數(shù)調(diào)用的。
?
匯編語(yǔ)言提供了一條指令,call ptr,其功能是把CS:IP (指令段:指令指針,決定著下一條執(zhí)行指令的地址)壓棧,并且修改CPU的指令指針,作一個(gè)跳轉(zhuǎn)。在函數(shù)結(jié)束的地方,我們使用另一條指令,ret,其功能是把棧中的返回地址取出,并且跳轉(zhuǎn)到那條指令。
?
在這里匯編語(yǔ)言只提供了指令跳轉(zhuǎn)的命令,作為函數(shù)調(diào)用另一個(gè)重要組成部分的參數(shù)傳遞,其方式就很靈活,你可以通過(guò)寄存器傳值,可以通過(guò)調(diào)用棧傳值,可以通過(guò)某一塊具體的內(nèi)存?zhèn)髦?類(lèi)似全局變量)。然后在被調(diào)用函數(shù)中,從寄存器,棧或者是內(nèi)存中讀取這些信息。想象一下如果被調(diào)用函數(shù)是某一個(gè)程序員所編寫(xiě)的,調(diào)用者是另一個(gè)程序員,那么他倆之間對(duì)于參數(shù)的傳遞方式就有了一個(gè)約定。
?
高級(jí)語(yǔ)言的出現(xiàn),把這個(gè)問(wèn)題隱藏了起來(lái)。我們?cè)诰帉?xiě)一般的c++程序的時(shí)候,通常不需要顧慮參數(shù)傳遞的底層實(shí)現(xiàn),但是,這并不意味著這一問(wèn)題不再出現(xiàn)——我們只是把責(zé)任推給了編譯器。編譯器作為一個(gè)計(jì)算機(jī)程序,總是遵照一定的規(guī)則工作,每一個(gè)規(guī)則對(duì)應(yīng)了一種調(diào)用約定。
?
久而久之,那些經(jīng)典的規(guī)則所產(chǎn)生的調(diào)用約定,就成了耳熟能詳?shù)亩?/p>
?
耳熟能詳?shù)恼{(diào)用約定
在介紹這些調(diào)用規(guī)范之前,我想先說(shuō)明的是,下面所涉及的調(diào)用規(guī)范是在32位x86處理器windows平臺(tái)上的。把范疇限定在32位處理器的原因是:16位處理器已經(jīng)退出CPU的歷史舞臺(tái),64微處理器無(wú)論是IA64還是AMD64都只有一個(gè)調(diào)用規(guī)范——只有32位處理器呈現(xiàn)百家成名,百花齊放的景象。(對(duì)了,你當(dāng)然明白調(diào)用規(guī)范是綁定在處理器架構(gòu)上的概念,因?yàn)樗婕疤嗟闹T如寄存器之類(lèi)的處理器架構(gòu)細(xì)節(jié)。)聚焦于windows則是因?yàn)槲椰F(xiàn)在的工作只涉及這一平臺(tái)。
下表的出處來(lái)自于The Old New Thing以及張羿的csdn專(zhuān)欄,并作了適當(dāng)修改。
首先來(lái)看所有的調(diào)用規(guī)范都遵循的規(guī)定:返回值存儲(chǔ)在EDX:EAX中,EDI,ESI,EBP,EBX是保留的存儲(chǔ)器。(即函數(shù)可以任意使用這些寄存器,無(wú)需擔(dān)心破壞了調(diào)用者的寄存器狀態(tài))
調(diào)用約定名稱(chēng)
?清理堆棧
?參數(shù)壓棧順序
?備注
?
cdecl
?調(diào)用者 (Caller)
?從右往左?
?因?yàn)槭钦{(diào)用者清理Stack,因此允許變參 (如printf)
?
stdcall
?被調(diào)用者 (Callee)
?從右往左?
?一般在Windows API和COM中使用,也是.NET和Native代碼調(diào)用的缺省Calling Convention。
順便提一下,Windows中API的Calling Convention所使用到的WINAPI宏在PC機(jī)上是__stdcall,而在WinCE上則是__cdecl,并非一成不變。
?
Thiscall (Microsoft)
?被調(diào)用者 (Callee)
?從右往左
?基本上等價(jià)stdcall, 除了this指針用ECX傳遞
?
Fastcall (Microsoft)
?被調(diào)用者 (Callee)
?從右往左
?和Stdcall類(lèi)似,但是會(huì)選擇兩個(gè)從左往右數(shù)最先可以放在寄存器里面的參數(shù)放在ECX和EDX中
?
大家可能對(duì)清理堆棧,參數(shù)壓棧順序這些概念不是很清楚,在這里我會(huì)通過(guò)一個(gè)具體的例子來(lái)說(shuō)明。下面列出了一小段程序和它的匯編代碼:
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中對(duì)Test的調(diào)用對(duì)應(yīng)了如下的匯編代碼:
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
?
在這個(gè)例子中,我們可以觀(guān)察到如下信息:
1. 壓棧順序:棧中首先壓入的是0A(十進(jìn)制中的10),是最后一個(gè)參數(shù),其次是’a’,最后是5,所以說(shuō)__stdcall的壓棧順序是從右向左。
2. 返回值存放在eax中:在call指令之后,把eax的值存入到[ebp-4]中,對(duì)應(yīng)了c++代碼中對(duì)a的賦值,可見(jiàn)eax是返回值的存放之所。
3. 被調(diào)用函數(shù)清理?xiàng)#涸赾all指令和mov指令沒(méi)有額外的其他指令,可見(jiàn)之前放到棧里的參數(shù),都已經(jīng)被函數(shù)Test清理了(Test的最后一條指令是ret 0c),把棧的指針調(diào)整了三個(gè)變量的位置。
4. 函數(shù)更名:細(xì)心的讀者會(huì)發(fā)現(xiàn)call指令后面跟的是如同亂碼般的test!ILT+10(?TestYGHHDFZ),這是編譯器做的手腳(name mangling),不同的調(diào)用規(guī)范下,編譯器會(huì)按照不同的規(guī)則對(duì)函數(shù)進(jìn)行更名。我不想細(xì)究的原因在于:一方便,函數(shù)更名的規(guī)則本身就在變化,我目前使用的編譯器,會(huì)按照以前__thiscall的規(guī)則來(lái)更名__stdcall的函數(shù)。另一方面,許多debuger比如windbg,會(huì)自動(dòng)的把命名調(diào)整回來(lái)。
如何指定調(diào)用約定
通常,我們真正需要考慮到調(diào)用約定的場(chǎng)景,是對(duì)一些外部類(lèi)庫(kù)的使用。舉例來(lái)說(shuō),如果我們要調(diào)用的函數(shù)由另外一個(gè)類(lèi)庫(kù)提供,那么,我們需要根據(jù)這個(gè)函數(shù)所聲明的調(diào)用約定來(lái)使用這個(gè)函數(shù)。也就是說(shuō),我們要告訴編譯器,請(qǐng)按照這個(gè)調(diào)用約定,生成相關(guān)的代碼,來(lái)使用那個(gè)來(lái)自于類(lèi)庫(kù)的函數(shù)。對(duì)于MSVC的編譯器來(lái)說(shuō),有下面的這些開(kāi)關(guān):
編譯器開(kāi)關(guān)
?調(diào)用規(guī)范
?
/Gd
?__cdecl
?
/Gr
?__fastcall
?
/Gz
?__stdcall
?
其中/Gz是c++的默認(rèn)選項(xiàng)。
?
另外一個(gè)例子是,提供給別人的回調(diào)函數(shù),需要根據(jù)調(diào)用者的要求,聲明調(diào)用約定,舉一個(gè)例子來(lái)說(shuō),在windows中開(kāi)始一個(gè)新的線(xiàn)程。
這時(shí)候,可以在函數(shù)聲明的語(yǔ)句中,在返回值類(lèi)型后面插入相關(guān)的調(diào)用規(guī)范,如前面的例子中所示。
view plaincopy to clipboardprint?
int __stdcall Test(int a, char b, short c)?
int __stdcall Test(int a, char b, short c)?
如果你是一個(gè).NET用戶(hù)(終于,我可以談及一些我們的產(chǎn)品了),那么你在P/Invoke的時(shí)候仍然需要調(diào)用約定。DllImportAttibute中,有一個(gè)字段CallingConvention,就是對(duì)應(yīng)這個(gè)需求生成的。
?
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) ;?
調(diào)用約定的用武之地
看了上面的介紹之后,你可能會(huì)想,我們只需要根據(jù)文檔上聲明的調(diào)用約定,在自己的代碼中指定相應(yīng)的調(diào)用約定就可以了。那么,了解清楚每一個(gè)調(diào)用約定的具體內(nèi)容對(duì)我們有什么幫助呢?
我認(rèn)為,了解調(diào)用約定首先可以幫助我們深入了解函數(shù)調(diào)用部分的匯編代碼的原理。有很多時(shí)候,錯(cuò)誤的使用了調(diào)用規(guī)范是一個(gè)很難察覺(jué)的bug。
其次,了解調(diào)用約定在只擁有公共符號(hào)(public symbol)進(jìn)行調(diào)試的時(shí)候?qū)ξ覀儙椭艽螅卜?hào)通常只能讓我們觀(guān)察到調(diào)用棧信息。那么了解了調(diào)用約定之后,我們至少能利用調(diào)用棧找到函數(shù)參數(shù),函數(shù)返回值等信息。
?
總結(jié)以及下期預(yù)告
今天我花費(fèi)了蠻多筆墨講解調(diào)用規(guī)范,對(duì)于這一系列的主題“調(diào)用棧”來(lái)說(shuō),調(diào)用規(guī)范是一個(gè)息息相關(guān)的概念。下一次,我將通過(guò)一個(gè)windbg調(diào)試腳本來(lái)觀(guān)察遵循stdcall的調(diào)用棧,作為這一系列的收尾,敬請(qǐng)期待。
本文來(lái)自CSDN博客,轉(zhuǎn)載請(qǐng)標(biāo)明出處:http://blog.csdn.net/mountaintaiII/archive/2009/03/12/3985729.aspx