Stu*_*ndi 6 c++ debugging gdb segmentation-fault
我希望能得到一些关于调试我已经解决了两天的问题的见解。情况是这样的
libMyA.so和libMyB.so,它们是产品的一部分。libMyC.a并且libMyD.alibMyA.so我libMyB.so有单元测试,它们基本上是命令行可执行文件,它们调用共享对象导出的一些函数,blackboxA并且blackboxB.libMyB.so使用由 导出的函数libMyA.so。libMyA.so在init函数中调用了几个函数libMyB.so(只是生成了几个STL容器)。发生的事情是这样的:
blackboxA运行顺利并通过所有测试。blackboxB也通过了所有测试,但在终止时它会引发SIGSEGV.gdb 告诉我,这种情况发生在对象析构函数内部SIGSEGV的终结器执行期间:libMyB.sostd::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
Run Code Online (Sandbox Code Playgroud)
我知道,当静态库在进程中由多个共享对象链接时,在静态库中的全局或命名空间范围内定义的 std::string 对象可能会出现问题,并且在那些范围内没有的字符串对象已略读libMyC.a和libMyD.a到目前为止成功。
我还修改了blackboxB主要功能仅包含一个return 0-SIGSEGV持久化。如果我修改libMyB.so为不再从其libMyA.soinit 函数中调用任何内容,则会SIGSEGV消失。
是否有任何我不知道的方法可以检测当 SIGSEGV 发生时 libc 试图清理的实际对象?gdb 确实指出了std::string析构函数,但除此之外什么也没有(甚至std::string无法访问成员)。valgrind 也没有多大帮助......
哦,我差点忘了最重要的一点:当使用 -O0 构建时,一切正常,只有 -O2 构建崩溃。
感谢您对这场噩梦的任何意见...
注意:这个答案是由一位不愿透露姓名但希望有所帮助的同事提供给我的。我对解决这个问题没有任何功劳。
我在工作中遇到了类似症状的问题。这个答案概述了我如何解决这个问题。大多数/所有这些信息都可以在互联网上的其他地方找到,但我找不到像这样整合的信息,而且,作为一个不“知情”的人,一开始对我来说并不是很明显(而且只是现在对我来说更明显了)。如果我有任何问题,请提前道歉...
我正在使用的机器的一些信息:
$ 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
Run Code Online (Sandbox Code Playgroud)
一个最小的工作示例:
common.h:
#include <string>
struct Common {
static const std::string s;
};
Run Code Online (Sandbox Code Playgroud)
common.cpp:
#include "common.h"
const std::string Common::s("common");
Run Code Online (Sandbox Code Playgroud)
main.cpp:
#include <iostream>
#include "common.h"
int main(void) {
std::cout << Common::s << std::endl;
return 0;
}
Run Code Online (Sandbox Code Playgroud)
构建(调试符号有帮助,但可能不是绝对必要的):
$ 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
Run Code Online (Sandbox Code Playgroud)
运行(请注意,它可能运行良好......):
$ # 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)
$
Run Code Online (Sandbox Code Playgroud)
检查核心:
$ 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 ?? ()
Run Code Online (Sandbox Code Playgroud)
据我所知,一些说明:
在启动时, before ,静态析构函数在静态初始化时main注册(或类似)。__cxa_atexit然后,在“常规”退出之前,程序会以相反的顺序执行并调用已注册的析构函数。
__cxa_atexit需要 3 个参数。第一个是一个函数(例如 dtor 类)。第二个是第一个参数中函数的参数(例如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
...
Run Code Online (Sandbox Code Playgroud)
帧 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)
Run Code Online (Sandbox Code Playgroud)
请注意,上面的 pag/log 设置是可选的。在这个例子中没有什么大的区别,但在我正在使用的“实际”程序中,__cxa_atexit断点被达到了数千次(并且花了几分钟才达到临时断点main)。
在输出中查找重复的寄存器行:
grep "^\$rdi" __cxa_atexit.txt | sort | uniq -d
Run Code Online (Sandbox Code Playgroud)
我检查地址%rdi作为一个自我感觉良好的健全性检查:
(gdb) x/i 0x...
0x... <_ZNSsD2Ev>: ...
$ c++filt _ZNSsD2Ev
std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()
Run Code Online (Sandbox Code Playgroud)
或者,使用 gdbi shared或i proc map列表来获取偏移量,并在适当的二进制文件上使用反汇编程序(如果需要,不要忘记偏移偏移量)。
相关的回溯看起来像这样:
#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
Run Code Online (Sandbox Code Playgroud)
如果您查看第 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
Run Code Online (Sandbox Code Playgroud)
在这台机器上构建时,上面的概述将不起作用,因为 dtor 不是用 注册 dtor __cxa_atexit,而是由 、 等函数__tcf_0包装__tcf_1。已注册__cxa_atexit。所以你可能有多个(可能略有不同?)__tcf_*函数来破坏相同的静态。如果您想在程序启动时捕获此问题,您可能需要进行一些编程(反)汇编检查(我不知道该怎么做)。您可以尝试在程序关闭时捕获此问题,中断对~string、free、_int_free等的调用,将第一个参数与之前的第一个参数进行比较,然后将第一个参数保存在某处以供将来比较(gdb/python?)。或者,您可以进行一些慷慨的“定期”日志记录并查找 double free 的事后分析。
这个变化似乎已经发生在 gcc-4.3.0 中。请参阅gcc-g++-4.3.0.tar.{gz,bz2}、文件gcc/cp/decl.c、函数start_cleanup_fn、register_dtor_fn。变更日志片段:
2007-05-31 马克·米切尔 <电子邮件>
* decl.c (get_atexit_fn_ptr_type):新函数。
(get_atexit_node):使用它。
(start_cleanup_fn):同样。 (register_dtor_fn):尽可能
使用对象的析构函数,而不是单独的清理函数。 ...