当使用g++进行静态链接pthread时,为什么会导致段错误?

39
#include <iostream>
#include <map>
#include <thread>

#define SIZE 1024
#define AMOUNT 100000
#define THREADS 4

class A
{
private:
    char a[SIZE];
};

void test()
{
    std::cout << "test start\n";
    std::map<int, A*> container;
    for(int i=0; i<AMOUNT; i++)
    {
        A* a = new A();
        std::pair<int, A*>p = std::make_pair(i, a);
        container.insert(p);
    }

    std::cout << "test release\n";
    for(int i=0; i<AMOUNT; i++)
    {
        auto iter = container.find(i);
        delete iter->second;
        container.erase(iter);
    }
    std::cout << "test end\n";
}

int main()
{
    std::thread ts[THREADS];
    for(int i=0; i<THREADS; i++)
    {
        ts[i] = std::thread(test);
    }

    for(std::thread& x: ts)
    {
        x.join();
    }

    return 0;
}

上面是一段简单的C++代码。

使用以下命令进行编译:g++ -pthread -o one one.cpp -Wall -std=c++11 -O3

ldd one,输出如下:

    linux-vdso.so.1 =>  (0x00007ffebafce000)
    libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fb47352a000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fb473313000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fb4730f4000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb472d2a000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fb472a22000)
    /lib64/ld-linux-x86-64.so.2 (0x00005654c5112000)

运行 ./one,一切正常。

然后我尝试一个静态链接:g++ -pthread -o one one.cpp -Wall -std=c++11 -O3 -static

ldd one,得到:

    not a dynamic executable

但是当我运行它时,出了一些问题...

test start
Segmentation fault (core dumped)

重新使用-g编译,然后gdb会显示:

wang[00:35][~/test]$ gdb one
GNU gdb (Ubuntu 7.10-1ubuntu2) 7.10
Copyright (C) 2015 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from one...done.
(gdb) run
Starting program: /home/wang/test/one 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7ffa700 (LWP 3623)]
test start
[New Thread 0x7ffff77f8700 (LWP 3624)]
test start
[New Thread 0x7ffff6ff7700 (LWP 3625)]
test start
[New Thread 0x7ffff67f6700 (LWP 3626)]
test start

Program received signal SIGSEGV, Segmentation fault.
0x0000000000000000 in ?? ()
(gdb) 

为什么要这样做?

更新 ==============================

使用 boost::thread 库(版本:1.60),

替换 std::threadboost::thread,并进行静态链接,

g++ -pthread -o one1 one.cpp -Wall -std=c++11 -O3 -I /opt/boost/include/ -L /opt/boost/lib/ -lboost_system -lboost_thread -static

没有发生任何问题!

困惑......


1
看起来很像这个错误报告中的问题。顺便问一下,为什么要静态链接呢?它能工作吗?为什么不直接使用:g++ -o one one.cpp -Wall -std=c++11 -O3 pthread - P.P
非常感谢!这救了我的一天,我正在使用带有g++的gem5,因此必须进行静态链接,第一个答案对我有效。我的GEM5错误信息是panic: Page table fault when accessing virtual address 0 - Y00
在使用boost :: asio包含后,这个问题也消失了。奇怪。 - Kidsunbo
2个回答

75

首先,解决方案在这里。这里的方法可行:

更新: 自Ubuntu 18.04起, 您还需要链接librt(添加-lrt):

g++ -o one one.cpp -Wall -std=c++11 -O3 -static -lrt -pthread \
    -Wl,--whole-archive -lpthread -Wl,--no-whole-archive

(continue with original answer)

g++ -o one one.cpp -Wall -std=c++11 -O3 -static -pthread \
    -Wl,--whole-archive -lpthread -Wl,--no-whole-archive

当您使用-pthread时,编译器已经链接了pthread(根据平台不同,还会定义额外的宏,例如-D_REENTRANT,详见这个问题)。因此,如果-pthread意味着-lpthread,那么在进行静态链接时为什么还需要指定-lpthread?而Wl,--whole-archive是什么意思?
理解弱符号
在Unix上,使用ELF文件格式,其中包含弱符号和强符号的概念。引用自维基百科页面
“默认情况下,在没有任何注释的情况下,对象文件中的符号是强符号。在链接期间,强符号可以覆盖同名的弱符号。相反,共享名称的两个强符号会在链接时间产生链接错误。”
在动态库和静态库方面存在微妙差异。在静态库中,即使第一个符号是弱符号,链接器也会停止查找强符号。为了强制它查找所有符号(就像它为动态链接库所做的那样),ld支持--whole-archive选项。

