【问题标题】:SIGSEGV in _dl_fini_dl_fini 中的 SIGSEGV
【发布时间】:2016-02-05 16:46:15
【问题描述】:

我希望对调试我已经纠结了两天的问题有一些见解。情况是这样的

  • 我正在处理两个共享对象文件,我们称它们为 libMyA.solibMyB.so,它们是产品的一部分。
  • 这两个共享对象文件分别链接了两个静态库libMyC.alibMyD.a
  • libMyA.solibMyB.so 我有单元测试,它们基本上是命令行可执行文件,它们调用共享对象导出的一些函数,blackboxAblackboxB
  • libMyB.so 使用libMyA.so 导出的函数。在libMyB.so的init函数中调用了libMyA.so的几个函数(只是生成了几个STL容器)。

会发生什么:

  • blackboxA 运行平稳并通过了所有测试。
  • blackboxB 也通过了所有测试,但在终止时会引发 SIGSEGV

gdb 告诉我 SIGSEGV 发生在 libMyB.so 的终结器在 std::basic_string<char> 对象的析构函数内执行期间:

#0  0x00007ffff74a0bc3 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#1  0x00007ffff74a0c13 in std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#2  0x00007ffff6b6cd1d in __cxa_finalize (d=0x7ffff7dd4d80) at cxa_finalize.c:56
#3  0x00007ffff7b1d7b6 in __do_global_dtors_aux () from ./libMinosCVC.so.3
#4  0x00007fffffffe3a0 in ?? ()
#5  0x00007fffffffe480 in ?? ()
#6  0x00007ffff7b9a541 in _fini () from ./libMinosCVC.so.3
#7  0x00007fffffffe480 in ?? ()
#8  0x00007ffff7de992d in _dl_fini () at dl-fini.c:259

我知道,当静态库由进程中的多个共享对象链接时,在静态库的全局或命名空间范围内定义的 std::string 对象可能会出现问题,并且已略过libMyC.a 和 @ 987654341@ 用于迄今为止未成功的范围内的字符串对象。

我还修改了blackboxB,使其主要功能仅包含return 0 - SIGSEGV 保持不变。如果我修改 libMyB.so 以在其 init 函数中不再调用来自 libMyA.so 的任何内容,SIGSEGV就会消失。

当 SIGSEGV 发生时,是否有任何我不知道的方法来检测 libc 试图清理的实际对象? gdb 确实指出了std::string 析构函数,但除此之外没有其他内容(甚至无法访问std::string 成员)。 valgrind 也没有多大帮助...

哦,我差点忘了上面的樱桃:使用 -O0 构建时一切正常,只有 -O2 构建崩溃。

感谢您对这场噩梦的任何意见...

【问题讨论】:

  • 在我删除了 std::string 的所有声明和定义之后,我现在终于摆脱了 SIGSEGV,我可以在 libMyB.so 的源代码中找到。仍然不明白,为什么在这种情况下这些实际上可能是一个问题 - 我在静态库中已经在某种程度上理解了,但是共享库......?底线是我需要更多地了解ld 的行为。我不会“回答”这个问题(我知道它可能仍未得到回答),因为在这种情况下,消除症状并不能真正作为答案......

标签: c++ debugging gdb segmentation-fault


【解决方案1】:

注意:这个答案是由一位希望保持匿名但希望有所帮助的同事提供给我的。我没有解决这个问题。

我在工作中遇到了类似症状的问题。 这个答案概述了我如何解决这个问题。 大多数/所有这些信息都可以在互联网上的其他地方找到,但我找不到像这样整合的信息,而且,作为一个不“知情”的人,一开始对我来说并不是很明显(而且只是现在对我来说更明显了)。 如果我有任何错误,请提前道歉......


关于我正在使用的机器的一些信息:

$ cat /etc/redhat-release
Red Hat Enterprise Linux Server release 6.9 (Santiago)
$ g++ --version
g++ (GCC) 4.4.7 20120313 (Red Hat 4.4.7-18)
...
$ /lib64/libc.so.6
GNU C Library stable release version 2.12, by Roland McGrath et al.
...
$ uname -srm
Linux 2.6.32-696.6.3.el6.x86_64 x86_64

