UNIX编程-库接口冲突排查及解决

Posted by 周思进 on February 26, 2022

搞嵌入式开发,经常会涉及多个资源库的集成开发,不同资源往往属于不同的团队人员开发,这也造成在一些通用接口上存在接口实现冲突的情况。下面针对可能遇到接口冲突的几种场景及问题解决办法进行简单说明(这里暂不讨论弱符号设置情况)。

一、最终程序链接包含同名函数的两个目标文件

// test1.c
#include <stdio.h>
void test()
{
    printf("This is test1.\n");
    return ;
}

// test1.c
#include <stdio.h>
void test()
{
    printf("This is test1.\n");
    return ;
}

#include <stdio.h>
#include "test.h"

int main(int argc, char const *argv[])
{
    test();
    return 0;
}

进行编译

gcc -c test1.c -o test1.o
gcc -c test2.c -o test2.o
gcc main.c test1.o test2.o
/usr/bin/ld: test2.o: in function `test':
test2.c:(.text+0x0): multiple definition of `test'; test1.o:test1.c:(.text+0x0): first defined here

可见目标文件链接到同一程序中,不能存在同名函数的情况,否则直接就编译不过报错了。

只能将其中重名的函数进行改名处理。


二、静态库和目标文件链接到程序或者两个静态库链接到程序

这里重要的一点是要清楚静态库是怎么组成的,可以看下之前的文章《UNIX编程-静态库》。

a) 静态库就是将一个个.o目标文件打包生成的.a文件,在程序链接的时候,只将要查找的符号所在的.o目标文件拿出来去链接

b) gcc 在链接的时候,当查找到目标函数的第一个符号后,就停止查找了。

根据上面两条规则,实际程序链接就存在如下三种情况:
1、如果目标文件链接顺序在前,静态库里的.o就不会抽取出来链接了,也就不会报接口冲突

2、如果静态库顺序在前,就会将静态库里的.o 抽取出来,然后跟后面的目标文件一同链接到程序,这就会报重复定义了

3、两个都是静态库,这样实际程序使用的就是先链接库的接口实现,后面静态库的接口实现不会链接到最终程序里,也就不会报接口冲突

下面是程序运行验证结果:

#情况1  不报冲突
[email protected]:~/nfs/test/static $ make
gcc -c test1.c -o test1.o
ar crv libtest1.a test1.o
gcc -c test2.c -o test2.o
gcc main.c -L./  test2.o -ltest1
[email protected]:~/nfs/test/static $ ./a.out
This is test2.

#情况2  冲突
gcc -c test1.c -o test1.o
ar crv libtest1.a test1.o
gcc -c test2.c -o test2.o
gcc main.c -L./ -ltest1  test2.o
/usr/bin/ld: test2.o: in function `test':
test2.c:(.text+0x0): multiple definition of `test'; .//libtest1.a(test1.o):test1.c:(.text+0x0): first defined here

#情况3 不报冲突,哪个链接在前,实际就调用哪个
gcc -c test1.c -o test1.o
ar crv libtest1.a test1.o
gcc -c test2.c -o test2.o
ar crv libtest2.a test2.o
gcc main.c -L./ -ltest2  -ltest1
[email protected]:~/nfs/test/static $ ./a.out
This is test2.
[email protected]:~/nfs/test/static $ make
gcc -c test1.c -o test1.o
ar crv libtest1.a test1.o
gcc -c test2.c -o test2.o
ar crv libtest2.a test2.o
gcc main.c -L./  -ltest1 -ltest2
./[email protected]:~/nfs/test/static $ ./a.out
This is test1.

可见用静态库就可能存在接口实现冲突,且链接没有发现,实际运行调用接口出错的可能。

针对该情况,要么在链接静态库的时候,加上 ‘-Wl,–whole-archive’选项,该选项强制连接器将后面库中所有符号都链接进来,使用‘-Wl,–no-whole-archive’选项结束

如下增加 test3.c 以验证该选项效果:

#include <stdio.h>

void test3(void)
{
    printf("This is test3.\n");
    return ;
}

main 函数仍旧只调用 test1 里的 test 接口,运行如下:

gcc -c test1.c -o test1.o
ar crv libtest1.a test1.o
gcc -c test3.c -o test3.o
ar crv libtest3.a test3.o
gcc main.c -L./  -ltest1 -ltest3
[email protected]:~/nfs/test/static $ nm a.out | tail -n 5
         U [email protected]@GLIBC_2.4
000103a0 t register_tm_clones
00010314 T _start
0001042c T test
00021028 D __TMC_END__

可以看到最终程序里的函数符号表只有 test 接口
关于 nm 可以看 《Linux命令-nm

