一、问题起源
项目开发中需要用到了客户提供的第三方动态库,将链接第三方库的操作添加到makefile中之后,发现无论如何调用第三方库中的接口都会返回错误。
后来通过查看第三方库的符号信息,发现第三方库中的符号与OPENSSL库的符号冲突,而makefile中链接OPENSSL的库在前第三方的库在后,所以在程序运行时自然的链接到了OPENSSL库中的符号并调用了OPENSSL库中的接口,最终导致返回失败。
二、初步解决
既然是库链接顺序的问题,那么调整库的链接顺序应该可以解决该问题。实际上也确实如此,调整链接顺序后第三方库中的所有接口都可以正确返回。本以为问题可以这样简单又完美的得到解决,殊不知更大的坑还在后面。
三、新的问题
涉及第三方库的接口调试完成后开始进行整个系统的测试,发现每当系统进行某项业务时都会发生内存泄漏。而在调用第三方库之前是明显没有这样的问题的,取消第三方库的链接和相关业务后果然就不存在内存泄漏的问题了。所以问题终究还是调用的库有关系,因为库的符号冲突,哪个库链接在前就采用哪个库的接口,而两项业务又不能使用同样的接口,要想解决该问题就必须解决库冲突的问题。
四、问题研究
为了更好的研究问题,制作两个简单的库libA.so和libB.so,其实现如下:
/* func_A.c */
#include<stdio.h>
//内部函数,被out_A()函数调用
int out()
{
printf("a\n");
return 0;
}
//用于外部调用
int out_A()
{
out();
printf("A\n");
return 0;
}
/* func_B.c */
#include<stdio.h>
//内部函数,被out_B()函数调用
int out()
{
printf("b\n");
return 0;
}
//用于外部调用
int out_B()
{
out();
printf("B\n");
return 0;
}
其中的外部调用函数分别为out_A()和out_B();调用目标:调用out_A()输出a和A;调用out_B()输出b和B。
动态库测试
编译动态库
gcc -fPIC -shared -o libA.so func_A.c
gcc -fPIC -shared -o libB.so func_B.c
然后对单个动态库分别进行测试,发现均可以得到预期结果。 当两个库同时调用时,测试代码如下:
#include<stdio.h>
extern int out_A();
extern int out_B();
int main()
{
out_A();
out_B();
return 0;
}
编译并链接两个库:
gcc -o exeAB test.c -L. -lA -lB
执行后发现调用out_B()输出a和B。
改变库的链接顺序:
gcc -o exeAB test.c -L. -lB -lA
执行后发现调用out_A()输出b和A。
使用nm或者readelf指令查看库的符号信息:
nm libA.so | grep out
可以看到out函数为全局符号(default),即其对所有调用均可见。
五、解决方案
1、使用动态加载库方式处理:
既然是库冲突,那么在调用到的时候再加载库(采用动态加载库的方式)应该可以解决冲突的问题。这里需要使用dlopen(动态加载库)函数,使用时引入头文件 dlfcn.h,编译时增加库链接 -ldl。
这里libA.so采用动态加载的方式,libB.so依然采用普通调用的方式:
#include<stdio.h>
#include<dlfcn.h>
extern int out_A();
extern int out_B();
typedef int (*func_pt)();
int main()
{
void *handle = NULL;
func_pt func = NULL;
if((handle = dlopen("./libA.so", RTLD_LAZY)) == NULL)
{
printf("dlopen %s\n", dlerror());
return -1;
}
func = dlsym(handle, "out_A");
func();
dlclose(handle);
printf("~~\n");
out_B();
}
可以看到,动态加载依然没有输出预期结果。
其实,可以在打开动态加载库的时候加上RTLD_DEEPBIND选项;RTLD_DEEPBIND选项可以设定dlopen载入的库首先从自己和它的依赖库中查找符号,然后再去全局符号中去查找。
#include<stdio.h>
#include<dlfcn.h>
extern int out_A();
extern int out_B();
typedef int (*func_pt)();
int main()
{
void *handle = NULL;
func_pt func = NULL;
if((handle = dlopen("./libA.so", RTLD_LAZY | RTLD_DEEPBIND)) == NULL)
{
printf("dlopen %s\n", dlerror());
return -1;
}
func = dlsym(handle, "out_A");
func();
dlclose(handle);
printf("~~\n");
out_B();
}
可以看到,加上RTLD_DEEPBIND选项后输出了预期结果。
虽然动态加载库的方法可以解决库冲突的问题,但是,这种方法写起来太折腾,如果一个库中含多个外部接口,写起来简直是噩梦。
2、使用编译选项控制符号导出:
既然是符号冲突导致的异常,那么可以把库符号信息进行修改,把库内部调用的符号限定为只能内部调用,外部调用的符号限定为可公共调用。
要想达到上面的目的,需要在函数前增加__attribute__ 前缀来控制。
修改libA.so和libB.so的实现,并重新编译生成libA1.so和libB1.so。
/*func_A1.c*/
#include<stdio.h>
#define DLL_PUBLIC __attribute__((visibility("default")))
#define DLL_LOCAL __attribute__((visibility("hidden")))
//内部函数,被out_A()函数调用
DLL_LOCAL int out()
{
printf("a\n");
return 0;
}
//用于外部调用
DLL_PUBLIC int out_A()
{
out();
printf("A\n");
return 0;
}
/*func_B1.c*/
#include<stdio.h>
#define DLL_PUBLIC __attribute__((visibility("default")))
#define DLL_LOCAL __attribute__((visibility("hidden")))
//内部函数,被out_B()函数调用
DLL_LOCAL int out()
{
printf("b\n");
return 0;
}
//用于外部调用
DLL_PUBLIC int out_B()
{
out();
printf("B\n");
return 0;
}
gcc -fPIC -shared -o libA1.so func_A1.c
gcc -fPIC -shared -o libB1.so func_B1.c
此时再用nm指令查看符号信息:
nm libA1.so | grep out
看到out符号标志变为t,表示该符号只能库内部自己使用。使用新的库再次测试两个库同时调用的情况:
#include<stdio.h>
extern int out_A();
extern int out_B();
int main()
{
out_A();
out_B();
return 0;
}
编译:
gcc -o exeAB test.c -L. -lA -lB
运行:./exeAB
得到了预期结果:
上面的方法虽然可以解决问题,但是如果库中符号(函数)很多,一个个修改显然不现实。其实可以编译时设置默认函数不导出,只在需要导出的函数前面加前缀。以FuncA.c为例:
/*func_A2.c*/
#include<stdio.h>
#define DLL_PUBLIC __attribute__((visibility("default")))
//内部函数,被out_A()函数调用
int out()
{
printf("a\n");
return 0;
}
//用于外部调用
DLL_PUBLIC int out_A()
{
out();
printf("A\n");
return 0;
}
编译时,增加-fvisibility=hidden 参数,这样未增加前缀的函数都不会导出
gcc -fPIC -shared -fvisibility=hidden -o libA2.so func_A2.c
用nm指令查看符号信息:
nm libA2.so | grep out