4

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

 1 year ago
source link: http://just4coding.com/2022/11/22/linux-symbol/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

2022-11-22 MISC

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

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

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

基础库libhello v1

目录结构如下:

[root@default symbols]# tree libhello1
libhello1
├── hello.c
├── hello.h
└── Makefile

0 directories, 3 files

hello.c:

#include <stdio.h>

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

hello.h:

extern void print_hello();

Makefile:

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

目录结构如下:

[root@default symbols]# tree libhello2/
libhello2/
├── hello.c
├── hello.h
└── Makefile

0 directories, 3 files

hello.c:

#include <stdio.h>

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

hello.h:

extern void print_hello();

Makefile:

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

目录结构:

[root@default symbols]# tree so1/
so1/
├── Makefile
├── so1.c
└── so1.h

0 directories, 3 files

so1.c:

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

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

print_hello();
}

so1.h:

extern void hello_so1();

Makefile:

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

目录结构:

[root@default symbols]# tree so2/
so2/
├── Makefile
├── so2.c
└── so2.h

0 directories, 3 files

so2.c:

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

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

print_hello();
}

so2.h:

extern void hello_so2();

Makefile:

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.

可执行程序

目录结构:

[root@default symbols]# tree main/
main/
├── main.c
└── Makefile

0 directories, 2 files

main.c:

#include <stdio.h>

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

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

hello_so2();

return 0;
}

Makefile:

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.

整体测试程序结构
[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的结果:

[root@default main]# ./main
hello in so1
hello v1.0
hello in so2
hello v1.0

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

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

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

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

readelf的输出更容易区分:

[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的引用:

[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中:

[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的依赖和加载顺序:

[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的顺序颠倒, 编译的程序依赖为:

[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, 运行结果:

[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选项可以让动态库优先使用自己的符号:

-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选项:

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程序:

[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将符号默认可见性修改对外不可见, 需要由外部使用的符号需要显示声明, 如:

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

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

我们修改so1Makefile:

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:

[root@default main]# ./main
hello in so1
hello v1.0
hello in so2
hello v2.0

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

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

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK