linux下不同库出现符号冲突的解决方式
在linux下开发时,可能会碰到不同库导出同名函数符号,导致行为异常的问题。
举个例子:我的test.cpp需要链接liba.so以及libb.so,并且两个so库中都有一个同名的函数符号 PrintInt,那在 test.cpp 中我想调用liba.so的PrintString时实际会调用函数符号呢?答案是根据链接顺序来。先链接先加载哪个库就会使用哪个库的符号。假设我们先链接加载的libb.so,那test实际会调用的就是libb.so中的PrintInt,这很明显不符合我们的期望,并且很可能会更一步导致crash。
下面我就来介绍几种方式,用于解决类似问题。
假设libb.so的代码可控,那我们可以采用以下的几种方式:
1.最简单的方式,直接修改PrintInt函数名
void PrintIntB(int i);
2.添加命名空间,从而改变实际导出的符号名
namespace B
{
void PrintInt(int i);
};
3.将函数定义为 static,使其对外不可见
static void PrintInt(int i);
4.设置函数属性 visibility=hidden,使其对外不可见(不导出)
__attribute__((visibility ("hidden"))) void PrintInt(int i);
顺便提一下,如果假设只需要在so中导出部分函数,我们可以在编译时添加 -fvisibility=hidden 选项,让所有符号默认都不导出,而对需要导出的函数设置属性 visibility=default,这样做的好处是:
1)增加了代码和库的安全性,外部没法直接调用未导出的符号;
2)可能会减小库的体积,对于不需要导出也没有内部引用的符号代码,链接器可能会进行优化不链入到目标文件中
5.使用g++的 --version-script 选项指定如何导出
编译命令行如下,export.lst里面编写导出内容:
g++ -O2 -g test.cpp -o test -Wl,--version-script=/xxx/xxx/export.lst
1)只导出指定的函数
global表示需要导出的函数名,local表示不需要导出的函数名,使用通配符表示剩余函数都不需要导出
{
global:
MyFun1;
MyFun2;
local:
*;
};
2)对导出的函数增加版本信息
以PrintInt函数举例,增加版本号v1后,最终导出的函数符号名会加上版本号 PrintInt@@v1,_Z8PrintInti@@v1,我在示例中写两种导出符号示例的目标是为了演示c和c++导出符号的差异,具体可以看文章末尾的注。通过这种加版本号的方式导出,也能避免符号冲突。
v1 {
global:
PrintInt;
_Z8PrintInti;
local:
*;
};
当无法改动libb.so的代码时,可以采用以下方式:
使用 dlmopen 函数动态加载 libb.so。它和 dlopen 函数的不同点在于,它可以使用单独的命名空间加载库中的符号,从而避免符号重名冲突的问题。
具体调用方法如下:
// 使用新的命名空间延迟加载libb.so
void* handle = dlmopen(LM_ID_NEWLM, "/xxx/xxx/libb.so", RTLD_LAZY);
if (!handle) {
...
}
// 定义PrintInt函数指针
typedef void (* PRINTINT)(int);
// 加载函数符号(c++导出符号)
PRINTINT PrintInt = (PRINTINT)dlsym(handle, "_Z8PrintInti");
if (!PrintInt) {
...
}
// 调用
PrintInt(10);
// 关闭
dlclose(handle);
顺带说一下,有的同学可能会问,如果我的库导出的函数是一个类的成员函数该如何动态加载符号呢?
好吧,我们也来通过一个例子说明一下。
// libb.so的源文件
// 接口定义
class MyInterFace
{
public:
// 返回MyClass对象指针,具体代码就省略了
static MyInterFace* Create();
public:
virtual void PrintInt(int i) = 0;
protected:
virtual ~MyInterFace() {}
};
// 实现
class MyClass : public MyInterFace
{
public:
...
public:
void PrintInt(int i)
{
int n = d_ + i;
std::cout << "n = " << n << std::endl;
}
private:
int d_ = 10;
};
// test.cpp代码
// 使用新的命名空间延迟加载libb.so
void* handle = dlmopen(LM_ID_NEWLM, "/xxx/xxx/libb.so", RTLD_LAZY);
if (!handle) {
...
}
// 定义MyInterFace::Create函数指针
typedef MyInterFace* (* CREATE)();
// 加载函数符号
CREATE Create = (CREATE)dlsym(handle, "_ZN11MyInterFace6CreateEv");
if (!Create) {
...
}
// 调用
MyInterFace* interface = Create();
...
// 定义MyInterFace::PrintInt函数指针,注意第一个参数为this指针
typedef void (* PRINTINT)(MyInterFace*, int);
// 加载函数符号
PRINTINT PrintInt = (PRINTINT)dlsym(handle, "_ZN11MyInterFace8PrintIntEi");
if (!PrintInt) {
...
}
// 调用,这边需要传对象指针作为this
PrintInt(interface, 100);
// 关闭
dlclose(handle);
在上面的例子里面需要传interface指针作为调用MyInterFace::PrintInt函数时的this指针,所以在定义函数指针的时候,需要定义两个形参。这种方式在x64平台下是没有问题的,原因在于x64前6个参数是依次通过 %rdi、%rsi、%rdx、%rcx、%r8、%r9寄存器传参的,而调用非静态类成员函数时的this指针也是通过 %rdi 寄存器传递的,也就是说和调用普通函数时第一个参数的传参方式是一致,因此就可以像示例中那样定义函数指针以及进行后续的传参调用。
需要注意的是,在其它cpu平台这种方式可能是有问题的,原因在于上面说的传参方式上。比如x86下,调用约定为thiscall,this指针一般是通过 %ecx 寄存器传递的(不同的编译器可能会有所差异,比如会通过压栈的方式传递)。因此像示例中那样的定义方式,可能无法正确传递this指针,也就不能正确调用。
好了,文章就先写到这里,欢迎小伙伴们共同讨论。
未经许可,请勿转载!
注:强调函数符号而不是函数,原因在于C/C++写的同名函数可能会因为编译器的不同或者导出方式的不同导致实际的符号名不同,比如gcc编译 .c 文件或者使用 extern "c" 修饰,则导出符号名为PrintInt,而g++编译 .cpp 文件默认导出符号名为 _Z8PrintInti