函数签名(functionsignature)与符号修饰
(namedecoration)【转】
先来总结⼀下Sam看这篇⽂章的要点:
1. 函数名称 不能完全标识 ⼀个函数;因此我们⽤“函数签名(function signature)”来唯⼀标识⼀个函数
2. “函数签名”经过不同“编译器/链接器”的“名称修饰(name decoration)”得到不同的“修饰后名称(decorated name)”。由于不同编译器采⽤不同的名字修饰⽅法,必然会导致由不同编译器产⽣的⽬标⽂件⽆法正常链接,这是导致不同编译器之间不能互操作的主要原因之⼀。
3. 解析GCC的C++名称修饰——binutils⾥⾯的⼀个⼩⼯具c++filt可以解析被修饰过的名称:
$ c++filt _ZN1N1C4funcEi
N::C::func(int)
4. 解析Visual C++的名称修饰——Microsoft提供⼀个API:UnDecorateSymbolName()
-----------------------------------------------------------------------------------------------------------------------------------
约在20世纪70年代以前,编译器编译源代码产⽣⽬标⽂件时,符号名与相应的变量和函数的名字是⼀样的。⽐如⼀个汇编源代码⾥⾯包含了⼀个函数 foo,那么汇编器将它编译成⽬标⽂件以后,foo在⽬标⽂件中的相对应的符号名也是foo。当后来UNIX平台和C语⾔发明时,已经存在了相当多的使⽤ 汇编编写的库和⽬标⽂件。这样就产⽣了⼀个问题,那就是如果⼀个C程序要使⽤这些库的话,C语⾔中不可以使⽤这些库中定义的函数和变量的名字作为符号名, 否则将会跟现有的⽬标⽂件冲突。⽐如有个⽤汇编编写的库中定义了⼀个函数叫做main,那么我们在C语⾔⾥⾯就不可以再定义⼀个main函数或变量了。同 样的道理,如果⼀个C语⾔的⽬标⽂件要⽤到⼀个使⽤Fortran语⾔编写的⽬标⽂件,我们也必须防⽌它们的名称冲突。
为了防⽌类似的符号名冲突,UNIX下的C语⾔就规定,C语⾔源代码⽂件中的所有全局的变量和函数经过编译以后,相对应的符号名前加上下划 线"_"。⽽Fortran语⾔的源代码经过编译以后,所有的符号名前加上"_",后⾯也加上"_"。⽐如⼀个C语⾔函数"foo",那么它编译后的符号 名就是"_foo";如果是Fortran语⾔,就是"_foo_"。
这种简单⽽原始的⽅法的确能够暂时减少多种语⾔⽬标⽂件之间的符号冲突的概率,但还是没有从根本上解决符号冲突的问题。⽐如同⼀种语⾔编写的⽬标⽂ 件还有可能会产⽣符号冲突,当程序很⼤时,不同的模块由多个部门(个⼈)开发,它们之间的命名规范如果不严格,则有可能导致冲突。于是像C++这样的后来 设计的语⾔开始考虑到了这个问题,增加了名称空间(Namespace)的⽅法来解分数查询网站
决多模块的符号冲突问题。
但是随着时间的推移,很多操作系统和编译器被完全重写了好⼏遍,⽐如UNIX也分化成了很多种,整个环境发⽣了很⼤的变化,上⾯所提到的跟 Fortran和古⽼的汇编库的符号冲突问题已经不是那么明显了。在现在的Linux下的GCC编译器中,默认情况下已经去掉了在C语⾔符号前加"_"的 这种⽅式;但是Windows平台下的编译器还保持的这样的传统,⽐如Visual C++编译器就会在C语⾔符号前加"_",GCC在Windows平台下的版本(cygwin、mingw)也会加"_"。GCC编译器也可以通过参数选 项"-fleading-underscore"或"-fno-leading-underscore"来打开和关闭是否在C语⾔符号前加上下划线。
C++符号修饰
众所周知,强⼤⽽⼜复杂的C++拥有类、继承、虚机制、重载、名称空间等这些特性,它们使得符号管理更为复杂。最简单的例⼦,两个相同名字的函数 func(int)和func(double),尽管函数名相同,但是参数列表不同,这是C++⾥⾯函数重载的最简单的⼀种情况,那么编译器和链接器在链 接过程中如何区分这两个函数呢?为了⽀持C++这些复杂的特性,⼈们发明了符号修饰(Name Decoration)或符号改编(Name Mangling)的机制,下⾯我们来看看C++的符号修饰机制。
⾸先出现的⼀个问题是C++允许多个不同参数类型的函数拥有⼀样的名字,就是所谓的函数重载;另外C++还在语⾔级别⽀持名称空间,即允许在不同的名称空间有多个同样名字的符号。⽐如清单3-4这
段代码:
清单3-4  C++ 函数的名称修饰
部结构我们在这⾥先不展开了,在下⼀章分析静态链接过程的时候,我们还会详细地分析重定位表的结构。
[cpp]
1. int func(int);
2. float func(float);
3. class C {
4. int func(int);
5. class C2 {
6. int func(int);
7. };
8. };
9. namespace N {
10. int func(int);
11. class C {
12. int func(int);
13. };
14. }
故事新编
这段代码中有6个同名函数叫func,只不过它们的返回类型和参数及所在的名称空间不同。我们引⼊⼀个术语叫做函数签名(Function Signature),函数签名包含了⼀个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息。函数签名⽤于识别不同的函数,就像签 名⽤于识别不同的⼈⼀样,函数的名字只是函数签名的⼀部分。由于上⾯6个同名函数的参数类型及所处的类和名称空间不同,我们可以认为它们的函数签名不同。 在编译器及链接器处理符号时,它们使⽤某种名称修饰的⽅法,使得每个函数签名对应⼀个修饰后名称(Decorated Name)。编译器在将C++源代码编译成⽬标⽂件时,会将函数和变量的名字进⾏修饰,形成符号名,也就是
说,C++的源代码编译后的⽬标⽂件中所使⽤的 符号名是相应的函数和变量的修饰后名称。C++编译器和链接器都使⽤符号来识别和处理函数和变量,所以对于不同函数签名的函数,即使函数名相同,编译器和 链接器都认为它们是不同的函数。上⾯的6个函数签名在GCC编译器下,相对应的修饰后名称如表3-18所⽰。
表3-18
修饰后名称(符号
调协函数签名
名)
int func(int)_Z4funci
float func(float)
_Z4funcf int C::func(int)
_ZN1C4funcEi int
C::C2::func(int)
_ZN1C2C24funcEi int N::func(int)
_ZN1N4funcEi int N::C::func(int)_ZN1N1C4funcEi符号名字
GCC的基本C++名称修饰⽅法如下:所有的符号都以"_Z"开头,对于嵌套的名字(在名称空间或在类⾥⾯的),后⾯紧跟"N",然后是各个名称空 间和类的名字,每个名字前是名字字符串长度,再以"E"结尾。⽐如N::C::func经过名称修饰以后就是_ZN1N1C4funcE。对于⼀个函数来 说,它的参数列表紧跟在"E"后⾯,对于int类型来说,就是字母"i"。所以整个N::C::func(int)函数签名经过修饰为
laffis_ZN1N1C4funcEi。更为具体的修饰⽅法我们在这⾥不详细介绍,有兴趣的读者可以参考GCC的名称修饰标准。幸好这种名称修饰⽅法我们平时程序 开发中也很少⼿⼯分析名称修饰问题,所以⽆须很详细地了解这个过程。binutils⾥⾯提供了⼀个叫"c++filt"的⼯具可以⽤来解析被修饰过的名 称,⽐如:
签名和名称修饰机制不光被使⽤到函数上,C++中的全局变量和静态变量也有同样的机制。对于全局变量来说,它跟函数⼀样都是⼀个全局可见的名称,它也遵循 上⾯的名称修饰机制,⽐如⼀个名称空间foo中的全局变量bar,它修饰后的名字为:_ZN3foo3barE。值得注意的是,变量的类型并没有被加⼊到 修饰后名称中,所以不论这个变量是整形还是浮点型甚⾄是⼀个全局对象,它的名称都是⼀样的。
名称修饰机制也被⽤来防⽌静态变量的名字冲突。⽐如main()函数⾥⾯有⼀个静态变量叫foo,⽽func()函数⾥⾯也有⼀个静态变量叫
foo。为了区分这两个变量,GCC会将它们的符号名分别修饰成两个不同的名字_ZZ4mainE3foo和_ZZ4funcvE3foo,这样就区分了 这两个变量。
不同的编译器⼚商的名称修饰⽅法可能不同,所以不同的编译器对于同⼀个函数签名可能对应不同的修饰后名称。⽐如上⾯的函数签名中在Visual C++编译器下,它们的修饰后名称如表3-19所⽰。
表3-19
函数签名
修饰后名称int func(int)
?func@@YAHH@Z float
func(float)
func@@YAMM@Z int C::func(int)
?
func@C@@AAEHH@Z int
C::C2::func(int)
?func@C2@C@@AAEHH@Z int N::func(int)?func@N@@YAHH@Z
[cpp]
1. $ c++filt _ZN1N1C4funcEi
2. N::C::func(int)
int
N::C::func(int)?func@C@N@@AAEHH@Z
我们以int N::C::func(int)这个函数签名来猜测Visual C++的名称修饰规则(当然,你只须⼤概了解这个修饰规则就可以了)。修饰后名字由"?"开头,接着是函数名由"@"符号结尾的函数名;后⾯跟着由"@" 结尾的类名"C"和名称空间"N",再⼀个"@"表⽰函数的名称空间结束;第⼀个"A"表⽰函数调⽤类型为"__cdecl"(函数调⽤类型我们将在第4 章详细介绍),接着是函数的参数类型及返回值,由"@
"结束,最后由"Z"结尾。可以看到函数名、参数的类型和名称空间都被加⼊了修饰后名称,这样编译器 和链接器就可以区别同名但不同参数类型或名字空间的函数,⽽不会导致link的时候函数多重定义。
Visual C++的名称修饰规则并没有对外公开,当然,⼀般情况下我们也⽆须了解这套规则,但是有时候可能须要将⼀个修饰后名字转换成函数签名,⽐如在链接、调试程 序的时候可能会⽤到。Microsoft提供了⼀个UnDecorateSymbolName()的API,可以将修饰后名称转换成函数签名。下⾯这段代 码使⽤UnDecorateSymbolName()将修饰后名称转换成函数签名:
由于不同的编译器采⽤不同的名字修饰⽅法,必然会导致由不同编译器编译产⽣的⽬标⽂件⽆法正常相互链接,这是导致不同编译器之间不能互操作的主要原因之⼀。我们后⾯的关于C++ ABI和COM的这⼀节将会详细讨论这个问题。[cpp]
1. /* 2-4.c
2. * Compile: cl 2-4.c /link Dbghelp.lib
3. * Usage: 2-
< DecroatedName
4. */
5. #include <Windows.h>
6. #include <Dbghelp.h>
7. int main( int argc, char* argv[] )
8. {
9. char buffer[256];
10.    if(argc == 2)
11. {
12. UnDecorateSymbolName( argv[1], buffer, 256, 0 );
13. printf( buffer );
14. }
15. else
16. {
17. printf( "Usage: DecroatedName\n" );
彩虹的微笑歌词18. }
19.    return 0;
20. }