名字修飾(name decoration),也稱為名字重整名字改編(name mangling),是現代計算機程序設計語言編譯器用於解決由於程序實體的名字必須唯一而導致的問題的一種技術。

它提供了在函數結構體或其它的數據類型的名字中編碼附加信息一種方法,用於從編譯器中向鏈接器傳遞更多語義信息。

該需求產生於程序設計語言允許不同的條目使用相同的標識符,包括它們占據不同的命名空間(典型的命名空間是由一個模塊、一個類或顯式的namespace指示來定義的)或者有不同的簽名(例如函數重載)。

任何由編譯器產生的目標代碼通常與另一部分的目標代碼(產生於同一款或不同款的編譯器)通過鏈接器把它們鏈接起來。鏈接器需要一大堆每個程序實體信息。例如正確鏈接一個函數需要它的名字、參數個數和它們的類型,等等。

C語言的名字修飾

雖然在不支持函數重載的程序設計語言(例如C語言和經典Pascal語言)中基本上不需要名字修飾,但是它們在一些情況下它們還是用了名字修飾來提供了函數的附加信息。 例如,目標於微軟Windows平台的編譯器支持許多調用約定。這用於決定哪個參數傳入子程序的方式和結果返回的方式。因為不同的調用約定彼此不兼容,所以編譯器根據的調用約定重整了鏈接符號。

名字修飾方案由微軟公司首創,目前已經被許多其它的編譯器非正式採用,例如Digital Mars公司、Borland公司以及Windows移植版的GNU GCC。該方案甚至被其它語言採用,例如PascalD語言DelphiFortranC#。這允許用這此語言寫的子程序使用不同於自身默認的調用約定來調用或被調用於已存在的Windows庫。

當編譯下列C語言代碼的的時候:

int _cdecl    f (int x) { return 0; }
int _stdcall  g (int y) { return 0; }
int _fastcall h (int z) { return 0; }

32位編譯器對其分別進行名字修飾後的結果是:

_f
_g@4
@h@4

對於stdcallfastcall調用約定的名字修飾方案中,函數分別被編碼為_name@X@name@X,其中X是形參列表的參數中的十進制的字節數,包括用fastcall傳入寄存器的。而對於cdecl調用約定,簡單地在函數名前加上一條下劃線。

注意Windows的64位Microsoft C的調用約定中沒有前導下劃線。在一些很罕見的地方,這個差異可能導致代碼移植到64位上的時候產生無法解析的外部符號。例如Fortran代碼可以使用'alias'(別名)來鏈接到C方法,如下所示:

SUBROUTINE f()
!DEC$ ATTRIBUTES C, ALIAS:'_f' :: f
END SUBROUTINE

這在32位平台下編譯鏈接得很好,但是在64位的平台將導致無法解析的外部符號'_f'。一個可行的辦法是完全不使用'alias'(其中方法名典型的在C語言和Fortran語言中需要大寫化),或使用BIND選項:

SUBROUTINE f() BIND(C,NAME="f")
END SUBROUTINE

Visual Basic 6這樣的較老的語言,也需要在聲明DLL的輸出函數時使用Alias,例如:

Public Declare Function test2 Lib "PackingDLL.dll" Alias "_test2@4" (ByVal param As Integer) As Integer

在C語言中,多數編譯器還改編在翻譯單元中的靜態函數和變量(和在C++中的聲明為靜態或放置在匿名名字空間中的函數和變量),使用與非靜態版本相同的修改規則。如果有着相同的名字(和C++中的參數)的函數,也定義和使用在不同的翻譯單元中,它也改編為相同的名字,這潛在的會導致衝撞。但是,如果它們分別在自己的翻譯單元中被調用,則它們將不是等價的。編譯器通常自由的對這些函數施加任意改編,因為直接從其他翻譯單元訪問這些函數是非法的,所以它們永遠不需要在不同的目標代碼之間鏈接。為了防止鏈接衝突,編譯器將使用標準的改編,但使用所謂的'local'符號。在鏈接很多這種翻譯單元的時候,可能出現多個有相同名字的的函數定義,但是結果代碼依據調用來自何處而只鏈接它自己的那個函數。這通常使用重定位英語Relocation (computing)機制來完成。

