在加载了 dlopen 的库中使用 std::thread 会导致 sigsegv

using std::thread in a library loaded with dlopen leads to a sigsev

我最近在使用 std::threaddlopen 时发现了一个奇怪的行为。

基本上,当我在使用 dlopen 加载的库中执行 std::thread 时,我会收到一个 sigsev。库本身是针对 pthread link 的,调用 dlopen 的可执行文件不是。

一旦我 link 针对 pthread 的可执行文件或库本身一切正常。但是,我们使用的是基于插件的基础架构,我们不知道应用程序本身是否针对 pthread linked。因此,link 始终针对 pthread 的可执行文件不是一个选项。

请找到附件中的一些代码来重现问题。目前我不确定是什么原因导致了这个问题。是 gcc、glibc、libstdc++ 还是 ld.so 的问题?有没有一种方便的方法来解决这个问题?我看起来像这个 glibc bug 是相关的,但我使用的是 glibc2.27(debian 测试)。

从库中调用 pthread_create 本身似乎可行。

hello.cpp

#include <thread>
#include <iostream>

void thread()
{
    std::thread t ([](){std::cout << "hello world" << std::endl;});
    t.join();
}

extern "C" {
    void hello()
    {
        thread();
    }
}

example.cpp

#include <iostream>
#include <dlfcn.h>

/** code from https://www.tldp.org/HOWTO/html_single/C++-dlopen/
*/
int main() {

    std::cout << "C++ dlopen demo\n\n";

    // open the library
    std::cout << "Opening hello.so...\n";
    void* handle = dlopen("./libhello.so", RTLD_LAZY);

    if (!handle) {
        std::cerr << "Cannot open library: " << dlerror() << '\n';
        return 1;
    }

    // load the symbol
    std::cout << "Loading symbol hello...\n";
    typedef void (*hello_t)();

    // reset errors
    dlerror();
    hello_t hello = (hello_t) dlsym(handle, "hello");
    const char *dlsym_error = dlerror();
    if (dlsym_error) {
        std::cerr << "Cannot load symbol 'hello': " << dlsym_error <<
            '\n';
        dlclose(handle);
        return 1;
    }

    // use it to do the calculation
    std::cout << "Calling hello...\n";
    hello();

    // close the library
    std::cout << "Closing library...\n";
    dlclose(handle);
}

build.sh(构建并执行上面的示例。示例 1 崩溃)

#!/bin/bash

echo "g++ -shared -fPIC -std=c++14 hello.cpp -o libhello.so -pthread"
g++ -shared -fPIC -std=c++14 hello.cpp -o libhello.so -pthread

echo "g++ example.cpp -o example1 -ldl"
g++ example.cpp -o example1 -ldl

echo "g++ example.cpp -o example2 -ldl -pthread"
g++ example.cpp -o example2 -ldl -pthread

echo "g++ example.cpp -o example3 -ldl -lhello -L ./"
g++ example.cpp -o example3 -ldl -lhello -L ./

export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:$(pwd)

echo "===== example1 ====="
./example1
echo "===== end      ====="

echo "===== example2 ====="
./example2
echo "===== end      ====="

echo "===== example3 ====="
./example3
echo "===== end      ====="

编辑