gcc -c test1.c -o test1.o
ar crv libtest1.a test1.o
gcc -c test3.c -o test3.o
ar crv libtest3.a test3.o
# gcc main.c -L./  -ltest1 -ltest3
gcc main.c -L./  -ltest1  -Wl,--whole-archive -ltest3 -Wl,--no-whole-archive
[email protected]:~/nfs/test/static $ nm a.out | tail -n 5
000103a0 t register_tm_clones
00010314 T _start
0001042c T test
00010448 T test3
00021028 D __TMC_END__

可以看到即使 main 函数里未使用到 test3 接口,也会链接进去。

固只要在链接静态库的时候,加上上述选项就可以发现是否存在接口冲突。


三、动态库和目标文件;静态库和动态库;动态库和动态库

因为动态库是运行时按需加载,其目标文件是没有复制到运行程序里的,固动态库中存在同名函数在链接过程无论哪种情况都是不会出现编译报错的。

至于程序实际调用哪个接口,下面分情况说明。

1、动态库和目标文件(.o文件)

[email protected]:~/nfs/test/static $ make
gcc -c test1.c -o test1.o
gcc -fPIC -shared test2.c -o libtest2.so
gcc main.c -Wl,-rpath=./ -L./ -ltest2 test1.o
[email protected]:~/nfs/test/static $ nm a.out | tail -n 5
         U [email protected]@GLIBC_2.4
00010424 t register_tm_clones
00010398 T _start
000104b0 T test
00021028 D __TMC_END__
[email protected]:~/nfs/test/static $ ./a.out
This is test1.

可见 test 已经是明确的代码段指向了,运行 a.out 程序,只会输出 test1 的打印结果。


2、静态库和动态库

[email protected]:~/nfs/test/static $ make
gcc -c test1.c -o test1.o
ar crv libtest1.a test1.o
gcc -fPIC -shared test2.c -o libtest2.so
gcc main.c -Wl,-rpath=./ -L./ -ltest2 -ltest1
[email protected]:~/nfs/test/static $ nm a.out | tail -n 5
0001046c T main
00010408 t register_tm_clones
0001037c T _start
         U test
00021028 D __TMC_END__
[email protected]:~/nfs/test/static $ ./a.out
This is test2.
[email protected]:~/nfs/test/static $ gcc main.c -Wl,-rpath=./ -L./ -ltest1 -ltest2
[email protected]:~/nfs/test/static $ nm a.out | tail -n 5
         U [email protected]@GLIBC_2.4
00010424 t register_tm_clones
00010398 T _start
000104b0 T test
00021028 D __TMC_END__
[email protected]:~/nfs/test/static $ ./a.out
This is test1.

可见程序实际调用哪个接口,由链接顺序决定。

但其实本文中的例子其实比较简单,如果程序要调用的接口A虽然已经在先链接的动态库中查询到符号,但如果其他接口只在静态库中实现,而从静态库中抽取的.o文件又包含了接口A,那最终程序调用的接口A则仍旧是静态库里的,就跟链接顺序无关了。

// test1.c  增加 test1 接口
#include <stdio.h>
void test()
{
    printf("This is test1.\n");
    return ;
}

void test1()
{
    printf("This is test111.\n");
    return ;
}

// main.c 中增加test1调用,代码省略

运行接口如下

gcc -c test1.c -o test1.o
ar crv libtest1.a test1.o
gcc -fPIC -shared test2.c -o libtest2.so
gcc main.c -Wl,-rpath=./ -L./ -ltest2 -ltest1
[email protected]:~/nfs/test/static $ ./a.out
This is test1.
This is test111.


3、动态库和动态库

这个就是明确由链接时的顺序决定,代码不做演示了。
运行时的链接查找过程可以通过 LD_DEBUG 来展示,如下:

[email protected]:~/nfs/test/static $ LD_DEBUG=symbols ./a.out
... 省略
     21064:	symbol=test;  lookup in file=./a.out [0]
     21064:	symbol=test;  lookup in file=./libtest2.so [0]
...


综上,解决接口冲突的方式如下:
1、提前发现存在接口冲突问题
对于链接静态库可以增加 ‘-Wl,–whole-archive’ 选项
对于链接动态库,目前我也没想到什么好的办法,有意见的朋友可以告知我下

2、对于冲突的接口可以考虑用宏定义
比如自己开发的A库里定义了个接口叫 test, 接口集成其他部门的B库里也自己实现了个 test,那可以考虑将自己开发的A库中的接口名称替换下。

这里不是指全部使用到的地方都进行替换,实际只需将实现的接口名称修改下,比如修改成 test_v2,而将头文件的声明修改成如下:

#define test(void) test_v2(void)

这样调用的地方都无需做修改。