[C/C++]函數參數的入棧順序與可變參數的實現

#include
void foo(int x, int y, int z)
{
        printf("x = %d at [%X]\n", x, &x);
        printf("y = %d at [%X]\n", y, &y);
        printf("z = %d at [%X]\n", z, &z);
}
int main(int argc, char *argv[])
{
        foo(100, 200, 300);
        return 0;
}
運行結果: 
x = 100 at [BFE28760]
y = 200 at [BFE28764]
z = 300 at [BFE28768]

C 程序棧底爲高地址,棧頂爲低地址,因此上面的實例可以說明函數參數入棧順序的確是從右至左的。可到底爲什麼呢?查了一直些文獻得知,參數入棧順序是和具體 編譯器實現相關的。比如,Pascal語言中參數就是從左到右入棧的,有些語言中還可以通過修飾符進行指定,如Visual C++。即然兩種方式都可以,爲什麼C語言要選擇從右至左呢?
進一步發現,Pascal語言不支持可變長參數,而C語言支持這種特色,正是這個原 因使得C語言函數參數入棧順序爲從右至左。具體原因爲:C方式參數入棧順序(從右至左)的好處就是可以動態變化參數個數。通過棧堆分析可知,自左向右的入 棧方式,最前面的參數被壓在棧底。除非知道參數個數,否則是無法通過棧指針的相對位移求得最左邊的參數。這樣就變成了左邊參數的個數不確定,正好和動態參 數個數的方向相反。
因此,C語言函數參數採用自右向左的入棧順序,主要原因是爲了支持可變長參數形式。換句話說,如果不支持這個特色,C語言完全 和Pascal一樣,採用自左向右的參數入棧方式。可變參數 主要通過第一個定參數來確定參數列表,所以從右至左入棧後,函數調用時pop出第一個參數就是 參數列表的第一個確定參數,就OK了……


這兒其實還涉及到C語言中調用約定所採用的方式,下面簡單的介紹一下:

__stdcall與C調用約定(__cdecl)的區別

 

C調用約定在返回前,要作一次堆棧平衡,也就是參數入棧了多少字節,就要彈出來多少字節.這樣很安全.

有一點需要注意:stdcall調用約定如果採用了不定參數,即VARARG的話,則和C調用約定一樣,要由調用者來作堆棧平衡.

(1)_stdcall是 Pascal方式清理C方式壓棧,通常用於Win32 Api中,函數採用從右到左的壓棧方式,自己在退出時清空堆棧。VC將函數編譯後會在函數名前面加上下劃線前綴,在函數名後加上"@"和參數的字節數。 int f(void *p) -->> _f@4(在外部彙編語言裏可以用這個名字引用這個函數)

在WIN32 API中,只有少數幾個函數,如wspintf函數是採用C調用約定,其他都是stdcall

(2)C調用約定(即用 __cdecl關鍵字說明)(The C default calling convention)按從右至左的順序壓參數入棧,由調用者把參數彈出棧。對於傳送參數的內存棧是由調用者來維護的(正因爲如此,實現可變參數 vararg的函數(如printf)只能使用該調用約定)。另外,在函數名修飾約定方面也有所不同。 _cdecl是C和C++程序的缺省調用方式。每一個調用它的函數都包含清空堆棧的代碼,所以產生的可執行文件大小會比調用_stdcall函數的大。函 數採用從右到左的壓棧方式。VC將函數編譯後會在函數名前面加上下劃線前綴。

(3)__fastcall調用的主 要特點就是快,因爲它是通過寄存器來傳送參數的(實際上,它用ECX和EDX傳送前兩個雙字(DWORD)或更小的參數,剩下的參數仍舊自右向左壓棧傳 送,被調用的函數在返回前清理傳送參數的內存棧),在函數名修飾約定方面,它和前兩者均不同。__fastcall方式的函數採用寄存器傳遞參數,VC將 函數編譯後會在函數名前面加上"@"前綴,在函數名後加上"@"和參數的字節數。

(4)thiscall僅僅應用於"C++"成員函數。this指針存放於CX/ECX寄存器中,參數從右到左壓。thiscall不是關鍵詞,因此不能被程序員指定。

