4

当“正确”链接(进一步解释)时,下面的两个函数调用都会在 pthread 调用实现时无限期阻塞,cv.notify_one并且cv.wait_for

// 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++
  • 带黄金链接器
  • -lpthread作为依赖提供

使用以下版本的相关工具:

  • Linux Mint 18.3 Sylvia
  • binutils 2.26.1-1ubuntu1~16.04.6
  • g++ 4:5.3.1-1ubuntu1
  • libc6:amd64 2.23-0ubuntu10

所以我们最终得到:

  • __pthread_key_create在 PLT 中定义为 WEAK 符号
  • 没有libpthread.so作为 ELF 中的依赖项

如此处所示:

$ 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

另一方面,对于以下任何一种情况,我们都不会遇到错误:

  • 铿锵++
  • bfd 链接器
  • 没有明确的-lpthread
  • -lpthread但与-Wl,--no-as-needed

注意:这次我们有:

  • NOTYPE并且没有libpthread.so依赖
  • WEAKlibpthread.so依赖

如此处所示:

$ 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)

可以在这里找到编译/运行的完整示例:https ://github.com/aurzenligl/study/tree/master/cpp-pthread

什么时候使用 pthread 破坏 shlib并且在 ELF 中找不到依赖项__pthread_key_create?动态链接器是否从(stubs) 获取 pthread 符号而不是?WEAKlibpthread.solibc.solibpthread.so

4

1 回答 1

4

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

让我们从如何使用 POSIX 线程链接程序开始。

The compiler's -pthread flag is all you should need. It's a compiler flag, so you should use it both when compiling code that uses threads and when linking the final executable. When you use -pthread on the link step, the compiler will provide the -lpthread flag automatically, and in the right place in the link line.

通常,您只会在链接最终可执行文件时使用它,而不是在链接共享库时使用它。如果您只是想让您的库线程安全,但不想强制使用您的库的每个程序都与 pthreads 链接,那么您需要使用运行时检查来查看 pthreads 库是否已加载,并调用仅当它是 pthread API 时。在 Linux 上,这通常通过检查“金丝雀”来完成——例如,对任意符号__pthread_key_create(如没有它。

但是,在您的情况下,您的库libodr.so几乎取决于线程,因此将其与-pthread标志链接是合理的。

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

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

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

现在让我们看看第二种故障模式,如果您Notify()与g++、gold 和.Get()libodr.so-lpthread

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

那么为什么它只在__pthread_key_createisFUNC而不是时阻塞NOTYPE呢?我认为符号的类型是红鲱鱼,真正的问题是由于 gold 没有记录由未添加为所需库的库解析的引用的符号版本。wait_for调用的实现,在和pthread_cond_timedwait中都有两个版本。加载程序可能会将引用绑定到错误的版本,导致无法解锁互斥锁而导致死锁。我为黄金制作了一个临时补丁来记录这些版本,这使得程序可以正常工作。不幸的是,这不是解决方案,因为该补丁可能导致 ld.so 在其他情况下崩溃。libpthreadlibc

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

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

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

于 2018-06-09T19:15:21.740 回答