声明类型 vector<shared_ptr<int>> 的变量时出现段错误

Segfault on declaring a variable of type vector<shared_ptr<int>>

代码

这是给出段错误的程序。

#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;
}

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


背景

我们在 Amazon 使用一个构建系统,它以 几乎 独立于机器的方式构建和部署二进制文件(libbin)。对于我们的案例,这基本上意味着它将可执行文件(从上面的程序构建)部署到 $project_dir/build/bin/ 几乎 它的所有依赖项(即共享库)到 $project_dir/build/lib/.为什么我使用短语 "almost" 是因为对于 libc.solibm.sold-linux-x86-64.so.2 和其他一些可能的共享库,可执行文件从系统中挑选(即来自 /lib64 )。请注意,应该$project_dir/build/lib 中选择 libstdc++

现在我运行如下:

$ 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=全部

我还尝试通过为段错误启用 LD_DEBUG=all 来查看链接器信息。我发现了一些可疑的东西,因为它搜索 pthread_once 符号,当它找不到这个时,它给出了段错误(这是我对以下输出片段的解释):

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 成功 运行 的案例!


问题

我知道像这样调试非常困难,可能我没有提供很多关于环境和所有的信息。但是,我的问题仍然是:这个段错误可能 root-cause 是什么?如何进一步调试并找到它?一旦我找到问题,修复就会很容易。


编译器和平台

我在 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>,而是使用原始代码和 运行 通过使用 LD_PRELOAD 预加载 libboost_system.so 如下:

$ LD_PRELOAD=$project_dir/build/lib/libboost_system.so $project_dir/build/bin/run

它运行成功了!

接下来我在 libboost_system.so 上做了 ldd,它给出了一个库列表,其中两个是:

  /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 (或 both ),可以满足某些要求或避免问题!不过,我仍然不知道问题的根本原因。


编译和链接选项

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

为了看到build这两种情况的区别(when-it-workswhen-it-gives-segfault),我通过将 -v 传递给 GCC 以 verbose 模式构建它,以查看编译阶段和它实际传递给 [= 的选项54=](编译器)和 collect2(链接器)。

(请注意,为简洁起见,使用 dollar-sign 和虚拟路径对路径进行了编辑。)

$/gcc-4.9.4/cc1plus -quiet -v -I /a/include -I /b/include -iprefix $/gcc-4.9.4/ -MMD main.cpp.d -MF main.cpp.o.d -MT main.cpp.o -D_GNU_SOURCE -D_REENTRANT -D __USE_XOPEN2K8 -D _LARGEFILE_SOURCE -D _FILE_OFFSET_BITS=64 -D __STDC_FORMAT_MACROS -D __STDC_LIMIT_MACROS -D NDEBUG $/lab/main.cpp -quiet -dumpbase main.cpp -msse -mfpmath=sse -march=core2 -auxbase-strip main.cpp.o -g -O3 -Wall -Wextra -std=gnu++1y -version -fdiagnostics-color=auto -ftemplate-depth=128 -fno-operator-names -o /tmp/ccxfkRyd.s

无论是否有效,cc1plus 的 command-line 参数都是完全相同的。完全没有区别。这似乎不是很有帮助。

但是,区别在于链接时间。这是我看到的,对于它工作的情况

$/gcc-4.9.4/collect2 -plugin $/gcc-4.9.4/liblto_plugin.so
-plugin-opt=$/gcc-4.9.4/lto-wrapper -plugin-opt=-fresolution=/tmp/cchl8RtI.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lpthread -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --eh-frame-hdr -m elf_x86_64 -export-dynamic -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o run /usr/lib/../lib64/crt1.o /usr/lib/../lib64/crti.o $/gcc-4.9.4/crtbegin.o -L/a/lib -L/b/lib -L/c/lib -lpthread --as-needed main.cpp.o -lboost_timer -lboost_wave -lboost_chrono -lboost_filesystem -lboost_graph -lboost_locale -lboost_thread -lboost_wserialization -lboost_atomic -lboost_context -lboost_date_time -lboost_iostreams -lboost_math_c99 -lboost_math_c99f -lboost_math_c99l -lboost_math_tr1 -lboost_math_tr1f -lboost_math_tr1l -lboost_mpi -lboost_prg_exec_monitor -lboost_program_options -lboost_random -lboost_regex -lboost_serialization -lboost_signals -lboost_system -lboost_unit_test_framework -lboost_exception -lboost_test_exec_monitor -lbz2 -licui18n -licuuc -licudata -lz -rpath /a/lib:/b/lib:/c/lib: -lstdc++ -lm -lgcc_s -lgcc -lpthread -lc -lgcc_s -lgcc $/gcc-4.9.4/crtend.o /usr/lib/../lib64/crtn.o

如您所见,-lpthread被提到了两次!第一个 -lpthread(后面是 --as-needed)是 missing ,因为它给出段错误 。这就是这两种情况的区别。


两种情况下nm -C的输出

有趣的是,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

这可能是 libstdc++ ABI 之间的细微不匹配导致的问题。 GCC 4.9 不是 Red Hat Enterprise Linux 5 上的系统编译器,所以不太清楚您在那里使用的是什么(DTS 3?)。

众所周知,语言环境实现对 ABI 不匹配非常敏感。在 gcc-help 列表中查看此线程:

你最好的办法是弄清楚 libstdc++ 的哪些位链接在哪里,并以某种方式实现一致性(通过隐藏符号,或重新编译内容以使它们兼容)。

调查 Red Hat 开发人员工具集中用于 libstdc++ 的混合链接模型可能也很有用(其中较新的位是静态链接的,但大部分 C++ 标准库使用现有的系统 DSO) , 但如果您需要对当前语言功能的支持,Red hat Enterprise Linux 5 中的系统 libstdc++ 可能太旧了。

考虑到崩溃点,以及预加载 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();
  }

__gthread_active_p() return 如果您的程序是针对 pthread linked 的,则为真,特别是它会检查 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() 变为 return true。

__gthread_once 是另一个内联符号,应该始终转发到 pthread_once

如果不调试就很难猜出发生了什么,但我怀疑你正在点击 __gthread_active_p() 的真正分支,即使它不应该,然后程序崩溃,因为没有 pthread_once 打电话。

编辑: 所以我做了一些实验,我看到在 std::locale::_S_initialize 中崩溃的唯一方法是如果 __gthread_active_p return 是真的,但 pthread_once 不是 linked英寸

libstdc++ 不会 link 直接反对 pthread,但它导入了一半的 pthread_xx 作为弱对象,这意味着它们可以是未定义的并且不会导致 link呃错误。

显然 linking pthread 会使崩溃消失,但如果我是对的,主要问题是你的 libstdc++ 认为它在多线程可执行文件中,即使我们这样做了不是 link pthread in.

现在,__gthread_active_p 使用 __pthread_key_create 来决定我们是否有线程。这在您的可执行文件中定义为弱对象(可以是 nullptr 并且仍然可以)。由于 shared_ptr,我 99% 确定该符号在那里(删除它并再次检查 nm 以确保)。 所以,不知何故 __pthread_key_create 被绑定到一个有效地址,可能是因为你的 linker 标志中的最后一个 -lpthread。 您可以通过在 locale_init.cc:315 处放置一个断点并检查您选择的分支来验证这一理论。

EDIT2:

评论摘要,只有满足以下所有条件才能重现该问题:

  1. 使用ld.gold代替ld.bfd
  2. 使用--as-needed
  3. 强制 __pthread_key_create 的弱定义,在本例中是通过 std::shared_ptr.
  4. 的实例化
  5. 不是 link 到 pthread,或 link 到 pthread --as-needed 之后

回答评论中的问题:

Why does it use gold by default?

默认情况下它使用 /usr/bin/ld,在大多数发行版中它是 /usr/bin/ld.bfd/usr/bin/ld.gold 的符号 link。可以使用 update-alternatives 操纵此类默认值。我不确定为什么在你的情况下它是 ld.gold,据我所知 RHEL5 默认附带 ld.bfd

And why does gold not add pthread.so dependency to the binary if it is needed?

因为需要什么的定义有些模糊。 man ld 说(强调我的):

--as-needed

--no-as-needed

This option affects ELF DT_NEEDED tags for dynamic libraries mentioned on the command line after the --as-needed option. Normally the linker will add a DT_NEEDED tag for each dynamic library mentioned on the command line, regardless of whether the library is actually needed or not. --as-needed causes a DT_NEEDED tag to only be emitted for a library that at that point in the link satisfies a non-weak undefined symbol reference from a regular object file or, if the library is not found in the DT_NEEDED lists of other needed libraries, a non-weak undefined symbol reference from another needed dynamic library. Object files or libraries appearing on the command line after the library in question do not affect whether the library is seen as needed. This is similar to the rules for extraction of object files from archives. --no-as-needed restores the default behaviour.

现在,根据 this bug reportgold 支持 "non weak undefined symbol" 部分,而 ld.bfd 根据需要查看弱符号。 TBH 我对此没有完全理解,关于 link 是否将其视为 ld.gold 错误或 libstdc++ 错误有一些讨论。

Why do I need to mention -pthread and -lpthread both? (-pthread is passed by default by our build system, and I've pass -lpthread to make it work with gold is used).

-pthread-lpthread 做不同的事情(参见 pthread vs lpthread)。我的理解是前者应该是后者。

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

之前

还值得一提的是,我无法在我的系统 (GCC 7.2) 上重现此问题,即使使用 gold linker。 所以我怀疑它已经在更新版本的 libstdc++ 中得到修复,这也可以解释为什么如果你使用系统标准库它不会出现段错误。