一、符号命名的规则
众所周知,强大的C++相较于C增添了许多功能。这其中就包括类、命名空间和重载这些特性。
对于类来说,不同类中可以定义名字相同的函数和变量,彼此不会相互干扰。命名空间可以保证在各个不同名字空间内的类、函数和变量名字不会互相影响。而重载可以保证即使在同一个命名空间内的同一个类中,函数名字也可以相同,只要参数不一样就可以。
这样的设计方便了程序开发者,不用担心不同开发者都定义相同名字的函数的问题。但是,这也使得符号管理变得更为复杂。
对于在不同类中的同名函数,或者在不同名字空间中的同名函数,或者在同一名字空间或类中的同名重载函数,在最终的编译和链接过程中是怎么将它们区分开来的呢?为了支持C++这些特性,人们发明了所谓的符号改编(Name Mangling)机制。
其原理其实很简单,就是按照函数所在名字空间、类以及参数的不同,按照一定规则对函数进行重命名。不同的编译器其命名规则都不尽相同,这里我们主要介绍GNU C++编译器所使用的规则。主要分为以下几种情况:
全局变量:
即在命名空间和类之外的变量,改编后的符号名就是变量名,也就是不做任何修改。
全局函数:
以“_Z”开头,然后是函数名字符的个数,接着是函数名,最后是函数参数的别名。
关于函数参数的别名,后面还会有详细的介绍。
类或命名空间中的变量或函数:
以“_ZN”开头,然后是变量或函数所在名字空间或类名字的字符长度,然后接着的是真正的名字空间或类名,然后是变量或函数名的长度和变量或函数名,后面紧跟字母“E”,最后如果是函数的话则跟参数别名,如果是变量则什么都不用加。
构造函数和析构函数
以”_ZN”开头,然后是构造函数所在名字空间和类名字的字符长度,然后接着的是真正的名字空间或类名,然后构造函数接“C1”或者“C2”,析构函数接“D1”或者“D2”,然后加上字母“E”,最后接函数参数别名结束。
每个基本类型的别名如下表:
参数类型 | 参数别名 |
---|---|
void | v |
wchar_t | w |
bool | b |
char | c |
signed char | a |
unsigned char | h |
short | s |
unsigned short | t |
int | i |
unsigned int | j |
long | l |
unsigned long | m |
long long或__int64 | x |
unsigned long long或unsigned __int64 | y |
__int128 | n |
unsigned __int128 | o |
float | f |
double | d |
long double或__float80 | e |
__float128 | g |
二、符号表
两个库 libA.so, libB.so, 内部有相同的内部函数名 TestFun
这个问题烦扰我很长时间了,特上来请教各位大侠。
假设存在两个库 libA.so, libB.so, 内部有相同的内部函数名 TestFunc, 但是各自实现不一样。
在libA.so中, 函数 ComputeA 使用到了 TestFunc
在libB.so中, 函数 ComputeB 使用到了 TestFunc
其中函数ComputeA, ComputeB都在可执行程序 program中使用到。program是采用下面这种方式生成的:
g++ -L./ -g -o program program.o -Wl,-rpath=./ -lA -lB
然后在实际运行program过程中发现:
在调用函数 ComputeB 用到了居然是 libA.so中的 TestFunc, 而不是 libB.so中的 TestFunc, 以至于程序就崩溃了。
也就是说, 两个包含相同函数的库,在链接时,会先在第一个链接库中查找,如果查找到了就不会去 下一个库中查找。
在不修改libA.so, 和 libB.so 两个库的情况下, 让 program程序查找符号表时,优先在各自的库中查找
在编译链接生成libA.so, libB.so时, 加上链接选项: -Wl,-Bsymbolic
-Wl,-Bsymbolic -Wl,--version-script,version 的连接选项,意思是用 version 文件中的脚本指定其导出哪些函数。
而Linux中动态运行库的符号是在运行时进行重定位的,我们可以用objdump -R
看到libmylib.so的重定位表,中间有foo符号,
符号的查找(resolve,或者叫决议、绑定、解析)和符号的重定位(relocation)
符号的查找/绑定在每一个步骤中都可能会发生(严格说编译时并不是符号的绑定,它只会牵涉到局部符号(static)的绑定,它是基于token的语法和语义层面的“绑定”)
链接时的符号绑定做了很多工作,这也是我们经常看到ld会返回”undefined reference to …”或”unresolved symbol …”,这就是ld找不到符号,
但并不是所有的链接时都会把符号绑定进行到底的,只要在生成最终的执行程序之前把所有的符号全部绑定完成就可以了。
这样我们可以理解为什么链接动态库或静态库是即使有符号找不到也不会报错(但如果是局部符号找不到那时一定会报错的,因为链接器知道已经没有机会再找到了)
由于编译和链接都不能保证所有符号都已经解析,因此我们通过nm查看.o或者.a或者.so文件时会看到U符号,即undefined符号,那都是待字闺中的代表(无视剩女的后果是当你生成最终的执行程序时报告unresolved symbol…)
加载时的绑定,其实加载时对于符号的绑定和链接对应的执行程序做的事情基本类似,需要重复劳动的原因是OS无法保证加载程序时当初的原配是否还在,通过绑定来找到符号在那个模块的那个地址。
懒惰的OS总是抱着侥幸的心理想可能有些符号不需要绑定,加载时就不理它们了,直到运行时不能不用时火烧眉头再来绑定,此所谓延迟绑定,在Linux里通过PLT实现
所以类似地,重定位也可以分为
链接时重定位
加载时重定位
运行时重定位
如果编译.o文件时有-fPIC,那就会是PLT代码,即运行时重定位,如果只在链接时指定-shared,那就是加载时运行。
加载器在进行符号解析和重定位时,会把找到的符号及对应的模块和地址放入一个全局符号表(Global Symbol Table),但GST中是不能放多个同名的符号的,否则使用的人就要抓狂了,所以加载器在往全局符号表中加入item时会解决同名符号冲突的问题,即引入符号优先级,原则其实很简单:当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略。所以在Linux中,发生全局符号冲突真是太正常了,主执行程序中的符号可能会覆盖动态库中的符号(因为主程序肯定是第一个加载的),甚至动态库a中符号也会覆盖动态库b中的符号。这又被称为共享对象全局符号介入(Global Symbol Interpose)。
对于动态库和动态库中的符号冲突,又牵涉到另一个问题,即动态库的加载顺序问题.
加载器的原则是广度优先(BFS),即首先加载主程序,然后加载所有主程序显式依赖所有动态库,然后加载动态库依赖的动态库,直到全部加载完毕。 动态加载动态库(dlopen)的问题,原则也是一样 ,真正的逻辑同样发生在_dl_runtime_resolve中
有两种使用方法指定visibility属性
编译时指定,指定编译选项**-fvisibility=hidden**
,则该编译出的.o文件中的所有符号均为外部不可访问
代码中指定,即上面例子中的代码,void attribute((visibility (“hidden”))) foo(); 则foo()就只能在模块内部使用了。