記錄一次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