C++語言的名字修飾

C++編譯器是名字修飾使用得出名的編譯器。第一個C++編譯器的實作是翻譯成C語言源代碼,以便於讓C編譯器編譯成目標代碼。正因如此,符號名必須遵守C語言的標識符規則。直至後來,能直接產生機器語言或組合語言的編譯器出現了以後,系統的鏈接器也是基本上不支持C++的符號的,所以仍然需要名字修飾。

C++語言並沒有規定一個標準的名字修飾方式,所以各款編譯器都使用各自的名字修飾方式。C++還有一套複雜的語言特性,例如模板命名空間運算符重載。這改變了基於上下文或用法的特定符號的意義。關於這些特性的元數據能夠用改編(修飾)調試符號的名字來消除二義性。正因為這些特性的名字修飾系統並沒有跨編譯器標準化,所以幾乎沒有鏈接器可以鏈接不同編譯器產生的目標代碼。

簡單樣例

考慮一個下面的C++程序中的兩個f()的定義:

int  f (void) { return 1; }
int  f (int)  { return 0; }
void g (void) { int i = f(), j = f(0); }

這些是不同的函數,除了函數名相同以外沒有任何關係。如果不做任何改變直接把它們當成C代碼,結果將導致一個錯誤——C語言不允許兩個函數同名。所以,C++編譯器將會把它們的類型信息編碼成符號名,結果類似下面的的代碼:

int  __f_v (void) { return 1; }
int  __f_i (int)  { return 0; }
void __g_v (void) { int i = __f_v(), j = __f_i(0); }

注意g()也被名字修飾了,雖然沒有任何名字衝突。名字修飾應用於C++的任何符號。

複雜樣例

一個更複雜一點的樣例,下面考慮一個現實生活中的例子,該例子被GNU GCC 3.x的名字修飾規則實現過。改編下列的示例類,改編過的符號在各自的標識符名字下面顯示。

namespace wikipedia 
{
   class article 
   {
   public:
      std::string format (void); 
         /* = _ZN9wikipedia7article6formatEv */

      bool print_to (std::ostream&); 
         /* = _ZN9wikipedia7article8print_toERSo */

      class wikilink 
      {
      public:
         wikilink (std::string const& name);
            /* = _ZN9wikipedia7article8wikilinkC1ERKSs */
      };
   };
}

全部被改編過的符號由_Z開頭(注意用下劃線加大寫英文字母是C語言的保留標識符),所以與用戶標識符的衝突可以被避免)。嵌套的名字(包括命名空間和類),後面再接一個N,最後一個E。例如wikipedia::article::format將成為:

_ZN·9wikipedia·7article·6format·E  

函數後面接形參的類型信息,例如format()是一個形參為void的函數,於是就接一個v,結果是:

_ZN·9wikipedia·7article·6format·E·v

對於print_to,使用了一個標準類型std::ostream(或更準確地說是std::basic_ostream<char, char_traits<char> >),有着特殊的別名So,所以,這個類型的一個引用類型就是RSo,這個函數的完整名字是:

_ZN·9wikipedia·7article·8print_to·E·RSo

不同編譯器如何名字修飾相同的函數

無論多麼平凡的C++標識符,名字修飾規則都沒有標準方式,所以不同的編譯器產商(甚至相同編譯器的不同版本,或相同編譯器在不同平台上)的名字修飾規則都截然不同,也就意味着基本上都不兼容。看看C++編譯器是怎麼名字修飾相同的函數的:

