Keep learning, keep living...

0%

Linux动态链接库符号冲突解决

最近遇到一个so库符号冲突的问题, 可以总结为:

  • 动态库so1中静态编译了某基础库
  • 动态库so2中动态链接了该基础库的另一版本
  • 可执行程序动态链接了这两个so
  • 程序执行到so2中函数时, 调用了so1中的基础库的符号, 而恰好该基础库两个版本的同名函数不兼容, 因而出现了崩溃.

下面通过demo代码来说明如何解决这个问题.

基础库libhello v1

目录结构如下:

1
2
3
4
5
6
7
[root@default symbols]# tree libhello1
libhello1
├── hello.c
├── hello.h
└── Makefile

0 directories, 3 files

hello.c:

1
2
3
4
5
#include <stdio.h>

void print_hello() {
printf("hello v1.0\n");
}

hello.h:

1
extern void print_hello();

Makefile:

1
2
3
4
5
6
7
8
9
CFLAGS=-fPIC

static:
gcc -c $(CFLAGS) *.c
ar -rcu libhello.a *.o

clean:
rm -rf *.o
rm -rf *.a

libhello1目录下执行make将基础库libhello v1编译为静态库: libhello.a.

基础库libhello v2

目录结构如下:

1
2
3
4
5
6
7
[root@default symbols]# tree libhello2/
libhello2/
├── hello.c
├── hello.h
└── Makefile

0 directories, 3 files

hello.c:

1
2
3
4
5
#include <stdio.h>

void print_hello() {
printf("hello v2.0\n");
}

hello.h:

1
extern void print_hello();

Makefile:

1
2
3
4
5
6
7
8
9
CFLAGS=-fPIC

so1:
gcc -c $(CFLAGS) *.c
gcc -o libhello.so -shared $(CFLAGS) *.o

clean:
rm -rf *.o
rm -rf *.so

libhello2目录下执行make将基础库libhello v2编译为动态库: libhello.so.

动态库so1

目录结构:

1
2
3
4
5
6
7
[root@default symbols]# tree so1/
so1/
├── Makefile
├── so1.c
└── so1.h

0 directories, 3 files

so1.c:

1
2
3
4
5
6
7
8
#include <stdio.h>
#include "hello.h"

void hello_so1() {
printf("hello in so1\n");

print_hello();
}

so1.h:

1
extern void hello_so1();

Makefile:

1
2
3
4
5
6
7
8
9
10
CFLAGS=-I../libhello1 -fPIC
LDFLAGS=

so1:
gcc -c $(CFLAGS) *.c
gcc -o libso1.so -shared $(CFLAGS) $(LDFLAGS) *.o ../libhello1/libhello.a

clean:
rm -rf *.o
rm -rf *.so

动态库so1使用静态库libhello.a, 在so1目录下执行make生成libso1.so.

动态库so2

目录结构:

1
2
3
4
5
6
7
[root@default symbols]# tree so2/
so2/
├── Makefile
├── so2.c
└── so2.h

0 directories, 3 files

so2.c:

1
2
3
4
5
6
7
8
#include <stdio.h>
#include "hello.h"

void hello_so2() {
printf("hello in so2\n");

print_hello();
}

so2.h:

1
extern void hello_so2();

Makefile:

1
2
3
4
5
6
7
8
9
10
CFLAGS=-I../libhello2/ -L../libhello2/ -fPIC
LDFLAGS=-Wl,-rpath=../libhello2/

so2:
gcc -c $(CFLAGS) *.c
gcc -o libso2.so -shared $(CFLAGS) $(LDFLAGS) *.o -lhello

clean:
rm -rf *.o
rm -rf *.so

动态库so2动态链接基础库libhello.so, 在so2目录下执行make生成libso2.so.

可执行程序

目录结构:

1
2
3
4
5
6
[root@default symbols]# tree main/
main/
├── main.c
└── Makefile

0 directories, 2 files

main.c:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

#include "so1.h"
#include "so2.h"

