gcc解决Linux多个动态库间的符号冲突问题

(145) 2024-04-29 11:01:01

系列文章目录


文章目录

  • 系列文章目录
  • 前言
  • 一、符号冲突解决办法?
  • 二、代码实现验证
    • 1.如何减少符号的导出
    • 2.优先使用本动态库中的符号
    • 在这里插入图片描述
  • 三、补充知识点
  • 总结

前言

c和c++开发人员或多或少都使用过Linux动态库,符号冲突暴露的时机可能是编译期,也可能是运行时。编译期的符号冲突一般不会产生太大的影响,如果是自己的项目代码, 只要找到冲突的符号,重命名其中某个符号就能解决,但是如果是其他外部库的代码,这个就不好改别人的代码,但是也可以将冲突的外部库进行封装来解决。如果是运行时的的符号冲突,一般就是动态库导致的。
如下面一个例子,我们期望的结果与我们实际的结果却出现了不一样的现象。即符号冲突导致,本文将介绍如何解决符号冲突的问题。
gcc解决Linux多个动态库间的符号冲突问题 (https://mushiming.com/)  第1张


一、符号冲突解决办法?

在Linux下编译动态库的时候,所有的符号默认都是导出的,也就是动态库中的函数名,类名等,在外部都是可见的。我们在使用动态库没有出现问题之前,都不会去关注这些动态库中的符号是不是导出的。大多数时候,动态库中符号冲突也不会出现,因为出现不同动态库中相同的函数名或者类名的情况也是很少的。

解决动态库符号冲突的两个方法
1、一个是减少导出的符号
2、一个是优先使用本动态库中的符号

a)如何减少符号的导出
windows下,dll动态库中的符号默认是不导出的,只有在需要导出的函数或者类名前加__declspec(dllexport)才会导出。

linux下支持只导出指定符号的方法。
1、加编译器选项fvisibility=hidden,加了这个选项后,默认的符号都不会导出
2、在需要导出的函数或者类名前加__attribute__ ((visibility(“default”)))

b)如何优先使用本地动态库
其中导出必须的符号,虽然大大降低了多个动态库间的符号冲突问题。然而,这还不能解决所有的问题,很多动态库在制作的时候都是默认的把所有符号导出,我们没法保证自己的动态库不错误地引用到其他动态库中的符号。
解决办法有两个:
1、一种是在编译期解决,就是在编译动态库的是加参数-Wl,-Bsymbolic 这个参数是传给链接器的,这个编译参数的作用是:优先使用本动态库中的符号,而不是全局符号。这样即使其他动态库导出的符号和自己动态库中的符号同名,冲突也不会发生,运行自己动态库程序的时候会使用自己本动态库中的函数和类。
2、一种是在加载动态的是时候解决,如果你没法重新编译动态,可以在加载动态库的时候自己使用dlopen函数加载动态库,然后在增加RTLD_DEEPBIND这个标志(待验证)

RTLD_DEEPBIND (since glibc2.3.4)将符号的查找范围放在此共享对象的全局范围之前。这意味着自包含对象将优先使用自己的符号,而不是全局符号,这些符号包含在已加载的对象中
You should use RTLD_DEEPBIND when you want to ensure that symbols looked up in the loaded library start within the library, and its dependencies before looking up the symbol in the global namespace.
This allows you to have the same named symbol being used in the library as might be available in the global namespace because of another library carrying the same definition; which may be wrong, or cause problems.

二、代码实现验证

1.如何减少符号的导出

main.cpp

#include <cstdio>
#include <iostream>
int funcA(int, int);
int funcB(int, int);

int main() { 
   
	std::cout<< funcA(2, 1) <<std::endl;
	std::cout<< funcB(2, 1) <<std::endl;
    return 0;
}

libA.cpp

#include <iostream>

int subfunc(int a, int b) { 
   
    return a + b;
}

int funcA(int a, int b) { 
   
    return subfunc(a, b);
}

libB.cpp

#include <iostream>

int subfunc(int a, int b) { 
   
    return a - b;
}

int funcB(int a, int b) { 
   
    return subfunc(a, b);
}

