LD_PRELOAD共享库构造函数调用dlopen("libc.so.6")在Firefox中阻塞问题排查
你碰到了一个典型的动态链接器初始化阶段死锁问题,而且只在Firefox这种启动流程复杂的进程中触发,我们来一步步拆解原因和解决方案:
核心原因分析
当你通过LD_PRELOAD加载共享库时,它的构造函数会在进程启动的极早期执行——早到甚至libc本身的一些核心组件(比如线程锁、内存分配器的内部结构)都还没完成初始化。而dlopen("libc.so.6")本身会触发动态链接器的一系列操作,其中涉及calloc分配内存,而calloc又依赖libc内部的互斥锁来保证线程安全。如果此时这个锁还没被正确初始化,或者被其他初始化线程持有,就会陷入无限阻塞的死锁状态。
Firefox的启动流程和普通程序不同:它会提前启动多个线程、加载大量组件,对动态链接器的状态要求更严格,这就把普通程序中可能隐藏的初始化顺序问题放大了。
可行的解决方案
1. 完全避免手动dlopen libc
libc是每个Linux进程默认加载的核心库,你根本不需要手动调用dlopen("libc.so.6")来获取它的符号——直接使用libc的函数即可。如果你的代码是为了动态获取某个特定版本的libc符号,换个思路:通过dlsym(RTLD_NEXT, "函数名")来获取当前进程中已加载的libc的符号,这样就绕开了重新dlopen libc的操作。
2. 延迟初始化逻辑
把原本放在共享库构造函数里的dlopen操作,移到第一次使用相关功能的时候执行,而不是在进程启动阶段。比如:
static void init_my_lib() { // 这里执行dlopen等初始化操作 void *handle = dlopen(...); // ... } void my_exported_function() { static pthread_once_t once = PTHREAD_ONCE_INIT; pthread_once(&once, init_my_lib); // 后续业务逻辑 }
这样可以确保初始化操作在进程完全启动、libc所有组件都准备好之后才执行,从根源上避免初始化顺序冲突。
3. 检查libc初始化状态再执行
如果你必须在构造函数中做操作,可以先检查libc的初始化状态(注意这依赖glibc的内部实现,不同版本可能有变化)。比如glibc中可以检查__libc_initialized变量:
#include <stdio.h> extern int __libc_initialized; __attribute__((constructor)) void my_constructor() { if (__libc_initialized) { // 安全执行dlopen操作 void *handle = dlopen("libc.so.6", RTLD_LAZY); // ... } else { // 延迟到进程退出前执行(或其他合适时机) atexit(init_my_lib); } }
不过这种方法属于hack,不推荐长期使用,因为glibc的内部变量可能会在版本更新中变化。
4. 调试确认锁的持有者
如果上面的方法都没解决,你可以用gdb进一步定位死锁的具体原因:
- 启动Firefox时附加gdb:
gdb firefox - 当进程阻塞时,执行
info threads查看所有线程 - 对每个线程执行
thread <线程ID>+bt,找到持有__GI___pthread_mutex_lock对应的锁的线程,确认是不是libc的初始化线程和你的构造函数线程发生了锁竞争。
总结
最稳妥的方案是延迟初始化或者直接使用已加载的libc符号,尽量避免在共享库构造函数中执行依赖libc完整初始化的操作——尤其是dlopen这种会触发动态链接器复杂逻辑的调用。
内容的提问来源于stack exchange,提问作者Error




