符号修饰与函数签名
符号
符号修饰与函数签名
约在20世纪70年代以前,编译器编译源代码产生目标文件的时候,符号名与相应的变量和函数的名字是一样的。
但是现在库和目标文件越来越多,如果我们想要使用一个库的时候,我们就不能使用库中定义的函数和变量。否则就会有符号名冲突。为了防止符号名冲突,UNIX下的C语言就规定,C语言源代码文件中的所有全局变量和函数经过编译后,相对应的符号名前加上下划线”_”。
这种简单而原始的方法确实能够解决符号冲突的概率,但是当程序很大时,不同模块由多个部门开发,他们之间的命名规范如果不严格,则很有可能导致冲突。于是像C++这样的后来设计的语言开始考虑到了这个问题,增加了 名称空间(Namespace)的方法来解决多模块的符号冲突问题。
随着时间的推移,很多操作系统和编译器被完全重写,符号冲突问题不是那么明显了,现在的Linux下的GCC编译器中,默认情况下已经去掉了在C语言符号中加”_”的方式。只有GCC在windows平台下的版本(mingw,cygwin)和 Visual C++ 编译器会在C语言符号前加”_“。
C++符号修饰
众所周知,强大而又复杂的C++拥有类,继承,虚机制,重载,名称空间等这些特性,它们使得符号管理更为复杂。最简单的例子,两个相同名字的函数func(int)和func(float),尽管函数名相同,但是参数列表不同,这是C++里面函数重载的最简单的一种情况,那么编译器和链接器在链接过程中如何区分这两个函数呢?为了支持C++这些复杂的特性,人们发明了 符号修饰(Name Decoration)或 符号改编(Name Mangling)的机制。
我们引入一个术语叫做 函数签名(Function Signnature),函数签名包含了一个函数的信息,包括函数名,它的参数类型,它所在的类和名称空间及其他信息。函数签名用于识别不同的函数,就像签名用于识别不同的人一样,函数的名字只是函数签名的一部分。
不同编译器厂商的名称修饰方法可能不同,所以不同的编译器对于同一个函数签名可能对应不同的修饰后名称。
由于不同的编译器采用不同的名字修饰方法,必然会导致由不同的编译器编译产生的目标文件无法正常相互链接,这是导致不同编译器之间不能互操作的主要原因之一。
extern “C”
C++为了与C语言兼容,在符号管理上,C++有一个用来声明或定义一个C的符号的”extern “C””关键字用法。
C++编译器会将在extern “C”的大括号内部的代码当做C语言代码处理。
很多时候我们会碰到有些头文件声明了一些C语言的函数和全局变量,但是这个头文件可能会被C语言代码或C++代码包含。当我们用C语言代码包含的时候不会有任何问题,但是如果我们用C++的代码包含,那么它就会将文件中的符号和变量名用C++的修饰方法来修饰。这样链接器就无法与C语言中的函数进行链接。
所以对于C++来说,必须用extern “C”来声明这个函数。但C语言又不支持extern “C”的语法,这时候我们就会用下面的代码来声明这个函数
1 |
|
如果当前编译单元是C++代码,那么func函数会在extern “C”里面被声明;如果是C语言代码,就直接声明。上面这段代码中的技巧几乎在所有的系统头文件里面都被用到。
强符号与弱符号
我们经常在编程中碰到一种情况叫符号重复定义。多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误。
这种符号的定义可以被称为 强符号(strong Symbol)。有些符号的定义可以被称为 弱符号(Weak Symbol)。对于C/C++来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。我们也可以通过GCC的”__attribute__((weak))”来定义任何一个强符号为弱符号。
针对强弱符号的概念,链接器就会按照如下规则处理与选择被多次定义的全局符号:
- 不允许强符号多次定义,如果有多个强符号多次定,则链接器报符号重复定义错误。
- 如果一个符号在一个目标文件中是强符号,在其他文件中都是弱符号,那么选这强符号。
- 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个(尽量不要使用多个不同类型的弱符号,否则容易导致很难发现的程序错误)。
强引用和弱引用
目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,他们需要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为 强引用。与之相对应还有一种 弱引用,在处理弱引用时,如果符号有定义,则链接器将该符号的引用决议;如果该符号未定义,则链接器对于该引用不报错。
对于未定义的弱引用,链接器一般默认其为0。或者是一个特殊的值,以便于程序代码能够识别。
这种 弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块可以正常使用;如果我们去掉了功能模块,那么程序也可以正常链接,只是缺少了相应的功能。
调试信息
目标文件里面还可能保留的是调试信息。几乎所有现代的编译器都支持源代码级别的调试,比如我们可以在函数里面设置断点,可以监视变量变化。
调试信息段一般在目标文件中占用很大的空间,往往比程序的代码和数据大出好几倍。所以当我们开发完程序并要将它发布的时候,需要把这些对用户没有用的调试信息去掉,以节省大量的空间,在linux下,我们可以使用 strip命令来去掉ELF文件中的调试信息。