(5)naked call。 當採用1-4的調用約定時,如果必要的話,進入函數時編譯器會產生代碼來保存ESI,EDI,EBX,EBP寄存器,退出函數時則產生代碼恢復這些寄存器的內容。

  (這些代碼稱作 prolog and epilog code,一般,ebp,esp的保存是必須的).

  但是naked call不產生這樣的代碼。naked call不是類型修飾符,故必須和_declspec共同使用。

  關鍵字 __stdcall、__cdecl和__fastcall可以直接加在要輸出的函數前。它們對應的命令行參數分別爲/Gz、/Gd和/Gr。缺省狀態爲/Gd,即__cdecl。

  要完全模仿PASCAL調用約定首先必須使用__stdcall調用約定,至於函數名修飾約定,可以通過其它方法模仿。還有一個值得一 提的是WINAPI宏,Windows.h支持該宏,它可以將出函數翻譯成適當的調用約定,在WIN32中,它被定義爲__stdcall。使用 WINAPI宏可以創建自己的APIs。

綜上,其實只有PASCAL調用約定的從左到右入棧的.而且PASCAL不能使用不定參數個數,其參數個數是一定的。

簡單總結一下上面的幾個調用方式:

調用約定

堆棧清除

參數傳遞

__cdecl

調用者

從右到左,通過堆棧傳遞

__stdcall

函數體

從右到左,通過堆棧傳遞

__fastcall

函數體

從右到左,優先使用寄存器(ECX,EDX),然後使用堆棧

thiscall

函數體

this指針默認通過ECX傳遞,其他參數從右到左入棧





一.函數修飾符:

函數名字修飾(Decorated Name) 方式

    函 數的名字修飾(Decorated Name)就是編譯器在編譯期間創建的一個字符串,用來指 明函數的定義或原型。LINK程序或其他工具有時需要指定函數的 名字修飾來定位函數的正確位置。多數情況下程序員並不需要知道函數的名字修飾,LINK程序或 其他工具會自動區分他們。當然,在某些情況下需要指定函數的名字修飾,例如在C++程序中, 爲了讓LINK程序或其他工具能夠匹配到正確的函數名字,就必須爲重載函數和一些特殊的 函數(如構造函數和析構函數)指定名字裝飾。另一種需要指定函數的名字修飾的情況是在彙編程序中調用C或C++的 函數。如果函數名字,調用約定,返回值類型或函數參數有任何改變,原來的名字修飾就不再有效,必須指定新的名字修飾。C和C++程 序的函數在內部使用不同的名字修飾方式,下面將分別介紹這兩種方式。

1._cdecl  
 
按 從右至左的順序壓參數入棧,由調用者把參數彈出棧。對於“C”函數或者變量,修飾名是在函數名前 加下劃線。對於“C++”函數,有所不同。 
如 函數void   test(void)的修飾名是_test; 對於不屬於一個類的“C++”全局函數,修飾名是?test@@ZAXXZ。 
這 是MFC缺省調用約定。由於是調用者負責把參數彈出棧,所以可以給函數定義個數不定 的參數,如printf函數。 
2._stdcall  
按從右至 左的順序壓參數入棧,由被調用者把參數彈出棧。對於“C”函數或者變量,修飾名以下劃線爲前 綴,然後是函數名,然後是符號“@”及參數的字節數,如函數int   func(int   a,   double   b)的修飾名是_func@12。 對於“C++”函數,則有所不同。 
所 有的Win32   API函數都遵循該約定。 
3._fastcall  
頭兩 個DWORD類型或者佔更少字節的參數被放入ECX和EDX寄 存器,其他剩下的參數按從右到左的順序壓入棧。由被調用者把參數彈出棧,對於“C”函數或者 變量,修飾名以“@”爲前綴,然後是函數名,接着是符號“@”及 參數的字節數,如函數int   func(int   a,  double   b)的 修飾名是@func@12。對於“C++”函 數,有所不同。 
未來的編譯器可能使用不同的寄存器來存放參數。 
4.thiscall  
僅僅應 用於“C++”成員函數。this指 針存放於CX寄存器,參數從右到左壓棧。thiscall不 是關鍵詞,因此不能被程序員指定。 
5.naked   call  
採用1-4的 調用約定時,如果必要的話,進入函數時編譯器會產生代碼來保存ESI,EDI,EBX,EBP寄 存器,退出函數時則產生代碼恢復這些寄存器的內容。naked   call不產生這樣的代 碼。 
naked   call不 是類型修飾符,故必須和_declspec共同使用,如下: 
__declspec(   naked   )   int   func(   formal_parameters   )  
{  
//   Function   body  
}