一个极简的工作示例:

common.h:

#include <string>
struct Common {
  static const std::string s;
};

common.cpp:

#include "common.h"
const std::string Common::s("common");

main.cpp:

#include <iostream>
#include "common.h"
int main(void) {
  std::cout << Common::s << std::endl;
  return 0;
}

构建(调试符号有帮助,但可能不是绝对必要的):

$ g++ -g -shared -fPIC common.cpp -o libone.so
$ g++ -g -shared -fPIC common.cpp -o libtwo.so
$ g++ -g main.cpp -L. -lone -ltwo -o main

运行(注意它可能运行良好...):

$ # turn on core dumping
$ ./main
common
*** glibc detected *** ./main: double free or corruption (...): 0x... ***
======= Backtrace: =========
/lib64/libc.so.6[0x...]
/lib64/libc.so.6[0x...]
/usr/lib64/libstdc++.so.6(_ZNSsD1Ev+0x...)[0x...]
/lib64/libc.so.6(__cxa_finalize+0x...)[0x...]
libtwo.so(+0x...)[0x...]
======= Memory map: ========
...
Aborted (core dumped)
$

检查核心:

$ gdb -c core.<pid> -e main
...
Core was generated by `./main'.
Program terminated with signal 6, Aborted.
#0  0x... in raise () from /lib64/libc.so.6
(gdb) bt
#0  0x... in raise () from /lib64/libc.so.6
#1  0x... in abort () from /lib64/libc.so.6
#2  0x... in __libc_message () from /lib64/libc.so.6
#3  0x... in malloc_printerr () from /lib64/libc.so.6
#4  0x... in _int_free () from /lib64/libc.so.6
#5  0x... in std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string () from /usr/lib64/libstdc++.so.6
#6  0x... in __cxa_finalize () from /lib64/libc.so.6
#7  0x... in __do_global_dtors_aux () from /libtwo.so
#8  0x... in ?? ()

据我所知,一些说明:

在启动时,在 main 之前,静态析构函数在静态初始化时注册到 __cxa_atexit(或类似)。 然后,在“常规”退出之前,程序通过并以相反的顺序调用已注册的析构函数。 __cxa_atexit 接受 3 个参数。 第一个是函数(例如 dtor 类)。 第 2 个是第一个 arg 中函数的 arg(例如 std::string*)。 第三个参数我会忽略... 在 Linux 上的 x86-64(我正在开发的平台)上,第一个、第二个参数分别在 %rdi%rsi 中传递。 这个想法是打破__cxa_atexit,记录寄存器中的内容,并在程序启动完成后查找重复项。

一些与寄存器内容相关的上下文会很有用。 gdb 回溯看起来像这样:

(gdb) bt
#0  0x... in __cxa_atexit_internal () from /lib64/libc.so.6
#1  0x... in __static_initialization_and_destruction_0 (...) at common.cpp:2
#2  0x... in global constructors keyed to _ZN6Common1sE () at common.cpp:3
#3  0x... in __do_global_ctors_aux () from libone.so
#4  0x... in _init () from libone.so
...

帧 3/4 让您查看要查看的二进制文件(例如,用于反汇编)。 框架 2 让您大致了解静态与哪个源代码块相关联。 框架 1 让您在适当的二进制文件中查找。 在为第 1 帧列出的指令之前的几条指令,您应该会看到哪个地址被加载到 %rsi


$ gdb --args ./main
...
(gdb) b __cxa_atexit
Breakpoint 1 at 0x...
(gdb) comm
...
>silent
>printf "$rdi %p $rsi %p\n", $rdi, $rsi
>bt 4
>c
>end
(gdb) set pag off
(gdb) set log redirect on
(gdb) set log file __cxa_atexit.txt
(gdb) set log on
Redirecting output to __cxa_atexit.txt.
(gdb) start
(gdb) set log off
Done logging to __cxa_atexit.txt.
(gdb)

请注意,上面的页面/日志设置是可选的。 在这个例子中差别不大,但在我正在使用的“实际”程序中,__cxa_atexit 断点被到达数千次(并且花了几分钟时间才到达 main 的临时断点)。

在输出中查找重复的寄存器行:

grep "^\$rdi" __cxa_atexit.txt | sort | uniq -d

我检查了%rdi 中的地址,感觉良好:

(gdb) x/i 0x...
   0x... <_ZNSsD2Ev>: ...

$ c++filt _ZNSsD2Ev
std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()

或者,使用 gdb 的 i sharedi proc map 列表获取偏移量并在适当的二进制文件上使用反汇编程序(不要忘记 offset your offset if needed)。

相关的回溯看起来像这样:

#0  0x... in __cxa_atexit_internal () from /lib64/libc.so.6
#1  0x... in __static_initialization_and_destruction_0 (...) at common.cpp:2
#2  0x... in global constructors keyed to common.cpp(void) () at common.cpp:2
#3  0x... in __do_global_ctors_aux () from libone.so

如果您查看第 3 帧中列出的二进制文件,在第 1 帧中列出的指令之前的几条指令中,您应该会看到在调用 __cxa_atexit 之前加载到 %rsi 的内容。 在这个例子中,objdump 用“绝对偏移量”和_ZN6Common1sE@@Base-0x80 对我进行注释。 “绝对偏移量”应与 readelf -rW 输出的“偏移量”列下列出的相应符号相匹配。

第 1 帧的源代码行列表告诉您哪个静态正在“重复”,您可以跟踪回溯以查看它是如何包含在两个不同的二进制文件中的,将其与构建二进制文件的方式进行比较等。 如果您在构建时没有调试符号,您将无法获得源代码行列表,并且您可能需要进行一些反汇编才能确定要查看源代码的哪个位置。


在开始时,我列出了一些机器信息。 这是我可以访问的另一台机器:

$ cat /etc/redhat-release
Red Hat Enterprise Linux Server release 5.4 (Tikanga)
$ g++ --version
g++ (GCC) 4.1.2 20080704 (Red Hat 4.1.2-46)
...
$ /lib64/libc.so.6
GNU C Library stable release version 2.5, by Roland McGrath et al.
...
$ uname -srm
Linux 2.6.18-164.el5 x86_64

上面的大纲在这台机器上构建时不起作用,因为不是用__cxa_atexit注册dtor,而是用__tcf_0__tcf_1等函数包装dtor。在__cxa_atexit注册。 所以你可能有多个(可能略有不同?)__tcf_* 破坏相同静态的函数。 如果您想在程序启动时捕捉到这一点,您可能需要进行一些程序化(反)汇编检查(我不知道该怎么做)。 您可以尝试在程序关闭时捕捉到这一点,中断对~stringfree_int_free 等的调用,将第一个参数与之前的第一个参数进行比较,然后将第一个参数保存在某处以供将来比较(gdb/python? )。 或者,您可以进行一些慷慨的“常规”日志记录并寻找双重免费的事后分析。

更改似乎已在 gcc-4.3.0 中进行。 见gcc-g++-4.3.0.tar.{gz,bz2},文件gcc/cp/decl.c,函数start_cleanup_fnregister_dtor_fn。 ChangeLog sn-p:

2007-05-31 马克·米切尔

      * decl.c (get_atexit_fn_ptr_type):新函数。
(get_atexit_node):使用它。
(start_cleanup_fn):同样。
(register_dtor_fn):使用对象的析构函数,而不是a
尽可能单独的清理功能。
...

【讨论】:

  • 很好的阐述!但是对于g++ 7.5 版,我无法链接两个相同的共享库。我有类似的情况,将同一个库过度链接到可执行文件和可执行文件的依赖项。只是想确认这是同一个问题。
  • 似乎与双链接到同一个库有关。我遇到同样的问题。好帖子。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多