名字修饰

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

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

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

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

## C 語言的名字修飾

雖然在不支援函式多載的程式設計語言（例如 C 語言和經典 Pascal 語言）中基本上不需要名字修飾，但是它們在一些情況下它們還是用了名字修飾來提供了函式的附加資訊。
例如，目標於微軟 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 方法，如下所示：

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

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

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

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

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

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

### 簡單樣例

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

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

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

### 複雜樣例

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

全部被改編過的符號由 **\_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<char, char\_traits<char> >），有著特殊的別名 **So**，所以，這個類型的一個參照類型就是 **RSo**，這個函式的完整名字是：

```
_ZN·_9_**wikipedia**·_7_**article**·_8_**print\_to**·E·**RSo**
```

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

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

注:

* 在 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++ 慣常的做法：

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

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

於是，例如這樣的代碼

就能使用正確的、未經名字修飾過的 strcmp 和 memset。如果沒有使用 extern "C"，那麼 SunPro C++ 編譯器會產生等價於下面的 C 代碼：

而這些連結符號並不存在於 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 去修飾

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

輸出：

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

## Java 的名字修飾

在 Java 語言中，方法或類的**簽章**包含了它的名字以及它的參數和可適用的返回值類型。簽章的格式是有文件說明的，因為 Java 語言、編譯器和.class 檔案的格式都是全部一起設計的（並且一開始就是物件導向和互操作性的）。

### 為內部和匿名類建立唯一的名字

匿名類的作用域局限於它們的父類別，所以編譯器必須為內部類產生一個「合格」的公開名字，來避免與其它相同命名空間的同名類類衝突。類似的，匿名類必須有「假的」公開名字（因為匿名類的概念僅存在於編譯器內，而不存在於執行時）。所以，編譯下列的 Java 程式：

將產生如下三個**.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 把這些名字改成單底線加上封閉類的名字，例如：

## Objective-C 的名字修飾

本質上，Objective-C 存在兩種形式的方法，**類別方法**（靜態方法）和**實例化方法**。Objective-C 的方法聲明如下：

```
+ _method name_: _argument_ _name_1:_parameter_1 ...
– _method name_: _argument_ _name_1:_parameter_1 ...
```

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

實例化方法是這樣子的：

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

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

在編譯期間，建立了一個把文字表述（例如 \_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 的名字修飾，可以使用：

### Free Pascal

Free Pascal 支援函式多載和運算子多載，所以它也使用名字修飾來支援這些特性。另外，Free Pascal 能夠呼叫由其它語言寫的的外部模組定義的符號，也能匯出自己的符號供其它語言呼叫。更多資訊，詳見 Free Pascal Programmer's Guide（頁面存檔備份，存於網際網路檔案館）的子頁面 Chapter 6.2（頁面存檔備份，存於網際網路檔案館）和 Chapter 7.1（頁面存檔備份，存於網際網路檔案館）。

## 參見

* 語言繫結
* 跨語言函式介面
* 呼叫約定
* Stropping
* 應用程式介面 – API
* 應用二進位介面 – ABI
* 應用層虛擬機器的比較
* Java 本地介面
* SWIG – 產生於多種語言到多種語言的開源介面繫結
* Microsoft Visual C++ 名字修飾

## 參考資料

## 外部連結

* 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（頁面存檔備份，存於網際網路檔案館）