二.函數調用

千萬要注意,C不支持默認參數

----------------------------------------------------------------------------------------------------------------

註釋:默認參數:比如說下面的函數

int fun(int a,int b,int c=3)

{

}

c就 是指定的默認實參,通常在函數原型中指定。這裏給了3作爲默認參數。用平常的時候調用這個函數fun(4,5,6);那 麼就是a=4,b=4,c=6。如果這樣調用fun(1,2)那 麼就是a=1,b=2,c=3,這裏c沒 有指定,因爲c是默認實參,已經有了默認值,這裏c就 是採用默認值3。

爲什麼默認實參必須是函數參數表中 最右邊的參數。把上面的函數改下

int fun(int a=3,int b,int c)

{

}

這樣調用fun(1,2), 這樣就是a=1,b=2,而c根 本就沒有賦到值,就出錯了。這些參數都是一一對應的。

--------------------------------------------------------------------------------------------------------------------

C/C++支 持可變參數個數的函數定義,這一點與C/C++語言函數參數調用時入棧順序有 關,
首先引用其他網友的一段文字,來描述函數調用,及參數入棧:
C支持可變參數的函數, 這裏的意思是C支持函數帶有可變數量的參數,最常見的例子就
是 我們十分熟悉的printf()系列函數。我們還知道在函數調用 時參數是自右向左壓棧的。如果可變參數函數的一般形式是:
    f(p1, p2, p3, …)
那麼參數進棧(以及出棧)的順序是:
    …
    push p3
    push p2
    push p1
    call f
    pop p1
    pop p2
    pop p3
    …
我可以得到這樣一個結論:如果支持可變參數的函數,那麼參數進棧的順序幾乎必然是
自右向左 的。並且,參數出棧也不能由函數自己完成,而應該由調用者完成。

這個結論的後半部分是不難理解的, 因爲函數自身不知道調用者傳入了多少參數,但是
調用者知道,所以調用者應該負責將所有參數 出棧。

在可變參數函數的一般形式中,左邊 是已經確定的參數,右邊省略號代表未知參數部分。對於已經確定的參數,它在棧上的位置也必須是確定的。否則意味着已經確定的參數
是 不能定位和找到的,這樣是無法保證函數正確執行的。衡量參數在棧上的位置,就是
離開確切的 函數調用點(call f)有多遠。已經確定的參數,它在棧上的位置,不應該
依 賴參數的具體數量,因爲參數的數量是未知的!

所以,選擇只能是,已經確定的參 數,離開函數調用點有確定的距離(較近)。滿足這
個條件,只有參數入棧遵從自右向左規則。 也就是說,左邊確定的參數後入棧,離函數
調用點有確定的距離(最左邊的參數最後入棧,離函 數調用點最近)。

這樣,當函數開始執行後,它能找到 所有已經確定的參數。根據函數自己的邏輯,它負
責尋找和解釋後面可變的參數(在離開調用點 較遠的地方),通常這依賴於已經確定的
參數的值(典型的如prinf()函 數的格式解釋,遺憾的是這樣的方式具有脆弱性)。

據說在pascal中 參數是自左向右壓棧的,與C的相反。對於pascal這 種只支持固定參數函
數的語言,它沒有可變參數帶來的問題。因此,它選擇哪種參數進棧方式都 是可以的。
甚至,其參數出棧是由函數自己完成的,而不是調用者,因爲函數的參數的類型和數 量
是完全已知的。這種方式比採用C的 方式的效率更好,因爲佔用更少的代碼量(在C中,
函 數每次調用的地方,都生成了參數出棧代碼)。

C++爲 了兼容C,所以仍然支持函數帶有可變的參數。但是在C++中 更好的選擇常常是函數

重載。





原文地址鏈接:http://blog.sina.com.cn/s/blog_54f82cc2010133mn.html

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