我忘了说:如果我是 运行 使用 LD_DEBUG=all 的错误示例(即示例 1),程序在查找 pthread_create 期间崩溃。更有趣的是,之前查找 pthread_create 成功了:

  8111:     symbol=_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_;  lookup in file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]
  8111:     binding file ./libhello.so [0] to /usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]: normal symbol `_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_' [GLIBCXX_3.4]
  8111:     symbol=pthread_create;  lookup in file=./example1 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libdl.so.2 [0]
  8111:     symbol=pthread_create;  lookup in file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libm.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libgcc_s.so.1 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib64/ld-linux-x86-64.so.2 [0]
  8111:     symbol=pthread_create;  lookup in file=./libhello.so [0]
  8111:     symbol=pthread_create;  lookup in file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libm.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libgcc_s.so.1 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libpthread.so.0 [0]
  8111:     binding file ./libhello.so [0] to /lib/x86_64-linux-gnu/libpthread.so.0 [0]: normal symbol `pthread_create' [GLIBC_2.2.5]
  8111:     symbol=_ZTVNSt6thread6_StateE;  lookup in file=./example1 [0]
  8111:     symbol=_ZTVNSt6thread6_StateE;  lookup in file=/lib/x86_64-linux-gnu/libdl.so.2 [0]
  8111:     symbol=_ZTVNSt6thread6_StateE;  lookup in file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]
  8111:     binding file ./libhello.so [0] to /usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]: normal symbol `_ZTVNSt6thread6_StateE' [GLIBCXX_3.4.22]
  ...
  8111:     binding file ./libhello.so [0] to ./libhello.so [0]: normal symbol `_ZNSt10_Head_baseILm0EPNSt6thread6_StateELb0EE7_M_headERS3_'
  8111:     symbol=_ZNSt6thread15_M_start_threadESt10unique_ptrINS_6_StateESt14default_deleteIS1_EEPFvvE;  lookup in file=./example1 [0]
  8111:     symbol=_ZNSt6thread15_M_start_threadESt10unique_ptrINS_6_StateESt14default_deleteIS1_EEPFvvE;  lookup in file=/lib/x86_64-linux-gnu/libdl.so.2 [0]
  8111:     symbol=_ZNSt6thread15_M_start_threadESt10unique_ptrINS_6_StateESt14default_deleteIS1_EEPFvvE;  lookup in file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]
  8111:     binding file ./libhello.so [0] to /usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]: normal symbol `_ZNSt6thread15_M_start_threadESt10unique_ptrINS_6_StateESt14default_deleteIS1_EEPFvvE' [GLIBCXX_3.4.22]
  8111:     symbol=pthread_create;  lookup in file=./example1 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libdl.so.2 [0]
  8111:     symbol=pthread_create;  lookup in file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libm.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libgcc_s.so.1 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib64/ld-linux-x86-64.so.2 [0]
  ./build.sh: line 18:  8111 Segmentation fault      (core dumped) LD_DEBUG=all ./example1
  ===== end      =====

Once I link the executable against pthread or the library itself everything works fine. However, we are using a plugin based infrastructure, where we do not know if the application itself is linked against pthread or not. Therefore, it is not an option to link the executable always against pthread.

相反:很少有系统支持应用程序成为"suddenly multithreaded"(您的系统显然不支持)。

如果你需要支持潜在多线程插件,那么你必须开始多线程就绪,这是通过link针对libpthread实现的,或者更方便地通过添加 -pthread 标志来编译和 link 主要可执行文件的行。

Is it a problem of gcc, glibc, libstdc++, or the ld.so

这是 libstdc++ 的问题 -- GLIBC 确实支持 "suddenly multithreaded" 执行,GCC 根本不是运行时环境的一部分,ld.so 是 GLIBC 的一部分。

我可以提供一些关于为什么会出现段错误的背景,但遗憾的是没有解决方案。

这似乎是 libstdc++ 的一个问题:从技术上讲,这个庞大的单体库依赖于 libpthread,但出于充分的理由,它们不会 link 反对 libpthread.现在为了能够从根本不使用线程的程序中加载 libstdc++,丢失的符号(例如 pthread_create)必须出现在某个地方。所以 libstdc++ 将它们定义为弱符号。

这些弱符号还用于在运行时检测是否实际加载了libpthread。对于一个旧的 ABI,甚至有一个 check in _M_start_thread,如果没有加载 pthread 而不是调用一个弱定义的 nullptr,它会导致一个有意义的异常——这是我不希望我最坏的敌人发生的事情。

不幸的是,新 ABI 的 运行 时间检查丢失了。相反,通过在编译调用 _M_start_thread 的代码时创建依赖项并将指向 pthread_create 的指针传递到此函数中,可以为 pthread_create 创建一个 link-time check。不幸的是,这个指针被丢弃了,仍然使用了仍然很弱的 nullptr 指针。

现在 linking/loading 中的某些内容导致弱定义的 pthread_create 在您的问题案例中不会被覆盖。我不确定适用于那里的确切解析规则 - 我认为它与 libpthread 正在加载时 libstdc++ 已经完全加载有关。如果有任何其他答案可以澄清这一点,我将很高兴。不幸的是,除了 link 使用 -lpthreadLD_PRELOAD=libpthread.so 主应用程序(我不会真正推荐)之外,似乎也没有普遍可行的选择来解决这个问题。

问题出在libstdc++

  • 对于 C 程序,这不会发生。
  • 使用 libc++ 构建的 C++ 程序也不会发生这种情况。
  • 对于使用 libstdc++ 静态 构建的 C++ 程序,这也不会发生。
  • 对于使用 libc++ 构建的库,即使调用程序是使用 libstdc++ 动态构建的,也不会发生这种情况。
  • 当程序使用 RTLD_GLOBAL 打开库时,这也不会发生。

因此,一种解决方案是切换到 libc++。显然,这只有在从不导出任何依赖于任何 std:: 类型的接口的情况下才有效。特别是,一个只导出 C 兼容接口的库应该没问题。

另一个解决方案是让你的库加载 RTLD_GLOBAL(你可能必须将它分成两部分,主要的和一个小存根,它只加载主要的 RTLD_GLOBAL) .

同时应该针对 libstdc++ 提交错误并等待修复。没有理由让它像那样被打破。

如果以上 none 是可行的选项,那么唯一的解决方案似乎涉及调用者和多线程模块之间的完全隔离。使多线程模块成为一个单独的可执行文件,从你的插件中 fork-exec 它,通过管道编组 arguments/results to/from 它。

最后,总是有在调用程序中预加载 libpthread 的丑陋解决方法。