More information 編譯器, void h(int) ...
編譯器 void h(int) void h(int, char) void h(void)
GCC 3.x及更高 _Z1hi _Z1hic _Z1hv
Clang 1.x及更高[1]
Intel C++ 8.0 for Linux
HP aC++ A.05.55 IA-64
IAR EWARM C++ 5.4 ARM
IAR EWARM C++ 7.4 ARM _Z<number>hi _Z<number>hic _Z<number>hv
GCC 2.9.x h__Fi h__Fic h__Fv
HP aC++ A.03.45 PA-RISC
Microsoft Visual C++ v6-v10 (修飾詳情) ?h@@YAXH@Z ?h@@YAXHD@Z ?h@@YAXXZ
Digital Mars C++
Borland C++ v3.1 @h$qi @h$qizc @h$qv
OpenVMS C++ v6.5 (ARM mode) H__XI H__XIC H__XV
OpenVMS C++ v6.5 (ANSI mode) CXX$__7H__FIC26CDH77 CXX$__7H__FV2CB06E8
OpenVMS C++ X7.1 IA-64 CXX$_Z1HI2DSQ26A CXX$_Z1HIC2NP3LI4 CXX$_Z1HV0BCA19V
SunPro CC __1cBh6Fi_v_ __1cBh6Fic_v_ __1cBh6F_v_
Tru64 C++ v6.5 (ARM mode) h__Xi h__Xic h__Xv
Tru64 C++ v6.5 (ANSI mode) __7h__Fi __7h__Fic __7h__Fv
Watcom C++ 10.6 W?h$n(i)v W?h$n(ia)v W?h$n()v
Close

注:

  • 在OpenVMS VAX和Alpha(但不是IA-64)和Tru64上的Compaq C++編譯器有兩套不同的名字修飾方式。原始的,標準前的方式是ARM模式,基於描述於《C++ Annotated Reference Manual (ARM)》中的名字修飾規則。伴隨着C++98標準的新特性的到來,尤其是模板,ARM方式變得越來越不合適——它不能編碼確定的函數類型,或者對不同函數產生了相同的改編符號。所以它就被新的ANSI模型取代,該模型支持全部的ANSI模板,但是並不能向後兼容。
  • 在IA-64中,存在一種標準的ABI(見外部連結)。它規定了一種標準的名字修飾方式,並且被全部的IA-64編譯器使用。此外,GNU GCC 3.x也在其它非Intel架構上採用了在這個標準中規定的名字修飾方式。
  • Visual Studio和Windows SDK包含了能給定一個已被名字修飾過的符號就能輸出C風格函數聲明的undname程序。
  • 在Microsoft Windows中,Intel編譯器[2]Clang[3]為了兼容性使用了Visual C++的名字修飾規則。

從C++中鏈接時的C符號的處理

最常見的C++慣常的做法:

#ifdef __cplusplus 
extern "C" {
#endif
    /* ... */
#ifdef __cplusplus
}
#endif

這種寫法用於確保下符號是未被C++編譯器名字修飾過的——這種代碼能使得C++編譯器編譯出的二進制目標代碼中的鏈接符號是未經過C++名字修飾過的,就像C編譯器一樣。就像C語言定義是未名字修飾過的一樣,C++編譯器需要防止名字修飾這些標識符。

例如,C標準字符串庫<string.h>通常包含了類似這樣子的

#ifdef __cplusplus
extern "C" {
#endif

void *memset (void *, int, size_t);
char *strcat (char *, const char *);
int   strcmp (const char *, const char *);
char *strcpy (char *, const char *);

#ifdef __cplusplus
}
#endif

於是,例如這樣的代碼

if (strcmp(argv[1], "-x") == 0) 
    strcpy(a, argv[2]);
else 
    memset (a, 0, sizeof(a));

就能使用正確的、未經名字修飾過的strcmpmemset。如果沒有使用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運行庫中(例如 libc)。因此將導致鏈接錯誤。

C++標準化的名字修飾

標準化的C++名字修飾規則似乎能夠在編譯器實現之間帶來更大的互操作性,但是事實上,這樣的標準化自身並不能保證C++編譯器的互操作性,並且它甚至能製造互操作性是可能的並且是安全的一種錯覺。名字修飾僅僅是需要C++實現決定的許多ABI細節之一。其它ABI方面例如異常處理虛表的設計、結構體和棧幀填充等等,也導致了不同的互不兼容的C++實現。再者,規定一個特定的名字修飾規則會導致在實現限制(例如:符號長度限制)指揮的名字修飾方式的系統上的一些問題。名字修飾的一個標準化的需求,也會阻礙不完全不需要名字修飾的實現——例如明白C++語言的鏈接器。

所以,C++標準並沒有嘗去標準化名字修飾。相反地,《Annotated C++ Reference Manual》(又叫做ARM, ISBN 0-201-51459-1, 第7.2.1c節)主動提倡使用截然不同的名字修飾方式來防止ABI層面不兼容的鏈接,例如異常處理虛表設計。

