_dl_fini

时间:2016-02-05 16:46:15

标签: c++ debugging gdb sigsegv

我希望对调试我已经摔跤两天的问题有一些见解。这就是情况

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

这是怎么回事:

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

gdb告诉我SIGSEGVlibMyB.so对象的析构函数中执行std::basic_string<char>的终结符期间发生{/ 1}}:

#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

我知道,当静态库由进程中的多个共享对象链接并且已经浏览libMyC.a时,在静态库中的全局或命名空间作用域中定义的std :: string对象可能会有问题。和libMyD.a表示那些范围内的字符串对象到目前为止没有成功。

我还修改了blackboxB到主要功能仅包含return 0 - SIGSEGV持续存在的程度。如果我修改libMyB.so以便在其初始化函数中从libMyA.so更长时间调用任何内容,SIGSEGV就会消失。

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

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

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

1 个答案:

答案 0 :(得分:0)

注意:此答案由一位希望匿名的同事提供给我,但希望有所帮助。对于解决这个问题,我没有任何贡献。

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


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

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

build(调试符号有帮助,但可能并非严格必要):

$ 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个参数。 第一个是函数(例如class dtor)。 2nd是第1个arg中的函数的arg(例如std::string*)。 第三arg我会忽略... 在Linux上的x86-64(我正在开发的平台)上,分别在%rdi%rsi中传递了第一,第二个参数。 想法是中断__cxa_atexit,记录寄存器中的内容,并在程序启动时查找重复项。

一些与寄存器内容一起使用的上下文将很有用。 gdb backtrace看起来像这样:

(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)

请注意,上面的pag / log设置是可选的。 在此示例中并没有太大区别,但是在我正在使用的“实际”程序中,__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帧中列出的指令之前有几条指令,您应该看到在调用%rsi之前已将哪些内容加载到__cxa_atexit中。 在此示例中,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

在计算机上构建时,以上概述不起作用,因为该dtor而不是用__cxa_atexit__tcf_0等功能包装,而不是用__tcf_1注册dtor。已向__cxa_atexit注册。 因此您可能拥有多个(可能略有不同?)__tcf_*函数,这些函数会破坏相同的静态变量。 如果您想在程序启动时发现这一点,则可能必须进行一些程序化(反汇编)检查(我不知道该怎么做)。 您可以尝试在程序关闭时捕获此错误,中断对~stringfree_int_free等的调用,将第一个arg与先前的第一个arg进行比较,然后将第一个arg保存在将来的某个地方比较(gdb / python?)。 或者,您可以进行一些“常规”记录,并查找double free的事后检验。

更改似乎已在gcc-4.3.0中进行。 请参见gcc-g++-4.3.0.tar.{gz,bz2},文件gcc/cp/decl.c,功能start_cleanup_fnregister_dtor_fn。 ChangeLog代码段:

  

2007-05-31马克·米切尔<电子邮件>

     

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