g++ -fPIC libA.cpp -shared -o libA.so
g++ -fPIC libB.cpp -shared -o libB.so
g++ main.cpp libA.so libB.so -o main
export LD_LIBRARY_PATH=.
./main

发现顺序的不同导致了程序输出内容不同,究其原因就是那潜在的符号冲突。
我们期望的结果是3,1(funcA和funcB各自调用不同的subfunc实现),
实际得到的结果是3,3(funcA和funcB都调用了libA中的subfunc实现)
我们通过readelf来查看符号,结果如下图
可见libA和libB里面都有subfunc符号,名字完全一样,而且都是GLOBAL的
GLOBAL的符号即全局的符号,同名的全局符号会被认为是同一个符号,由于main先加载了libA,得到了libA中的subfunc符号,再加载libB时,就把libB中的subfunc忽略了。

gcc解决Linux多个动态库间的符号冲突问题 (https://mushiming.com/)  第2张
gcc解决Linux多个动态库间的符号冲突问题 (https://mushiming.com/)  第3张

解决方案:
1.将部分函数隐藏

__attribute__ ((visibility ("hidden"))) int subfunc(int a, int b) { 
   
    return a + b;
}
int funcA(int a, int b) { 
   
    return subfunc(a, b);
}

重新编译,readelf执行结果如下,subfunc已经被隐藏了
gcc解决Linux多个动态库间的符号冲突问题 (https://mushiming.com/)  第4张

2.将部分函数导出
编译参数需要加上-fvisibility=hidden:
g++ -fPIC libA.cpp -shared -fvisibility=hidden -o libA.so

int subfunc(int a, int b) { 
   
    return a + b;
}
__attribute__ ((visibility ("default"))) int funcA(int a, int b) { 
   
    return subfunc(a, b);
}

重新编译,readelf执行结果如下,subfunc已经被隐藏了gcc解决Linux多个动态库间的符号冲突问题 (https://mushiming.com/)  第5张

2.优先使用本动态库中的符号

libE.h与libE.cpp

#pragma once
extern "C" { 
   
    int funcA(int a, int b);
}

#include <iostream>
#include "libE.h"
int subfunc(int a, int b) { 
   
    return a + b;
}
int funcA(int a, int b) { 
   
    return subfunc(a, b);
}

libF.h与libF.cpp

#pragma once
extern "C" { 
   
    int funcA(int a, int b);
}

#include <iostream>
#include "libF.h"
int subfunc(int a, int b) { 
   
    return a - b;
}
int funcA(int a, int b) { 
   
    return subfunc(a, b);
}
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <iostream>
#include "libE.h"
#include "libF.h"
#define SO_PATH_A "./libE.so"
#define SO_PATH_B "./libF.so"
typedef int (*func)(int, int);
int main()
{ 
   
	{ 
   
		void *dlhandler_a;
		func func_a = NULL;
		dlhandler_a = dlopen(SO_PATH_A, RTLD_LAZY);
		if (NULL == dlhandler_a){ 
   
			fprintf(stderr, "%s\n", dlerror());
			exit(-1);
		}
		dlerror();
		func_a = (func)dlsym(dlhandler_a, "funcA");
		if (func_a)
			printf("%d\n", func_a(1, 2));
		else	
			std::cout<< "func a null" <<std::endl;
		dlclose(dlhandler_a);
	}
	{ 
   
		void *dlhandler_b;
		func func_b = NULL;
		dlhandler_b = dlopen(SO_PATH_B, RTLD_LAZY);
		if (NULL == dlhandler_b){ 
   
			fprintf(stderr, "%s\n", dlerror());
			exit(-1);
		}
		dlerror();
		func_b = (func)dlsym(dlhandler_b, "funcA");
		if (func_b)
			printf("%d\n", func_b(1, 2));
		else	
			std::cout<< "func b null" <<std::endl;
		dlclose(dlhandler_b);
	}
    return 0;
}

g++ -fPIC libE.cpp -I./ -shared -o libE.so
g++ -fPIC libF.cpp -I./ -shared -o libF.so
g++ main2.cpp -I./ libE.so libF.so -ldl -o main2
根据加载动态库的顺序,不同函数库相同的函数符号,先加载的符号地址可用,后加载的符号地址被覆盖。
gcc解决Linux多个动态库间的符号冲突问题 (https://mushiming.com/)  第6张
解决方法
在创建动态链接库时,gcc/g++选项中添加编译选项-Wl,-Bsymbolic.
其中Wl表示将紧跟其后的参数,传递给连接器ld。
g++ -fPIC libE.cpp -I./ -Wl,-Bsymbolic -shared -o libE.so
g++ -fPIC libF.cpp -I./ -Wl,-Bsymbolic -shared -o libF.so
g++ main2.cpp -I./ libE.so libF.so -ldl -o main2
Bsymbolic表示强制采用本地的全局变量定义,这样就不会出现动态链接库的全局变量定义被应用程序/动态链接库中的同名定义给覆盖了

gcc解决Linux多个动态库间的符号冲突问题 (https://mushiming.com/)  第7张

三、补充知识点

a)gcc库连接的顺序问题
GCC在链接过程中,对参数中的库的顺序是有要求的,参数右侧的库会先于左侧的库加载,也就是说参数的解析是从右往左的。

>  假设库B依赖与库A,则链接的时候要写为:gcc -o bin -lB -lA  
>  如果写为: gcc -o bin -lA -lB 则在B中引用的A中的内容就会无法链接通过。

一句话,越是被别人调用的越底层的库,就越放在后面;越是调用别人的越上层的库,就越放在前面。

b)运行指定动态库的四种方法及查找动态库优先级顺序
编译时指定的 -L的目录,只是在程序链接成可执行文件时使用的。程序执行时动态链接库加载不到动态链接库。运行时动态库查找方式与优先级顺序按如下:
1.程序链接时指定链接库的位置,使用-wl,-rpath=<link_path>参数,<link_path>就是链接库的路径。
如:

g++ -o test -L. -llib -Wl,rpath=./ test.cpp

2.将链接库的目录添加到/etc/ld.so.conf文件中或者添加到/etc/ld.so.conf.d/*.conf中,然后使用ldconfig进行更新,进行动态链接库的运行时动态绑定。
如:

vim /etc/ld.so.conf.d/foo.conf

/usr/local/lib
ldconfig
  1. 在环境变量LD_LIBRARY_PATH 中指明库的搜索路径。
    如:
export LD_LIBRARY_PATH=/opt/gtk/lib:$LD_LIBRARY_PATH
  1. 查找默认路径/lib和/usr/lib
    把动态库拷贝到/usr/lib目录下或者把动态库拷贝到/lib目录下。

c)linux 静态库与动态库链接的优先级
GCC链接的时候,一般通过-l来链接动态库和静态库,但是优先动态库的使用即*.so。
如何优先链接静态库?
1.强制链接某一个静态库的时候,可以直接使用静态库的名字,包括后缀名和前缀,如libcurl.a

gcc -o test -I./include test.cpp libcurl.a

2.通过-static,这是静态链接,要求链接所有的库都有对应的静态库, -shared此选项将尽量使用动态库,为默认选项

gcc -static -o test -I./include test.cpp -L./lib -lcurl

3.当库的静态和动态版本都可用时,使用下面2个选项进行切换。
–Bdynamic : 用于在各种可能的情况下为共享动态绑定设置首选项。-Bstatic : 将绑定只限制于静态库。必须跟-Wl搭配使用。
如下例子: -Wl,-Bstatic指示跟在后面的-lxxx选项链接的都是静态库,-Wl,-Bdynamic指示跟在后面的-lxxx选项链接的都是动态库。

g++ -L. -o main main.cpp -Wl,-Bstatic -ltesta -Wl,-Bdynamic -ltestso

总结

通过本文的例子,应该对符号冲突的解决办法有了一定的理解。

参考:https://blog.csdn.net/qq_38350702/article/details/106128157
参考:https://blog.csdn.net/found/article/details/105263450

THE END

发表回复