程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 小覽call stack(調用棧) (二)——調用約定

小覽call stack(調用棧) (二)——調用約定

編輯:關於C++

在上一篇博客中小覽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中

大家可能對清理堆棧,參數壓棧順序這些概念不是很清楚,在這裡我會通過 一個具體的例子來說明。下面列出了一小段程序和它的匯編代碼:

#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的調用對應了如下的匯編代碼:

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中開始一個新的線程。

這時候,可以在函數聲明的語句中,在返回值類型後面插入相關的調用規范 ,如前面的例子中所示。

int __stdcall Test(int a, char b, short c)
int __stdcall Test(int a, char b, short c) 

如果你是一個.NET用戶(終於,我可以談及一些我們的產品了),那麼你在 P/Invoke的時候仍然需要調用約定。DllImportAttibute中,有一個字段 CallingConvention,就是對應這個需求生成的。

[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的調用棧,作為這一系列的收尾,敬請期待。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved