这是给出段错误的程序。

#include <iostream>
#include <vector>
#include <memory>

int main()
{
    std::cout << "Hello World" << std::endl;

    std::vector<std::shared_ptr<int>> y {};

    std::cout << "Hello World" << std::endl;
}

当然,程序本身没有绝对没有错。段故障的根本原因取决于段的构建和运行环境。

背景

在亚马逊,我们使用一个构建系统,该系统以几乎与机器无关的方式构建和部署二进制文件(libbin)。对于我们的情况,这基本上意味着它将可执行文件(从上述程序构建)部署到$project_dir/build/bin/中,几乎所有依赖项(即共享库)都部署到$project_dir/build/lib/中。我之所以使用短语“几乎”是因为对于诸如libc.solibm.sold-linux-x86-64.so.2以及可能还有其他少数几个共享库,可执行文件是从系统中选择的(即从/lib64)。注意,应该从libstdc++中选择$project_dir/build/lib

现在,我将其运行如下:
$ LD_LIBRARY_PATH=$project_dir/build/lib ./build/bin/run

segmentation fault

但是,如果我运行它,而没有设置LD_LIBRARY_PATH。运行正常。

诊断程序

1. ldd

这是这两种情况的ldd信息(请注意,我编辑了输出以提及存在差异之处的完整版本的库)
$ LD_LIBRARY_PATH=$project_dir/build/lib ldd ./build/bin/run

linux-vdso.so.1 =>  (0x00007ffce19ca000)
libstdc++.so.6 => $project_dir/build/lib/libstdc++.so.6.0.20
libgcc_s.so.1 =>  $project_dir/build/lib/libgcc_s.so.1
libc.so.6 => /lib64/libc.so.6
libm.so.6 => /lib64/libm.so.6
/lib64/ld-linux-x86-64.so.2 (0x0000562ec51bc000)

并且没有LD_LIBRARY_PATH:
$ ldd ./build/bin/run

linux-vdso.so.1 =>  (0x00007fffcedde000)
libstdc++.so.6 => /usr/lib64/libstdc++.so.6.0.16
libgcc_s.so.1 => /lib64/libgcc_s-4.4.6-20110824.so.1
libc.so.6 => /lib64/libc.so.6
libm.so.6 => /lib64/libm.so.6
/lib64/ld-linux-x86-64.so.2 (0x0000560caff38000)

2. gdb出现段错误时
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7dea45c in _dl_fixup () from /lib64/ld-linux-x86-64.so.2
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.209.62.al12.x86_64
(gdb) bt
#0  0x00007ffff7dea45c in _dl_fixup () from /lib64/ld-linux-x86-64.so.2
#1  0x00007ffff7df0c55 in _dl_runtime_resolve () from /lib64/ld-linux-x86-64.so.2
#2  0x00007ffff7b1dc41 in std::locale::_S_initialize() () from $project_dir/build/lib/libstdc++.so.6
#3  0x00007ffff7b1dc85 in std::locale::locale() () from $project_dir/build/lib/libstdc++.so.6
#4  0x00007ffff7b1a574 in std::ios_base::Init::Init() () from $project_dir/build/lib/libstdc++.so.6
#5  0x0000000000400fde in _GLOBAL__sub_I_main () at $project_dir/build/gcc-4.9.4/include/c++/4.9.4/iostream:74
#6  0x00000000004012ed in __libc_csu_init ()
#7  0x00007ffff7518cb0 in __libc_start_main () from /lib64/libc.so.6
#8  0x0000000000401021 in _start ()
(gdb)

3. LD_DEBUG =全部

我还尝试通过为segfault案例启用LD_DEBUG=all来查看链接器信息。我发现了一些可疑的东西,因为它搜索pthread_once符号,当找不到它时,就会出现段错误(这是我对以下输出片段BTW的解释):
initialize program: $project_dir/build/bin/run

symbol=_ZNSt8ios_base4InitC1Ev;  lookup in file=$project_dir/build/bin/run [0]
symbol=_ZNSt8ios_base4InitC1Ev;  lookup in file=$project_dir/build/lib/libstdc++.so.6 [0]
binding file $project_dir/build/bin/run [0] to $project_dir/build/lib/libstdc++.so.6 [0]: normal symbol `_ZNSt8ios_base4InitC1Ev' [GLIBCXX_3.4]
symbol=_ZNSt6localeC1Ev;  lookup in file=$project_dir/build/bin/run [0]
symbol=_ZNSt6localeC1Ev;  lookup in file=$project_dir/build/lib/libstdc++.so.6 [0]
binding file $project_dir/build/lib/libstdc++.so.6 [0] to $project_dir/build/lib/libstdc++.so.6 [0]: normal symbol `_ZNSt6localeC1Ev' [GLIBCXX_3.4]
symbol=pthread_once;  lookup in file=$project_dir/build/bin/run [0]
symbol=pthread_once;  lookup in file=$project_dir/build/lib/libstdc++.so.6 [0]
symbol=pthread_once;  lookup in file=$project_dir/build/lib/libgcc_s.so.1 [0]
symbol=pthread_once;  lookup in file=/lib64/libc.so.6 [0]
symbol=pthread_once;  lookup in file=/lib64/libm.so.6 [0]
symbol=pthread_once;  lookup in file=/lib64/ld-linux-x86-64.so.2 [0]

