如何解决C程序中不同静态库之间的符号冲突问题

如何解决C程序中不同静态库之间的符号冲突问题

之前在将helix player移植到ios平台时遇到过这个问题,现在整理一下,给自己做个总结,也希望能对别人有所帮助。

问题的描述:

如果helix在ffmpeg之前是一个小有名气的开源的播放引擎,由Realnetworks维护,像nokia的塞班系统上都用的是这个播放引擎,而且现在国内的一些手机上还有它的影子。helix将各个功能模块以动态库的形式进行组织管理。当播放一个视频时,比如rmvb,它会解析fileformat,然后知道要加载哪个模块来解析这个文件; decode时,也是根据codec的fourcc来判断加载哪个decoder库。

这种组织方式确实有一些好处,比如各个模块相对独立,每个模块只有在用到时才会被加载,用完后一段时间内不再使用的话会自动卸载。但是由于一些基础的静态库会被每个动态库都包含,所以整个程序所占的内存减小了,但占的磁盘增加了。

Helix在android平台上工作得很好,但是当我们想要把它移植到iOS平台时,却遇到了一个大麻烦,因为iOS不允许APP中包含动态库,否则app审查通不过。没办法,只得将这些程序编译成静态库。 这样基础库只有一份了,能够节省一些磁盘空间。

在这个过程中我们遇到的一个问题是: 有几个格式相同但版本不同的编解码器(codec),它们是用c语言写的,它们的代码结构和文件名相同,而且很多函数名都一样。由于c语言的函数编译之后没有mangling,导致最终程序中会有多个同名的函数,这在链接的时候能通过(gcc会选择所遇到第一个同名函数,所以这些同名函数就不能保证哪个会先被链接进可执行程序),但是在执行的时候就会出现各种奇怪的现象,严重的甚至crash。

这里需要理解静态库方式和动态库方式的区别,动态库方式时不会有问题(这里指的是dlopen这种显式的方式,如果是隐式的方式,即将动态库和可执行程序一起链接的话,也会存在和静态库一样的问题),因为动态库本身就是一个独立的实体,它有自己的代码段和数据段,它里面的函数都是相对于它加载时的起始地址来定位的,加载地址在加载前是不确定的,但加载后就是确定的了。从某种意义上说,动态库有点类似于可执行程序。而静态库方式在链接时必须先找到所依赖的函数,然后确定它的偏移地址,这个地址相对于可执行程序的起始地址是固定的,这个地址是链接时确定的。


解决方法:

假设我们有libA和libB两个静态库,他们有很多同名函数,并且这两个库通常是在两个不同的目录下dir1和dir2。

由于我们有源码,所以可以通过修改源码的方式来区分不同的codec版本。如果是到源文件中一行一行地找,那样将会是一个噩梦。

幸运的是,我们有perl这样的脚本语言,它的正则表达式功能会让你感到游刃有余。对了,写一个perl脚本,自动地完成这个任务吧。

比如dir1/f1.c和dir2/f2.c都包含了func这个函数,那么我们可以在dir1/f1.c的开头加上:#define func LIBA_func, 在dir2/f2.c的开头加上:#define func LIBB_func。

在用到func的文件中也如法炮制,比如dir1/f3.c用到了dir1/f1.c中的func,则在dir1/f3.c开头加上:#define func LIBA_func

dir2/f4.c用到了dir2/f2.c中的func,则在dir2/f4.c开头加上:#define func LIBB_func

再重新编译这两个静态库,链接就OK了。


以下是具体步骤:

(1)先分别编译这两个静态库,得到libA.a和libB.a;

(2)用nm命令将他们的符号表给打印出来,保存到两个文件中;

(3)解析这两个含有符号表的文件,找到同时存在这两个文件中的那些符号以及它们所在文件的文件名;

(4)第三步得到了符号->文件名的映射关系,我们将其转换成文件名->符号的映射关系;

(5)对每个文件,它们应该同时存在于libA和libB所对应的两个目录下。我们对这个文件中的每个符号,像上面那样加上宏定义。

(6)再重新编译两个库就行了。


例子:

设我们在test目录下有两个库lib1和lib2,这两个库中都有一个duplicate_func函数。

如果我们将这两个库编译成动态库,再在main.cpp中调用之,显示的结果是正确的:

可以参考test目录下build.sh来编译:

      gcc -fPIC -shared -o lib1.so lib1/lib1.c lib1/caller1.c  

gcc -fPIC -shared -o lib2.so lib2/lib2.c lib2/caller2.c

gcc -o dlltest main.cpp ./lib1.so ./lib2.so -ldl

运行dlltest结果是:

$ ./dlltest lib1/caller1.c: caller1: hellolib1/lib1.c: duplicated_func: hellolib2/caller2.c: caller2: worldlib2/lib2.c: duplicated_func: world

如果我们不对这两个库做任何改动,直接编译成静态库再链接,则显示的结果不对:

可以参考test目录下build2.sh来编译:

dir=`pwd`cd lib1gcc -c lib1.c caller1.car -r $dir/lib1.a lib1.o caller1.oranlib $dir/lib1.acd $dircd lib2gcc -c lib2.c caller2.car -r $dir/lib2.a lib2.o caller2.oranlib $dir/lib2.acd $dirgcc -o statictest12 main2.cpp ./lib1.a ./lib2.agcc -o statictest21 main2.cpp ./lib2.a ./lib1.a

运行结果是:

$ ./statictest12caller1.c: caller1: hellolib1.c: duplicated_func: hellocaller2.c: caller2: worldlib1.c: duplicated_func: world$ ./statictest21caller1.c: caller1: hellolib2.c: duplicated_func: hellocaller2.c: caller2: worldlib2.c: duplicated_func: world

可以看到,在上面的静态库方式下,运行结果不符合我们的预期,并且运行结果和链接时静态库的顺序有关。

下面采用本文提到的办法来 解决符号冲突

(1) 先提取两个静态库中的符号信息:

./ extract_symbol.pl test/lib1.a test/lib2.a > ds.txt

extract_symbol.pl 负责将两个静态库中的符号表提取出来,然后将同名的函数和所在的文件列出来。

我们需要将它的结果保存到一个文件中,以便在modify.pl中使用。

上面例子中的同名函数是duplicate_func,所以extract_symbol.pl的结果是:

              printf: lib1.o caller1.o lib2.o caller2.o      

__func__.2181: lib1.o caller1.o lib2.o caller2.o

duplicated_func: lib1.o caller1.o caller2.o lib2.o

这个结果除了 duplicated_func外,还指出printf和__func__.2181重复了,这个没错,因为printf确实在两个库中都用到了。

所以我们需要在modify.pl中把一些我们不需要处理的函数排除掉,通常是一些libc里的函数,如printf,vsprintf,memcpy,strcmp等。

这个可以参考modify.pl中的数组: my @_exclude_symbols = qw/printf __func__.2181/;

这一步也许你会担心,我没发现 printf在两个库中都出现怎么办,没关系,因为如果你没把它加入到 _exclude_symbols中,则printf也会被加上宏定义,

比如#define printf LIB1_printf,这样在链接时会报错,因为这个是库函数,不是你自己定义的函数嘛。这时你再把它加到_exclude_symbols中就行了。


(2) 给每个需要解决冲突的文件加上一行特殊标记: 如 // by lfs

./ modify.pl -a 1 -f ds.txt -d test/lib1 -d test/lib2 -m LIB1 -m LIB2

modify.pl 在action=0时,只是将需要修改的文件列出来。

action=1时,在每个需要改动的文件头加上一行特殊的标记。

action=2时,会将宏定义插入到标记行的下面。

action=3时,会将宏定义删除,但标记行还在。

如果想把标记行也删除,可以通过源码管理工具来实现,比如在git中,git checkout就会恢复到没做任何修改的状态。


(3) 给每个同名函数加上宏定义:

./modify.pl -a 2 -f ds.txt -d test/lib1 -d test/lib2 -m LIB1 -m LIB2

(4)再编译之:

$ ./build2.sh

(5)运行结果为:

$ ./statictest12
caller1.c: caller1: hello
lib1.c: LIB1_duplicated_func: hello
caller2.c: caller2: world
lib2.c: LIB2_duplicated_func: world
$ ./statictest21
caller1.c: caller1: hello
lib1.c: LIB1_duplicated_func: hello
caller2.c: caller2: world
lib2.c: LIB2_duplicated_func: world


从结果可以看出,符号冲突解决后,结果正确。



感想与体会:

这个过程给我的体会是,做c/c++开发的,掌握一个像perl或python这样的脚本语言,有的时候真的是方便很多。

当然你可以用c++去实现上面的这些步骤,但用perl这样原生支持正则表达式的脚本语言,会事半功倍。

用perl来解析符号表,来处理文本相关的任务,再合适不过了。此外,shell也是一个好帮手。

推荐阅读

    excel怎么用乘法函数

    excel怎么用乘法函数,乘法,函数,哪个,excel乘法函数怎么用?1、首先用鼠标选中要计算的单元格。2、然后选中单元格后点击左上方工具栏的fx公

    excel中乘法函数是什么?

    excel中乘法函数是什么?,乘法,函数,什么,打开表格,在C1单元格中输入“=A1*B1”乘法公式。以此类推到多个单元。1、A1*B1=C1的Excel乘法公式

    标准差excel用什么函数?

    标准差excel用什么函数?,函数,标准,什么,在数据单元格的下方输入l标准差公式函数公式“=STDEVPA(C2:C6)”。按下回车,求出标准公差值。详细

    公共CPU接口类型的详细描述

    公共CPU接口类型的详细描述,,我们知道CPU是电脑的大脑, CPU的处理速度直接决定电脑的性能, 那你知道CPU发展到现在, 都那些CPU接口类型吗.

    excel常用函数都有哪些?

    excel常用函数都有哪些?,函数,哪些,常用,1、SUM函数:SUM函数的作用是求和。函数公式为=sum()例如:统计一个单元格区域:=sum(A1:A10)  统计多个

    常识硬件的计算机日常维护

    常识硬件的计算机日常维护,,硬件(防尘、防高温、防磁、防潮、防静电、防震) 应将电脑放在一个干净的房间,避免灰尘太多造成的不利影响,对各种

    移动硬盘如何使用移动硬盘维护知识

    移动硬盘如何使用移动硬盘维护知识,,现在移动硬盘的广泛使用和快节奏的工作使拆迁的一部分;驱动;人,我们说不;拆除;拆除手段,在硬盘有意无意的操

    台式电脑维护维修|台式电脑维修教程

    台式电脑维护维修|台式电脑维修教程,,1. 台式电脑维修教程一,保修没过的话送修;二,已过保修的话,一般也很难找的到会修电源的电脑店,电脑配置较