雖然如此,在一些平台上[4],全部C++ ABI都被標準化了,包括名字修飾。

C++名字修飾的現實影響

當C++符號從動態鏈接庫共享對象文件中導出時,名字修飾方式就不再是一個編譯器內部的事情的。不同的編譯器(或者同一款編譯器的不同版本)將產生不同的名字修飾方式的二進制文件。這意味着如果編譯器使用了不同方式創建了庫和程序經常將導致無法解決的符號。例如,如果一個系統中有多個C++編譯器(例如GNU GCC編譯器和操作系統供應商的編譯器)並且想安裝Boost C++ Libraries,那麼它需要編譯兩次——為操作系統供應商的編譯器編譯一次,為GCC再編譯一次。

為了安全目的,產生不兼容的目標代碼(基於不同的ABI,例如類和異常)的編譯器最好使用不同的名字修飾方式。這保證了這些不兼容性能夠在鏈接的時候被檢測出來,而不是一運行軟件的時候被發現(這會導致隱藏的bug和嚴重的穩定性問題)。

正因如此,名字修飾是對於任何C++相關的ABI都是一個要點。

通過c++filt去修飾

$ c++filt -n _ZNK3MapI10StringName3RefI8GDScriptE10ComparatorIS0_E16DefaultAllocatorE3hasERKS0_
Map<StringName, Ref<GDScript>, Comparator<StringName>, DefaultAllocator>::has(StringName const&) const

通過內建GCC ABI去修飾

#include <stdio.h>
#include <stdlib.h>
#include <cxxabi.h>

int main() {
	const char *mangled_name = "_ZNK3MapI10StringName3RefI8GDScriptE10ComparatorIS0_E16DefaultAllocatorE3hasERKS0_";
	int status = -1;
	char *demangled_name = abi::__cxa_demangle(mangled_name, NULL, NULL, &status);
	printf("Demangled: %s\n", demangled_name);
	free(demangled_name);
	return 0;
}

輸出:

Demangled: Map<StringName, Ref<GDScript>, Comparator<StringName>, DefaultAllocator>::has(StringName const&) const

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文件:

  • foo.class,包含了主類(外面的類)foo
  • foo$bar.class,包含了命名的內部類foo.bar
  • foo$1.class,包含了內部的匿名類(局部於foo.zark方法)

這些類名全是合法的(因為$符號允許用於JVM規範)並且這些名字對編譯器的產生來說是「安全」的,因為Java語言的定義禁止$符號出現在常規的Java類定義中。

Java的名字解析在運行時更為複雜,因為完全合格的類名在特定的Java類加載器的實例中是唯一的。類加載器是分級次序的,並且JVM的每個編程都有一個所謂的上下文類加載器,用來預防兩個不同的類加載器實例包含同名的類。系統首先嘗試使用根加載器(或系統加載器)來加載類,然後往下針對上下文類加載器分級加載。

Java本地接口(JNI)

Java本地接口(JNI)允許Java語言的程序調用其它語言寫的程序(通常是C或C++)。有兩個名稱解析與此有關,這兩種都沒有標準化的實現方式:

  • 從Java到本地名字的翻譯[5]
  • 常規的C++名字修飾

Python的名字修飾

Python語言的名字修飾用於類的「私有」(private)成員。這種類成員的名字由前導雙下劃線開頭,並且後綴下劃線不能多於一個。例如__thing將被名字修飾,___thing__thing_同樣也會被名字修飾,但是__thing____thing___就不會被名字修飾。Python運行時庫不限制訪問這些成員,名字修飾只是用來避免擁有同名成員的派生類發生名字衝突。

遇到需要名字修飾的時候,Python把這些名字改成單下劃線加上封閉類的名字,例如:

>>> class Test(object):
...     def __mangled_name(self):
...         pass
...     def normal_name(self):
...         pass
... 
>>> [*Test.__dict__]
['__module__', '_Test__mangled_name', 'normal_name', '__dict__', '__weakref__', '__doc__']

Objective-C的名字修飾

本質上,Objective-C存在兩種形式的方法,類方法(靜態方法)和實例化方法。Objective-C的方法聲明如下:

