记录一次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


viyon
6 声望0 粉丝

引用和评论

0 条评论