但是在成功运行的情况下,我看不到任何pthread_once!

问题

我知道这样很难调试,而且可能我还没有提供很多有关环境及所有方面的信息。但是,我的问题仍然是:此段错误的可能根本原因是什么?如何进一步调试并找到?一旦发现问题,修复将很容易。

编译器和平台

我在RHEL5上使用 GCC 4.9

实验

E#1

如果我评论以下行:
std::vector<std::shared_ptr<int>> y {};

它可以编译并正常运行!

E#2

我只是在程序中包含以下 header :
#include <boost/filesystem.hpp>

并据此进行链接。现在,它可以正常运行而不会出现任何段错误。因此,似乎通过依赖libboost_system.so.1.53.0.,可以满足一些要求,或者可以解决问题!

E#3

由于我在使可执行文件与libboost_system.so.1.53.0链接时看到了它的工作原理,因此我逐步进行了以下操作。

我不使用代码本身中的#include <boost/filesystem.hpp>,而是使用原始代码并通过使用libboost_system.so预先加载LD_PRELOAD来运行它,如下所示:
$ LD_PRELOAD=$project_dir/build/lib/libboost_system.so $project_dir/build/bin/run

它成功运行了!

接下来,我在ldd上执行了libboost_system.so,它给出了一个lib列表,其中两个是:
  /lib64/librt.so.1
  /lib64/libpthread.so.0

因此,我没有预加载libboost_system,而是分别预加载了librtlibpthread:
$ LD_PRELOAD=/lib64/librt.so.1 $project_dir/build/bin/run

$ LD_PRELOAD=/lib64/libpthread.so.0 $project_dir/build/bin/run

在这两种情况下,它都能成功运行。

现在,我的结论是,通过加载librtlibpthread(或两者),可以满足某些要求或可以解决问题!不过,我仍然不知道问题的根本原因。

编译和链接选项

由于构建系统很复杂,默认情况下有很多选项。因此,我尝试使用CMake的-lpthread命令显式添加set,然后它起作用了,因为我们已经看到,通过预加载libpthread可以起作用!

为了查看这两种情况之间的构建差异(当它工作时和当它出现段错误时),我通过将-v传递给GCC以详细模式进行构建,以查看编译阶段及其实际通过的选项到cc1plus(编译器)和collect2(链接器)。

(请注意,为简洁起见,已使用美元符号和虚拟路径对路径进行了编辑。)



不管是否有效,cc1plus的命令行参数都完全相同。完全没有区别。这似乎不是很有帮助。

但是,区别在于链接时间。我看到的是(适用于):



如您所见,两次被提及为两次!在给定segfault 的情况下,第一个-lpthread(后跟-lpthread)缺少。那是这两种情况之间的唯一区别。

两种情况下--as-needed的输出

有趣的是,两种情况下nm -C的输出都是相同的(如果忽略了第一列中的整数值)。
0000000000402580 d _DYNAMIC
0000000000402798 d _GLOBAL_OFFSET_TABLE_
0000000000401000 t _GLOBAL__sub_I_main
0000000000401358 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 w _Jv_RegisterClasses
                 U _Unwind_Resume
0000000000401150 W std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_destroy()
0000000000401170 W std::vector<std::shared_ptr<int>, std::allocator<std::shared_ptr<int> > >::~vector()
0000000000401170 W std::vector<std::shared_ptr<int>, std::allocator<std::shared_ptr<int> > >::~vector()
0000000000401250 W std::vector<std::unique_ptr<int, std::default_delete<int> >, std::allocator<std::unique_ptr<int, std::default_delete<int> > > >::~vector()
0000000000401250 W std::vector<std::unique_ptr<int, std::default_delete<int> >, std::allocator<std::unique_ptr<int, std::default_delete<int> > > >::~vector()
                 U std::ios_base::Init::Init()
                 U std::ios_base::Init::~Init()
0000000000402880 B std::cout
                 U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000402841 b std::__ioinit
                 U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
                 U operator delete(void*)
                 U operator new(unsigned long)
0000000000401510 r __FRAME_END__
0000000000402818 d __JCR_END__
0000000000402818 d __JCR_LIST__
0000000000402820 d __TMC_END__
0000000000402820 d __TMC_LIST__
0000000000402838 A __bss_start
                 U __cxa_atexit
0000000000402808 D __data_start
0000000000401100 t __do_global_dtors_aux
0000000000402820 t __do_global_dtors_aux_fini_array_entry
0000000000402810 d __dso_handle
0000000000402828 t __frame_dummy_init_array_entry
                 w __gmon_start__
                 U __gxx_personality_v0
0000000000402838 t __init_array_end
0000000000402828 t __init_array_start
00000000004012b0 T __libc_csu_fini
00000000004012c0 T __libc_csu_init
                 U __libc_start_main
                 w __pthread_key_create
0000000000402838 A _edata
0000000000402990 A _end
000000000040134c T _fini
0000000000400e68 T _init
0000000000401028 T _start
0000000000401054 t call_gmon_start
0000000000402840 b completed.6661
0000000000402808 W data_start
0000000000401080 t deregister_tm_clones
0000000000401120 t frame_dummy
0000000000400f40 T main
00000000004010c0 t register_tm_clones

最佳答案

考虑到崩溃点,并且预加载libpthread似乎可以解决这一问题,我认为这两种情况的执行在 locale_init.cc:315 上有所不同。以下是代码摘录:

  void
  locale::_S_initialize()
  {
#ifdef __GTHREADS
    if (__gthread_active_p())
      __gthread_once(&_S_once, _S_initialize_once);
#endif
    if (!_S_classic)
      _S_initialize_once();
  }

如果您的程序与pthread链接,则__gthread_active_p()返回true,特别是它检查pthread_key_create是否可用。在我的系统上,此符号在“/usr/include/c++/7.2.0/x86_64-pc-linux-gnu/bits/gthr-default.h”中定义为static inline,因此它可能是违反ODR的原因。

请注意,LD_PRELOAD=libpthread,so将始终导致__gthread_active_p()返回true。
__gthread_once是另一个内联符号,应始终转发到pthread_once

很难猜测如果不进行调试会发生什么情况,但是我怀疑即使没有使用__gthread_active_p(),您也会遇到它的真正分支,该程序随后崩溃,因为没有pthread_once可以调用。

编辑:
因此,我做了一些实验,发现std::locale::_S_initialize崩溃的唯一方法是__gthread_active_p返回true,但没有链接pthread_once

libstdc++不会直接针对pthread进行链接,但是会将pthread_xx的一半导入为弱对象,这意味着它们可以是未定义的,并且不会引起链接器错误。

显然,链接pthread将使崩溃消失,但是,如果我是对的,主要问题是,即使我们没有链接pthread,您的libstdc++仍认为它在多线程可执行文件内。

现在,__gthread_active_p使用__pthread_key_create来确定我们是否有线程。这在可执行文件中被定义为弱对象(可以为nullptr,但仍然可以)。我99%确信由于shared_ptr而存在该符号(将其删除并再次检查nm以确保正确)。
因此,某种程度上__pthread_key_create会绑定(bind)到有效地址,这可能是由于链接器标志中的最后-lpthread所致。
您可以通过在locale_init.cc:315处放置一个断点并检查您采用的分支来验证这一理论。

EDIT2 :

意见摘要,只有在满足以下所有条件的情况下,此问题才可以重现:
  • 使用ld.gold代替ld.bfd
  • 使用--as-needed
  • 强制弱化__pthread_key_create,在这种情况下,通过实例化std::shared_ptr
  • 不链接到pthread,或在 pthread之后链接--as-needed

  • 要回答评论中的问题:



    默认情况下,它使用/usr/bin/ld,在大多数发行版中,它都是/usr/bin/ld.bfd/usr/bin/ld.gold的符号链接(symbolic link)。可以使用update-alternatives操纵这种默认值。我不确定在您的情况下为什么是ld.gold,据我了解,RHEL5缺省将ld.bfd附带。



    因为对所需内容的定义有些阴暗。 man ld说(重点是我):



    现在,根据this bug reportgold遵循“非弱 undefined symbol ”部分,而ld.bfd根据需要看到弱符号。 TBH我对此没有完全的了解,关于该链接是否被视为ld.gold错误或libstdc++错误,有一些讨论。


    -pthread-lpthread做不同的事情(请参阅pthread vs lpthread)。据我了解,前者应暗示后者。

    无论如何,您可能只能传递一次-lpthread,但是您需要在--as-needed之前传递它,或者在最后一个库之后和--no-as-needed之前使用-lpthread

    还值得一提的是,即使使用黄金连接器,我也无法在我的系统(GCC 7.2)上重现此问题。
    因此,我怀疑它已在最新版本的libstdc++中得到修复,该版本也可以解释如果使用系统标准库,为什么它不会进行段错误。

    08-25 08:34