

Linux动态链接库符号冲突解决
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.

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
动态链接so1
和so2
, 在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.so
和libso2.so
, 因而先加载libso1.so
和libso2.so
, 接下来再处理libso1.so
和libso2.so
的依赖才会加载到libhello.so
. 而libso1.so
和libso2.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
, 将so1
和so2
的顺序颠倒, 编译的程序依赖为:
[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_so1
和hello_so2
的print_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.
我们修改so1
的Makefile
, 加上-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_so1
和hello_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
来将所有静态库的符号屏蔽.
我们修改so1
的Makefile
:
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_so1
和hello_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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK