代码之家  ›  专栏  ›  技术社区  ›  aurzenligl

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

  •  4
  • aurzenligl  · 技术社区  · 6 年前

    当链接“正确”(进一步解释)时,下面的两个函数调用都无限期地在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中定义为弱符号
    • 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连接器
    • 没有明确 -LP-线程
    • -LP-线程 但是随着 -Wl,--no-as-needed

    注:这次我们有:

    • NOTYPE 而且没有 libpthread.so文件 附属国
    • WEAK libpthread.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 _ pthread_key_创建 弱的 而且没有 libpthread.so文件 ELF中的依赖项是否可以找到?动态链接器是否从 libc.so (存根)而不是 libpthread.so文件 ?

    1 回复  |  直到 6 年前
        1
  •  4
  •   Cary Coutant    6 年前

    这里发生了很多事情:海湾合作委员会和克朗之间的差异,gnu ld和gold之间的差异,以及 --as-needed 链接器标志,两种不同的故障模式,甚至可能有些时间问题。

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

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

    通常,只在链接最终可执行文件时使用,而在链接共享库时不使用。如果您只是想使库线程安全,但不想强制使用库链接pthreads的每个程序,那么您需要使用运行时检查来查看pthreads库是否已加载,并且仅在pthread库已加载时才调用pthread api。在Linux上,这通常是通过检查“canary”来完成的——例如,对任意符号进行弱引用,如 __pthread_key_create ,只有在加载库时才会定义,如果程序链接时没有该库,则其值为0。

    但在你的情况下,你的图书馆 libodr.so 很大程度上取决于线程,因此将它与 -线程 旗帜。

    这就引出了第一种失败模式:如果在两个链接步骤中都使用g++和gold,程序将抛出 std::system_error 并表示需要启用多线程。这是因为 ——根据需要 旗帜。海湾合作委员会通行证 ——根据需要 默认为链接器,而clang(显然)没有。用 ——根据需要 ,链接器将只记录解析强引用的库依赖项。由于对pthread api的所有引用都很弱,因此它们都不足以告诉链接器libpthread.so应该添加到依赖项列表中(通过 DT_NEEDED 动态表中的条目)。更改为Clang或添加 -Wl,--no-as-needed flag解决了这个问题,程序将加载pthread库。

    但是,等等,为什么在使用gnu链接器时不需要这样做呢?它使用相同的规则:只有强引用才会将库记录为依赖项。不同之处在于gnu ld还考虑来自其他共享库的引用,而gold只考虑来自常规对象文件的引用。结果发现pthread库提供了几个libc符号的重写定义,并且有来自 libstdc++.so 一些符号(例如, write )那些有力的参考资料足以让GNU LD记录下来 libpthread.so 作为一种依赖。与设计相比,这更像是一个意外;我不认为将gold更改为考虑来自其他共享库的引用实际上是一个健壮的修复。我认为正确的解决方案是让gcc --no-as-needed -LP-线程 使用时标记 -线程

    这就引出了一个问题:为什么在使用posix线程和gold链接器时,这个问题不会一直出现。但是这是一个小的测试程序;一个大的程序几乎可以肯定包含了对那些libc符号的强引用 libpthread.so文件 重写。

    现在让我们看看第二种失败模式,其中 Notify() Get() 如果链接到 李博德 使用g++、gold和 -LP-线程 .

    通知() ,在调用 cv.notify_one() . 您只需要按住锁来设置ready标志;如果我们更改它以便在此之前释放锁,则线程调用 获取() 将在300毫秒后超时,并且不会阻塞。所以这真的是 notify_one() 那是阻塞,程序死锁是因为 获取() 在等同一把锁。

    为什么只有在 _ pthread_key_创建 FUNC 而不是 NOTYPE ?我认为符号的类型是一个红鲱鱼,而真正的问题是由于gold没有记录符号版本以供未添加为所需库的库解决的引用。实施 wait_for 电话 pthread_cond_timedwait ,两个版本都有 libpthread libc . 加载程序可能将引用绑定到了错误的版本,导致无法解锁互斥锁而导致死锁。我给gold做了一个临时补丁来记录这些版本,这使得程序能够工作。不幸的是,这不是一个解决方案,因为该修补程序可能导致ld.so在其他情况下崩溃。

    我试着改变 cv.wait_for(...) cv.wait(lock, []{ return ready; }) ,并且程序在所有场景中都能完美运行,这进一步表明问题出在 线程状态等待 .

    底线是 --不需要 flag将修复这个非常小的测试用例的问题。任何更大的都可能在没有额外标志的情况下工作,因为在 LIPP线程 . (例如,添加对 std::this_thread::sleep_for 在任何地方 在里面 odr.cpp 添加对的强引用 nanosleep ,提出 LIPP线程 在需要的列表中。)

    更新: 我已经验证了失败的程序链接到错误的 线程状态等待 . 对于glibc 2.3.2来说, pthread_cond_t 类型已更改,使用该类型的api的旧版本已更改,以动态分配新的(更大的)结构并将指向该结构的指针存储在原始类型中。所以现在,如果消费线程到达 cv.wait_for 在生产线到达之前 cv.notify_one ,执行 瓦伊塔 调用旧版本的 线程状态等待 ,它初始化它认为是旧的 线程状态 在里面 cv 用指针指向新的 线程状态 . 之后,当另一个线程到达 简历通知一 ,其实现假定 履历 包含新样式 线程状态 而不是指向一个的指针,所以它调用 pthread_mutex_lock 用指针指向新的 线程状态 而不是指向互斥锁的指针。它锁定了可能的互斥锁,但它永远不会被解锁,因为另一个线程解锁了真正的互斥锁。

    推荐文章