c++進階(二)函式呼叫約定及函式名稱修飾符

firedragonpzy發表於2012-07-13
幾乎每種程式語言都有函式的概念,而作為函式,就一定有引數的概念;一般來說,引數的傳遞是通過堆疊來實現的,堆疊是一種先入後出的資料結構,使用Push()把引數壓入棧中,使用Pop()把引數彈出棧,而且Push()和Pop()必須成對出現;
重要的一點:函式呼叫約定不僅決定了發生函式呼叫時函式引數的入棧順序,還決定了是由呼叫者函式還是被呼叫者函式負責清除函式棧中的引數並還原棧;
不過,對於函式的引數來說,有兩種不同的處理方法:
第一種方法:把引數按照原順序(函式定義時的順序)壓入棧中,然後使用CALL指令呼叫函式的地址,而函式使用Pop()把引數從棧中彈出然後處理;由於堆疊的先入後出特性,所以這種方法對呼叫者有利,因為在實際呼叫的時候,呼叫者得到的是正序的引數(編譯階段由編譯器負責壓棧),而被調函式得到的是反序的引數;
第二種方法:把引數按照反序(函式定義時的順序的逆序)壓入棧中,然後使用CALL指令呼叫函式地址,而函式使用POP把引數彈出棧然後處理;這種方法對被呼叫者有利,因為被調函式得到的是正序的引數,而函式的壓棧操作時在編譯階段由編譯器做的;
C語言和PASCAL語言分別使用者兩種方式:C語言使用正序壓棧反序出棧的方式,PASCAL語言使用反序壓棧正序出棧的方式;而Windows使用的呼叫方式跟PASCAL語言的呼叫方式是一樣的,所以,以前老的C程式設計師編寫Windows程式時需要使用關鍵字PASCAL來明確指出使用PASCAL的呼叫規則,現在一般不再使用PASCAL關鍵字了,而使用__stdcall說明符,表示使用的是標準呼叫;
現在稱為標準呼叫的就是PASCAL語言的呼叫方式:反序壓棧,正序出棧;這樣做的確有好處,因為壓棧行為是編譯器編譯好的(編譯時行為),雖然麻煩,但是隻在編譯階段做一次,而出棧行為是程式實際執行時的呼叫行為(執行時行為),需要反覆多次執行,使用正序的引數,對被調函式來說,處理上方便不少;
函式呼叫約定由很多種方式,常見的有__cdecl、__stdcall、__fastcall,C++的編譯器還支援__thiscall方式,不少的C/C++編譯器還支援naked call方式;
1.__cdecl方式:
這種方式是編譯器預設的函式呼叫方式,所有非C++成員函式和那些沒有使用__stdcall或__fastcall宣告的函式預設都是__cdecl方式;__cdecl使用的是C語言的呼叫方式:函式引數按照從右向左的順序入棧(反序入棧),並由函式呼叫者負責在呼叫時把引數彈出棧,呼叫結束時清空棧中的引數;由於每次函式呼叫都要由編譯器產生清除/還原堆疊的程式碼,所以使用__cdecl方式編譯出來的程式比使用__stdcall方式編譯出來的程式要大很多,但是__cdecl呼叫方式是由呼叫者函式負責把引數彈出和清空棧中的函式引數,所以,__cdecl方式支援可變引數,比如printf和Windows的API wsprintf就是__cdecl呼叫方式;對於C函式,__cdecl方式的名字修飾符約定是在函式名稱前面新增一個下劃線;對於C++函式,除非特別使用extern "C",C++函式使用不同的名字修飾符;
2.__stdcall方式:
這種方式的函式呼叫約定將函式引數按照從右向左的順序壓入棧中,函式呼叫時由被呼叫函式負責把引數彈出棧,並在呼叫結束時由被呼叫函式負責清除函式棧中的引數;除非使用指標或引用型別的引數,其它所有引數的傳遞都是採用傳值的方式傳遞;對於C函式,__stdcall的名稱修飾方式是在函式名稱前面新增下劃線,在函式後面新增@符號和函式引數的大小;例如: _function_name@number;
3.__fastcall方式:
這種函式呼叫約定方式在可能的情況下使用暫存器傳遞引數,通常是前兩個DWORD型別的引數或者是較小的引數使用ECX和EDX暫存器傳遞,其餘的引數按照從右向左的順序壓入棧中(反序入棧),由被呼叫函式負責在呼叫時彈出棧中的引數,並在呼叫結束時(函式返回之前)清除函式棧中的引數;編譯器使用兩個@來修飾函式的名字,後面跟十進位制數字表示函式引數列表的大小;比如:@function_name@number;需要注意的是:__fastcall函式呼叫約定在不同的編譯器上可能有不同的實現;比如:16位的編譯器和32位的編譯器,另外,在使用內嵌彙編程式碼時,還要注意不能和編譯器使用的暫存器有衝突;
4.thiscall方式:
這種方式僅僅用於C++類的成員函式的呼叫,函式引數按照從右向左的順序入棧,類例項的指標this通過暫存器ECX傳遞,組要注意的是,thiscall不是C++的關鍵字,不能使用thiscall來宣告函式,thiscall關鍵字只能由編譯器使用;
5.naked call方式:
在使用呼叫約定__cdecl、__stdcall、__fastcall和thiscall時,編譯器會在需要的時候自動在函式開始處(進入函式時)新增儲存ESI、EDI、EBX、EBP暫存器的程式碼,在退出函式時恢復這些暫存器的內容;而使用naked call方式不會產生這樣的程式碼;這也就是為什麼稱其為naked的原因吧.naked call不是型別修飾符,故必須與__declspec共同使用;如下:
__declspec(naked) int Func(parameters-list)
{ ... }

