C/C++函數調用約定與函數名稱修飾規則探讨
使用C/C++語言開發軟體的程式員經常碰到這樣的問題:有時候是程式編譯沒有 問題,但是連結的時候總是報告函數不存在(經典的LNK 2001錯誤),有時候是程式編譯和連結都沒有錯誤,但是隻要調用庫中的函數就會出現堆棧異常。這些現象通常是出現在C和C++的代碼混合使用的情況下或 在C++程式中使用第三方的庫的情況下(不是用C++語言開發的),其實這都是函數調用約定(Calling Convention)和函數名修飾(Decorated Name)規則惹的禍。函數調用方式決定了函數參數入棧的順序,是由調用者函數還是被調用函數負責清除棧中的參數等問題,而函數名修飾規則決定了編譯器使 用何種名字修飾方式來區分不同的函數,如果函數之間的調用約定不比對或者名字修飾不比對就會産生以上的問題。本文分别對C和C++這兩種程式設計語言的函數調 用約定和函數名修飾規則進行詳細的解釋,比較了它們的異同之處,并舉例說明了以上問題出現的原因。
函數調用約定(Calling Convention)
函數調用約定不僅決定了發生函數調用時函數參數的入棧順序,還決定了是由調用者函數還是被調用函數負責清除棧中的參數,還原堆棧。函數調用約定有很多方 式,除了常見的__cdecl,__fastcall和__stdcall之外,C++的編譯器還支援thiscall方式,不少C/C++編譯器還支援 naked call方式。這麼多函數調用約定常常令許多程式員很迷惑,到底它們是怎麼回事,都是在什麼情況下使用呢?下面就分别介紹這幾種函數調用約定。
1.__cdecl
編譯器的指令行參數是/Gd。__cdecl方式是C/C++編譯器預設的函數調用約定,所有非C++成員函數和那些沒有用__stdcall或__fastcall聲明的函數都預設是__cdecl方式,它使用C函數調用方式,函數參數按照從右向左的順序入棧,函數調用者負責清除棧中的參數, 由于每次函數調用都要由編譯器産生清除(還原)堆棧的代碼,是以使用__cdecl方式編譯的程式比使用__stdcall方式編譯的程式要大很多,但是 __cdecl調用方式是由函數調用者負責清除棧中的函數參數,是以這種方式支援可變參數,比如printf和windows的API wsprintf就是__cdecl調用方式。對于C函數,__cdecl方式的名字修飾約定是在函數名稱前添加一個下劃線;對于C++函數,除非特别使用extern "C",C++函數使用不同的名字修飾方式。
2.__fastcall
編譯器的指令行參數是/Gr。__fastcall函數調用約定在可能的情況下使用寄存器傳遞參數,通常是前兩個 DWORD類型的參數或較小的參數使用ECX和EDX寄存器傳遞,其餘參數按照從右向左的順序入棧,被調用函數在傳回之前負責清除棧中的參數。編譯器使用兩個@修飾函數名字,後跟十進制數表示的函數參數清單大小,例如:@[email protected]。需要注意的是__fastcall函數調用約定在不同的編譯器上可能有不同的實作,比如16位的編譯器和32位的編譯器,另外,在使用内嵌彙編代碼時,還要注意不能和編譯器使用的寄存器有沖突。
3.__stdcall
編譯器的指令行參數是/Gz,__stdcall是Pascal程式的預設調用方式,大多數Windows的API也是__stdcall調用約定。__stdcall函數調用約定将函數參數從右向左入棧,除非使用指針或引用類型的參數,所有參數采用傳值方式傳遞,由被調用函數負責清除棧中的參數。對于C函數,__stdcall的名稱修飾方式是在函數名字前添加下劃線,在函數名字後添加@和函數參數的大小,例如:[email protected]
4.thiscall
thiscall隻用在C++成員函數的調用,函數參數按照從右向左的順序入棧,類執行個體的this指針通過ECX寄存器傳遞。需要注意的是thiscall不是C++的關鍵字,不能使用thiscall聲明函數,它隻能由編譯器使用。
5.naked call
采用前面幾種函數調用約定的函數,編譯器會在必要的時候自動在函數開始添加儲存ESI,EDI,EBX,EBP寄存器的代碼,在退出函數時恢複這些寄存器 的内容,使用naked call方式聲明的函數不會添加這樣的代碼,這也就是為什麼稱其為naked的原因吧。naked call不是類型修飾符,故必須和_declspec共同使用。
VC的編譯環境預設是使用__cdecl調用約定,也可以在編譯環境的Project Setting...菜單-》C/C++ =》Code Generation項選擇設定函數調用約定。也可以直接在函數聲明前添加關鍵字__stdcall、__cdecl或__fastcall等單獨确定函 數的調用方式。在Windows系統上開發軟體常用到WINAPI宏,它可以根據編譯設定翻譯成适當的函數調用約定,在WIN32中,它被定義為 __stdcall。
函數名字修飾(Decorated Name)方式
函數的名字修飾(Decorated Name)就是編譯器在編譯期間建立的一個字元串,用來指明函數的定義或原型。LINK程式或其他工具有時需要指定函數的名字修飾來定位函數的正确位置。 多數情況下程式員并不需要知道函數的名字修飾,LINK程式或其他工具會自動區分他們。當然,在某些情況下需要指定函數的名字修飾,例如在C++程式中, 為了讓LINK程式或其他工具能夠比對到正确的函數名字,就必須為重載函數和一些特殊的函數(如構造函數和析構函數)指定名字裝飾。另一種需要指定函數的 名字修飾的情況是在彙程式設計式中調用C或C++的函數。如果函數名字,調用約定,傳回值類型或函數參數有任何改變,原來的名字修飾就不再有效,必須指定新的 名字修飾。C和C++程式的函數在内部使用不同的名字修飾方式,下面将分别介紹這兩種方式。
1. C編譯器的函數名修飾規則
對于__stdcall調用約定,編譯器和連結器會在輸出函數名前加上一個下劃線字首,函數名後面加上一個“@”符号和其參數的位元組數,例如[email protected]。__cdecl調用約定僅在輸出函數名前加上一個下劃線字首,例如_functionname。__fastcall調用約定在輸出函數名前加上一個“@”符号,後面也是一個“@”符号和其參數的位元組數,例如@[email protected]。
2. C++編譯器的函數名修飾規則
C++的函數名修飾規則有些複雜,但是資訊更充分,通過分析修飾名不僅能夠知道函數的調用方式,傳回值類型,參數個數甚至參數類型。不管 __cdecl,__fastcall還是__stdcall調用方式,函數修飾都是以一個“?”開始,後面緊跟函數的名字,再後面是參數表的開始辨別和 按照參數類型代号拼出的參數表。對于__stdcall方式,參數表的開始辨別是“@@YG”,對于__cdecl方式則是“@@YA”,對于__fastcall方式則是“@@YI”。參數表的拼寫代号如下所示:
X--void
D--char
E--unsigned char
F--short
H--int
I--unsigned int
J--long
K--unsigned long(DWORD)
M--float
N--double
_N--bool
U--struct
....
指針的方式有些特别,用PA表示指針,用PB表示const類型的指針。後面的代号表明指針類型,如果相同類型的指針連續出現,以“0”代替,一個“0” 代表一次重複。U表示結構類型,通常後跟結構體的類型名,用“@@”表示結構類型名的結束。函數的傳回值不作特殊處理,它的描述方式和函數參數一樣,緊跟 着參數表的開始标志,也就是說,函數參數表的第一項實際上是表示函數的傳回值類型。參數表後以“@Z”辨別整個名字的結束,如果該函數無參數,則以“Z”辨別結束。下面舉兩個例子,假如有以下函數聲明:
int Function1(char *var1,unsigned long);
其函數修飾名為“[email protected]@[email protected]”,而對于函數聲明:
void Function2();
其函數修飾名則為“[email protected]@YGXXZ” 。
對于C++的類成員函數(其調用方式是thiscall),函數的名字修飾與非成員的C++函數稍有不同,首先就是在函數名字和參數表之間插入以“@”字元引導的類名;其次是參數表的開始辨別不同,公有(public)成員函數的辨別是“@@QAE”,保護(protected)成員函數的辨別是“@@IAE”,私有(private)成員函數的辨別是“@@AAE”,如果函數聲明使用了const關鍵字,則相應的辨別應分别為“@@QBE”,“@@IBE”和“@@ABE”。如果參數類型是類執行個體的引用,則使用“AAV1”,對于const類型的引用,則使用“ABV1”。下面就以類CTest為例說明C++成員函數的名字修飾規則:
class CTest
{
......
private:
void Function(int);
protected:
void CopyInfo(const CTest &src);
public:
long DrawText(HDC hdc, long pos, const TCHAR* text, RGBQUAD color, BYTE bUnder, bool bSet);
long InsightClass(DWORD dwClass) const;
......
};
對于成員函數Function,其函數修飾名為“[email protected]@@[email protected]”,字元串“@@AAE”表示這是一個私有函數。成員函數CopyInfo隻有一個參數,是對類CTest的const引用參數,其函數修飾名為“[email protected]@@[email protected]@Z”。 DrawText是一個比較複雜的函數聲明,不僅有字元串參數,還有結構體參數和HDC句柄參數,需要指出的是HDC實際上是一個HDC__結構類型的指 針,這個參數的表示就是“[email protected]@”,其完整的函數修飾名為“[email protected]@@[email protected]@JPBDUtagRG[email protected]@[email protected]”。InsightClass是一個共有的const函數,它的成員函數辨別是“@@QBE”,完整的修飾名就是“?InsightCla[email protected]@@[email protected]”。
無論是C函數名修飾方式還是C++函數名修飾方式均不改變輸出函數名中的字元大小寫,這和PASCAL調用約定不同,PASCAL約定輸出的函數名無任何修飾且全部大寫。
3.檢視函數的名字修飾
有兩種方式可以檢查你的程式中的函數的名字修飾:使用編譯輸出清單或使用Dumpbin工具。使用/FAc,/FAs或/FAcs指令行參數可以讓編譯器 輸出函數或變量名字清單。使用dumpbin.exe /SYMBOLS指令也可以獲得obj檔案或lib檔案中的函數或變量名字清單。此外,還可以使用 undname.exe 将修飾名轉換為未修飾形式。
函數調用約定和名字修飾規則不比對引起的常見問題
函數調用時如果出現堆棧異常,十有八九是由于函數調用約定不比對引起的。比如動态連結庫a有以下導出函數:
long MakeFun(long lFun);
動态庫生成的時候采用的函數調用約定是__stdcall,是以編譯生成的a.dll中函數MakeFun的調用約 定是_stdcall,也就是函數調用時參數從右向左入棧,函數傳回時自己還原堆棧。現在某個程式子產品b要引用a中的MakeFun,b和a一樣使用 C++方式編譯,隻是b子產品的函數調用方式是__cdecl,由于b包含了a提供的頭檔案中MakeFun函數聲明,是以MakeFun在b子產品中被其它 調用MakeFun的函數認為是__cdecl調用方式,b子產品中的這些函數在調用完MakeFun當然要幫着恢複堆棧啦,可是MakeFun已經在結束 時自己恢複了堆棧,b子產品中的函數這樣多此一舉就引起了棧指針錯誤,進而引發堆棧異常。宏觀上的現象就是函數調用沒有問題(因為參數傳遞順序是一樣 的),MakeFun也完成了自己的功能,隻是函數傳回後引發錯誤。解決的方法也很簡單,隻要保證兩個子產品的在編譯時設定相同的函數調用約定就行了。
在了解了函數調用約定和函數的名修飾規則之後,再來看在C++程式中使用C語言編譯的庫時經常出現的LNK 2001錯誤就很簡單了。還以上面例子的兩個子產品為例,這一次兩個子產品在編譯的時候都采用__stdcall調用約定,但是a.dll使用C語言的文法編 譯的(C語言方式),是以a.dll的載入庫a.lib中MakeFun函數的名字修飾就是“[email protected]”。b包含了a提供的頭檔案中MakeFun函數聲明,但是由于b采用的是C++語言編譯,是以MakeFun在b子產品中被按照C++的名字修飾規則命名為“?MakeFu[email protected]@[email protected]”,編譯過程相安無事,連結程式時c++的連結器就到a.lib中去找“[email protected]@[email protected]”,但是a.lib中隻有“_Ma[email protected]”,沒有“[email protected]@[email protected]”,于是連結器就報告:
error LNK2001: unresolved external symbol [email protected]@[email protected]
解決的方法和簡單,就是要讓b子產品知道這個函數是C語言編譯的,extern "C"可以做到這一點。一個采用C語言編譯的庫應該考慮到使用這個庫的程式可能是C++程式(使用C++編譯器),是以在設計頭檔案時應該注意這一點。通常應該這樣聲明頭檔案:
#ifdef _cplusplus
extern "C" {
#endif
long MakeFun(long lFun);
#ifdef _cplusplus
}
#endif
這樣C++的編譯器就知道MakeFun的修飾名是“[email protected]”,就不會有連結錯誤了。
許多人不明白,為什麼我使用的編譯器都是VC的編譯器還會産生“error LNK2001”錯誤?其實,VC的編譯器會根據源檔案的擴充名選擇編譯方式,如果檔案的擴充名是“.C”,編譯器會采用C的文法編譯,如果擴充名是 “.cpp”,編譯器會使用C++的文法編譯程式,是以,最好的方法就是使用extern "C"。
==============================================
大家會注意到,像gcc之類的編譯器也是采用__cdecl方式,這樣對于printf(“%d %d“,i++,i++)之類的輸出結果的疑問就迎刃而解了。
另外引出的一個問題是C語言的變參數函數,對于這部分估計很少大學會涉及到,但這的确是C語言很常見的一個特性,比如printf等。
上面提到對于可變參數的支援,需要由函數調用者負責清除棧中的函數參數,但為什麼非要這樣才可以支援變參數呢?
函數調用所涉及到的參數傳遞是通過棧來實作的,子函數從棧中讀取傳遞給它的參數,如果是從左向右壓棧的話那麼子函數的最後一個參數在棧頂,然後依次是倒數第二個參數......;如果是從右向左壓棧的話那麼子函數的第一個參數在棧頂,然後依次是第二個參數......,壓棧的順序問題決定了子函數讀取其參數的位置。對于變參數的函數本身來講,并不知道有幾個參數,需要某些資訊才能知道,對于printf來講,就是從前面的格式化字元串fmt來分析出來,一共有幾個參數。是以被調函數傳回清理棧的方式,對于可變參數是無法實作的,因為被調函數不知道要彈出參數的數量。而對于函數調用着,自己傳遞給被調函數多少參數(通過把參數壓棧)當然是一清二楚的,這樣被調函數傳回後的堆棧清理也就可以做到準确無誤了(不會多也不會少地把參數清理幹淨)。(該色部分屬于個人了解,如有錯誤,還望指點,以免誤人子弟!謝謝!)
http://hi.baidu.com/wangpeng1314/blog/item/a9c239128817e6cac2fd78a6.html 1,首先,怎麼得到參數的值。對于一般的函數,我們可以通過參數對應在參數清單裡的辨別符來得到。但是參數可變函數那些可變的參數是沒有參數辨別符的,它隻有“…”,是以通過辨別符來得到是不可能的,我們隻有另辟途徑。 我們知道函數調用時都會配置設定棧空間,而函數調用機制中的棧結構如下圖所示: | ...... | ------------------ | 參數2 | ------------------ | 參數1 | ------------------ | 傳回位址 | ------------------ |調用函數運作狀态| ------------------ 可見,參數是連續存儲在棧裡面的,那麼也就是說,我們隻要得到可變參數的前一個參數的位址,就可以通過指針通路到那些可變參數。但是怎麼樣得到可變參數的 前一個參數的位址呢?不知道你注意到沒有,參數可變函數在可變參數之前必有一個參數是固定的,并使用辨別符,而且通常被聲明為char*類 型,printf函數也不例外。這樣的話,我們就可以通過這個參數對應的辨別符來得到位址,進而通路其他參數變得可能。我們可以寫一個測試程式來試一下: #include <stdio.h> void va_test(char* fmt,...);//參數可變的函數聲明 void main() { int a=1,c=55; char b='b'; va_test("",a,b,c);//用四個參數做測試 } void va_test(char* fmt,...) //參數可變的函數定義,注意第一個參數為char* fmt { char *p=NULL; p=(char *)&fmt;//注意不是指向fmt,而是指向&fmt,并且強制轉化為char *,以便一個一個位元組通路 for(int i = 0;i<16;i++)//16是通過計算的值(參數個數*4個位元組),隻是為了測試,暫且将就一下 { printf("%.4d ",*p);//輸出p指針指向位址的值 p++; } } 編譯運作的結果為 0056 0000 0066 0000 | 0001 0000 0000 0000 | 0098 0000 0000 0000 | 0055 0000 0000 0000 由運作結果可見,通過這樣方式可以逐一獲得可變參數的值。 至于為什麼通常被聲明為char*類型,我們慢慢看來。 2,怎樣确定參數類型和數量 通過上述的方式,我們首先解決了取得可變參數值的問題,但是對于一個參數,值很重要,其類型同樣舉足輕重,而對于一個函數來講參數個數也非常重要,否則就 會産生了一系列的麻煩來。通過通路存儲參數的棧空間,我們并不能得到關于類型的任何資訊和參數個數的任何資訊。我想你應該想到了——使用char *參數。Printf函數就是這樣實作的,它把後面的可變參數類型都放到了char *指向的字元數組裡,并通過%來辨別以便與其它的字元相差別,進而确定了參數類型也确定了參數個數。其實,用何種方式來到達這樣的效果取決于函數的實作。 比如說,定義一個函數,預知它的可變參數類型都是int,那麼固定參數完全可以用int類型來替換char*類型,因為隻要得到參數個數就可以了。 3,言歸正傳 我想到了這裡,大概的輪廓已經呈現出來了。本來想就此作罷的(我的惰性使然),但是一想到如果不具實用性便可能是一堆廢物,枉費我打了這麼些字,決定還是繼續下去。 我是比較抵制用那些不明是以的宏定義的,是以在上面的闡述裡一點都沒有涉及定義在<stdarg.h>的va(variable- argument)宏。事實上,當時讓我産生極大疑惑和好奇的正是這幾個宏定義。但是現在我們不得不要去和這些宏定義打打交道,畢竟我們在讨生計的時候還 得用上他們,這也是我曰之為“言歸正傳”的理由。 好了,我們來看一下那些宏定義。 打開<stdarg.h>檔案,找一下va_*的宏定義,發現不單單隻有一組,但是在各組定義前都會有宏編譯。宏編譯訓示的是不同硬體平台和 編譯器下用怎樣的va宏定義。比較一下,不同之處主要在偏移量的計算上。我們還是拿個典型又熟悉的——X86的相關宏定義: 1)typedef char * va_list; 2)#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) 3)#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) 4)#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) 5)#define va_end(ap) ( ap = (va_list)0 ) 我們逐一看來: 第一個我想不必說了,類型定義罷了。第二個是頗有些來頭的,我們也不得不搞懂它,因為後面的兩個關鍵的宏定義都用到了。不知道你夠不夠細心,有沒有發現在 上面的測試程式中,第二個可變參數明明是char類型,可是在輸出結果中占了4個byte。難道所有的參數都會占4個byte的空間?那如果是 double類型的參數,且不是會丢失資料!如果你不嫌麻煩的話,再去做個測試吧,在上面的測試程式中用一個double類型(長度為8byte)和一個 long double類型(長度為10byte)做可變參數。發現什麼?double類型占了8byte,而long double占了12byte。好像都是4的整數倍哦。不得不引出另一個概念了“對齊(alignment)”,所謂對齊,對Intel80x86 機器來說就是要求每個變量的位址都是sizeof(int)的倍數。原來我們搞錯了,char類型的參數隻占了1byte,但是它後面的參數因為對齊的關 系隻能跳過3byte存儲,而那3byte也就浪費掉了。那為什麼要對齊?因為在對齊方式下,CPU 的運作效率要快得多(舉個例子吧,要說明的是下面的例子是我從網上摘錄下來的,不記得出處了。 示例:如下圖,當一個long 型數(如圖中long1)在記憶體中的位置正好與記憶體的字邊界對齊時,CPU 存取這個數隻需通路一次記憶體,而當一個long 型數(如圖中的long2)在記憶體中的位置跨越了字邊界時,CPU 存取這個數就需要多次通路記憶體,如i960cx 通路這樣的數需讀記憶體三次(一個BYTE、一個SHORT、一個BYTE,由CPU 的微代碼執行,對軟體透明),是以對齊方式下CPU 的運作效率明顯快多了。 1 8 16 24 32 ------- ------- ------- --------- | long1 | long1 | long1 | long1 | ------- ------- ------- --------- | | | | long2 | ------- ------- ------- --------- | long2 | long2 | long2 | | ------- ------- ------- --------- | ....)。好像扯得有點遠來,但是有助于對_INTSIZEOF(n)的了解。位操作對于我來說是玄的東東。單個位運算還應付得來,而這樣一個表達式擺在面前就暈了。怎麼辦?菜鳥自有菜的辦法。(待續) Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=14721 --------------------------------------------------------------------------------------------------------------------- C語言中的可變參數函數 CSDN Blog推出文章指數概念,文章指數是對Blog文章綜合評分後推算出的,綜合評分項分别是該文章的點選量,回複次數,被網摘收錄數量,文章長度和文章類型;滿分100,每月更新一次。 第一篇 C語言程式設計中有時會遇到一些參數個數可變的函數,例如printf()函數,其函數原型為: int printf( const char* format, ...); 它除了有一個參數format固定以外,後面跟的參數的個數和類型是可變的(用三個點“…”做參數占位符),實際調用時可以有以下的形式: printf("%d",i); printf("%s",s); printf("the number is %d ,string is:%s", i, s); 一個簡單的可變參數的C函數 先看例子程式。該函數至少有一個整數參數,其後占位符…,表示後面參數的個數不定。在這個例子裡,所有的輸入參數必須都是整數,函數的功能隻是列印所有參數的值。函數代碼如下: //示例代碼1:可變參數函數的使用 #include "stdio.h" #include "stdarg.h" void simple_va_fun(int start, ...) { va_list arg_ptr; int nArgValue =start; int nArgCout="0"; //可變參數的數目 va_start(arg_ptr,start); //以固定參數的位址為起點确定變參的記憶體起始位址。 do { ++nArgCout; printf("the %d th arg: %d",nArgCout,nArgValue); //輸出各參數的值 nArgValue = va_arg(arg_ptr,int); //得到下一個可變參數的值 } while(nArgValue != -1); return; } int main(int argc, char* argv[]) { simple_va_fun(100,-1); simple_va_fun(100,200,-1); return 0; } 下面解釋一下這些代碼。從這個函數的實作可以看到,我們使用可變參數應該有以下步驟: ⑴由于在程式中将用到以下這些宏: void va_start( va_list arg_ptr, prev_param ); type va_arg( va_list arg_ptr, type ); void va_end( va_list arg_ptr ); va在這裡是variable-argument(可變參數)的意思。 這些宏定義在stdarg.h中,是以用到可變參數的程式應該包含這個頭檔案。 ⑵函數裡首先定義一個va_list型的變量,這裡是arg_ptr,這個變量是存儲參數位址的指針.因為得到參數的位址之後,再結合參數的類型,才能得到參數的值。 ⑶然後用va_start宏初始化⑵中定義的變量arg_ptr,這個宏的第二個參數是可變參數清單的前一個參數,即最後一個固定參數。 ⑷然後依次用va_arg宏使arg_ptr傳回可變參數的位址,得到這個位址之後,結合參數的類型,就可以得到參數的值。 ⑸設定結束條件,這裡的條件就是判斷參數值是否為-1。注意被調的函數在調用時是不知道可變參數的正确數目的,程式員必須自己在代碼中指明結束條件。至于為什麼它不會知道參數的數目,在看完這幾個宏的内部實作機制後,自然就會明白。 第二篇 C語言之可變參數問題 C語言中有一種長度不确定的參數,形如:"…",它主要用在參數個數不确定的函數中,我們最容易想到的例子是printf函數。 原型: int printf( const char *format [, argument]... ); 使用例: printf("Enjoy yourself everyday!/n"); printf("The value is %d!/n", value); 這種可變參數可以說是C語言一個比較難了解的部分,這裡會由幾個問題引發一些對它的分析。 注意:在C++中有函數重載(overload)可以用來差別不同函數參數的調用,但它還是不能表示任意數量的函數參數。 問題:printf的實作 請問,如何自己實作printf函數,如何處理其中的可變參數問題? 答案與分析: 在标準C語言中定義了一個頭檔案專門用來對付可變參數清單,它包含了一組宏,和一個va_list的typedef聲明。一個典型實作如下: typedef char* va_list; #define va_start(list) list = (char*)&va_alist #define va_end(list) #define va_arg(list, mode)/ ((mode*) (list += sizeof(mode)))[-1] 自己實作printf: #include int printf(char* format, …) { va_list ap; va_start(ap, format); int n = vprintf(format, ap); va_end(ap); return n; } 問題:運作時才确定的參數 有沒有辦法寫一個函數,這個函數參數的具體形式可以在運作時才确定? 答案與分析: 目前沒有"正規"的解決辦法,不過獨門偏方倒是有一個,因為有一個函數已經給我們做出了這方面的榜樣,那就是main(),它的原型是: int main(int argc,char *argv[]); 函數的參數是argc和argv。 深入想一下,"隻能在運作時确定參數形式",也就是說你沒辦法從聲明中看到所接受的參數,也即是參數根本就沒有固定的形式。常用的辦法是你可以通過定 義一個void *類型的參數,用它來指向實際的參數區,然後在函數中根據根據需要任意解釋它們的含義。這就是main函數中argv的含義,而argc,則用來表明實際 的參數個數,這為我們使用提供了進一步的友善,當然,這個參數不是必需的。 雖然參數沒有固定形式,但我們必然要在函數中解析參數的意義,是以,理所當然會有一個要求,就是調用者和被調者之間要對參數區内容的格式,大小,有效性等所有方面達成一緻,否則南轅北轍各說各話就慘了。 問題:可變長參數的傳遞 有時候,需要編寫一個函數,将它的可變長參數直接傳遞給另外的函數,請問,這個要求能否實作? 答案與分析: 目前,你尚無辦法直接做到這一點,但是我們可以迂回前進,首先,我們定義被調用函數的參數為va_list類型,同時在調用函數中将可變長參數清單轉換為va_list,這樣就可以進行變長參數的傳遞了。看如下所示: void subfunc (char *fmt, va_list argp) { ... arg = va_arg (fmt, argp); ... } void mainfunc (char *fmt, ...) { va_list argp; va_start (argp, fmt); subfunc (fmt, argp); va_end (argp); ... } 問題:可變長參數中類型為函數指針 我想使用va_arg來提取出可變長參數中類型為函數指針的參數,結果卻總是不正确,為什麼? 答案與分析: 這個與va_arg的實作有關。一個簡單的、示範版的va_arg實作如下: #define va_arg(argp, type) / (*(type *)(((argp) += sizeof(type)) - sizeof(type))) 其中,argp的類型是char *。 如果你想用va_arg從可變參數清單中提取出函數指針類型的參數,例如 int (*)(),則va_arg(argp, int (*)())被擴充為: (*(int (*)() *)(((argp) += sizeof (int (*)())) -sizeof (int (*)()))) 顯然,(int (*)() *)是無意義的。 解決這個問題的辦法是将函數指針用typedef定義成一個獨立的資料類型,例如: typedef int (*funcptr)(); 這時候再調用va_arg(argp, funcptr)将被擴充為: (* (funcptr *)(((argp) += sizeof (funcptr)) - sizeof (funcptr))) 這樣就可以通過編譯檢查了。 問題:可變長參數的擷取 有這樣一個具有可變長參數的函數,其中有下列代碼用來擷取類型為float的實參: va_arg (argp, float); 這樣做可以嗎? 答案與分析: 不可以。在可變長參數中,應用的是"加寬"原則。也就是float類型被擴充成double;char, short被擴充成int。是以,如果你要去可變長參數清單中原來為float類型的參數,需要用va_arg(argp, double)。對char和short類型的則用va_arg(argp, int)。 問題:定義可變長參數的一個限制 為什麼我的編譯器不允許我定義如下的函數,也就是可變長參數,但是沒有任何的固定參數? int f (...) { ... } 答案與分析: 不可以。這是ANSI C 所要求的,你至少得定義一個固定參數。 這個參數将被傳遞給va_start(),然後用va_arg()和va_end()來确定所有實際調用時可變長參數的類型和值。 --------------------------------------------------------------------------------------------------------------------- 如何判别可變參數函數的參數類型? 函數形式如下: void fun(char* str,...) { ...... } 若傳的參數個數大于1,如何判别第2個以後傳參的參數類型??? 最好有源碼說明! 沒辦法判斷的 如樓上所說,例如printf( "%d%c%s ", ....)是通過格式串中的%d, %c, %s來确定後面參數的類型,其實你也可以參考這種方法來判斷不定參數的類型. 無法判斷。可變參數實作主要通過三個宏實作:va_start, va_arg, va_end。 六、 擴充與思考 個數可變參數在聲明時隻需"..."即可;但是,我們在接受這些參數時不能"..."。va函數實作的關鍵就是如何得到參數清單中可選參數,包括參數的值 和類型。以上的所有實作都是基于來自stdarg.h的va_xxx的宏定義。 <思考>能不能不借助于va_xxx,自己實作VA呢?,我想到的方法是彙編。在C中,我們當然就用C的嵌入彙編來實作,這應該是可以做得到 的。至于能做到什麼程度,穩定性和效率怎麼樣,主要要看你對記憶體和指針的控制了。 參考資料 1.IEEE和OpenGroup聯合開發的Single Unix specification Ver3;BR> 2.Linux man手冊; 3.x86彙編,還有一些安全編碼方面的資料。 --------------------------------------------------------------------------------------------------------------------- [轉帖]對C/C++可變參數表的深層探索 C/C++語言有一個不同于其它語言的特性,即其支援可變參數,典型的函數如printf、scanf等可以接受數量不定的參數。如: printf ( "I love you" ); printf ( "%d", a ); printf ( "%d,%d", a, b ); 第一、二、三個printf分别接受1、2、3個參數,讓我們看看printf函數的原型: int printf ( const char *format, ... ); 從函數原型可以看出,其除了接收一個固定的參數format以外,後面的參數用"…"表示。在C/C++語言中,"…"表示可以接受不定數量的參數,理論上來講,可以是0或0以上的n個參數。 本文将對C/C++可變參數表的使用方法及C/C++支援可變參數表的深層機理進行探索。 一. 可變參數表的用法 1、相關宏 标準C/C++包含頭檔案stdarg.h,該頭檔案中定義了如下三個宏: void va_start ( va_list arg_ptr, prev_param ); type va_arg ( va_list arg_ptr, type ); void va_end ( va_list arg_ptr ); 在這些宏中,va就是variable argument(可變參數)的意思;arg_ptr是指向可變參數表的指針;prev_param則指可變參數表的前一個固定參數;type為可變參數 的類型。va_list也是一個宏,其定義為typedef char * va_list,實質上是一 char型指針。char型指針的特點是++、--操作對其作用的結果是增1和減1(因為sizeof(char)為1),與之不同的是int等其它類型 指針的++、--操作對其作用的結果是增sizeof(type)或減sizeof(type),而且sizeof (type)大于1。 通過va_start宏我們可以取得可變參數表的首指針,這個宏的定義為: #define va_start ( ap, v ) ( ap = (va_list)&v + _INTSIZEOF(v) ) 顯而易見,其含義為将最後那個固定參數的位址加上可變參數對其的偏移後指派給ap,這樣ap就是可變參數表的首位址。其中的_INTSIZEOF宏定義為: #define _INTSIZEOF(n) ((sizeof ( n ) + sizeof ( int ) - 1 ) & ~( sizeof( int ) - 1 ) ) va_arg宏的意思則指取出目前arg_ptr所指的可變參數并将ap指針指向下一可變參數,其原型為: #define va_arg(list, mode) ((mode *)(list =(char *) ((((int)list + (__builtin_alignof(mode)<=4?3:7)) &(__builtin_alignof(mode)<=4?-4:-8))+sizeof(mode))))[-1] 對這個宏的具體含義我們将在後面深入讨論。 而va_end宏被用來結束可變參數的擷取,其定義為: #define va_end ( list ) 可以看出,va_end ( list )實際上被定義為空,沒有任何真實對應的代碼,用于代碼對稱,與va_start對應;另外,它還可能發揮代碼的"自注釋"作用。所謂代碼的"自注釋",指的是代碼能自己注釋自己。 下面我們以具體的例子來說明以上三個宏的使用方法。 2、一個簡單的例子 #include <stdarg.h> int max ( int num, ... ) { int m = -0x7FFFFFFF; va_list ap; va_start ( ap, num ); for ( int i= 0; i< num; i++ ) { int t = va_arg (ap, int); if ( t > m ) { m = t; } } va_end (ap); return m; } int main ( int argc, char* argv[] ) { int n = max ( 5, 5, 6 ,3 ,8 ,5); cout << n; return 0; } 函數max中首先定義了可變參數表指針ap,而後通過va_start ( ap, num )取得了參數表首位址(賦給了ap),其後的for循環則用來周遊可變參數表。這種周遊方式與我們在資料結構教材中經常看到的周遊方式是類似的。 函數max看起來簡潔明了,但是實際上printf的實作卻遠比這複雜。max函數之是以看起來簡單,是因為: (1) max函數可變參數表的長度是已知的,通過num參數傳入; (2) max函數可變參數表中參數的類型是已知的,都為int型。 而printf函數則沒有這麼幸運。首先,printf函數可變參數的個數不能輕易的得到,而可變參數的類型也不是固定的,需由格式字元串進行識别(由%f、%d、%s等确定),是以則涉及到可變參數表的更複雜應用。 下面我們以執行個體來分析可變參數表的進階應用。 二. 進階應用 下面這個程式是我們為某嵌入式系統(該系統中CPU的字長為16位)編寫的在螢幕上顯示格式字元串的函數DrawText,它的用法類似于 int printf ( const char *format, ... )函數,但其輸出的目标為嵌入式系統的液晶顯示螢幕(LED)。 /// // 函數名稱: DrawText // 功能說明: 在顯示屏上繪制文字 // 參數說明: xPos ---橫坐标的位置 [0 .. 30] // yPos ---縱坐标的位置 [0 .. 64] // ... 可以同數字一起顯示,需設定标志(%d、%l、%x、%s) /// extern void DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, ... ) { BYTE lpData[100]; //緩沖區 BYTE byIndex; BYTE byLen; DWORD dwTemp; WORD wTemp; int i; va_list lpParam; memset( lpData, 0, 100); byLen = strlen( lpStr ); byIndex = 0; va_start ( lpParam, lpStr ); for ( i = 0; i < byLen; i++ ) { if( lpStr[i] != ’%’ ) //不是格式符開始 { lpData[byIndex++] = lpStr[i]; } else { switch (lpStr[i+1]) { //整型 case ’d’: case ’D’: wTemp = va_arg ( lpParam, int ); byIndex += IntToStr( lpData+byIndex, (DWORD)wTemp ); i++; break; //長整型 case ’l’: case ’L’: dwTemp = va_arg ( lpParam, long ); byIndex += IntToStr ( lpData+byIndex, (DWORD)dwTemp ); i++; break; //16進制(長整型) case ’x’: case ’X’: dwTemp = va_arg ( lpParam, long ); byIndex += HexToStr ( lpData+byIndex, (DWORD)dwTemp ); i++; break; default: lpData[byIndex++] = lpStr[i]; break; } } } va_end ( lpParam ); lpData[byIndex] = ’/0’; DisplayString ( xPos, yPos, lpData, TRUE); //在螢幕上顯示字元串lpData } 在這個函數中,需通過對傳入的格式字元串(首位址為lpStr)進行識别來獲知可變參數個數及各個可變參數的類型,具體實作展現在for循環中。譬 如,在識别為%d後,做的是va_arg ( lpParam, int ),而獲知為%l和%x後則進行的是va_arg ( lpParam, long )。格式字元串識别完成後,可變參數也就處理完了。 在項目的最初,我們一直苦于不能找到一個好的辦法來混合輸出字元串和數字,我們采用了分别顯示數字和字元串的方法,并分别指定坐标,程式條理被破壞。而且,在混合顯示的時候,要給各類資料分别人工計算坐标,我們感覺頭疼不已。以前的函數為: //顯示字元串 showString ( BYTE xPos, BYTE yPos, LPBYTE lpStr ) //顯示數字 showNum ( BYTE xPos, BYTE yPos, int num ) //以16進制方式顯示數字 showHexNum ( BYTE xPos, BYTE yPos, int num ) 最終,我們用DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, ... )函數代替了原先所有的輸出函數,程式得到了簡化。就這樣,兄弟們用得爽翻了。 三. 運作機制探索 通過第2節我們學會了可變參數表的使用方法,相信喜歡抛根問底的讀者還不甘心,必然想知道如下問題: (1)為什麼按照第2節的做法就可以獲得可變參數并對其進行操作? (2)C/C++在底層究竟是依靠什麼來對這一文法進行支援的,為什麼其它語言就不能提供可變參數表呢? 我們帶着這些疑問來一步步進行摸索。 3.1 調用機制反彙編 反彙編是研究文法深層特性的終極良策,先來看看2.2節例子中主函數進行max ( 5, 5, 6 ,3 ,8 ,5)調用時的反彙編: 1. 004010C8 push 5 2. 004010CA push 8 3. 004010CC push 3 4. 004010CE push 6 5. 004010D0 push 5 6. 004010D2 push 5 7. 004010D4 call @ILT+5(max) (0040100a) 從上述反彙編代碼中我們可以看出,C/C++函數調用的過程中: 第一步:将參數從右向左入棧(第1~6行); 第二步:調用call指令進行跳轉(第7行)。 這兩步包含了深刻的含義,它說明 C/C++預設的調用方式為由調用者管理參數入棧的操作,且入棧的順序為從右至左,這種調用方式稱為_cdecl調 用。x86系統的入棧方向為從高位址到低位址,故第1至n個參數被放在了位址遞增的堆棧内。在被調用函數内部,讀取這些堆棧的内容就可獲得各個參數的值, 讓我們反彙編到max函數的内部: int max ( int num, ...) { 1. 00401020 push ebp 2. 00401021 mov ebp,esp 3. 00401023 sub esp,50h 4. 00401026 push ebx 5. 00401027 push esi 6. 00401028 push edi 7. 00401029 lea edi,[ebp-50h] 8. 0040102C mov ecx,14h 9. 00401031 mov eax,0CCCCCCCCh 10. 00401036 rep stos dword ptr [edi] va_list ap; int m = -0x7FFFFFFF; 11. 00401038 mov dword ptr [ebp-8],80000001h va_start ( ap, num ); 12. 0040103F lea eax,[ebp+0Ch] 13. 00401042 mov dword ptr [ebp-4],eax for ( int i= 0; i< num; i++ ) 14. 00401045 mov dword ptr [ebp-0Ch],0 15. 0040104C jmp max+37h (00401057) 16. 0040104E mov ecx,dword ptr [ebp-0Ch] 17. 00401051 add ecx,1 18. 00401054 mov dword ptr [ebp-0Ch],ecx 19. 00401057 mov edx,dword ptr [ebp-0Ch] 20. 0040105A cmp edx,dword ptr [ebp+8] 21. 0040105D jge max+61h (00401081) { int t= va_arg (ap, int); 22. 0040105F mov eax,dword ptr [ebp-4] 23. 00401062 add eax,4 24. 00401065 mov dword ptr [ebp-4],eax 25. 00401068 mov ecx,dword ptr [ebp-4] 26. 0040106B mov edx,dword ptr [ecx-4] 27. 0040106E mov dword ptr [t],edx if ( t > m ) 28. 00401071 mov eax,dword ptr [t] 29. 00401074 cmp eax,dword ptr [ebp-8] 30. 00401077 jle max+5Fh (0040107f) m = t; 31. 00401079 mov ecx,dword ptr [t] 32. 0040107C mov dword ptr [ebp-8],ecx } 33. 0040107F jmp max+2Eh (0040104e) va_end (ap); 34. 00401081 mov dword ptr [ebp-4],0 return m; 35. 00401088 mov eax,dword ptr [ebp-8] } 36. 0040108B pop edi 37. 0040108C pop esi 38. 0040108D pop ebx 39. 0040108E mov esp,ebp 40. 00401090 pop ebp 41. 00401091 ret 分析上述反彙編代碼,對于一個真正的程式員而言,将是一種很大的享受;而對于初學者,也将使其受益良多。是以請一定要賴着頭皮認真研究,千萬不要被吓倒! 行1~10進行執行函數内代碼的準備工作,儲存現場。第2行對堆棧進行移動;第3行則意味着max函數為其内部局部變量準備的堆棧空間為50h位元組;第11行表示把變量n的記憶體空間安排在了函數内部局部棧底減8的位置(占用4個位元組)。 第12~13行非常關鍵,對應着va_start ( ap, num ),這兩行将第一個可變參數的位址指派給了指針ap。另外,從第12行可以看出num的位址為ebp+0Ch;從第13行可以看出ap被配置設定在函數内部局部棧底減4的位置上(占用4個位元組)。 第22~27行最為關鍵,對應着va_arg (ap, int)。其中,22~24行的作用為将ap指向下一可變參數(可變參數的位址間隔為4個位元組,從add eax,4可以看出);25~27行則取目前可變參數的值賦給變量t。這段反彙編很奇怪,它先移動可變參數指針,再在指派指令裡面回過頭來取先前的參數值 賦給t(從mov edx,dword ptr [ecx-4]語句可以看出)。Visual C++同學玩得有意思,不知道碰見同樣的情況Visual Basic等其它同學怎麼玩? 第36~41行恢複現場和堆棧位址,執行函數傳回操作。 痛苦的反彙編之旅差不多結束了,看了這段反彙編我們總算弄明白了可變參數的存放位置以及它們被讀取的方式,頓覺全省輕松! 2、特殊的調用約定 除此之外,我們需要了解C/C++函數調用對參數占用空間的一些特殊約定,因為在_cdecl調用協定中,有些變量類型是按照其它變量的尺寸入棧的。 例如,字元型變量将被自動擴充為一個字的空間,因為入棧操作針對的是一個字。 參數n實際占用的空間為( ( sizeof(n) + sizeof(int) - 1 ) & ~( sizeof(int) - 1 ) ),這就是第2.1節_INTSIZEOF(v)宏的來曆! 既然如此,前面給出的va_arg ( list, mode )宏為什麼玩這麼大的飛機就很清楚了。這個問題就留個讀者您來分析 |