引用自man ld

--whole-archive: 对于--whole-archive选项之后命令行中提到的每个归档文件,将归档文件中的每个目标文件都包含在链接中,而不是搜索所需的目标文件。这通常用于将归档文件转换为共享库,强制将结果共享库中的每个目标文件都包含在内。此选项可以多次使用。

它还解释了从gcc开始,您必须将选项传递为-Wl,--whole-archive

使用此选项时,请注意两点:首先,gcc不知道此选项,因此您必须使用-Wl,-whole-archive。其次,在列出档案清单之后不要忘记使用-Wl,-no-whole-archive,因为gcc会将其自己的档案列表添加到您的链接中,您可能不希望该标志也影响到它们。

并且再次解释了如何关闭它:

--no-whole-archive: 关闭--whole-archive选项对后续档案文件的影响。

pthread和libstdc++中的弱符号

弱符号的用例之一是能够使用优化的实现进行交换。另一个是使用存根,如果需要可以稍后替换。

例如,fputc概念上由printf使用 )需要遵循 POSIX 的线程安全要求并进行同步,这是代价高昂的。在单线程环境中,您不希望支付成本。因此,实现可以将同步函数实现为空存根,并声明函数为弱符号。
稍后,如果链接了多线程库(例如 pthread),则明显不打算支持单线程。在链接多线程库时,链接器可以用真实的同步函数(定义为强符号并由线程库实现)替换存根。另一方面,如果没有链接多线程库,可执行文件将使用同步功能的存根。
glibc(提供fputc)和 pthreads 似乎正是使用这个技巧。有关详情,请参阅在glibc中使用弱符号的问题。上述示例摘自这个答案nm允许您详细查看它,这与上述引用的答案一致:
$ nm /usr/lib/libc.a 2>/dev/null | grep pthread_mutex_lock
w __pthread_mutex_lock
... (repeats)

"w"代表"weak",因此静态链接的libc库包含__pthread_mutex_lock作为弱符号。静态链接的pthread库将其作为强符号包含:
$ nm /usr/lib/libpthread.a 2>/dev/null | grep pthread_mutex_lock
             U pthread_mutex_lock
pthread_mutex_lock.o:
00000000000006a0 T __pthread_mutex_lock
00000000000006a0 T pthread_mutex_lock
0000000000000000 t __pthread_mutex_lock_full

回到示例程序

通过查看动态链接可执行文件的共享库依赖关系,我得到了在我的机器上几乎与ldd相同的输出:

$ ldd one
linux-vdso.so.1 (0x00007fff79d6d000)
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00007fcaaeeb3000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007fcaaeb9b000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007fcaae983000)
libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007fcaae763000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fcaae3bb000)
/lib64/ld-linux-x86-64.so.2 (0x00007fcaaf23b000)

使用ltrace打印库调用,会得到以下输出:
$ ltrace -C ./one 
std::ios_base::Init::Init()(0x563ab8df71b1, 0x7ffdc483cae8, 0x7ffdc483caf8, 160) = 0
__cxa_atexit(0x7fab3023bc20, 0x563ab8df71b1, 0x563ab8df7090, 6)         = 0
operator new(unsigned long)(16, 0x7ffdc483cae8, 0x7ffdc483caf8, 192)    = 0x563ab918bc20
std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)())(0x7ffdc483c990, 0x7ffdc483c998, 0x7fab2fa52320, 0x7fab2fa43a80) = 0
operator new(unsigned long)(16, 0x7fab2f6a1fb0, 0, 0x800000)            = 0x563ab918bd70
std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)())(0x7ffdc483c990, 0x7ffdc483c998, 0x7fab2fa52320, 0x7fab2fa43a80) = 0
operator new(unsigned long)(16, 0x7fab2eea0fb0, 0, 0x800000)            = 0x563ab918bec0
std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)())(0x7ffdc483c990, 0x7ffdc483c998, 0x7fab2fa52320, 0x7fab2fa43a80test start
) = 0
operator new(unsigned long)(16, 0x7fab2e69ffb0, 0, 0x800000)            = 0x563ab918c010
std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)())(0x7ffdc483c990, 0x7ffdc483c998, 0x7fab2fa52320, 0x7fab2fa43a80test start
test start
) = 0
std::thread::join()(0x7ffdc483c9a0, 0x7fab2de9efb0, 0, 0x800000test start
test release
test release
test release
test release
test end
test end
test end
test end
)        = 0
std::thread::join()(0x7ffdc483c9a8, 0x7fab2eea19c0, 0x7fab2f6a2700, 0)  = 0
std::thread::join()(0x7ffdc483c9b0, 0x7fab2e6a09c0, 0x7fab2eea1700, 0)  = 0
std::thread::join()(0x7ffdc483c9b8, 0x7fab2de9f9c0, 0x7fab2e6a0700, 0)  = 0
+++ exited (status 0) +++

