关于链接中的同名符号问题

问题描述

今天遇到一个奇怪的问题:

我要把一个进程A中的全局变量通过UDP协议转发给另一个进程B,但是这些全局变量都分散定义在A的若干动态库中。

本来想的是,先在A的主程序中用extern将这些全局变量声明为外部符号,然后正常使用这些符号即可。

但神奇的是,主程序中全局变量的地址和动态库中全局变量的地址竟然是不同的。

排查了半天,发现是因为定义这个全局变量的源文件参与了两次编译,形成了两个动态库,而这两个动态库又都参与了进程A的链接。

问题分析

首先,同一份源文件参与两次编译不是问题,也不难理解,可以等效认为把两份相同内容的源文件各自独立地编译成了两个不相干的动态库。

所以问题的关键是,主程序链接的两个动态库包含了同名符号,为啥不报错?

更深入一点,从编译和链接的最终结果来讲,必然遵循ODR(One Definition Rule)原则,是不允许同名符号存在的。那么当参与链接的各个目标文件中存在同名符号时,链接器将如何处理呢?

基础知识回顾

程序源文件经过编译器加工之后,可以得到二进制的目标文件。按照CSAPP的标准,目标文件分三种:

  1. 可执行目标文件,可以直接被加载到内存中执行;
  2. 可重定位目标文件(.o文件),需要经过链接器进行符号解析和重定位之后,才能形成可执行目标文件;静态库文件(.a文件)可以被看作是若干.o文件的集合;
  3. 共享目标文件(.so文件),也就是常说的动态库文件。

对于链接器

  1. 其输入可以是.o文件,.so文件和.a文件;
  2. 其输出是可执行目标文件;
  3. 其中间所做的处理是符号解析和重定位;

链接器工作时的核心原则是,解析全部“有效输入文件”的符号引用并为其重定位。“有效输入文件”是指真正会被链接器处理的目标文件。

当喂给链接器一个静态库时,并不是这个静态库包含的所有.o文件都会被链接器处理,而是只有能够为符号解析做出贡献的才被处理,其余的.o文件直接丢弃。这意味着,如果如果两个参与链接的静态库包含了被主程序引用的同名符号,也是不会报错的,因为排在后面的静态库会被简单地丢弃。动态库同理,但是细节暂不作深入。

对于局部符号,定义和引用都发生在单个模块内部,所以很容易处理,而且由于编译器的约束,也不会存在同名符号问题;所以问题的关键在于跨模块的全局符号如何做引用解析,以及如何解决同名符号的问题。

鉴于链接器的有效输入其实只有.o文件和.so文件,所以只用分析这两种情况:

可重定位目标文件的链接

遵循CSAPP定义的三条规则:

  1. 存在多个强符号(初始化了的全局变量和有函数体的函数),链接报错;
  2. 存在一个强符号和多个弱符号(未初始化的全局变量),强符号有效;
  3. 只存在多个弱符号时,未定义的行为。

动态库文件的链接

不存在强弱符号的概念,只存在先来后到的区别。动态链接的符号解析和重定位由加载器完成。加载器工作时,会根据BFS原则找到主程序的依赖树,然后按照依赖关系将每个库的符号录入全局符号表(Global Sysmbol Table)。若在录入时发现GST中已经存在该符号,则将该符号丢弃,不做覆盖。

问题结论

参与链接的不同库文件中,如果定义了相同名称的全局变量或者函数,将造成链接器符号污染。C++推荐在不同的库中增加独立的命名空间来完美解决。另外,强弱符号规则的适用范围是静态链接,动态链接不适用。

参考

  1. https://zhuanlan.zhihu.com/p/418114185
  2. https://www.zhihu.com/question/535131534/answer/2508684340
  3. https://zhuanlan.zhihu.com/p/358763616
  4. https://blog.csdn.net/answer966480/article/details/107532008