注意:VC的編譯環境預設使用的是__cdecl的呼叫約定;程式設計師也可以自行設定呼叫約定;比如:在Windows系統上寫程式碼的時候,常常用到WINAPI巨集,編譯器會根據編譯器的設定把這個巨集編譯成適當的呼叫約定;在WIN32中,它被編譯成__stdcall;
還有的資料上有以下的觀點:
C語言呼叫約定:引數正序入棧,反序出棧;
PASCAL語言呼叫約定:引數反序入棧,正序出棧;屬標準呼叫;Windows及Windows API就採用這種方式;

函式名稱修飾符方式:
函式的名稱修飾符就是編譯器在編譯期間建立的一個字串,用來指明函式的定義或原型.LINK程式或其它工具有時需要指定函式的名稱修飾符來定位函式的正確位置;多數情況下,程式設計師不需要知道函式的名稱修飾符,LINK程式會自動區分它們.當然在某些情況下需要指定函式的名稱修飾符,例如在C++程式中為了讓LINK程式或其它工具能夠匹配到正確的函式名字,就必須為過載函式和一些特殊函式(建構函式或解構函式)指定名稱修飾符.另一種需要指定函式名稱修飾符的情況是在彙編程式中呼叫C或C++的函式,如果函式名字、呼叫約定、返回值型別或函式引數有任何改變,那麼原來的名字修飾符就不再有效了,必須指定新的名稱修飾符.C和C++程式的函式在內部使用不同的名字修飾符規則:
1、C編譯器的函式名稱修飾規則:
對於__stdcall呼叫約定,編譯器和連結器會在輸出函式的名稱前面加上一個下劃線,函式名稱後面加上一個@符號和一個表示引數大小(位元組數)的數字,例如:_FunctionName@number; __cdecl呼叫約定僅在輸出函式名稱前面加上一個下劃線,例如:_FunctionName; __fastcall呼叫約定會在輸出函式名稱前面加上一個@符號,後面加上一個@符號和一個表示引數大小的數字,例如:@FunctionName@number;
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型別的指標;後面的代號表明指標型別,如果相同型別的指標連續出現,則以"O"代替,一個"O"表示重複一次.U表示結構型別,通常後面跟結構體的名稱,用"@@"表示結構型別名的結束.函式的返回值不做特殊處理,它的描述方式和函式引數一樣,緊跟著參數列的開始標識,也就是說,函式參數列的第一項實際上表示函式的返回值型別;參數列後面以"@Z"標識整個名字的結束,如果該函式沒有引數,則以"Z"標識結束.
例1:函式宣告: int Function1(char*, unsigned long);
其函式名稱修飾符為"?Function1@@YGHPADK@Z"
例2:函式宣告: void Function(void);
其函式名稱修飾符為"?Function2@@YGXXZ";
成員函式名稱修飾符規則:
C++類的成員函式採用thiscall的呼叫約定,成員函式的名稱修飾符規則與非成員函式的名稱修飾符規則有所不同;首先就是在函式名稱與參數列之間插入以字元"@"引導的類名;其次是參數列的開始標識不同,public成員函式的參數列開始標識是"@@QAE",protected成員函式的參數列的開始標識是"@@IAE",private成員函式的參數列的開始標識是"@@AAE";如果函式宣告中使用了const關鍵字,則成員函式的名稱修飾符又有所不同了,此時,public成員函式的參數列的開始標識是"@@QBE",protected成員函式的參數列的開始標識是"@@IBE",private成員函式的參數列的開始標識是"@@ABE";如果引數型別是類例項的引用,則使用"AAV1",對於const型別的引用,則使用"ABV1";例如:
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()的名稱修飾符是"?Function@CTest@@AAEXH@Z",字串"@@AAE"表示該成員函式是私有成員函式;成員函式CopyInfo()只有一個引數,而且是對CTest類例項的一個常引用,其修飾符名稱是"?CopyInfo@CTest@@IAEXABV1@@Z";成員函式DrawText()是個比較複雜的函式,不僅有字串引數,還有結構體引數和HDC控制程式碼引數,需要指出的是,HDC實際上是一個HDC__結構型別的指標,這個引數的表示就是"PAUHDC__@@",成員函式DrawText的完整的名稱修飾符是"?DrawText@CTest@@QAEJPAUHDC__@@JPBDUtagRGBQUAD@@E_N@Z";成員函式InsightClass()是個const型別的共有成員函式,其參數列開始標識是"@@QBE",完整的名稱修飾符就是"?InsightClass@CTest@@QBEJK@Z";
總之,無論是C函式名稱修飾規則還是C++函式名稱修飾規則,均不會改變輸出函式名稱的大小寫,這與PASCAL的函式呼叫約定不同,PASCAL約定輸出函式的名稱無任何修飾且全部大寫;
檢視函式的名稱修飾符:
在Windows下,有兩種方法檢視函式的名稱修飾符:使用編譯輸出列表或者是dumpbin工具;使用/FAc、/FAs或/FAcs命令列引數可以讓編譯器輸出函式或變數名稱列表;使用dumpbin.exe /SYMBOLS命令也可以獲得obj檔案或lib檔案中的函式名或變數名列表;此外,還可以使用undname.exe將修飾符名轉換成未修飾形式;
如果要在C++程式中呼叫C語言編譯的函式或庫,通常要按照下面的方法來宣告C語言函式或標頭檔案:
#ifdef __cplusplus
extern "C" {
#endif
long Function(long);
#ifdef __cplusplus
}
#endif
VC的編譯器會根據原始檔的副檔名來選擇編譯方式,如果原始檔副檔名是.c,則編譯器會採用C語言的語法來編譯,如果原始檔的副檔名是.cpp,則編譯器會採用C++的語法來編譯;所以,最好的方法就是使用extern "C";
gcc編譯器採用的也是__cdecl方式的呼叫約定;這樣的話,就很容易實現可變引數的函式了;函式呼叫所涉及到的引數傳遞是通過函式的棧來實現的,子函式從棧中讀取傳遞給它的引數,如果引數是從左向右壓棧的話,那麼子函式的最後一個引數在棧頂,然後依次是倒數第二個引數,...;如果是從右向左壓棧的話,那麼子函式的第一個引數在棧頂,然後依次是第二個引數,...;這樣的話,引數壓棧的順序決定了子函式讀取其引數的位置;對於具有可變引數的函式來說,並不知道具體有幾個引數,需要知道某些資訊才行,比如:printf()函式,就是從前面的格式化字串引數fmt中分析出引數的個數;所以,被調函式返回時清理棧的方式,對於可變引數是無法實現的,因為被調函式並不知道需要彈出的引數的數量;而對於呼叫者函式自己來說,自己傳遞給被調函式多少個引數(通過把引數壓棧時統計得知)當然是一清二楚的,這樣被調函式返回之後,棧的清理工作就可以做到準確無誤了(不多不少地把棧中的引數清理乾淨);


摘自:[url]http://bdxnote.blog.163.com/blog/static/84442352010017361476/[/url]

相關文章