作为一个例子,调用std::thread::join,它很可能会在内部使用pthread_join。这个符号可以在ldd输出中列出的(动态链接的)库中找到,即在libstdc++.so.6libpthread.so.0中:
$ nm /usr/lib/libstdc++.so.6 | grep pthread_join
                 w pthread_join

$ nm /usr/lib/libpthread.so.0 | grep pthread_join
0000000000008280 T pthread_join

在动态链接的可执行文件中,链接器将弱符号替换为强符号。在本例中,我们需要对静态链接库强制执行相同的语义。这就是为什么需要 -Wl,--whole-archive -lpthread -Wl,--no-whole-archive 的原因。
找到它有点试错。至少我没有找到关于这个主题的清晰文档。我认为这是因为Linux上的静态链接变得越来越少见, 而动态链接通常是使用库的规范方法(有关比较,请参见Static linking vs dynamic linking)。我看到并且亲自努力了一段时间才让它工作的最极端的例子是静态链接 TBB
附录:Autotools 的解决方法
如果您正在使用 Autotools 作为构建系统,则需要一个解决方法,因为 automake 不允许您在 LDADD 中设置选项。不幸的是,您不能编写:
(Makefile.am)
mytarget_LDADD = -Wl,--whole-archive -lpthread -Wl,--no-whole-archive

作为解决方法,您可以通过在configure.ac中定义标志并像这样使用它们来避免检查:
(configure.ac)
WL_WHOLE_ARCHIVE_HACK="-Wl,--whole-archive"
WL_NO_WHOLE_ARCHIVE_HACK="-Wl,--no-whole-archive"
AC_SUBST(WL_WHOLE_ARCHIVE_HACK)
AC_SUBST(WL_NO_WHOLE_ARCHIVE_HACK)

(Makefile.am)
mytarget_LDADD = @WL_WHOLE_ARCHIVE_HACK@ -lpthread @WL_NO_WHOLE_ARCHIVE_HACK@

1
我有完全相同的问题。动态链接的可执行文件运行良好(正确的结果和valgrind很高兴)。我第一次尝试创建静态链接的可执行文件失败了(valgrind错误,在thread.join()上出现段错误)。你的解决方案(谢谢!)只部分地解决了我的问题。我可以创建一个多线程代码的静态链接可执行文件(似乎没有段错误),但是我有很多valgrind错误。 - Alexander
多年前,当我加入我现在工作的项目时,他们正在使用XView。当我重新编写构建时,我发现一些工具神秘地死锁了。XView提供了自己的libc API实现(read、write、select等),以允许一种“任务交换”,以确保GUI在阻塞时不停止渲染。一些库链接的顺序改变了,因此在遇到libxview之前就遇到了libc、libpthread、libsocket(等),导致死锁。 - Brian Vandenberg
1
以下程序:https://github.com/cirosantilli/linux-kernel-module-cheat/blob/4aff114c4c654014a97ef23b1513dda5409e79f3/userland/cpp/thread_get_id.cpp 即使我尝试使用额外的选项进行编译,仍然会导致段错误: g++ thread_get_id.cpp -Wall -std=c++11 -O3 -static -pthread -Wl,--whole-archive -lpthread -Wl,--no-whole-archive 在Ubuntu 18.04,GCC 7.4.0中。 - Ciro Santilli OurBigBook.com
1
确认。我也遇到了分段错误(相同的设置:Ubuntu 18.04,gcc 7.4.0)。有趣的例子。 - Philipp Claßen
@PhilippClaßen 我正在使用 g++ (Debian 8.3.0-2) 8.3.0,但这种方法不起作用。:( 如果我找到解决方法,我会在这里发布更新。 - Y00

4
当我链接一个使用pthread的预编译C++ .a 归档文件时,我遇到了类似的问题。在我的情况下,除了需要加入-Wl,--whole-archive -lpthread -Wl,--no-whole-archive之外,我还需要为每个弱符号执行-Wl,-u,...
我的症状是运行时崩溃,当使用gdb进行反汇编时,我可以看到崩溃发生在callq 0x0之后,这看起来很可疑。做了一些搜索并发现其他人也在静态链接pthread时看到了这个问题。
我使用nm找到要强制解析的符号,并进行链接后,我可以看到callq 0x0指令已更新为各种符号地址,如pthread_mutex_lock等。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接