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

时间:2018-06-07 23:26:21

标签: c++ linux gcc pthreads gold-linker

当链接"正确" (进一步解释),下面的两个函数调用都无限期地阻塞了实现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

时才会重现问题
  • with g ++
  • with gold linker
  • 提供-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 linker
  • 没有明确的-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_key_createWEAK并且ELF中没有libpthread.so依赖项时,使用pthread打破shlib是什么?动态链接器是否从libc.so(存根)而不是libpthread.so获取pthread符号?

1 个答案:

答案 0 :(得分:4)

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

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

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

通常,您只在链接最终可执行文件时使用它,而不是在链接共享库时使用它。如果您只是想让您的库线程安全,但又不想强制每个使用您的库的程序与pthreads链接,那么您需要使用运行时检查来查看是否已加载pthreads库,只有在调用pthread API时才调用它。在Linux上,这通常通过检查&#34; canary&#34; - 例如,对__pthread_key_create之类的任意符号进行弱引用,只有在加载库时才会定义,如果程序在没有它的情况下被链接,则会为0。

但是,在您的情况下,您的库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记录为依赖。这更像是一场意外而非设计;我不认为改变黄金来考虑来自其他共享库的引用实际上是一个强大的修复。我认为正确的解决方案是,当您使用--no-as-needed时,GCC会将-lpthread放在-pthread标记的前面。

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

现在让我们看看第二种失败模式,如果你将Notify()与g ++,gold和Get()联系起来,libodr.so-lpthread都会无限期阻止。

Notify()中,当您致电cv.notify_one()时,您在功能结束时持有锁。你真的只需要按住锁来设置就绪标志;如果我们更改它以便在此之前释放锁,那么调用Get()的线程将在300 ms后超时,并且不会阻塞。因此,notify_one()阻止了Get()的呼叫,并且该程序因为__pthread_key_create正在等待同一个锁而死锁。

那么为什么只有当FUNCNOTYPE而不是wait_for时才会阻止?我认为符号的类型是红色鲱鱼,真正的问题是由于黄金没有记录由未被添加为所需库的库解析的引用的符号版本这一事实。 。 pthread_cond_timedwait调用libpthread的实施,libccv.wait_for(...)都有两个版本。加载程序可能将引用绑定到错误的版本,导致无法解锁互斥锁导致死锁。我为黄金制作了一个临时补丁来记录这些版本,这使得程序运行起来。不幸的是,这不是解决方案,因为该补丁可能会导致ld.so在其他情况下崩溃。

我尝试将cv.wait(lock, []{ return ready; })更改为pthread_cond_timedwait,并且该程序在所有情况下都运行良好,这进一步表明问题出在--no-as-needed

最重要的是,添加libpthread标志将解决这个非常小的测试用例的问题。任何更大的东西都可能在没有额外标志的情况下工作,因为您将增加在std::this_thread::sleep_for中对符号进行强引用的几率。 (例如,在odr.cpp中的任意位置添加对nanosleep 的调用会添加对libpthread的强引用,这会将pthread_cond_timedwait放入所需列表中。 )

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