最近遇到一个so
库符号冲突的问题, 可以总结为:
- 动态库
so1
中静态编译了某基础库 - 动态库
so2
中动态链接了该基础库的另一版本 - 可执行程序动态链接了这两个
so
- 程序执行到
so2
中函数时, 调用了so1
中的基础库的符号, 而恰好该基础库两个版本的同名函数不兼容, 因而出现了崩溃.
下面通过demo
代码来说明如何解决这个问题.
基础库libhello v1
目录结构如下:
1 | [root@default symbols]# tree libhello1 |
hello.c
:
1 |
|
hello.h
:
1 | extern void print_hello(); |
Makefile
:
1 | CFLAGS=-fPIC |
在libhello1
目录下执行make
将基础库libhello v1
编译为静态库: libhello.a
.
基础库libhello v2
目录结构如下:
1 | [root@default symbols]# tree libhello2/ |
hello.c
:
1 |
|
hello.h
:
1 | extern void print_hello(); |
Makefile
:
1 | CFLAGS=-fPIC |
在libhello2
目录下执行make
将基础库libhello v2
编译为动态库: libhello.so
.
动态库so1
目录结构:
1 | [root@default symbols]# tree so1/ |
so1.c
:
1 |
|
so1.h
:
1 | extern void hello_so1(); |
Makefile
:
1 | CFLAGS=-I../libhello1 -fPIC |
动态库so1
使用静态库libhello.a
, 在so1
目录下执行make
生成libso1.so
.
动态库so2
目录结构:
1 | [root@default symbols]# tree so2/ |
so2.c
:
1 |
|
so2.h
:
1 | extern void hello_so2(); |
Makefile
:
1 | CFLAGS=-I../libhello2/ -L../libhello2/ -fPIC |
动态库so2
动态链接基础库libhello.so
, 在so2
目录下执行make
生成libso2.so
.
可执行程序
目录结构:
1 | [root@default symbols]# tree main/ |
main.c
:
1 |
|
Makefile
:
1 | CFLAGS=-I../so1/ -I../so2/ |
可执行程序main
动态链接so1
和so2
, 在main
目录下执行make
生成可执行程序main
.
整体测试程序结构
1 | [root@default symbols]# tree . |
分析与解决
执行main
的结果:
1 | [root@default main]# ./main |
从结果可以看到hello_so2
调用了libso1.so
中的print_hello
函数.
我们查看libso1.so
的符号表:
1 | [root@default main]# nm ../so1/libso1.so |grep print_hello |
T/t
表示代码区的符号, T
表示是全局可见符号, t
表示库内部本地可见符号.
readelf
的输出更容易区分:
1 | [root@default main]# readelf -s ../so1/libso1.so |grep print_hello |
libso1.so
中的print_hello
为全局可见符号.
libso2.so
中只包含对print_hello
的引用:
1 | [root@default main]# readelf -s ../so2/libso2.so |grep print_hello |
另一个print_hello
符号定义位于libhello.so
中:
1 | [root@default main]# readelf -s ../libhello2/libhello.so |grep 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
的依赖和加载顺序:
1 | [root@default main]# ldd main |
如果修改main
程序的Makefile
, 将so1
和so2
的顺序颠倒, 编译的程序依赖为:
1 | [root@default main]# ldd main |
可以看到这次先加载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 | [root@default main]# LD_PRELOAD=../libhello2/libhello.so ./main |
可以看到这次hello_so1
和hello_so2
的print_hello
都来自libhello.so
. 但这并不是我们所希望看到的结果, 我们希望libso1.so
使用它自己的print_hello
. 这可以通过链接选项-Bsymbolic
来实现. 根据ld
的文档, -Bsymbolic
选项可以让动态库优先使用自己的符号:
1 | -Bsymbolic |
我们修改so1
的Makefile
, 加上-Bsymbolic
选项:
1 | CFLAGS=-I../libhello1 -fPIC |
在so1
目录重新执行make
后, 再次运行main
程序:
1 | [root@default main]# LD_PRELOAD=../libhello2/libhello.so ./main |
可以看到hello_so1
和hello_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
来将所有静态库的符号屏蔽.
我们修改so1
的Makefile
:
1 | CFLAGS=-I../libhello1 -fPIC |
重新编译so1
后再次执行main
:
1 | [root@default main]# ./main |
hello_so1
和hello_so2
都调用了正确的符号, 符合我们的希望.
查看so1
的符号, 可以看到print_hello
的可见性为LOCAL
, 不再是全局可见:
1 | [root@default main]# readelf -s ../so1/libso1.so |grep print_hello |
参考
- https://stackoverflow.com/questions/2222162/how-to-apply-fvisibility-option-to-symbols-in-static-libraries
- https://stackoverflow.com/questions/6562403/i-dont-understand-wl-rpath-wl
- https://stackoverflow.com/questions/37531846/nm-symbol-output-t-vs-t-in-a-shared-so-library
- https://linux.die.net/man/1/ld
- https://www.gnu.org/software/gnulib/manual/html_node/LD-Version-Scripts.html
- https://sourceware.org/binutils/docs-2.20/ld/VERSION.html
- https://www.technovelty.org/c/symbol-versions-and-dependencies.html
- https://www.baeldung.com/linux/shared-object-filenames
- https://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html
- https://itnewbee.org/linux%E4%B8%8B%E5%8A%A8%E6%80%81%E5%BA%93%E5%8F%8A%E7%89%88%E6%9C%AC%E5%8F%B7%E6%8E%A7%E5%88%B6/
- https://rockhong.github.io/shared-library-intro.html
- https://www.akkadia.org/drepper/dsohowto.pdf
- https://www.akkadia.org/drepper/symbol-versioning
- https://cnblogs.com/welhzh/p/6730839.html
- https://www.cnblogs.com/lsgxeva/p/8257784.html
- https://blog.blahgeek.com/glibc-and-symbol-versioning/
- https://www.caichinger.com/elf.html
- https://refspecs.linuxfoundation.org/LSB_3.0.0/LSB-PDA/LSB-PDA.junk/symversion.html
- https://zohead.com/archives/mod-elf-glibc/