使用 libpthread 的共享库中的未定义行为,但在 ELF 中没有将其作为依赖项

undefined behavior in shared lib using libpthread, but not having it in ELF as dependency

当链接 "properly"(进一步解释)时,下面的两个函数调用无限期地阻塞在实现 cv.notify_onecv.wait_for:

的 pthread 调用上
// let's call it odr.cpp, which forms libodr.so

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void Notify() {
  std::chrono::milliseconds(100);
  std::unique_lock<std::mutex> lock(mtx);
  ready = true;
  cv.notify_one();
}

void Get() {
  std::unique_lock<std::mutex> lock(mtx);
  cv.wait_for(lock, std::chrono::milliseconds(300));
}

在以下应用程序中使用上述共享库时:

// let's call it test.cpp, which forms a.out

int main() {
  std::thread thr([&]() {
    std::cout << "Notify\n";
    Notify();
  });

  std::cout << "Before Get\n";
  Get();
  std::cout << "After Get\n";

  thr.join();
}

问题仅在链接时重现 libodr.so:

使用以下相关工具版本:

所以我们最终得到:

如图所示:

$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
    10: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __pthread_key_create

另一方面,我们没有遇到以下任何错误:

注意:这次我们有:

如图所示:

$ clang++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out 
 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
    24: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __pthread_key_create@GLIBC_2.2.5 (7)

$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=bfd -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out 
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
    14: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __pthread_key_create

$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out  0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
    18: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __pthread_key_create

$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold -Wl,--no-as-needed -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out 
 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
    10: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __pthread_key_create@GLIBC_2.2.5 (4)

compile/run 的完整示例可在此处找到:https://github.com/aurzenligl/study/tree/master/cpp-pthread

__pthread_key_createWEAK 并且找不到 ELF 中的 libpthread.so 依赖项时,什么会破坏使用 pthread 的 shlib?动态链接器是否从 libc.so(存根)而不是 libpthread.so 中获取 pthread 符号?

这里发生了很多事情:gcc 和 clang 之间的差异,gnu ld 和 gold 之间的差异,--as-needed linker 标志,两种不同的故障模式,甚至可能是一些时间问题。

让我们从如何 link 使用 POSIX 线程的程序开始。

编译器的 -pthread 标志就是您所需要的。这是一个编译器标志,因此在编译使用线程的代码和 link 最终执行 table 时都应该使用它。当您在 link 步骤中使用 -pthread 时,编译器将自动提供 -lpthread 标志,并在 link 行的正确位置。

通常,您只会在 link 最终执行 table 时使用它,而不会在 link 共享库时使用它。如果你只是想让你的库线程安全,但不想强制每个使用你的库的程序都使用 pthreads link,你会想要使用运行时检查来查看是否加载了 pthreads 库, 并且仅当它是时才调用 pthread API。在 Linux 上,这通常是通过检查 "canary" 来完成的——例如,对像 __pthread_key_create 这样的任意符号进行弱引用,只有在加载库时才会定义它,如果程序 linked 没有它,则值为 0。

但是,在您的情况下,您的库 libodr.so 很大程度上取决于线程,因此使用 -pthread 标志 link 它是合理的。

这给我们带来了第一个失败模式:如果您在两个 link 步骤中都使用 g++ 和 gold,程序会抛出 std::system_error 并提示您需要启用多线程。这是由于 --as-needed 标志。 GCC 默认将 --as-needed 传递给 linker,而 clang(显然)不会。使用 --as-needed,linker 将只记录解析强引用的库依赖项。由于对 pthread API 的所有引用都很弱,其中 none 足以告诉 linker libpthread.so 应该添加到依赖列表(通过 DT_NEEDED 条目在动态 table)。改成clang或者加一个-Wl,--no-as-needed标志就解决了这个问题,程序会加载pthread库

但是,等等,为什么在使用 Gnu linker 时不需要这样做?它使用相同的规则:只有强引用才会导致库被记录为依赖项。不同之处在于 Gnu ld 还考虑来自其他共享库的引用,而 gold 只考虑来自常规目标文件的引用。事实证明,pthread 库提供了几个 libc 符号的覆盖定义,并且 libstdc++.so 对其中一些符号(例如 write)有强引用。这些强引用足以让 Gnu ld 将 libpthread.so 记录为依赖项。这与其说是设计,不如说是意外。我不认为更改 gold 以考虑来自其他共享库的引用实际上是一个可靠的修复。我认为正确的解决方案是 GCC 在使用 -pthread.

时将 --no-as-needed 放在 -lpthread 标志前面

这引出了一个问题,即为什么在使用 POSIX 线程和黄金 linker 时这个问题不会一直出现。但这是一个小测试程序;一个更大的程序几乎肯定会包含对 libpthread.so 覆盖的某些 libc 符号的强引用。

现在让我们看一下第二种失败模式,如果您 link libodr.so 使用 g++、gold 和 [=13=,Notify()Get() 都会无限期地阻塞].

Notify() 中,您在调用 cv.notify_one() 时一直持有锁直到函数结束。你真的只需要持有锁来设置就绪标志;如果我们更改它以便在此之前释放锁,那么调用 Get() 的线程将在 300 毫秒后超时,并且不会阻塞。所以真正阻塞的是对 notify_one() 的调用,并且程序处于死锁状态,因为 Get() 正在等待同一个锁。

那为什么只有当__pthread_key_createFUNC而不是NOTYPE时才阻塞呢?我认为符号的类型是一个转移注意力的问题,真正的问题是 gold 没有记录由未添加为所需库的库解析的引用的符号版本。 wait_for 的实现调用了 pthread_cond_timedwait,它在 libpthreadlibc 中都有两个版本。加载器可能将引用绑定到错误的版本,导致无法解锁互斥体而导致死锁。我为 gold 做了一个临时补丁来记录这些版本,这让程序运行起来了。不幸的是,这不是解决方案,因为该补丁可能会导致 ld.so 在其他情况下崩溃。

我尝试将cv.wait_for(...)更改为cv.wait(lock, []{ return ready; }),程序在所有情况下都运行完美,这进一步表明问题出在pthread_cond_timedwait

底线是添加 --no-as-needed 标志将解决这个非常小的测试用例的问题。任何更大的东西都可能在没有额外标志的情况下工作,因为您将增加对 libpthread 中的符号进行强引用的可能性。 (例如,在 odr.cpp 中添加对 std::this_thread::sleep_for anywhere 的调用会添加对 nanosleep 的强引用,从而将 libpthread 放入需要的清单。)

更新: 我已经确认失败的程序 link 正在访问 pthread_cond_timedwait 的错误版本。对于 glibc 2.3.2,pthread_cond_t 类型已更改,使用该类型的旧版本 API 已更改为动态分配新的(更大的)结构并在原始类型中存储指向它的指针。所以现在,如果消费线程在生产线程达到 cv.notify_one 之前达到 cv.wait_forcv.wait_for 的实现将调用旧版本的 pthread_cond_timedwait,它会初始化它认为是cv 中的旧 pthread_cond_t 带有指向新 pthread_cond_t 的指针。之后,当另一个线程到达 cv.notify_one 时,它的实现假定 cv 包含一个新样式 pthread_cond_t 而不是指向一个的指针,因此它调用 pthread_mutex_lock指向新 pthread_cond_t 的指针而不是指向互斥体的指针。它锁定了那个可能的互斥锁,但它永远不会被解锁,因为另一个线程解锁了真正的互斥锁。