浅谈name mangling

很多语言都允许重载函数,这些函数在源代码中有相同的名字,却又不同的参数列表。这用到了一种叫做name mangling的技术。

名字修饰,又译作名字粉碎名字重整,译自英文name manglingname decoration,是现代计算机程序设计语言的编译器用于解决由于程序实体的名字必须唯一而导致的问题的一种技术。

它提供了在函数、结构体、类或其它的数据类型的名字中编码附加信息一种方法,用于从编译器中向链接器传递更多语义信息。

该需求产生于程序设计语言允许不同的条目使用相同的标识符,包括它们占据不同的命名空间(典型的命名空间是由一个模块、一个类或显式的namespace指示来定义的)或者有不同的签名(例如函数重载)。

接下来主要以c++的函数重载为例,来简要介绍一下name mangling。(当然,其他语言中也有name mangling,而且也c++还有很多其他地方需要name mangling,比如namespace,class,template等)。

name mangling出现的原因

c++源文件经过编译器和汇编器生成可重定位目标文件。链接器生成可执行目标文件。

不同的系统之间,目标文件的格式都不同,但基本的概念都是相同的。

每个可重定位目标模块中包含一个叫做符号表(.symtab)的部分。符号表包含了m所定义和引用的符号。一般分为三种:

  • 在m中定义并且能被其他模块引用的全局符号。对应于非静态的函数以及不带static的全局变量。
  • 在其他模块中定义并且被模块m引用的全局符号。这些符号也被称为外部(external)符号,对应于定义在其他模块的函数和变量。
  • 只被模块m定义和引用的本地符号。对应于带static的函数和全局变量。

要注意到,符号表中不包含对应于本地非静态程序变量的任何符号。因为这些符号在运行时栈中管理,链接器不care。

(可能有人对上面几段中模块一词比较疑惑。一个目标模块(object module)就是一个字节序列,而一个目标文件(object file)就是存储在存储器中的目标模块。)

而链接器做的主要工作,就是进行符号解析和重定位。而链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来。

从这里已经可以看出一些倪端了。c++要实现重载,必须让链接器能够区分这些重载函数。而不能像c语言中一样简单的将函数名作为符号。这就必须要用到name mangling。

ps:关于编译链接的过程我也不是很懂,这方面还需要后续的学习。

name mangling的基本方式

name mangling的基本原理就是将每个唯一的函数和参数列表组合编码成一个对链接器来说唯一的名字。换句话说,编译器和链接器需要一定的协议来规范符号的组织格式。

c++中的重载函数区分在于参数数量和某个参数类型的不同。所以区分函数的时候,需要充分考虑参数数量和参数类型这两个语义信息。

然而,c++并没有规定一个标准的name mangling方式,所以不同的编译器采用的各自的name mangling方式(甚至相同编译器的不同版本,或相同编译器在不同平台上,name mangling规则都截然不同)。所以几乎没有链接器可以链接不同编译器产生的目标代码。

以下就以gcc为例子来初步了解一下。

一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
int f(void) 
{
return 1;
}
int f(int)
{
return 0;
}
void g(void)
{
int i = f(), j = f(1);
}

编译得到可重定位目标文件,然后用gcc工具链中的nm列出目标文件的符号,可以看到

1
2
3
4
5
6
7
8
9
0000000000000000 b .bss
0000000000000000 d .data
0000000000000000 p .pdata
0000000000000000 r .rdata$zzz
0000000000000000 t .text
0000000000000000 r .xdata
000000000000000b T _Z1fi
0000000000000000 T _Z1fv
0000000000000019 T _Z1gv

这三列分别是 symbol value,symbol type, symbol name。 现在主要关注的就是最后一列的符号名(主要是因为前两个我也不知道是个啥= =。以后再填坑吧)。

可以看到int f(void) 在符号表中是_Z1fvint f(int)_Z1fivoid g(void)_Z1gv

大家也可以从这个简单的例子窥见gcc中name mangling的方式。不过本文只是简单的介绍一下name mangling出现的理由和通用策略,并非意在介绍具体某种编译器的name mangling编码规则,所以不会在这块深入。(啊,其实是因为我也不懂)。

最后再做一个实验,看一下涉及到namespace和class的name mangling。

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace t {
class c
{
public:
int f() {}
};
int f() {}
int f(int) {}
}
int f() {
t::c objc;
objc.f();
}

nm列出符号表(忽略其他符号),看一下这几个函数的符号名:

1
2
3
4
000000000000000f T _Z1fv
0000000000000000 T _ZN1t1c1fEv
0000000000000006 T _ZN1t1fEi
0000000000000000 T _ZN1t1fEv

ps:最后的最后,感觉自己还是有很多地方也不是很明白…还得接着学 啊 = =