1. 程式人生 > >C++ 編譯器的函數名修飾規則

C++ 編譯器的函數名修飾規則

_cdecl 開始 cti amp asc 編碼 常常 匹配 AC

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

函數的名字修飾(Decorated Name)就是編譯器在編譯期間創建的一個字符串,用來指明函數的定義或原型。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類型的指針。後面的代號表明指針類型,如果相同類型的指針連續出現,以“0”代替,一個“0”代表一次重復。U表示結構類型,通常後跟結構體的類型名,用“@@”表示結構類型名的結束。函數的返回值不作特殊處理,它的描述方式和函數參數一樣,緊跟著參數表的開始標誌,也就是說,函數參數表的第一項實際上是表示函數的返回值類型。參數表後以“@Z”標識整個名字的結束,如果該函數無參數,則以“Z”標識結束。下面舉兩個例子,假如有以下函數聲明:

int Function1 (char *var1,unsigned long);
其函數修飾名為“?Function1@@YGHPADK@Z”,而對於函數聲明:
void Function2();
其函數修飾名則為“?Function2@@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,其函數修飾名為“?Function@CTest@@AAEXH@Z”,字符串“@@AAE”表示這是一個私有函數。成員函數CopyInfo只有一個參數,是對類CTest的const引用參數,其函數修飾名為“?CopyInfo@CTest@@IAEXABV1@@Z”。DrawText是一個比較復雜的函數聲明,不僅有字符串參數,還有結構體參數和HDC句柄參數,需要指出的是HDC實際上是一個HDC__結構類型的指針,這個參數的表示就是“PAUHDC__@@”,其完整的函數修飾名為“?DrawText@CTest@@QAEJPAUHDC__@@JPBDUtagRGBQUAD@@E_N@Z”。InsightClass是一個共有的const函數,它的成員函數標識是“@@QBE”,完整的修飾名就是“?InsightClass@CTest@@QBEJK@Z”。

無論是C函數名修飾方式還是C++函數名修飾方式均不改變輸出函數名中的字符大小寫,這和PASCAL調用約定不同,PASCAL約定輸出的函數名無任何修飾且全部大寫。

3.查看函數的名字修飾

有兩種方式可以檢查你的程序中的函數的名字修飾:使用編譯輸出列表或使用Dumpbin工具。使用/FAc,/FAs或/FAcs命令行參數可以讓編譯器輸出函數或變量名字列表。使用dumpbin.exe /SYMBOLS命令也可以獲得obj文件或lib文件中的函數或變量名字列表。此外,還可以使用 undname.exe 將修飾名轉換為未修飾形式。

4.在高級語言中,通過函數調用約定來說明這兩個問題。常見的調用約定有:

__stdcall/_stdcall
__cdecl/_cdecl
__fastcall/_fastcall
__thiscall

__stdcall調用約定
__stdcall很多時候被稱為pascal調用約定,因為pascal是早期很常見的一種教學用計算機程序設計語言,其語法嚴謹,使用的函數調用約定就是stdcall。在Microsoft C++系列的C/C++編譯器中,常常用PASCAL宏來聲明這個調用約定,類似的宏還有WINAPI和CALLBACK。

__stdcall調用約定聲明的語法為(以前文的那個函數為例):

int __stdcall function(int a,int b)

stdcall的調用約定意味著:1)參數從右向左壓入堆棧,2)函數自身修改堆棧 3)函數名自動加前導的下劃線,後面緊跟一個@符號,其後緊跟著參數的尺寸

以上述這個函數為例,參數b首先被壓棧,然後是參數a,函數調用function(1,2)調用處翻譯成匯編語言將變成:

push 2 // 第二個參數入棧
push 1 // 第一個參數入棧
call function // 調用參數,註意此時自動把cs:eip入棧

而對於函數自身,則可以翻譯為:

push ebp // 保存ebp寄存器,該寄存器將用來保存堆棧的棧頂指針,可以在函數退出時恢復
mov ebp,esp // 保存堆棧指針
mov eax,[ebp + 8H] // 堆棧中ebp指向位置之前依次保存有 ebp,cs:eip,a,b,ebp + 8指向 a
add eax,[ebp + 0CH] // 堆棧中ebp + 1 2處保存了b
mov esp,ebp // 恢復esp
pop ebp
ret 8
而在編譯時,這個函數的名字被翻譯成_function@8

其中在函數開始處保留esp到ebp中,在函數結束恢復是編譯器常用的方法。
從函數調用看,2和1依次被push進堆棧,而在函數中又通過相對於ebp(即剛進函數時的堆棧指針)的偏移量存取參數。
函數結束後,ret 8 表示清理8個字節的堆棧,函數自己恢復了堆棧。

cdecl調用約定
cdecl 調用約定又稱為C調用約定,是C語言缺省的調用約定,它的定義語法是:

int function (int a ,int b) //不加修飾就是C調用約定
int __cdecl function(int a,int b) //明確指出C調用約定

cdecl調用約定的參數壓棧順序是和 stdcall是一樣的,參數首先由有向左壓入堆棧。
所不同的是,函數本身不清理堆棧,調用者負責清理堆棧。
由於這種變化,C 調用約定允許函數的參數的個數是不固定的,這也是C語言的一大特色。
對於前面的function函數,使用cdecl後的匯編碼變成:

調用處
push 1
push 2
call functionadd
esp,8 // 註意:這裏調用者在恢復堆棧

被調用函數_function處
push ebp // 保存ebp寄存器,該寄存器將用來保存堆棧的棧頂指針,可以在函數退出時恢復
mov ebp,esp // 保存堆棧指針
mov eax,[ebp + 8H] // 堆棧中ebp指向位置之前依次保存有 ebp, cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0CH] // 堆棧中ebp + 12處保存了b
mov esp,ebp // 恢復esp
pop ebp
ret // 註意,這裏沒有修改堆棧

MSDN中說,該修飾自動在函數名前加前導的下劃線,因此函數名在符號表中被記錄為_function,但是我在編譯時似乎沒有看到這種變化。
由於參數按照從右向左順序壓棧,因此最開始的參數在最接近棧頂的位置,因此當采用不定個數參數時,第一個參數在棧中的位置肯定能知道,只要不定的參數個數能夠根據第一個後者後續的明確的參數確定下來,就可以使用不定參數,例如對於CRT中的sprintf函數,定義為:

int sprintf(char* buffer,const char* format,...)

由於所有的不定參數都可以通過 format 確定,因此使用不定個數的參數是沒有問題的。

__fastcall調用約定
__fastcall調用約定和stdcall類似,它意味著:

函數的第一個和第二個DWORD參數(或者尺寸更小的)通過ecx和edx傳遞,其他參數通過從右向左的順序壓棧
被調用函數清理堆棧
函數名修改規則同stdcall
其聲明語法為:int __fastcall function(int a,int b)

C++ 編譯器的函數名修飾規則