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

38

代码

这是导致段错误的程序。

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

当然,程序本身没有任何问题。segfault的根本原因取决于其构建和运行的环境。

背景

我们在亚马逊使用一个构建系统,以一种几乎与机器无关的方式构建和部署二进制文件(libbin)。对于我们的情况,这基本上意味着它将可执行文件(从上述程序构建而来)部署到$project_dir/build/bin/中,将几乎所有依赖项(即共享库)部署到$project_dir/build/lib/中。我之所以用“几乎”这个短语,是因为对于共享库,如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 when it segfaults

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=all

我尝试启用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


问题

我知道这样调试非常困难,可能我没有提供关于环境等方面的很多信息。但是,我的问题是:这个段错误可能的根本原因是什么?如何进一步调试并找到它?一旦我找到了问题,修复将变得容易。


编译器和平台

我正在使用 RHEL5 上的 GCC 4.9


实验

E#1

如果我注释掉以下行:

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

编译和运行都很顺利!

E#2

我刚刚把以下头文件包含到我的程序中:

#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(或两者都是),满足了一些要求或绕过了问题!尽管我仍然不知道问题的根本原因。

编译和链接选项

由于构建系统很复杂,并且有许多默认选项。因此,我尝试使用CMake的set命令显式添加-lpthread,然后它就可以工作了,正如我们已经看到的,通过预装载libpthread它可以工作!
为了看到这两种情况(当它工作时当它给出segfault时)之间的构建差异,我以详细模式构建它,通过将-v传递给GCC,查看编译阶段和实际传递给cc1plus(编译器)和collect2(链接器)的选项。
请注意,路径已经被编辑为简洁起见,使用美元符号和虚拟路径。

$/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的命令行参数都完全相同。根本没有区别。这似乎并不是很有帮助。
然而,区别在于链接时间。下面是我看到的,在它工作时的情况下
您可以看到,-lpthread 被提及了两次!第一个 -lpthread(后面跟着 --as-needed)在给出段错误的情况下是缺少的。这是这两种情况之间唯一的区别。

两种情况下 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

4
从 C++ 的角度来看,这似乎是由于 ODR 违规引起的未定义行为。编译时使用的标准库符号必须与链接时使用的符号匹配,但在某些情况下可能不是这种情况。只需在目标机器上重新编译即可解决此问题。如果无法这样做,请检查哪些库版本是二进制兼容的,并确保目标机器具有并使用这样的版本。或者,您可以尝试静态链接一些库,而不是使用动态链接,但您可能无法对所有内容都这样做。 - nwp
只是一些随机的想法:pthread_oncelibthread中。如果您使用-pthread选项编译程序,它会解决问题吗?您说包含libboost_system.so.1.53.0解决了您的问题,但请注意,libboost_system.so.1.53.0链接到libpthread。从您提供的跟踪中,build/private/builds/RelWithDebInfo/runpools在可搜索文件列表中。问题:runpools需要与libphtreads链接吗? - Amadeus
可能你的 C++ 编译器没有使用现有的 libc 进行构建? - M.M
1
只是一些随机的想法:pthread_once 在静态变量的初始化中是必需的,可能被 locale 所需要。这是 C++11 语言支持的一部分,可以使用 -f(no)threadsafe-statics 来禁用/启用它。 - sbabbi
1
你有没有使用gold链接器的可能性?这个错误看起来非常相似... - sbabbi
显示剩余14条评论
2个回答

17
鉴于崩溃点以及预加载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() 如果程序链接了 pthread,就会返回 true,具体来说,它检查 pthread_key_create 是否可用。在我的系统中,这个符号被定义在 "/usr/include/c++/7.2.0/x86_64-pc-linux-gnu/bits/gthr-default.h" 中,是一个潜在的 ODR 冲突源,因为它是一个 static inline

请注意,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将使崩溃消失,但如果我没错的话,主要问题是你的libstdc ++认为它在多线程可执行文件中,即使我们没有链接pthread也是如此。现在,__gthread_active_p使用__pthread_key_create来决定我们是否有线程。在你的可执行文件中,这被定义为一个弱对象(可以为nullptr并且仍然很好)。我99%确定该符号存在是因为shared_ptr(删除它并再次检查nm以确保)。因此,一些方法可能会将__pthread_key_create绑定到有效地址,可能是由于链接器标志中的最后一个-lpthread。您可以通过在locale_init.cc:315处设置断点并检查您采取哪个分支来验证此理论。
编辑2:
评论摘要,只有满足以下所有条件才能重现问题:
使用ld.gold而不是ld.bfd 使用--as-needed 通过实例化std::shared_ptr来强制定义__pthread_key_create的弱定义。
不链接到pthread,或在--as-needed之后链接pthread
回答评论中的问题:

默认情况下,它使用/usr/bin/ld,在大多数发行版上是一个符号链接,指向/usr/bin/ld.bfd/usr/bin/ld.gold之一。可以使用update-alternatives来操纵这种默认设置。我不确定为什么在您的情况下是ld.gold,因为据我了解,RHEL5默认安装的是ld.bfd

如果需要,为什么gold不会将pthread.so依赖项添加到二进制文件中?

因为需要的定义有些含糊不清。man ld说(重点在于“mine”):

--as-needed

--no-as-needed

此选项影响命令行后面提到的用于动态库的ELF DT_NEEDED标记 --as-needed选项之后。通常,连接器将为命令行上提到的每个动态库添加DT_NEEDED标记,无论该库实际上是否需要。 --as-needed的作用是仅在在链接的某个点满足来自常规目标文件的非弱未定义符号引用,或者如果在其他所需库的DT_NEEDED列表中找不到该库,则为那个库发出DT_NEEDED标记。 命令行上出现的对象文件或库在该库之后不会影响是否看作需要该库。这类似于从存档中提取对象文件的规则。--no-as-needed恢复了默认行为。

现在,根据这个错误报告gold尊重“非弱未定义符号”部分,而ld.bfd将弱符号视为所需的。老实说,我对此没有完全理解,在该链接中有一些讨论,是否应将其视为ld.gold错误或libstdc ++错误。

为什么我需要同时提到-pthread和-lpthread?(我们的构建系统默认传递-pthread,并且我已经传递-lpthread以使它与使用gold)。

-pthread-lpthread执行不同的操作(请参见pthread vs lpthread)。 我的理解是前者应暗示后者。
无论如何,您可能只需要传递-lpthread一次,但需要在--as-needed之前进行,或在最后一个库之后使用--no-as-needed并在-lpthread之前使用。
值得一提的是,我无法在我的系统上复制此问题(GCC 7.2),即使使用gold链接器也是如此。 所以我怀疑它已经在更近期的版本的libstdc++中得到修复,这也可以解释为什么如果您使用系统标准库,则不会出现段错误。

__gthread_oncepthread_once 是相同的吗?它们似乎是不同的 API。前者似乎在 libgcc 中定义,而后者在 libpthread 中定义。 - Nawaz
__gthread_once是另一个内联符号,应始终转发到pthread_once。”它有文档记录吗? - Nawaz
@Nawaz ghtr-posix.h:696。但是根据某些配置,它也可能是gthr-single.h - sbabbi
1
@Nawaz 是的,它确实做了类似的事情。但是对于 libstdc++,有一个名为 GTHREAD_USE_WEAK 的构建标志,如果将其定义为 0,则显然会删除该检查。 我期望在这种情况下,生成的 libstdc++ 应明确链接到 pthread。我将尝试在没有该标志的情况下构建 libstdc++ 并查看会发生什么。 - sbabbi
我将 std::shared_ptr 行替换为 std::cout << ::__gthread_active_p() << std::endl;,结果在 gdb 中出现了相同的堆栈跟踪段错误... 这意味着问题出在 __gthread_active_p() 上。 - Nawaz
显示剩余4条评论

9

这很可能是由于libstdc++ ABI之间的微妙不匹配引起的问题。在Red Hat Enterprise Linux 5上,GCC 4.9不是系统编译器,因此您在使用什么(DTS 3?)并不清楚。

已知区域设置实现对ABI不匹配非常敏感。请参阅gcc-help列表中的此线程:

您最好找出哪些libstdc++位于何处,并以某种方式实现一致性(通过隐藏符号或重新编译使其兼容)。

还可以调查Red Hat Developer Toolset中使用的混合链接模型(其中新的部分链接静态,但大部分C ++标准库使用现有系统DSO),但如果您需要支持当前的语言功能,则Red hat Enterprise Linux 5中的系统libstdc++可能太旧了。


“GCC 4.9不是Red Hat Enterprise Linux 5系统编译器”这句话的意思是什么?你指的是GCC 4.9没有为RHEL5构建,因此可能无法在那里正常工作?你能否详细解释一下,因为这似乎是一个重要的观点? - Nawaz
Red Hat Enterprise Linux最初使用GCC 4.1,GCC 4.4作为选项(以gcc44软件包的形式)。这并不是非常重要,但它表明您可能会遇到ABI问题,因为您使用了多个编译器,它们没有以构建兼容二进制文件的方式构建。 - Florian Weimer
已经在问题中添加了编译和链接选项。请查看! - Nawaz

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