c, c++函式名編譯符號修飾符說明
函式名字修飾(Decorated Name)方式
函式的名字修飾(Decorated Name)就是編譯器在編譯期間建立的一個字串。用來指明函式的定義或原型。
LINK程式或其它工具有時須要指定函式的名字修飾來定位函式的正確位置。
多數情況下程式猿並不須要知道函式的名字修飾。LINK程式或其它工具會自己主動區分他們。
當然,在某些情況下須要指定函式的名字修飾,比如在C++程式中, 為了讓LINK程式或其它工具可以匹配到正確的函式名字,就必須為過載函式和一些特殊的函式(如建構函式和解構函式)指定名字裝飾。
還有一種須要指定函式的 名字修飾的情況是在彙編程式中呼叫C或C++的函式。
假設函式名字,呼叫約定。返回值型別或函式引數有不論什麼改變,原來的名字修飾就不再有效,必須指定新的 名字修飾。
C和C++程式的函式在內部使用不同的名字修飾方式,以下將分別介紹這兩種方式。
1. C編譯器的函式名修飾規則
對於__stdcall呼叫約定,編譯器和連結器會在輸出函式名前加上一個下劃線字首,函式名後面加上一個“@”符號和其引數的位元組數。比如 [email protected]。
__cdecl呼叫約定僅在輸出函式名前加上一個下劃線字首。比如_functionname。 __fastcall呼叫約定在輸出函式名前加上一個“@”符號。後面也是一個“@”符號和其引數的位元組數,比如 @
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”,假設函式宣告使用了constkeyword,則對應的標識應分別為
“@@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引用引數,其函式修飾名為 “?
Co[email protected]@@[email protected]@Z”。
DrawText是一個比較複雜的函式宣告。不僅有字串引數。還有結構體引數和HDC 控制代碼引數。須要指出的是HDC實際上是一個HDC__結構型別的指標,這個引數的表示就是“[email protected]@”,其完整的函式修飾名為 “?
[email protected]@@[email protected]@[email protected]@[email protected]”。InsightClass是一個共 有的const函式。它的成員函式標識是“@@QBE”,完整的修飾名就是“[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也完畢了自己的功 能,僅僅是函式返回後引發錯誤。解決辦法也非常easy。僅僅要保證兩個模組的在編譯時設定同樣的函式呼叫約定即可了。
在瞭解了函式呼叫約定和函式的名修飾規則之後,再來看在C++程式中使用C語言編譯的庫時常常出現的LNK 2001錯誤就非常easy了。還以上面樣例的兩個模組為例,這一次兩個模組在編譯的時候都採用__stdcall呼叫約定,可是a.dll使用C語言的語法編 譯的(C語言方式)。所以a.dll的載入庫a.lib中MakeFun函式的名字修飾就是“[email protected]”。
b包括了a提供的標頭檔案裡 MakeFun函式宣告,可是因為b採用的是C++語言編譯,所以MakeFun在b模組中被依照C++的名字修飾規則命名為 “[email protected]@[email protected]”,編譯過程相安無事,連結程式時c++的連結器就到a.lib中去找“?
[email protected]@[email protected]”,可是 a.lib中僅僅有“[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"。
1.__stdcall
以“?”標識函式名的開始。後跟函式名。 函式名後面以“@@YG”標識參數列的開始,後跟參數列。
參數列以代號表示: X--void 。 D--char。 E--unsigned char, F--short, H--int。 I--unsigned int, J--long, K--unsigned long。 M--float。 N--double, _N--bool, .... PA--表示指標。後面的代號表明指標型別,假設同樣型別的指標連續出現。以“0”取代,一個“0”代表一次反覆;
參數列的第一項為該函式的返回值型別,其後依次為引數的資料型別,指標標識在其所指資料型別前。
參數列後以“@Z”標識整個名字的結束。假設該函式無引數,則以“Z”標識結束。 其格式為“[email protected]@YG*****@Z”或“[email protected]@YG*XZ”, 比如 int Test1(char *var1。unsigned long)-----“?
[email protected]@[email protected]” void Test2() -----“?
[email protected]@YGXXZ”
2 __cdecl呼叫約定: 規則同上面的 _stdcall 呼叫約定,僅僅是參數列的開始標識由上面的“@@YG”變為“@@YA”。
3 __fastcall呼叫約定: 規則同上面的_stdcall呼叫約定,僅僅是參數列的開始標識由上面的“@@YG”變為“@@YI”。
VC++對函式的省缺宣告是"__cedcl",將僅僅能被C/C++呼叫。
CB在輸出函式宣告時使用4種修飾符號 :
__cdecl cb 的預設值,它會在輸出函式名前加 "_",並保留此函式名不變,引數依照從右到左的順序依次傳遞給棧,也能夠寫成_cdecl和cdecl形式。
__fastcall 修飾的函式的引數將盡可能的使用暫存器來處理,其函式名前加@,引數依照從左到右的順序壓棧;
__pascal 它說明的函式名使用 Pascal 格式的命名約定。
這時函式名所有大寫。
引數依照從左到右的順序壓棧;
__stdcall 使用標準約定的函式名。函式名不會改變。使用 __stdcall 修飾時。引數依照由右到左的順序壓棧,也能夠是_stdcall。
C語言函式呼叫約定
在C語言中。如果我們有這種一個函式:
int function(int a,int b)
呼叫時僅僅要用 result = function(1,2) 這種方式就能夠使用這個函式。可是,當高階語言被編譯成計算機能夠識別的機器碼時,有一個問題就出現來:在CPU中,計算機沒有辦法知道一個函式呼叫需 要多少個、什麼樣的引數,也沒有硬體能夠儲存這些引數。也就是說,計算機不知道怎麼給這個函式傳遞引數,傳遞引數的工作必須由函式呼叫者和函式本身來協
調。
為此。計算機提供了一種被稱為棧的資料結構來支援引數傳遞。
棧是一種先後進先出的資料結構,棧有一個儲存區、一個棧頂指標。棧頂指標指向堆疊中第一個可用的資料項(被稱為棧頂)。
使用者能夠在棧頂上方向棧中 增加資料。這個操作被稱為壓棧 (Push)。壓棧以後,棧頂自己主動變成新增加資料項的位置。棧頂指標也隨之改動。使用者也能夠從堆疊中取走棧頂。稱為彈出棧 (pop),彈出棧後,棧頂下的一個元素變成棧頂。棧頂指標隨之改動。
函式呼叫時,呼叫者依次把引數壓棧,然後呼叫函式。函式被呼叫以後,在堆疊中取得資料,並進行計算。函式計算結束以後。或者呼叫者、或者函式本身改動堆疊,使堆疊恢復原裝。
在引數傳遞中。有兩個非常重要的問題必須得到明白說明:
當引數個數多於一個時,依照什麼順序把引數壓入堆疊
函式呼叫後,由誰來把堆疊恢復原狀
在高階語言中,通過函式呼叫約定來說明這兩個問題。常見的呼叫約定有:
stdcall
cdecl
fastcall
thiscall
naked call
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
而在編譯時。這個函式的名字被翻譯成[email protected]
注意不同編譯器會插入自己的彙編程式碼以提供編譯的通用性,可是大體程式碼如此。
當中在函式開始處保留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)
為了說明這個呼叫約定,定義例如以下類和使用程式碼:
class A
{
public:
int function1(int a,int b);
int function2(int a,...);
};
int A::function1 (int a,int b)
{
return a+b;
}
int A::function2(int a,...)
{
va_list ap;
va_start(ap,a);
int i;
int result = 0;
for(i = 0 ; i < a ; i ++)
{
result += va_arg(ap,int);
}
return result;
}
void callee()
{
A a;
a.function1 (1,2);
a.function2(3,1,2,3);
}
// 以下這段彙編程式碼是原文章的。我認為有問題。還是自己反彙編看看
//函式function1呼叫0401C1D
push 200401C1F
push 100401C21
lea ecx,[ebp-8]00401C24
call function1
// 注意。這裡this沒有被入棧
//函式function2呼叫00401C29
push 300401C2B
push 200401C2D
push 100401C2F
push 300401C31
lea eax,[ebp-8]
這裡引入this指標00401C34
push eax00401C35
call function200401C3A
add esp,14h
下面程式碼是我改動分析的:
上面的C++程式碼,必須包括 stdarg.h ,提供動態引數標頭檔案