名字修飾

名字修飾（name decoration），也稱為名字重整、名字改編（name mangling），是現代電腦程式設計語言的編譯器用於解決由於程序實體的名字必須唯一而導致的問題的一種技術。

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

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

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

## C語言的名字修飾

雖然在不支持函數重載的程序設計語言（例如C語言和經典Pascal語言）中基本上不需要名字修飾，但是它們在一些情況下它們還是用了名字修飾來提供了函數的附加資訊。

例如，目標於微軟Windows平台的編譯器支持許多調用約定。這用於決定哪個參數傳入子程序的方式和結果返回的方式。因為不同的調用約定彼此不兼容，所以編譯器根據的調用約定重整了連結符號。

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

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

```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
```

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

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

```fortran
SUBROUTINE f()

DEC$ ATTRIBUTES C, ALIAS:'_f' :: f
END SUBROUTINE
```

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

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

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

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

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

## C++語言的名字修飾

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

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

### 簡單樣例

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

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

這些是不同的函數，除了函數名相同以外沒有任何關係。如果不做任何改變直接把它們當成C代碼，結果將導致一個錯誤——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的名字修飾規則實現過。改編下列的示例類，改編過的符號在各自的標識符名字下面顯示。

```cpp
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·''9'''''wikipedia'''·''7'''''article'''·''6'''''format'''·E
```

函數後面接形參的類型資訊，例如format是一個形參為void的函數，於是就接一個v，結果是：

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

對於print_to，使用了一個標準類型std::ostream（或更準確地說是std::basic_ostream >），有著特殊的別名So，所以，這個類型的一個引用類型就是RSo，這個函數的完整名字是：

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

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

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

編譯器｜void h(int)｜void h(int, char)｜void h(void)

GCC 3.x及更高｜_Z1hi｜_Z1hic｜_Z1hv

Clang 1.x及更高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｜_Zhi｜_Zhic｜_Zhv

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$nv

注:

- 在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編譯器和Clang為了兼容性使用了Visual C++的名字修飾規則。

### 從C++中連結時的C符號的處理

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

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

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

例如，C標準字串庫通常包含了類似這樣子的

```cpp
#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
```

於是，例如這樣的代碼

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

就能使用正確的、未經名字修飾過的strcmp和memset。如果沒有使用extern "C"，那麼SunPro C++編譯器會產生等價於下面的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層面不兼容的連結，例如異常處理和虛表設計。

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

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

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

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

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

### 通過c++filt去修飾

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

### 通過內建GCC ABI去修飾

```cpp
#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程序：

```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到本地名字的翻譯
- 常規的C++名字修飾

## Python的名字修飾

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

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

```pycon
>>> 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'' ''name''<sub>1</sub>:''parameter''<sub>1</sub> ...
– ''method name'': ''argument'' ''name''<sub>1</sub>:''parameter''<sub>1</sub> ...
```

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

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

實例化方法是這樣子的：

```objc
 – (id) value;
 – (id) setValue: (id) new_value;
```

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

```
_c_''Class''_''methodname''_''name''<sub>1</sub>_''name''<sub>2</sub>_ ...
```

這是實例化方法：

```
_i_''Class''_''methodname''_''name''<sub>1</sub>_''name''<sub>2</sub>_ ...
```

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編譯器來格式化，這樣它才能無視大小寫地用相同方式被連結。不同的編譯器用不同的方式來實現了，沒有發生過標準化。AIX和HP-UX的Fortran編譯器把標識符全轉成小寫（"foo"），而克雷Unicos的Fortran編譯器把標識符全轉成大寫（"FOO"）。GNU的g77編譯器把標識符轉成小寫後接一個下劃線（"foo_"），例外情況是：原先已經有下劃線的標識符（"FOO_BAR"）轉成後接兩個下劃線（"foo_bar__"），這是f2c設的約定。許多其它的編譯器，包括SGI的IRIX編譯器、gfortran和Intel的Fortran編譯器（不包括在Microsoft Windows上），都把標識符全部轉成小寫後接一個下劃線（"foo_"和"foo_bar_"）。在Microsoft Windows上，Intel Fortran編譯器預設為大寫不帶下劃線。

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

## Pascal的名字修飾

### Borland的Turbo Pascal/Delphi系列

為了避免Pascal的名字修飾，可以使用：

```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。

## 外部連結

- Linux Itanium ABI for C++，包含名字修飾方式。
- Macintosh C/C++ ABI Standard Specification
- c++filt —— 反向名字修飾被GNU/Intel編譯器編碼過的C++符號的過濾器
- undname —— 反向名字修飾的微軟Visual C++工具
- The Objective-C Runtime System —— 來自蘋果的 The Objective-C Programming Language 1.0
- Calling conventions for different C++ compilers Agner Fog 著，包含了多種不同的x86和x64的C++編譯器的名字修飾方式的細節描述（2011-06-08版本，第24至42頁）。
- C++ Name Mangling/Demangling 挺詳細地解釋了Visual C++編譯器的名字修飾方式
- PHP UnDecorateSymbolName 反向名字修飾Microsoft Visual C++的函數名的PHP腳本
- Mixing C and C++ Code
- Symbol management – 'Linkers and Loaders' by John R. Levine
- Name mangling demystified by Fivos Kefallonitis