int main(int argc, char **argv) {
hello_so1();

hello_so2();

return 0;
}

Makefile:

1
2
3
4
5
6
7
8
9
10
CFLAGS=-I../so1/ -I../so2/
LDFLAGS=-L../so1/ -L../so2/ -Wl,-rpath=../so1/,-rpath=../so2/

so2:
gcc -c $(CFLAGS) *.c
gcc -o main $(CFLAGS) $(LDFLAGS) -lso1 -lso2 *.o

clean:
rm -rf *.o
rm -rf main

可执行程序main动态链接so1so2, 在main目录下执行make生成可执行程序main.

整体测试程序结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@default symbols]# tree .
.
├── libhello1
│   ├── hello.c
│   ├── hello.h
│   └── Makefile
├── libhello2
│   ├── hello.c
│   ├── hello.h
│   └── Makefile
├── main
│   ├── main.c
│   └── Makefile
├── so1
│   ├── Makefile
│   ├── so1.c
│   └── so1.h
└── so2
├── Makefile
├── so2.c
└── so2.h

5 directories, 14 files
分析与解决

执行main的结果:

1
2
3
4
5
[root@default main]# ./main
hello in so1
hello v1.0
hello in so2
hello v1.0

从结果可以看到hello_so2调用了libso1.so中的print_hello函数.

我们查看libso1.so的符号表:

1
2
[root@default main]# nm ../so1/libso1.so  |grep print_hello
0000000000000711 T print_hello

T/t表示代码区的符号, T表示是全局可见符号, t表示库内部本地可见符号.

readelf的输出更容易区分:

1
2
3
[root@default main]# readelf -s ../so1/libso1.so  |grep print_hello
9: 0000000000000711 18 FUNC GLOBAL DEFAULT 11 print_hello
51: 0000000000000711 18 FUNC GLOBAL DEFAULT 11 print_hello

libso1.so中的print_hello为全局可见符号.

libso2.so中只包含对print_hello的引用:

1
2
3
[root@default main]# readelf -s ../so2/libso2.so  |grep print_hello
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND print_hello
51: 0000000000000000 0 FUNC GLOBAL DEFAULT UND print_hello

另一个print_hello符号定义位于libhello.so中:

1
2
3
[root@default main]# readelf -s ../libhello2/libhello.so |grep print_hello
9: 00000000000006a5 18 FUNC GLOBAL DEFAULT 11 print_hello
50: 00000000000006a5 18 FUNC GLOBAL DEFAULT 11 print_hello

这两个中符号定义中, 哪个生效是如何决定的呢?

Linux动态链接器(如, /lib64/ld-linux-x86-64.so.2)在加载动态链接库中的符号到全局符号表时, 如果相同的符号已经存在, 则后加入的符号将被忽略, 这叫做全局符号介入: Global symbol interpose. 而Linux动态链接器加载所依赖的动态库是按照广度优先的顺序进行的. 以我们的例子来说就是, main依赖libso1.solibso2.so, 因而先加载libso1.solibso2.so, 接下来再处理libso1.solibso2.so的依赖才会加载到libhello.so. 而libso1.solibso2.so的加载顺序是由链接时的顺序决定的.

可以使用ldd命令查看main的依赖和加载顺序:

1
2
3
4
5
6
7
[root@default main]# ldd main
linux-vdso.so.1 => (0x00007ffc11d11000)
libso1.so => ../so1/libso1.so (0x00007fb5c94fe000)
libso2.so => ../so2/libso2.so (0x00007fb5c92fc000)
libc.so.6 => /lib64/libc.so.6 (0x00007fb5c8f2e000)
libhello.so => ../libhello2/libhello.so (0x00007fb5c8d2c000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb5c9700000)

如果修改main程序的Makefile, 将so1so2的顺序颠倒, 编译的程序依赖为:

