约在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++ 函数的名称修饰
部结构我们在这里先不展开了,在下一章分析静态链接过程的时候,我们还会详细地分析重定位表的结构。
int func(int);
float func(float);
class C {
int func(int);
class C2 {
int func(int);
};
};
namespace N {
int func(int);
class C {
int func(int);
};
}
这段代码中有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)函数签名经过修饰为_ZN1N1C4funcEi。更为具体的修饰方法我们在这里不详细介绍,有兴趣的读者可以参考GCC的名称修饰标准。幸好这种名称修饰方法我们平时程序开发中也很少手工分析名称修饰问题,所以无须很详细地了解这个过程。binutils里面提供了一个叫"c++filt"的工具可以用来解析被修饰过的名称,比如:
$ c++filt _ZN1N1C4funcEi
N::C::func(int)
签名和名称修饰机制不光被使用到函数上,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
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()将修饰后名称转换成函数签名:
/* 2-4.c
- Compile: cl 2-4.c /link Dbghelp.lib
- Usage: 2-4.exe DecroatedName
/
#include <Windows.h>
#include <Dbghelp.h>
int main( int argc, char argv[] )
{
char buffer[256];
if(argc == 2)
{
UnDecorateSymbolName( argv[1], buffer, 256, 0 );
printf( buffer );
}
else
{
printf( “Usage: 2-4.exe DecroatedName\n” );
}
return 0;
}
由于不同的编译器采用不同的名字修饰方法,必然会导致由不同编译器编译产生的目标文件无法正常相互链接,这是导致不同编译器之间不能互操作的主要原因之一。
其他内容整理如下(原作者未知):
nm用于列出目标文件的符号清单,如果没有指定目标文件,则默认为“a.out”。nm的格式如下:
nm [‘-a’|‘–debug-syms’] [‘-g’|‘–extern-only’]
[‘-B’] [‘-C’|‘–demangle’[=style]] [‘-D’|‘–dynamic’]
[‘-S’|‘–print-size’] [‘-s’|‘–print-armap’]
[‘-A’|‘-o’|‘–print-file-name’][‘–special-syms’]
[‘-n’|‘-v’|‘–numeric-sort’] [‘-p’|‘–no-sort’]
[‘-r’|‘–reverse-sort’] [‘–size-sort’] [‘-u’|‘–undefined-only’]
[‘-t’ radix|‘–radix=’radix] [‘-P’|‘–portability’]
[‘–target=’bfdname] [‘-f’format|‘–format=’format]
[‘–defined-only’] [‘-l’|‘–line-numbers’] [‘–no-demangle’]
[‘-V’|‘–version’] [‘-X 32_64’] [‘–help’] [objfile…]
对于每一个符号,nm列出其值(the symbol value),类型(the symbol type)和其名字(the symbol name)。
如下例:
00000024 T cleanup_before_linux
00000018 T cpu_init
00000060 T dcache_disable
00000054 T dcache_enable
0000006c T dcache_status
00000000 T do_reset
0000003c T icache_disable
00000030 T icache_enable
00000048 T icache_status
上面的显示是使用nm cpu.o的输出,对于cleanup_before_linux这个符号来说,00000024是以16进制显示的其值,T为其类型,而cleanup_before_linux是其名字。可以看出,上面显示的cleanup_before_linux这个symbol的值实际上是该函数在text section中的偏移。但是,每个符号的值的具体含义依其类型而异。当然,对于每个符号的值,其类型、其值以及它们所属的section是密切相关的。
下面说明符号类型:对于每一个符号来说,其类型如果是小写的,则表明该符号是local的;大写则表明该符号是global(external)的。
符号
类型 说明
A 该符号的值是绝对的,在以后的链接过程中,不允许进行改变。这样的符号值,常常出现在中断向量表中,例如用符号来表示各个中断向量函数在中断向量表中的位置。
B 该符号的值出现在非初始化数据段(bss)中。例如,在一个文件中定义全局static int test。则该符号test的类型为b,位于bss section中。其值表示该符号在bss段中的偏移。一般而言,bss段分配于RAM中
C 该符号为common。common symbol是未初始话数据段。该符号没有包含于一个普通section中。只有在链接过程中才进行分配。符号的值表示该符号需要的字节数。例如在一个c文件中,定义int test,并且该符号在别的地方会被引用,则该符号类型即为C。否则其类型为B。
D 该符号位于初始话数据段中。一般来说,分配到data section中。例如定义全局int baud_table[5] = {9600, 19200, 38400, 57600, 115200},则会分配于初始化数据段中。
G 该符号也位于初始化数据段中。主要用于small object提高访问small data object的一种方式。
I 该符号是对另一个符号的间接引用。
N 该符号是一个debugging符号。
R 该符号位于只读数据区。例如定义全局const int test[] = {123, 123};则test就是一个只读数据区的符号。注意在cygwin下如果使用gcc直接编译成MZ格式时,源文件中的test对应_test,并且其符号类型为D,即初始化数据段中。但是如果使用m6812-elf-gcc这样的交叉编译工具,源文件中的test对应目标文件的test,即没有添加下划线,并且其符号类型为R。一般而言,位于rodata section。值得注意的是,如果在一个函数中定义const char *test = “abc”, const char test_int = 3。使用nm都不会得到符号信息,但是字符串“abc”分配于只读存储器中,test在rodata section中,大小为4。
S 符号位于非初始化数据区,用于small object。
T 该符号位于代码区text section。
U 该符号在当前文件中是未定义的,即该符号的定义在别的文件中。例如,当前文件调用另一个文件中定义的函数,在这个被调用的函数在当前就是未定义的;但是在定义它的文件中类型是T。但是对于全局变量来说,在定义它的文件中,其符号类型为C,在使用它的文件中,其类型为U。
V 该符号是一个weak object。
W The symbol is a weak symbol that has not been specifically tagged as a weak object symbol.
- 该符号是a.out格式文件中的stabs symbol。
? 该符号类型没有定义