记录一次glibc版本过低导致的程序无法正常加载的问题
2023.11.27
问题现象
一个程序使用C语言编写的,但由于某些原因,需要通过dlopen的方式调用go语言生成的so,在其它设备上可以正常运行,但在一个arm环境上运行的时候,发现无法正常运行,看到的现象是程序无任何响应,类似直接卡死了。私用gdb查看当前进程,线程信息及调用信息如下:
$3 = (void *) 0x7f98c31000
(gdb) info threads
Id Target Id Frame
* 1 LWP 4658 "test" get_func (ctx=0x200cd59d80,
func_name=0x200cf63ae0 "CheckTest")
at /root/codes/rechk.c:192
2 LWP 4666 "test" 0x0000007f92cd3a98 in pthread_cond_wait ()
from /lib/libpthread.so.0
(gdb) c
Thread 1 "dp" received signal SIGINT, Interrupt.
0x0000007fad50ca98 in pthread_cond_wait () from /lib/libpthread.so.0
(gdb) bt
#0 0x0000007fad50ca98 in pthread_cond_wait () from /lib/libpthread.so.0
#1 0x0000007f920031ac in _cgo_wait_runtime_init_done () at gcc_libinit.c:40
#2 0x0000007f92002d5c in CheckTest (
buf=buf@entry=0x7f96400013 "371329199910103717\"}", bufLen=bufLen@entry=18)
at _cgo_export.c:152
#3 0x0000007f940c2a54 in hit_cb (node=<optimized out>,
priv=0x7fc8279118)
at /root/codes/rule.c:1530
由于之前有类似的经验,发现少了go的一些基础线程,但不明白为什么会没有go的基础线程。既然能调用到so里面的函数,说明so应该是加载成功了。这也正是比较坑的地方,被逼无奈之下,只好从dlopen的函数就开始gdb,发现dlopen竟然报错了,报错信息如下:
(gdb) p dl_err_str
$4 = 0x7fb6a1dfc0 "dlopen: cannot load any more object with static TLS"
而且dlopen报错后,并没有执行里面的CheckTest函数,因为判断了dlopen失败后,并不会再继续后续流程。那为什么会卡在CheckTest函数中呢?于是继续运行程序,发现是第二次再调用hit_cb函数的时候,才会出现卡住的问题。第二次调用hit_cb函数的时候,由于上次调用dlopen失败,所以依然会再次调用dlopen函数打开so,而此次调用dlopen竟然成功了,但是并没有启动go的那些基础线程。于是上网搜索关于dlopen: cannot load any more object with static TLS的问题,发现网上有人贴出来是因为glibc版本问题导致的,原文链接是https://blog.csdn.net/jingyi130705008/article/details/117623889:
这是一个低版本glibc (< 2.23)的已知bug,通过dlopen加载一个动态链接库(DSO),并依次将其依赖的DSO也加载进来的时候。具体产生条件是:
glibc < 2.23
已经加载了超过14个含TLS的DSO
当前加载的DSO使用了static TLS
注意条件2,3。如果能够在加载14个含TLS的DSO前,提前加载含有static TLS的DSO,即可绕过这个问题。
具体做法:找到报错模块(比如paddle)如果可以单独import成功的话,调整import包的顺序,把出问题的包放在最前面import
查看当前系统的glibc版本:
# ldd --version
ldd (Ubuntu GLIBC 2.19kord) 2.19
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
通过readelf命令查看自己的so是否有TLS,
[root@test]# readelf -l libdi_test.so|grep TLS
TLS 0x00000000000c2e00 0x00000000002c2e00 0x00000000002c2e00
果然有TLS,于是去掉其它so中的__thread定义,再次加载此so,发现可以加载成功了,go的基础线程已经起来了,如下所示:
(gdb) info threads
Id Target Id Frame
* 1 LWP 4639 "dp" 0x0000007f7c50bf34 in nanosleep () from /lib/libc.so.6
2 LWP 4647 "dp" 0x0000007f801aca98 in pthread_cond_wait ()
from /lib/libpthread.so.0
3 LWP 1006 "dp" runtime.futex ()
at /root/go/src/runtime/sys_linux_arm64.s:662
4 LWP 1007 "dp" runtime.futex ()
at /root/go/src/runtime/sys_linux_arm64.s:662
5 LWP 1008 "dp" runtime.futex ()
at /root/go/src/runtime/sys_linux_arm64.s:662
6 LWP 1009 "dp" runtime.futex ()
at /root/go/src/runtime/sys_linux_arm64.s:662
7 LWP 1070 "dp" runtime.futex ()
at /root/go/src/runtime/sys_linux_arm64.s:662
8 LWP 1148 "ZMQbg/Reaper" 0x0000007f7c53759c in epoll_pwait ()
from /lib/libc.so.6
9 LWP 1149 "ZMQbg/IO/0" 0x0000007f7c53759c in epoll_pwait ()
from /lib/libc.so.6
而且功能也好用了,由此确定是TLS问题导致的。
解决方法
长期的解决办法是更新glibc的版本,但如果不能更新glibc库,那就只能把__thread去掉。当然,去掉__thread标识后导致的并发问题,还是需要通过别的手段去解决。
其它
为什么第二次dlopen能成功
这个问题暂时还未深入去研究。
TLS是什么
在这里,TLS是线程局部存储(Thread Local Storage)的缩写,而不是我们常说的安全协议中中的TLS。
参考资料
dlopen: cannot load any more object with static TLS问题解决-CSDN博客
dlopen:cannot load any more object with static TLS:_jingyi130705008的博客-CSDN博客
c++ - Cannot load any more object with static TLS - Stack Overflow