如何解决何避免多个C/C++动态库函数同名冲突

时间:2022-11-01 16:13:53

前言

现在的开发节奏越来越快,有大量现成的库来方便我们的开发者来使用,避免重复造*,而且有很多有生命力的开源社区。当然在使用过程中,你可能为这样的场景而头痛:

你的项目中用到了A库和B库,其中A库使用的v1版本的C库,B库使用的是V2版本的C库,v1和v2版本的C库还相差的比较大,因此你在编译此项目的时候链接了A和B库后,发现有些调用执行不了,或者崩溃,或是个异常的结果,总之我这里归结为多个C,C++动态库函数同名冲突。

案例分析

假设有动态库libFuncA.so和libFuncB.so,他们的内部实现分别为:

/*func_A.c*/
#include<stdio.h>
//内部函数
int sayHi()
{
printf("Hi,this is AAAAA\n");
return 0;
}
//外部调用函数
int sayOut()
{
sayHi();
printf("Use this to introduce AAAAA\n");
return 0;
}
/*func_B.c*/
#include<stdio.h>
//内部函数
int sayHi()
{
printf("Hi,this is BBBBB\n");
return 0;
}
//外部调用函数
int sayOut()
{
sayHi();
printf("Use this to introduce BBBBB\n");
return 0;
}

分别编译成动态库:
gcc -fPIC -shared -o libFuncA.so func_A.c
gcc -fPIC -shared -o libFuncB.so func_B.c

调用函数test.c:

#include<stdio.h>
extern int sayOut();
int main()
{
sayOut();
return 0;
}

则调用的时候,使用函数sayOut,实现的功能与编译时链接库顺序有关。


dlopen显示调用

可使用dlopen函数族,显式指定要调用的动态库。
详细用法如:dlopen
该函数族需设定打开模式,返回一个动态库的句柄,调用句柄和函数进行操作,完成后需要关闭。
使用时,需引入头文件 dlfcn.h,定义函数指针, 编译时增加 -rdynamic 参数和链接 -ldl

更改调用函数test.c

#include<stdio.h>
#include<dlfcn.h>
typedef int (*func_pt)();
int main()
{
void *handle = NULL;
func_pt func = NULL;

if((handle = dlopen("./libFuncA.so", RTLD_LAZY)) == NULL)
{
printf("dlopen %s\n", dlerror());
return -1;
}
//定义函数指针,在动态库中查找符号
func = dlsym(handle, "sayOut");
func();
dlclose(handle);

printf("+++++++++++++++++++++++++++++++++++++\n");

if((handle = dlopen("./libFuncB.so", RTLD_LAZY)) == NULL)
{
printf("dlopen %s\n", dlerror());
return -1;
}
func = dlsym(handle, "sayOut");
func();
dlclose(handle);
}

编译执行:gcc -g -o exec tt1.cpp -ldl


可以发现,我们能显式执行指定动态库的外部函数sayOut了。

这个简单的例子可以说明我们通过dlopen显示加载动态库符号表来调用动态库的内容,从而避免了多个动态库的符号表冲突。


为何内部函数sayHi都调用了链接顺序第一个的实现?
原因在于动态库中的内部函数没有设置限制,使得sayHi函数也暴露给外部,调用时自然选择第一个函数实现。
用nm指令可以看出,两个函数都暴露出来了


再次搜索,可以用gcc编译器的特性来设置动态库函数的导出控制。

可在函数前增加__attribute__ 前缀来控制
更改动态库函数如下:


/* func_A.c*/
#include<stdio.h>

#define DLL_PUBLIC __attribute__((visibility("default")))
#define DLL_LOCAL __attribute__((visibility("hidden")))

DLL_LOCAL int sayHi()
{
printf("Hi,this is AAAAA\n");
return 0;
}

DLL_PUBLIC int sayOut()
{
sayHi();
printf("Use this to introduce AAAAA\n");
return 0;

}
/* func_B.c*/
#include<stdio.h>

#define DLL_PUBLIC __attribute__((visibility("default")))
#define DLL_LOCAL __attribute__((visibility("hidden")))

DLL_LOCAL int sayHi()
{
printf("Hi,this is BBBBB\n");
return 0;
}

DLL_PUBLIC int sayOut()
{
sayHi();
printf("Use this to introduce BBBBB\n");
return 0;

}


重新编译动态库,用nm指令可以查看sayHi不再导出了


重新编译测试程序,也没问题了



如果两个动态库中相似函数很多,一个个加 __attribute__前缀也是很大工作量。此时可以编译时设置默认函数不导出,只在需要导出的函数前面加前缀。以libFuncA.c为例:

/*funcA.c*/
#include<stdio.h>

#define DLL_PUBLIC __attribute__((visibility("default")))

int sayHi()
{
printf("Hi,this is BBBBB\n");
return 0;
}

DLL_PUBLIC int sayOut()
{
sayHi();
printf("Use this to introduce BBBBB\n");
return 0;

}

编译时,增加-fvisibility=hidden 参数,则未增加前缀的函数都不会导出

-fvisibility=hidden, 默认改为隐藏属性。它与static的区别在于,它的边界范围是动态库,而static是文件,但两者都能做如上所述的优化(消除 got,got.plt)。需注意,-fvisibility=hidden须在编译源码时传入,否则不会起作用。


so之间符号覆盖的解决方案

简单的说就是不允许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。