gpt4 book ai didi

c++ - 使用libpthread在共享库中有未定义的行为,但在ELF中没有依赖关系

转载 作者:行者123 更新时间:2023-12-01 18:57:52 25 4
gpt4 key购买 nike

当“正确”链接(进一步说明)时,下面的两个函数调用在实现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++的
  • 带有金色链接器的
  • 提供-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中定义为弱符号
  • no libpthread.so作为E​​LF的依赖项

  • 如下所示:
    $ 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++
  • 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_key_createWEAK并且在ELF中找不到 libpthread.so依赖项时,使用pthread破坏shlib的原因是什么?动态链接程序是否从 libc.so(存根)中获取pthread符号而不是 libpthread.so

    最佳答案

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

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

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

    通常,仅在链接最终可执行文件时才使用它,而在链接共享库时则不使用它。如果您只是想使您的库线程安全,但又不想强制每个使用该库的程序与pthread链接,则希望使用运行时检查来查看是否已加载pthreads库,然后调用仅当pthread API存在时。在Linux上,这通常是通过检查“canary”来完成的,例如,对诸如__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记录为依赖项。这比设计更多的是事故;我不认为更改金币以考虑来自其他共享库的引用实际上不是一个可靠的解决方案。我认为正确的解决方案是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_createFUNC而不是NOTYPE时才阻止它?我认为符号的类型是红色鲱 fish ,而真正的问题是由以下事实造成的:黄金没有记录由未添加为所需库的库解析的引用的符号版本。 wait_for的实现调用pthread_cond_timedwait,它在libpthreadlibc中都有两个版本。加载程序可能会将引用绑定(bind)到错误的版本,从而可能因未能解锁互斥锁而导致死锁。我为黄金制作了一个临时补丁来记录这些版本,从而使该程序正常工作。不幸的是,这不是解决方案,因为该补丁可能导致ld.so在其他情况下崩溃。

    我尝试将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的指针初始化cv中的旧pthread_cond_t。之后,当另一个线程到达cv.notify_one时,其实现假定cv包含新样式的pthread_cond_t而不是指向该指针的指针,因此它将使用指向新pthread_mutex_lock的指针而不是指向互斥锁的指针来调用pthread_cond_t。它锁定了可能的互斥锁,但是它从未被解锁,因为另一个线程解锁了真正的互斥锁。

    关于c++ - 使用libpthread在共享库中有未定义的行为,但在ELF中没有依赖关系,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50751421/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com