fbpx
维基百科

名字修饰

名字修饰(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++编译器是怎么名字修饰相同的函数的:

编译器 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

注:

  • 在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(页面存档备份,存于互联网档案馆)。

参见

参考资料

  1. ^ Clang - Features and Goals: GCC Compatibility, 15 April 2013 [2020-09-22], (原始内容于2011-10-02) 
  2. ^ JBIntel_deleted_06032015. OBJ differences between Intel Compiler and VC Compiler. software.intel.com. [2020-09-22]. (原始内容于2019-03-29). 
  3. ^ MSVC compatibility. [13 May 2016]. (原始内容于2020-11-06). 
  4. ^ Itanium C++ ABI, Section 5.1 External Names (a.k.a. Mangling). [16 May 2016]. (原始内容于2021-01-01). 
  5. ^ Design Overview. docs.oracle.com. [2020-09-22]. (原始内容于2020-08-04). 
  6. ^ Summary of Mixed-Language Issues. User and Reference Guide for the Intel Fortran Compiler 15.0. Intel Corporation. [17 November 2014]. (原始内容于2016-08-17). 

外部链接

  • 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脚本
  • Name mangling demystified by Fivos Kefallonitis(页面存档备份,存于互联网档案馆

名字修饰, name, decoration, 也称为名字重整, 名字改编, name, mangling, 是现代计算机程序设计语言的编译器用于解决由于程序实体的名字必须唯一而导致的问题的一种技术, 它提供了在函数, 结构体, 类或其它的数据类型的名字中编码附加信息一种方法, 用于从编译器中向链接器传递更多语义信息, 该需求产生于程序设计语言允许不同的条目使用相同的标识符, 包括它们占据不同的命名空间, 典型的命名空间是由一个模块, 一个类或显式的namespace指示来定义的, 或者有不同的签名, 例如函数重载. 名字修饰 name decoration 也称为名字重整 名字改编 name mangling 是现代计算机程序设计语言的编译器用于解决由于程序实体的名字必须唯一而导致的问题的一种技术 它提供了在函数 结构体 类或其它的数据类型的名字中编码附加信息一种方法 用于从编译器中向链接器传递更多语义信息 该需求产生于程序设计语言允许不同的条目使用相同的标识符 包括它们占据不同的命名空间 典型的命名空间是由一个模块 一个类或显式的namespace指示来定义的 或者有不同的签名 例如函数重载 任何由编译器产生的目标代码通常与另一部分的目标代码 产生于同一款或不同款的编译器 通过链接器把它们链接起来 链接器需要一大堆每个程序实体信息 例如正确链接一个函数需要它的名字 参数个数和它们的类型 等等 目录 1 C语言的名字修饰 2 C 语言的名字修饰 2 1 简单样例 2 2 复杂样例 2 3 不同编译器如何名字修饰相同的函数 2 4 从C 中链接时的C符号的处理 2 5 C 标准化的名字修饰 2 6 C 名字修饰的现实影响 2 7 通过c filt去修饰 2 8 通过内建GCC ABI去修饰 3 Java的名字修饰 3 1 为内部和匿名类创建唯一的名字 3 2 Java本地接口 JNI 4 Python的名字修饰 5 Objective C的名字修饰 6 Fortran的名字修饰 7 Pascal的名字修饰 7 1 Borland的Turbo Pascal Delphi系列 7 2 Free Pascal 8 参见 9 参考资料 10 外部链接C语言的名字修饰 编辑虽然在不支持函数重载的程序设计语言 例如C语言和经典Pascal语言 中基本上不需要名字修饰 但是它们在一些情况下它们还是用了名字修饰来提供了函数的附加信息 例如 目标于微软Windows平台的编译器支持许多调用约定 这用于决定哪个参数传入子程序的方式和结果返回的方式 因为不同的调用约定彼此不兼容 所以编译器根据的调用约定重整了链接符号 名字修饰方案由微软公司首创 现已许多其它的编译器非正式采用 例如Digital Mars公司 Borland公司以及Windows移植版的GNU GCC 该方案甚至被其它语言采用 例如Pascal D语言 Delphi Fortran和C 这允许用这此语言写的子程序使用不同于自身默认的调用约定来调用或被调用于已存在的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 对于stdcall和fastcall调用约定的名字修饰方案中 函数分别被编码为 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 amp ZN9wikipedia7article8print toERSo class wikilink public wikilink std string const amp 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 lt char char traits lt char gt gt 有着特殊的别名So 所以 这个类型的一个引用类型就是RSo 这个函数的完整名字是 ZN 9wikipedia 7article 8print to E RSo 不同编译器如何名字修饰相同的函数 编辑 无论多么平凡的C 标识符 名字修饰规则都没有标准方式 所以不同的编译器产商 甚至相同编译器的不同版本 或相同编译器在不同平台上 的名字修饰规则都截然不同 也就意味着基本上都不兼容 看看C 编译器是怎么名字修饰相同的函数的 编译器 void h int void h int char void h void GCC 3 x及更高 Z1hi Z1hic Z1hvClang 1 x及更高 1 Intel C 8 0 for LinuxHP aC A 05 55 IA 64IAR EWARM C 5 4 ARMIAR EWARM C 7 4 ARM Z lt number gt hi Z lt number gt hic Z lt number gt hvGCC 2 9 x h Fi h Fic h FvHP aC A 03 45 PA RISCMicrosoft Visual C v6 v10 修饰详情 h YAXH Z h YAXHD Z h YAXXZDigital Mars C Borland C v3 1 h qi h qizc h qvOpenVMS C v6 5 ARM mode H XI H XIC H XVOpenVMS C v6 5 ANSI mode CXX 7H FIC26CDH77 CXX 7H FV2CB06E8OpenVMS C X7 1 IA 64 CXX Z1HI2DSQ26A CXX Z1HIC2NP3LI4 CXX Z1HV0BCA19VSunPro CC 1cBh6Fi v 1cBh6Fic v 1cBh6F v Tru64 C v6 5 ARM mode h Xi h Xic h XvTru64 C v6 5 ANSI mode 7h Fi 7h Fic 7h FvWatcom C 10 6 W h n i v W h n ia v W h n v注 在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标准字符串库 lt string h gt 通常包含了类似这样子的 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 就能使用正确的 未经名字修饰过的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运行库中 例如 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 lt StringName Ref lt GDScript gt Comparator lt StringName gt DefaultAllocator gt has StringName const amp const 通过内建GCC ABI去修饰 编辑 include lt stdio h gt include lt stdlib h gt include lt cxxabi h gt 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 输出 Demangled Map lt StringName Ref lt GDScript gt Comparator lt StringName gt DefaultAllocator gt has StringName const amp constJava的名字修饰 编辑在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把这些名字改成单下划线加上封闭类的名字 例如 gt gt gt class Test object def mangled name self pass def normal name self pass gt gt gt 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编译器来格式化 这样它才能无视大小写地用相同方式被链接 不同的编译器用不同的方式来实现了 没有发生过标准化 AIX和HP UX的Fortran编译器把标识符全转成小写 foo 而克雷Unicos 英语 Unicos 的Fortran编译器把标识符全转成大写 FOO GNU的g77编译器把标识符转成小写后接一个下划线 foo 例外情况是 原先已经有下划线的标识符 FOO BAR 转成后接两个下划线 foo bar 这是f2c 英语 f2c 设的约定 许多其它的编译器 包括SGI的IRIX编译器 gfortran和Intel的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 页面存档备份 存于互联网档案馆 参见 编辑语言绑定 跨语言函数接口 英语 Foreign function interface 调用约定 Stropping 英语 Stropping syntax 应用程序接口 API 应用二进制接口 ABI 应用层虚拟机的比较 英语 Comparison of application virtual machines Java本地接口 SWIG 产生于多种语言到多种语言的开源接口绑定 Microsoft Visual C 名字修饰参考资料 编辑 Clang Features and Goals GCC Compatibility 15 April 2013 2020 09 22 原始内容存档于2011 10 02 JBIntel deleted 06032015 OBJ differences between Intel Compiler and VC Compiler software intel com 2020 09 22 原始内容存档于2019 03 29 MSVC compatibility 13 May 2016 原始内容存档于2020 11 06 Itanium C ABI Section 5 1 External Names a k a Mangling 16 May 2016 原始内容存档于2021 01 01 Design Overview docs oracle com 2020 09 22 原始内容存档于2020 08 04 Summary of Mixed Language Issues User and Reference Guide for the Intel Fortran Compiler 15 0 Intel Corporation 17 November 2014 原始内容存档于2016 08 17 外部链接 编辑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 页面存档备份 存于互联网档案馆 取自 https zh wikipedia org w index php title 名字修饰 amp oldid 76085968, 维基百科,wiki,书籍,书籍,图书馆,

文章

,阅读,下载,免费,免费下载,mp3,视频,mp4,3gp, jpg,jpeg,gif,png,图片,音乐,歌曲,电影,书籍,游戏,游戏。