c++ - _dl_fini中的SIGSEGV

标签 c++ debugging gdb segmentation-fault

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

  • 我正在处理两个共享对象文件,我们将它们称为libMyA.solibMyB.so,它们是产品的一部分。
  • 这两个共享库文件各自链接两个静态库libMyC.alibMyD.a
  • libMyA.solibMyB.so我有单元测试,它们基本上是命令行可执行文件,它们调用共享对象导出的某些功能blackboxAblackboxB
  • libMyB.so利用libMyA.so导出的功能。在libMyA.so的init函数中调用了libMyB.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
    

    我知道,当静态库在过程中由多个共享对象链接,并且在静态库的字符串对象中略过了libMyC.alibMyD.a时,在静态库的全局或命名空间范围内定义的std::string对象可能会出现问题。到目前为止,没有成功。

    我还对blackboxB进行了修改,以使主要功能仅由return 0组成-SIGSEGV保持不变。如果我修改libMyB.so以在其init函数中不再从libMyA.so调用任何内容,则SIGSEGV消失。

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

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

    感谢您在这场噩梦中的投入...

    最佳答案

    注意:此答案是由希望匿名的同事提供的。在解决这个问题上我没有任何功劳。

    我在工作中遇到类似症状的问题。
    该答案概述了我如何解决该问题。
    大多数/所有这些信息都可以在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;
    }
    

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

    $ 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是第一个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帧的源代码行列表告诉您哪个静态变量被“复制”,您可以跟踪回溯以查看它如何被包含在两个不同的二进制文件中,并将其与二进制文件的构建方式进行比较,等等。
    如果构建时没有调试符号,则不会获得源代码行 list ,并且可能必须进行一些反汇编才能确定源代码中的位置。

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

    $ 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
    

    上述轮廓在 native 上构建时不起作用,因为该dtor而不是用__cxa_atexit注册dtor,而是由__tcf_0__tcf_1等函数包装。已使用__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  Mark Mitchell  <email>

            * decl.c (get_atexit_fn_ptr_type): New function.
            (get_atexit_node): Use it.
            (start_cleanup_fn): Likewise.
            (register_dtor_fn): Use the object's destructor, instead of a
            separate cleanup function, where possible.
            ...

    关于c++ - _dl_fini中的SIGSEGV,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35229310/

    相关文章:

    c++ - 使用 System::AnsiString 类

    c++ - 在 c/c++ 中包括结构和编译

    android - XMPP 客户端无法连接到 XMPP 本地主机服务器

    c - C内存的堆和静态部分之间的巨大差距是什么?

    c++ - MFC CFormView OnKeyDown 事件未触发

    c++ - SIGSEGV;丢失文件;使用 sscanf 运行程序时

    asp.net - 安装IE8后,为什么不能从Visual Studio 2005调试?

    c - 读取文件,但只插入文件中的最后一个字符串

    debugging - GDB 中的键映射

    访问指向结构 GDB 错误的指针时无法访问内存