1
2
3
4
5
6
7
[root@default main]# ldd main
linux-vdso.so.1 => (0x00007fffab9ea000)
libso2.so => ../so2/libso2.so (0x00007f91ac77b000)
libso1.so => ../so1/libso1.so (0x00007f91ac579000)
libc.so.6 => /lib64/libc.so.6 (0x00007f91ac1ab000)
libhello.so => ../libhello2/libhello.so (0x00007f91abfa9000)
/lib64/ld-linux-x86-64.so.2 (0x00007f91ac97d000)

可以看到这次先加载libso2.so了.

我们的实验程序先加载libso1.so, 因而libso1.so中的print_hello被加载到全局符号表中, 后续libhello.so中的print_hello符号忽略, 因而main程序中hello_so2调用的print_hello实际为libso1.so中的print_hello.

我们可以使用LD_PRELOAD环境变量来指定优先加载libhello.so, 运行结果:

1
2
3
4
5
[root@default main]# LD_PRELOAD=../libhello2/libhello.so ./main
hello in so1
hello v2.0
hello in so2
hello v2.0

可以看到这次hello_so1hello_so2print_hello都来自libhello.so. 但这并不是我们所希望看到的结果, 我们希望libso1.so使用它自己的print_hello. 这可以通过链接选项-Bsymbolic来实现. 根据ld的文档, -Bsymbolic选项可以让动态库优先使用自己的符号:

1
2
-Bsymbolic
When creating a shared library, bind references to global symbols to the definition within the shared library, if any. Normally, it is possible for a program linked against a shared library to override the definition within the shared library. This option is only meaningful on ELF platforms which support shared libraries.

我们修改so1Makefile, 加上-Bsymbolic选项:

1
2
3
4
5
6
7
8
9
10
CFLAGS=-I../libhello1 -fPIC
LDFLAGS=-Wl,-Bsymbolic

so1:
gcc -c $(CFLAGS) *.c
gcc -o libso1.so -shared $(CFLAGS) $(LDFLAGS) *.o ../libhello1/libhello.a

clean:
rm -rf *.o
rm -rf *.so

so1目录重新执行make后, 再次运行main程序:

1
2
3
4
5
[root@default main]# LD_PRELOAD=../libhello2/libhello.so ./main
hello in so1
hello v1.0
hello in so2
hello v2.0

可以看到hello_so1hello_so2各自调用了正确的print_hello.

这样能够得到我们所期望的结果, 但手动去指定加载动态库的方法既费时费力,又不具备通用性. 还需要寻找更优雅的解决方案. 我们的例子中, so1使用静态库libhello.a, 只是自用, 并不需要将这些符号提供给其他动态库使用. 我们应该控制这些符号的可见性. Linux动态库中的符号默认可见性为全局, 可以使用编译选项-fvisibility=hidden将符号默认可见性修改对外不可见, 需要由外部使用的符号需要显示声明, 如:

1
void __attribute ((visibility("default"))) hello_so1()

so1中的proto_hello符号来源于libhello.a, -fvisibility=hidden对来自静态库的符号并不生效. 可以使用链接选项-Wl,--exclude-libs,ALL来将所有静态库的符号屏蔽.

我们修改so1Makefile:

1
2
3
4
5
6
7
8
9
10
CFLAGS=-I../libhello1 -fPIC
LDFLAGS=-Wl,-Bsymbolic -Wl,--exclude-libs,ALL

so1:
gcc -c $(CFLAGS) *.c
gcc -o libso1.so -shared $(CFLAGS) $(LDFLAGS) *.o ../libhello1/libhello.a

clean:
rm -rf *.o
rm -rf *.so

重新编译so1后再次执行main:

1
2
3
4
5
[root@default main]# ./main
hello in so1
hello v1.0
hello in so2
hello v2.0

hello_so1hello_so2都调用了正确的符号, 符合我们的希望.

查看so1的符号, 可以看到print_hello的可见性为LOCAL, 不再是全局可见:

1
2
[root@default main]# readelf -s ../so1/libso1.so |grep print_hello
41: 00000000000006c1 18 FUNC LOCAL DEFAULT 11 print_hello
参考