解决Linux多个动态库间的符号冲突问题
那linux下有没有类似window下的显式导出功能呢,有的。下面是介绍一种常用的只导出指定符号的方法。1、加编译器选项fvisibility=hidden,加了这个选项后,默认的符号都不会导出2、在需要导出的函数或者类名前加__attribute__ ((visibility("default")))一种是在编译期解决,就是在编译动态库的是加参数-Wl,-Bsymbolic 这个参数是传给链接器
c和c++开发人员或多或少都使用过Linux动态库,但是很多时候我们都不会去深入了解其中的一些细节和原理,直到自己的程序出现莫名其妙的问题后才会去着手解决,我也是在遇到一些动态库的问题后才去深入寻找解决办法。比如项目中加载了多个动态库,自己的动态库本来只想调用自己动态库中的函数,但是却调用到了其他动态库的同名函数上了,导致运行时的程序崩溃。还有项目中用到了多个版本的openssl,这些版本的openssl怎么共存的问题。
符号冲突暴露的时机可能是编译期,也可能是运行时。编译期的符号冲突一般不会产生太大的影响,如果是自己的项目代码, 只要找到冲突的符号,重命名其中某个符号就能解决,但是如果是其他外部库的代码,这个就不好改别人的代码,但是也可以将冲突的外部库进行封装来解决。如果是运行时的的符号冲突,一般就是动态库导致的。
在Linux下编译动态库的时候,所有的符号默认都是导出的,也就是动态库中的函数名,类名等,在外部都是可见的。我们在使用动态库没有出现问题之前,都不会去关注这些动态库中的符号是不是导出的。大多数时候,动态库中符号冲突也不会出现,因为出现不同动态库中相同的函数名或者类名的情况也是很少的。顺便提一下,windows下,dll动态库中的符号默认是不导出的,只有在需要导出的函数或者类名前加__declspec(dllexport)才会导出。
那linux下有没有类似window下的显式导出功能呢,有的。下面是介绍一种常用的只导出指定符号的方法。
1、加编译器选项fvisibility=hidden,加了这个选项后,默认的符号都不会导出
2、在需要导出的函数或者类名前加__attribute__ ((visibility("default")))
只导出几个必须的符号,就大大降低了多个动态库间的符号冲突问题。然而,这还不能完全解决问题,很多动态库在制作的时候都是默认的把所有符号导出,你没法保证自己的动态库不错误地引用到其他动态库中的符号。解决的办法也有两种:
一种是在编译期解决,就是在编译动态库的时候加参数-Wl,-Bsymbolic 这个参数是传给链接器的,这个编译参数的作用是:优先使用本动态库中的符号,而不是全局符号。这样即使其他动态库导出的符号和自己动态库中的符号同名,冲突也不会发生,运行自己动态库程序的时候会使用自己本动态库中的函数和类。
一种是在加载动态库的时候解决,如果你没法重新编译动态库,可以在加载动态库的时候自己使用dlopen函数加载动态库,然后在增加RTLD_DEEPBIND这个标志,这个标志的解释是这样的:
-
RTLD_DEEPBIND (since glibc 2.3.4)将符号的查找范围放在此共享对象的全局范围之前。这意味着自包含对象将优先使用自己的符号,而不是全局符号,这些符号包含在已加载的对象中。
解决动态库符号冲突的两个方法,一个是减少导出的符号,一个是优先使用本动态库中的符号,这样就能最大限度的减少动态库间的符号冲突。
从以上可以看出,解决符号冲突的一个利器是封装,把代码封装成动态库,只暴露几个必须的符号,对外部看来,表现得就像一个黑盒子。
关于多个openssl版本共存的解决方案
就举我自己遇到的例子,项目中需要用到sm2加密,因为项目很早就引入的openssl,因此原来项目中使用的openssl的版本很低,不支持sm2加密,而如果要升级openssl,因为兼容问题,则以前的很多代码需要修改,代价太大。因此只能找其他的实现方案,后来再找到gmssl这个开源库有sm2加密,但是这个库也依赖于openssl,而且是比较高的版本,这样就会出现项目中要用到两个不同的openssl版本的情况。经过艰苦的百度研究之后,找到解决办法,就是封装。编译gmssl这个库的时候把它编译成静态库,我们暂且认为生成的静态库为gmssl.a,这个静态库中的符号也是全部导出的,然后再在gmssl.a的基础上,进一步封装sm2加密和解密的方法,封装成gmutil.so动态库,完全屏蔽openssl的头文件,而且这个gmutil.so动态只导出sm2加密和sm2解密的函数符号,这样在gmutil.so的调用者看来,它内部引用的openssl对外部看来就是完全不可见的,而且gmutil.so编译的时候也指明-Wl,-Bsymbolic参数,这样就不会引用到外部的openssl版本的符号。
这里需要提到一个很有用的编译选项:-Wl,--exclude-libs,ALL 这个选项的作用是隐藏依赖的静态库符号。因为我编译gmssl.a静态库的时候,是用别人制作的makefile编译的,它里面的符号全部导出,即使在编译gmutil.so的时候,使用编译选项fvisibility=hidden,gmssl.a里面的符号也还是全部导出的。因此,加了-Wl,--exclude-libs,ALL后才隐藏了gmssl.a里面所有的符号。
这里要先提一下,查看符号是不是导出,可以使用readelf命令查看,"导出"指的就是GLOBAL的符号,"不导出"或者说"隐藏"指的就是LOCAL,如下:
下面是我参考的博文:
(18条消息) 多个库使用openssl库 导致编译不过 - CSDN
(20条消息) linux c解决多个第三方so动态库包含不同版本openssl造成的符号冲突_found的博客-CSDN博客_openssl 多版本共存
主要是参考上面这篇文章的解决思路来展开
Shared Library Symbol Conflicts (on Linux) (holtstrom.com)
避免踩坑系列!关于GmSSL与OpenSSL的兼容安装 - 简书 (jianshu.com) -- 这个推荐gmssl库静态链接openssl库
(20条消息) linux动态链接库导出函数控制_zz460833359的博客-CSDN博客_linux 动态库导出函数
上面这篇文章告诉我们在动态库中怎么只导出指定的符号(函数),其中一个办法是通过gcc命令的-fvisibility=hidden 选项和 "__attribute__ ((visibility("default")))" 语法扩展 可以得到 vc中的__declspec(dllexport)"的效果
(20条消息) linux下动态库的符号冲突、隐藏和强制优先使用库内符号_mandagod的博客-CSDN博客
上面这篇文章就写了怎么在编译期间就指定优先使用动态库自己的符号,即编译选项-Wl,-Bsymbolic
怎么避免G++导出SO的符号? - 知乎 (zhihu.com)
上面的文章给出了一个很重要的编译参数:-Wl,--exclude-libs,ALL 隐藏依赖的静态库符号
还有查看so库的导出符号,使用:
readelf -s so文件 | grep GLOBAL
关于这个readelf还有一个值得提醒的地方,就是在符号名称上,如果符号名称后面没有带@xxx,表示引用的就是本动态库里面的函数或者对象,而如果符号名称后面带@xxx,这个就是指引用其他动态库的函数或者对象,xxx指的就是对应的动态库名称,例如:
[root@dev-cft-ap02 runtime]# readelf -s libddd.so | grep RSA_private_decrypt
107: 0000000000000000 0 FUNC GLOBAL DEFAULT UND RSA_private_decrypt@libcrypto.so.10 (5)
1565: 0000000000000000 0 FUNC GLOBAL DEFAULT UND RSA_private_decrypt@@libc
上面的RSA_private_decrypt函数就是引用libcrypto.so.10动态库的符号
[root@dev-cft-ap02 libs]# readelf -s libsss.so | grep RSA_private_decrypt
3609: 000000000010df10 7 FUNC LOCAL DEFAULT 12 RSA_private_decrypt
这个就是直接引用静态链接到自己so里面的的RSA_private_decrypt
这个信息就是区分自己调用的函数,有没有正确找到自己函数定义位置的关键信息,通过这个能知道程序在运行时是不是找错符号用错库了
关于动态库导出符号为什么要加 extern "C"
因为如果用的是C++编译器来编译代码,C++会有重载,编译某个函数的时候会改变这个函数的名称,导致原本的函数名称找不到,外部在调用这个函数的时候会找不到符号。比如test()这个函数,C++编译后的函数名称可能变成testEF_,而不是原本的test符号。extern “C”就是使用C的编译方式,这样就不会发生函数重命名
关于动态库加-fPIC编译选项
是为了生成位置无关的代码,这样多个程序就有可能共享同一个动态库。如果不加这个选项,动态库被加载的时候都要进行地址重定向到自己的进程空间,这样导致每一个使用这个so的进程都会拷贝一份副本。而加了-fPIC这个选项,动态库加载的时候就不需要重定向地址,及位置无关代码,这样多个进程就可以共享同一个so。
关于导出函数带命名空间的问题
如果想导出带命名空间的函数,那么在定义的时候一定要在前面加上命名空间,否则编译器会生成不正确的符号
例如.h头文件:
namespace GM
{
extern "C" UNIX_EXPORT void Test();
}
则在cpp定义文件上必须这么写:
void GM::Test()
{
cout << "this is a test" << endl;
}
如果定义的时候没有加GM::,则生成的符号为:
[root@KF-CFT-AP7 gmutil]# readelf -s libgmutil.so | grep Test
5449: 000000000003b165 46 FUNC LOCAL DEFAULT 11 _Z4Testv
如果加了GM::,生成的符号为:
[root@KF-CFT-AP7 gmutil]# readelf -s libgmutil.so | grep Test
152: 000000000003b165 46 FUNC GLOBAL DEFAULT 11 Test
6914: 000000000003b165 46 FUNC GLOBAL DEFAULT 11 Test
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)