+ method name: argument name1:parameter1 ...
– method name: argument name1:parameter1 ...

類方法用+表示,實例化方法用-表示。一個典型的類方法聲明是這樣子的:

 + (id) initWithX: (int) number andY: (int) number;
 + (id) new;

實例化方法是這樣子的:

  (id) value;
  (id) setValue: (id) new_value;

這樣方法聲明都有一個特定的內部表示法。當編譯的時候,任何一個方法都會按照下列類方法的方式來命名:

_c_Class_methodname_name1_name2_ ...

這是實例化方法:

_i_Class_methodname_name1_name2_ ...

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

在編譯期間,建立了一個把文字表述(例如_i_Point_value)映射到選擇器(類型為SEL)的表。管理選擇器比操作方法的文字表述更有效。注意一個選擇器只能匹配一個方法名,而不是它屬於的類——不同的類對同名方法可以有不同的實現。因此,方法的實現也給定了一個特定的標識符——這就叫實現指針,當然也給定了類型IMP

信息發送由編譯器編碼,調用id objc_msgSend (id receiver, SEL selector, ...)函數,或者它的表親,其中receiver是信息的接收者,並且SEL決定需要調用的方法。每個類都有各自的從選擇器映射到它們實現的表——實現指針指定實際方法實現的內存地址。類和實現的表是分開的。除了儲存在SEL中來用IMP查找表,函數是本質上是匿名的。

選擇器的SEL值在類間沒有變化。這使得多態成為可能。

Objective-C運行時庫負責維護方法的參數和返回值的信息。但是,這信息不是方法名的一部分,不同的類可能有很大的不同。

因為Objective-C不支持命名空間,所以沒有必要對類名進行名字修飾(這個在產生的二進制文件中確實發生過)。

Fortran的名字修飾

名字修飾對於Fortran編譯器也是必要的,因為原先這個語言是大小寫不敏感的。隨着語言的發展,產生了更多的名字修飾需求,這是因為Fortran 90標準附加的模塊和其它特性。名字修飾就成為了需要解決的一個特別常見的問題,因為需要調用來自其它語言(例如C語言)的Fortran庫(例如LAPACK)。

由於Fortran編譯器大小寫不敏感,子程序或函數的名字"FOO"必須被轉換成規範的大小寫方式,而且要由Fortran編譯器來格式化,這樣它才能無視大小寫地用相同方式被鏈接。不同的編譯器用不同的方式來實現了,沒有發生過標準化。AIXHP-UX的Fortran編譯器把標識符全轉成小寫("foo"),而克雷Unicos英語Unicos的Fortran編譯器把標識符全轉成大寫("FOO")。GNUg77編譯器把標識符轉成小寫後接一個下劃線("foo_"),例外情況是:原先已經有下劃線的標識符("FOO_BAR")轉成後接兩個下劃線("foo_bar__"),這是f2c英語f2c設的約定。許多其它的編譯器,包括SGIIRIX編譯器、gfortranIntel的Fortran編譯器(不包括在Microsoft Windows上),都把標識符全部轉成小寫後接一個下劃線("foo_"和"foo_bar_")。在Microsoft Windows上,Intel Fortran編譯器缺省為大寫不帶下劃線[6]

Fortran 90模塊中的標識符必須被進一步名字修飾,因為相同的子程序同可能在不同的模塊提供給不同的例程。

Pascal的名字修飾

Borland的Turbo Pascal/Delphi系列

為了避免Pascal的名字修飾,可以使用:

exports
  myFunc name 'myFunc',
  myProc name 'myProc';

Free Pascal

Free Pascal支持函數重載運算符重載,所以它也使用名字修飾來支持這些特性。另外,Free Pascal能夠調用由其它語言寫的的外部模塊定義的符號,也能導出自己的符號供其它語言調用。更多信息,詳見Free Pascal Programmer's Guide頁面存檔備份,存於網際網路檔案館)的子頁面Chapter 6.2頁面存檔備份,存於網際網路檔案館)和Chapter 7.1頁面存檔備份,存於網際網路檔案館)。

參見

參考資料

外部連結

Wikiwand in your browser!

Seamless Wikipedia browsing. On steroids.

Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.

Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.