最近遇到一个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/