名字修飾

名字修飾，是現代計算機程式設計語言的編譯器用於解決由於程式實體的名字必須唯一而導致的問題的一種技術。它提供了在函式、結構體、類或其它的資料型別的名字中編碼附加資訊一種方法，用於從編譯器中向連結器傳遞更多語義資訊。該需求產生於程式設計語言允許不同的條目使用相同的識別符號，包括它們佔據不同的名稱空間。任何由編譯器產生的目的碼通常與另一部分的目的碼透過連結器把它們連結起來。連結器需要一大堆每個程式實體資訊。例如正確連結一個函式需要它的名字、引數個數和它們的型別，等等。C語言的名字修飾.雖然在不支援函式過載的程式設計語言中基本上不需要名字修飾，但是它們在一些情況下它們還是用了名字修飾來提供了函式的附加資訊。例如，目標於微軟Windows平臺的編譯器支援許多呼叫約定。這用於決定哪個引數傳入子程式的方式和結果返回的方式。因為不同的呼叫約定彼此不相容，所以編譯器根據的呼叫約定重整了連結符號。名字修飾方案由微軟公司首創，現已許多其它的編譯器非正式採用，例如Digital Mars公司、Borland公司以及Windows移植版的GNU GCC。該方案甚至被其它語言採用，例如Pascal、D語言、Delphi、Fortran和C#。這允許用這此語言寫的子程式使用不同於自身預設的呼叫約定來呼叫或被調用於已存在的Windows庫。當編譯下列C語言程式碼的的時候：32位編譯器對其分別進行名字修飾後的結果是：_f_g@4@h@4對於stdcall和fastcall呼叫約定的名字修飾方案中，函式分別被編碼為_name@X和@name@X，其中X是形參列表的引數中的十進位制的位元組數，包括用fastcall傳入暫存器的。而對於cdecl呼叫約定，簡單地在函式名前加上一條下劃線。注意Windows的64位Microsoft C的呼叫約定中沒有前導下劃線。在一些很罕見的地方，這個差異可能導致程式碼移植到64位上的時候產生無法解析的外部符號。例如Fortran程式碼可以使用'alias'來連結到C方法，如下所示：SUBROUTINE f()!DEC$ ATTRIBUTES C, ALIAS:'_f' :: fEND SUBROUTINE這在32位平臺下編譯連結得很好，但是在64位的平臺將導致無法解析的外部符號'_f'。一個可行的辦法是完全不使用'alias'，或使用BIND選項：SUBROUTINE f() BIND(C,NAME="f")END SUBROUTINEVisual Basic 6這樣的較老的語言，也需要在宣告DLL的輸出函式時使用Alias，例如：Public Declare Function test2 Lib "PackingDLL.dll" Alias "_test2@4" (ByVal param As Integer) As Integer在C語言中，多數編譯器還改編在翻譯單元中的靜態函式和變數的函式，也定義和使用在不同的翻譯單元中，它也改編為相同的名字，這潛在的會導致衝撞。但是，如果它們分別在自己的翻譯單元中被呼叫，則它們將不是等價的。編譯器通常自由的對這些函式施加任意改編，因為直接從其他翻譯單元訪問這些函式是非法的，所以它們永遠不需要在不同的目的碼之間連結。為了防止連結衝突，編譯器將使用標準的改編，但使用所謂的'local'符號。在連結很多這種翻譯單元的時候，可能出現多個有相同名字的的函式定義，但是結果程式碼依據呼叫來自何處而只連結它自己的那個函式。這通常使用機制來完成。C++語言的名字修飾.C++編譯器是名字修飾使用得出名的編譯器。第一個C++編譯器的-{zh-cn:實現;zh-hant:實作}-是翻譯成C語言原始碼，以便於讓C編譯器編譯成目的碼。正因如此，符號名必須遵守C語言的識別符號規則。直至後來，能直接產生機器語言或-{zh-cn:組合語言;zh-hant:組合語言}-的編譯器出現了以後，系統的連結器也是基本上不支援C++的符號的，所以仍然需要名字修飾。C++語言並沒有規定一個標準的名字修飾方式，所以各款編譯器都使用各自的名字修飾方式。C++還有一套複雜的語言特性，例如類、模板、名稱空間和運算子過載。這改變了基於上下文或用法的特定符號的意義。關於這些特性的元資料能夠用改編除錯符號的名字來消除二義性。正因為這些特性的名字修飾系統並沒有跨編譯器標準化，所以幾乎沒有連結器可以連結不同編譯器產生的目的碼。簡單樣例.考慮一個下面的C++程式中的兩個f()的定義：這些是不同的函式，除了函式名相同以外沒有任何關係。如果不做任何改變直接把它們當成C程式碼，結果將導致一個錯誤——C語言不允許兩個函式同名。所以，C++編譯器將會把它們的型別資訊編碼成符號名，結果類似下面的的程式碼：注意g()也被名字修飾了，雖然沒有任何名字衝突。名字修飾應用於C++的任何符號。複雜樣例.一個更復雜一點的樣例，下面考慮一個現實生活中的例子，該例子被GNU GCC 3.x的名字修飾規則實現過。改編下列的示例類，改編過的符號在各自的識別符號名字下面顯示。namespace wikipediaclass articlepublic:std::string format (void);/* = _ZN9wikipedia7article6formatEv */bool print_to (std::ostream&amp;);/* = _ZN9wikipedia7article8print_toERSo */class wikilinkpublic:wikilink (std::string const&amp; name);/* = _ZN9wikipedia7article8wikilinkC1ERKSs */};全部被改編過的符號由_Z開頭，後面再接一個N，最後一個E。例如wikipedia::article::format將成為：_ZN·"9wikipedia·"7article·"6"format·E函式後面接形參的型別資訊，例如format()是一個形參為void的函式，於是就接一個v，結果是：_ZN·"9wikipedia·"7article·"6"format·E·v對於print_to，使用了一個標準型別std::ostream，有著特殊的別名So，所以，這個型別的一個引用型別就是RSo，這個函式的完整名字是：_ZN·"9wikipedia·"7article·"8"print_to·E·RSo不同編譯器如何名字修飾相同的函式.無論多麼平凡的C++識別符號，名字修飾規則都沒有標準方式，所以不同的編譯器產商的名字修飾規則都截然不同，也就意味著基本上都不相容。看看C++編譯器是怎麼名字修飾相同的函式的：注:從C++中連結時的C符號的處理.最常見的C++慣常的做法：#ifdef __cplusplusextern "C" {這種寫法用於確保下符號是未被C++編譯器名字修飾過的——這種程式碼能使得C++編譯器編譯出的二進位制目的碼中的連結符號是未經過C++名字修飾過的，就像C編譯器一樣。就像C語言定義是未名字修飾過的一樣，C++編譯器需要防止名字修飾這些識別符號。例如，C標準字串庫通常包含了類似這樣子的#ifdef __cplusplusextern "C" {void *memset (void *, int, size_t);char *strcat (char *, const char *);int strcmp (const char *, const char *);char *strcpy (char *, const char *);於是，例如這樣的程式碼if (strcmp(argv[1], "-x") == 0)strcpy(a, argv[2]);elsememset (a, 0, sizeof(a));就能使用正確的、未經名字修飾過的strcmp和memset。如果沒有使用extern "C"，那麼SunPro C++編譯器會產生等價於下面的C程式碼：if (__1cGstrcmp6Fpkc1_i_(argv[1], "-x") == 0)__1cGstrcpy6Fpcpkc_0_(a, argv[2]);else__1cGmemset6FpviI_0_ (a, 0, sizeof(a));而這些連結符號並不存在於C執行庫中。因此將導致連結錯誤。C++標準化的名字修飾.標準化的C++名字修飾規則似乎能夠在編譯器實現之間帶來更大的互操作性，但是事實上，這樣的標準化自身並不能保證C++編譯器的互操作性，並且它甚至能製造互操作性是可能的並且是安全的一種錯覺。名字修飾僅僅是需要C++實現決定的許多ABI細節之一。其它ABI方面例如異常處理、虛表的設計、結構體和棧幀填充等等，也導致了不同的互不相容的C++實現。再者，規定一個特定的名字修飾規則會導致在實現限制指揮的名字修飾方式的系統上的一些問題。名字修飾的一個標準化的需求，也會阻礙不完全不需要名字修飾的實現——例如明白C++語言的連結器。所以，C++標準並沒有嘗去標準化名字修飾。相反地，《Annotated C++ Reference Manual》主動提倡使用截然不同的名字修飾方式來防止ABI層面不相容的連結，例如異常處理和虛表設計。雖然如此，在一些平臺上，全部C++ ABI都被標準化了，包括名字修飾。C++名字修飾的現實影響.當C++符號從動態連結庫和共享物件檔案中匯出時，名字修飾方式就不再是一個編譯器內部的事情的。不同的編譯器並且想安裝Boost C++ Libraries，那麼它需要編譯兩次——為作業系統供應商的編譯器編譯一次，為GCC再編譯一次。為了安全目的，產生不相容的目的碼。正因如此，名字修飾是對於任何C++相關的ABI都是一個要點。透過c++filt去修飾.$ c++filt -n _ZNK3MapI10StringName3RefI8GDScriptE10ComparatorIS0_E16DefaultAllocatorE3hasERKS0_Map::has(StringName const&amp;) const透過內建GCC ABI去修飾.int main() {const char *mangled_name = "_ZNK3MapI10StringName3RefI8GDScriptE10ComparatorIS0_E16DefaultAllocatorE3hasERKS0_";int status = -1;char *demangled_name = abi::__cxa_demangle(mangled_name, NULL, NULL, &amp;status);printf("Demangled: %s\n", demangled_name);free(demangled_name);return 0;輸出：Java的名字修飾.在Java語言中，方法或類的簽名包含了它的名字以及它的引數和可適用的返回值型別。簽名的格式是有文件說明的，因為Java語言、編譯器和.class檔案的格式都是全部一起設計的。為內部和匿名類建立唯一的名字.匿名類的作用域侷限於它們的父類，所以編譯器必須為內部類產生一個“合格”的公開名字，來避免與其它相同名稱空間的同名類類衝突。類似的，匿名類必須有“假的”公開名字。所以，編譯下列的Java程式：public class foo {class bar {public int x;public void zark () {Object f = new Object () {public String toString() {return "hello";};將產生如下三個.class檔案：這些類名全是合法的並且這些名字對編譯器的產生來說是“安全”的，因為Java語言的定義禁止$符號出現在常規的Java類定義中。Java的名字解析在執行時更為複雜，因為完全合格的類名在特定的Java類載入器的例項中是唯一的。類載入器是分級次序的，並且JVM的每個程式設計都有一個所謂的上下文類載入器，用來預防兩個不同的類載入器例項包含同名的類。系統首先嚐試使用根載入器來載入類，然後往下針對上下文類載入器分級載入。Java本地介面.Java本地介面。有兩個名稱解析與此有關，這兩種都沒有標準化的實現方式：Python的名字修飾.Python語言的名字修飾用於類的“私有”成員。這種類成員的名字由前導雙下劃線開頭，並且字尾下劃線不能多於一個。例如codice_3將被名字修飾，codice_4和codice_5同樣也會被名字修飾，但是codice_6和codice_7就不會被名字修飾。Python執行時庫不限制訪問這些成員，名字修飾只是用來避免擁有同名成員的派生類發生名字衝突。遇到需要名字修飾的時候，Python把這些名字改成單下劃線加上封閉類的名字，例如：»&gt; class Test(object):... def __mangled_name(self):... pass... def normal_name(self):... pass»&gt; [*Test.__dict__]['__module__', '_Test__mangled_name', 'normal_name', '__dict__', '__weakref__', '__doc__']Objective-C的名字修飾.本質上，Objective-C存在兩種形式的方法，類方法和例項化方法。Objective-C的方法宣告如下：+ "method name": "argument" "name"1:"parameter"1 ...– "method name": "argument" "name"1:"parameter"1 ...類方法用+表示，例項化方法用-表示。一個典型的類方法宣告是這樣子的：+ (id) initWithX: (int) number andY: (int) number;+ (id) new;例項化方法是這樣子的：– (id) value;– (id) setValue: (id) new_value;這樣方法宣告都有一個特定的內部表示法。當編譯的時候，任何一個方法都會按照下列類方法的方式來命名：_c_"Class"_"methodname"_"name"1_"name"2_ ...這是例項化方法：_i_"Class"_"methodname"_"name"1_"name"2_ ...Objective-C語法中的冒號被翻譯成下劃線。所以Objective-C的屬於Point類的類方法 + (id) initWithX: (int) number andY: (int) number;將會被翻譯成_c_Point_initWithX_andY_，並且例項化方法 - (id) value;將會被翻譯成_i_Point_value。類的每一種方法都用這種方式標出。但是，為了在全部方法都用這種方式來表示的時候，能夠查詢到一個類能夠回應的方法是很繁瑣的。每個方法都賦予了唯一的符號。這樣的符號一般叫做選擇器。在Objective-C中，選擇器可以被直接管理——它們在Objective-C中有特定型別——SEL。在編譯期間，建立了一個把文字表述的表。管理選擇器比操作方法的文字表述更有效。注意一個選擇器只能匹配一個方法名，而不是它屬於的類——不同的類對同名方法可以有不同的實現。因此，方法的實現也給定了一個特定的識別符號——這就叫實現指標，當然也給定了型別IMP。資訊傳送由編譯器編碼，呼叫id objc_msgSend (id receiver, SEL selector, ...)函式，或者它的表親，其中receiver是資訊的接收者，並且SEL決定需要呼叫的方法。每個類都有各自的從選擇器對映到它們實現的表——實現指標指定實際方法實現的記憶體地址。類和實現的表是分開的。除了儲存在SEL中來用IMP查詢表，函式是本質上是匿名的。選擇器的SEL值在類間沒有變化。這使得多型成為可能。Objective-C執行時庫負責維護方法的引數和返回值的資訊。但是，這資訊不是方法名的一部分，不同的類可能有很大的不同。因為Objective-C不支援名稱空間，所以沒有必要對類名進行名字修飾。Fortran的名字修飾.名字修飾對於Fortran編譯器也是必要的，因為原先這個語言是大小寫不敏感的。隨著語言的發展，產生了更多的名字修飾需求，這是因為Fortran 90標準附加的模組和其它特性。名字修飾就成為了需要解決的一個特別常見的問題，因為需要呼叫來自其它語言。由於Fortran編譯器大小寫不敏感，子程式或函式的名字"FOO"必須被轉換成規範的大小寫方式，而且要由Fortran編譯器來格式化，這樣它才能無視大小寫地用相同方式被連結。不同的編譯器用不同的方式來實現了，沒有發生過標準化。AIX和HP-UX的Fortran編譯器把識別符號全轉成小寫。在Microsoft Windows上，Intel Fortran編譯器預設為大寫不帶下劃線。Fortran 90模組中的識別符號必須被進一步名字修飾，因為相同的子程式同可能在不同的模組提供給不同的例程。Pascal的名字修飾.Borland的Turbo Pascal/Delphi系列.為了避免Pascal的名字修飾，可以使用：exportsmyFunc name 'myFunc',myProc name 'myProc';Free Pascal.Free Pascal支援函式過載和運算子過載，所以它也使用名字修飾來支援這些特性。另外，Free Pascal能夠呼叫由其它語言寫的的外部模組定義的符號，也能匯出自己的符號供其它語言呼叫。更多資訊，詳見Free Pascal Programmer's Guide。