假如libgetthree.so libgetseven.so , 同时这两个so内部都用了internal_do_calculation()函数,并且各自定义了自己的internal_do_calculation()的实现,你会想当然的认为他们各自不影响,libgetthree和libgetseven会分别用自己的internal_do_calculation(),但事与愿违,你会发现都只会用其中一个so的符号。
他经历的过程如下:
- 当exe执行的时候,他会去寻找PublicGetThree符号,于是dynamic loader就会在libgetthree.so种进行reslove
- 当exe执行的时候,他会去寻找PublicGetSeven符号,于是dynamic loader就会在libgetseven.so种进行reslove
- 接下来开始寻找internal_do_calculation符号,他发现在libgethree.so中有,于是就全部用他的了
于是你会发现,libgetseven.so用的是其他库的这个符号,错误就产生了。取决于你怎么链接顺序,经过测试,发现跟第一个库的顺序有关
但是如果我们一旦隐藏比如three.cpp的符号
我们再重新链接,发现就正常了
可以总结出如果你对某个so库中的符号做了hidden,他实际上有两个作用:
- 对于自己来说,他告诉这个动态库对于这个符号不能从其他库中去读取,只能读取自己的这个internal符号(-Bsymbolic / -Bsymbolic-functions仅仅起到这个第一个作用)
- 对于其他库来说,这个库中的这个符号被隐藏了,自然只能用自己的符号了
通过上面这两个效果,你可以发现,无论你是hidden libgetthree.so还是libgetseven.so都是work的。
除了以上的好处之外,如果你控制得体,比如全局使用-fvisibility=hidden,对对应的public API使用__attribute__((visibility("default")))来暴露,这样有以下额外的好处:
- 你的库的load time会减少
- 你的整体的运行speed会提高,因为编译器知道他可以对devirtualization / inline functions做额外的优化
- 你的shared library的size大小会减小,因为编译器会从exported symbols table中丢失hidden symbols
坏处:
他会让你做单元测试更加困难,因为你已经把你的内部实现符号都给隐藏了,因此当你做unit testing的时候,你需要用default visibility来重新build.你可以借此来重新配置你的debug / release build flags:
Debug:
- -g - 加上debug info
- -O0 - 不提供任何优化(可以提供在开发阶段的debug使用体验)
Release:
- -fvisibility=hidden - 上面说的,可以提高效率
- -O2 - 优化大小和提高速度
有一个需要注意的地方就是关于异常C++ Exceptions,当binary code捕获住了一个exception的时候,他需要typeinfo的查找,但是typeinfo的symbols会随着你本身symbols的hidden而hidden.
通过linker的-Bsymbolic / -Bsymbolic-functions同样可以解决上面的问题(gcc是要加上-Wl,-Bsymbolic),比如你可以用如下的命令来进行编译
他会带来正确的结果:
但是如果那他跟hidden visibility做比较的话还是有诸多缺点:
- -Bsymbolic的方式仍然会export他们的符号,因此可以认为他们只解决了我们上面提到的两个作用的第一个作用,因此仍然有可能其他库会去使用他,如果我们交换了libseven.so以及libthree.so的位置,可能就会出现问题
- hidden visibility比-B的方式优化了对应的空间以及提高了运行速度
- 也不是说-Bsymbolic没有任何好处,他相对hidden visibility的手法让你写单元测试的时候只需要进行一次库的编译
除了以上提供的方法之外,你还能利用-fvisibilty=protected来达到效果,他跟hidden类似,可以保护你当前shared library的库的符号不会被其他库方便,但是他不保证你的库会去污染其他库的符号表,比如我们来看例子:
这个情况下是OK的,因为我们保护了Seven本身,因此他的内部符号只会用自己的
我们来看另外一个例子
这就出问题了,因为我们虽然保护了three本身,但是他的符号也确实污染到了libseven.so,因此输出了3
你也可以通过匿名空间来达到效果
注意不要把你的public API也给包了,仅仅包你的实现,然后你通过nm查看发现他们默认变成小t了
但是如果你不用匿名空间,而是带名字的空间,他是大T的会做污染
因此可以看到匿名空间的一个作用就是自动帮你把符号隐藏. 同样,如果你的函数定义成static静态的,你的函数符号默认也是hidden的,也能起到同样的效果
Dynamic Linking解决相关问题:
试想一下如果你的library A和B都对C有依赖,其中A用的是新的C,B用的是老的C。这里面可能存在数据结构的不一致,就会出现问题,但是正是因为有了dynamic linking,如果A和B对于major version的C是兼容的,那么dynamic linking会帮你解决这个问题,因为他们所有遇到过的C的符号通过上面的解释,都会保持一致,因为有override的行为在里面。 但是并不是说dynamic linking就是万能解药,静态编译static linking的一个原因就是拥有尽可能小的分发依赖。 另外一个原因就是你要测试所有版本的依赖非常困难,通过静态编译到一个特定的版本允许让你拥有这个依赖的一致行为