前言
现在的开发节奏越来越快,有大量现成的库来方便我们的开发者来使用,避免重复造轮子,而且有很多有生命力的开源社区。当然在使用过程中,你可能为这样的场景而头痛:
你的项目中用到了A库和B库,其中A库使用的v1版本的C库,B库使用的是V2版本的C库,v1和v2版本的C库还相差的比较大,因此你在编译此项目的时候链接了A和B库后,发现有些调用执行不了,或者崩溃,或是个异常的结果,总之我这里归结为多个C,C++动态库函数同名冲突。
案例分析
假设有动态库libFuncA.so和libFuncB.so,他们的内部实现分别为:
分别编译成动态库:
gcc -fPIC -shared -o libFuncA.so func_A.c
gcc -fPIC -shared -o libFuncB.so func_B.c
调用函数test.c:
则调用的时候,使用函数sayOut,实现的功能与编译时链接库顺序有关。
dlopen显示调用
可使用dlopen函数族,显式指定要调用的动态库。
详细用法如:dlopen
该函数族需设定打开模式,返回一个动态库的句柄,调用句柄和函数进行操作,完成后需要关闭。
使用时,需引入头文件 dlfcn.h,定义函数指针, 编译时增加 -rdynamic 参数和链接 -ldl
更改调用函数test.c
编译执行:gcc -g -o exec tt1.cpp -ldl
可以发现,我们能显式执行指定动态库的外部函数sayOut了。
这个简单的例子可以说明我们通过dlopen显示加载动态库符号表来调用动态库的内容,从而避免了多个动态库的符号表冲突。
为何内部函数sayHi都调用了链接顺序第一个的实现?
原因在于动态库中的内部函数没有设置限制,使得sayHi函数也暴露给外部,调用时自然选择第一个函数实现。
用nm指令可以看出,两个函数都暴露出来了
再次搜索,可以用gcc编译器的特性来设置动态库函数的导出控制。
可在函数前增加__attribute__ 前缀来控制
更改动态库函数如下:
重新编译动态库,用nm指令可以查看sayHi不再导出了
重新编译测试程序,也没问题了
如果两个动态库中相似函数很多,一个个加 __attribute__前缀也是很大工作量。此时可以编译时设置默认函数不导出,只在需要导出的函数前面加前缀。以libFuncA.c为例:
编译时,增加-fvisibility=hidden 参数,则未增加前缀的函数都不会导出
-fvisibility=hidden, 默认改为隐藏属性。它与static的区别在于,它的边界范围是动态库,而static是文件,但两者都能做如上所述的优化(消除 got,got.plt)。需注意,-fvisibility=hidden须在编译源码时传入,否则不会起作用。
简单的说就是不允许so之间出现符号覆盖,如果有符号覆盖基本可以肯定是出问题了。
那么万一用到的两个不同功能的so,比如是两个不同的开源项目的代码,由于是各自开发,出现了函数或变量名字相同的情况,应该怎么办呢?
答案简单粗暴,也最可靠,那就是改名。
话说回来,没考虑到符号冲突的so,质量要打个问号,能不用还是不要用。。。
如果是我们自己开发的so库,要注意
(1) 函数/变量/类加名字空间,如果是c函数就需要加前缀
(2) 不导出不需要的函数/变量/类
相同so版本兼容问题
新旧版本的兼容问题
动态库可能有新旧多个版本,并且新旧版本也可能不兼容。
可能有多个app依赖于这些不同版本的so库。
因此当一个so库被覆盖的时候,就可能出问题。
(1) 旧so覆盖新so,可能导致找不到新函数,
(2) 新so覆盖旧so,可能导致找不到旧的函数,
(3) 而更加隐蔽的问题是:新旧so里的同一个函数,语义已经不一样,即前置条件和效果不一样。
新旧版本的兼容关系
(1) 新版本完全兼容旧版本,只是新增了函数。
这种情况只需要新版本即可。
(2) 新版本删除了一些旧版函数,并且保持签名相同的语义相同(可能新增了函数)。
这种情况需要新旧版本同时存在。
(3) 新旧两个版本有一些相同签名但是语义不一样的函数。
这种情况是不予许的。
因为可能出现一个app必须同时依赖新旧两个版本,由于同一签名函数只能有一个实现,也就说另一个实现会被覆盖,就会出错。
新旧版本兼容的解决方法
由此我们知道,有两个解决方案:
(1) 新版本完全兼容旧版本,并保证新版本覆盖旧版本或者新旧版本共存。
这种方法太理想化。
实际情况下,新版本完全兼容旧版本比较难以做到,这要求函数一旦发布就不能改不能删,并且永远必须兼容。
(2) 新版本可以删除一些旧版函数,需保持签名相同的函数语义相同,并保证新旧版本共存。
这是可行的解决方法。
Linux的版本兼容解决方法
首先加版本号保证新旧版本可以共存,不会互相覆盖。版本号形如openssl.so.1.0.0。
其次新版本需保持和旧版本签名相同的函数语义相同。
这样已经可以解决问题了,但是还可以优化。
因为版本号分的太细,导致有很多的版本同时存在,其实不需要这么多版本。
仔细考虑一下:
(1) 如果新版本和旧版本的函数完全相同,只是fix bug:那么新版本需要替换掉旧版本,旧版本不需要保留。
(2) 如果新版本新增了函数:那么新版本可以替换掉旧版本,旧版本不需要保留。
(3) 如果新版本删除了函数:那么旧版本就需要保留。
如果linux系统下有新旧两个so,它怎么知道可不可以需不需要替换掉旧版本?
答案是通过版本号:
linux规定对于大版本号相同的一系列so,可以选出里面最新的so,用它替换掉其它的so。
这里所谓的替换,其实是建立了一个软链接,型如openssl.so.1,把它指向openssl.so.1.x.x.x系列so里面最新的那一个so。