我正在使用GCC编译器在Linux上工作.当我的C++程序崩溃时,我希望它能自动生成一个堆栈跟踪.
我的程序由许多不同的用户运行,它也可以在Linux,Windows和Macintosh上运行(所有版本都使用编译gcc
).
我希望我的程序能够在崩溃时生成堆栈跟踪,并且在用户下次运行它时,它会询问他们是否可以将堆栈跟踪发送给我,以便我可以追踪问题.我可以处理向我发送信息,但我不知道如何生成跟踪字符串.有任何想法吗?
对于Linux而言我相信Mac OS X,如果您使用的是gcc,或者任何使用glibc的编译器,您可以使用backtrace()函数execinfo.h
打印堆栈跟踪并在出现分段错误时正常退出.文档可以在libc手册中找到.
这是一个示例程序,它安装SIGSEGV
处理程序并stderr
在segfaults时打印堆栈跟踪.baz()
这里的函数导致触发处理程序的段错误:
#include#include #include #include #include void handler(int sig) { void *array[10]; size_t size; // get void*'s for all entries on the stack size = backtrace(array, 10); // print out all the frames to stderr fprintf(stderr, "Error: signal %d:\n", sig); backtrace_symbols_fd(array, size, STDERR_FILENO); exit(1); } void baz() { int *foo = (int*)-1; // make a bad pointer printf("%d\n", *foo); // causes segfault } void bar() { baz(); } void foo() { bar(); } int main(int argc, char **argv) { signal(SIGSEGV, handler); // install our handler foo(); // this will call foo, bar, and baz. baz segfaults. }
编译时-g -rdynamic
会在输出中获取符号信息,glibc可以使用它来创建一个很好的堆栈跟踪:
$ gcc -g -rdynamic ./test.c -o test
执行此操作可以获得此输出:
$ ./test Error: signal 11: ./test(handler+0x19)[0x400911] /lib64/tls/libc.so.6[0x3a9b92e380] ./test(baz+0x14)[0x400962] ./test(bar+0xe)[0x400983] ./test(foo+0xe)[0x400993] ./test(main+0x28)[0x4009bd] /lib64/tls/libc.so.6(__libc_start_main+0xdb)[0x3a9b91c4bb] ./test[0x40086a]
这显示了堆栈中每个帧的加载模块,偏移量和函数.在这里你可以看到在堆栈的顶部信号处理程序,以及libc函数之前main
除了main
,foo
,bar
,和baz
.
它比"man backtrace"更容易,有一个小文档库(GNU特定的)与glibc一起分发为libSegFault.so,我相信这是由Ulrich Drepper编写的,用于支持程序catchsegv(参见"man catchsegv").
这给了我们三种可能性.而不是运行"程序-o hai":
在catchsegv中运行:
$ catchsegv program -o hai
在运行时与libSegFault链接:
$ LD_PRELOAD=/lib/libSegFault.so program -o hai
在编译时链接libSegFault:
$ gcc -g1 -lSegFault -o program program.cc $ program -o hai
在所有3种情况下,您将获得更清晰的回溯,优化更少(gcc -O0或-O1)和调试符号(gcc -g).否则,您最终可能会得到一堆内存地址.
您还可以通过以下方式捕获更多堆栈跟踪信号:
$ export SEGFAULT_SIGNALS="all" # "all" signals $ export SEGFAULT_SIGNALS="bus abrt" # SIGBUS and SIGABRT
输出看起来像这样(注意底部的回溯):
*** Segmentation fault Register dump: EAX: 0000000c EBX: 00000080 ECX: 00000000 EDX: 0000000c ESI: bfdbf080 EDI: 080497e0 EBP: bfdbee38 ESP: bfdbee20 EIP: 0805640f EFLAGS: 00010282 CS: 0073 DS: 007b ES: 007b FS: 0000 GS: 0033 SS: 007b Trap: 0000000e Error: 00000004 OldMask: 00000000 ESP/signal: bfdbee20 CR2: 00000024 FPUCW: ffff037f FPUSW: ffff0000 TAG: ffffffff IPOFF: 00000000 CSSEL: 0000 DATAOFF: 00000000 DATASEL: 0000 ST(0) 0000 0000000000000000 ST(1) 0000 0000000000000000 ST(2) 0000 0000000000000000 ST(3) 0000 0000000000000000 ST(4) 0000 0000000000000000 ST(5) 0000 0000000000000000 ST(6) 0000 0000000000000000 ST(7) 0000 0000000000000000 Backtrace: /lib/libSegFault.so[0xb7f9e100] ??:0(??)[0xb7fa3400] /usr/include/c++/4.3/bits/stl_queue.h:226(_ZNSt5queueISsSt5dequeISsSaISsEEE4pushERKSs)[0x805647a] /home/dbingham/src/middle-earth-mud/alpha6/src/engine/player.cpp:73(_ZN6Player5inputESs)[0x805377c] /home/dbingham/src/middle-earth-mud/alpha6/src/engine/socket.cpp:159(_ZN6Socket4ReadEv)[0x8050698] /home/dbingham/src/middle-earth-mud/alpha6/src/engine/socket.cpp:413(_ZN12ServerSocket4ReadEv)[0x80507ad] /home/dbingham/src/middle-earth-mud/alpha6/src/engine/socket.cpp:300(_ZN12ServerSocket4pollEv)[0x8050b44] /home/dbingham/src/middle-earth-mud/alpha6/src/engine/main.cpp:34(main)[0x8049a72] /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe5)[0xb7d1b775] /build/buildd/glibc-2.9/csu/../sysdeps/i386/elf/start.S:122(_start)[0x8049801]
如果您想了解血腥细节,最好的来源是遗憾的来源:请参阅http://sourceware.org/git/?p=glibc.git;a=blob;f=debug/segfault.c及其父目录http://sourceware.org/git/?p=glibc.git;a=tree;f=debug
虽然已经建议在execinfo.h中使用backtrace()函数来打印堆栈跟踪并在出现分段错误时正常退出,但我没有提到确保所产生的回溯指向实际位置所需的复杂性.错误(至少对于某些架构 - x86和ARM).
进入信号处理程序时,堆栈帧链中的前两个条目包含信号处理程序内部的返回地址和libc中的一个sigaction()内部.在信号之前调用的最后一个函数的堆栈帧(即故障的位置)将丢失.
#ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #ifndef __USE_GNU #define __USE_GNU #endif #include#include #include #include #include #include #include /* This structure mirrors the one found in /usr/include/asm/ucontext.h */ typedef struct _sig_ucontext { unsigned long uc_flags; struct ucontext *uc_link; stack_t uc_stack; struct sigcontext uc_mcontext; sigset_t uc_sigmask; } sig_ucontext_t; void crit_err_hdlr(int sig_num, siginfo_t * info, void * ucontext) { void * array[50]; void * caller_address; char ** messages; int size, i; sig_ucontext_t * uc; uc = (sig_ucontext_t *)ucontext; /* Get the address at the time the signal was raised */ #if defined(__i386__) // gcc specific caller_address = (void *) uc->uc_mcontext.eip; // EIP: x86 specific #elif defined(__x86_64__) // gcc specific caller_address = (void *) uc->uc_mcontext.rip; // RIP: x86_64 specific #else #error Unsupported architecture. // TODO: Add support for other arch. #endif fprintf(stderr, "signal %d (%s), address is %p from %p\n", sig_num, strsignal(sig_num), info->si_addr, (void *)caller_address); size = backtrace(array, 50); /* overwrite sigaction with caller's address */ array[1] = caller_address; messages = backtrace_symbols(array, size); /* skip first stack frame (points here) */ for (i = 1; i < size && messages != NULL; ++i) { fprintf(stderr, "[bt]: (%d) %s\n", i, messages[i]); } free(messages); exit(EXIT_FAILURE); } int crash() { char * p = NULL; *p = 0; return 0; } int foo4() { crash(); return 0; } int foo3() { foo4(); return 0; } int foo2() { foo3(); return 0; } int foo1() { foo2(); return 0; } int main(int argc, char ** argv) { struct sigaction sigact; sigact.sa_sigaction = crit_err_hdlr; sigact.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(SIGSEGV, &sigact, (struct sigaction *)NULL) != 0) { fprintf(stderr, "error setting signal handler for %d (%s)\n", SIGSEGV, strsignal(SIGSEGV)); exit(EXIT_FAILURE); } foo1(); exit(EXIT_SUCCESS); }
signal 11 (Segmentation fault), address is (nil) from 0x8c50 [bt]: (1) ./test(crash+0x24) [0x8c50] [bt]: (2) ./test(foo4+0x10) [0x8c70] [bt]: (3) ./test(foo3+0x10) [0x8c8c] [bt]: (4) ./test(foo2+0x10) [0x8ca8] [bt]: (5) ./test(foo1+0x10) [0x8cc4] [bt]: (6) ./test(main+0x74) [0x8d44] [bt]: (7) /lib/libc.so.6(__libc_start_main+0xa8) [0x40032e44]
在信号处理程序中调用backtrace()函数的所有危险仍然存在,不应该被忽视,但我发现我在这里描述的功能在调试崩溃时非常有用.
值得注意的是,我提供的示例是在Linux for x86上开发/测试的.我也使用uc_mcontext.arm_pc
而不是在ARM上成功实现了这个uc_mcontext.eip
.
以下是该文章的链接,其中我了解了此实现的详细信息:http: //www.linuxjournal.com/article/6391
即使提供了正确的答案,描述了如何使用GNU libc backtrace()
函数1,我提供了自己的答案,描述了如何确保从信号处理程序的回溯指向故障的实际位置2,我看不到任何提及从回溯输出的demangling C++符号.
从C++程序获取回溯时,输出可以通过c++filt
1运行以解码符号或直接使用1.abi::__cxa_demangle
1 Linux和OS X
请注意c++filt
并且__cxa_demangle
是特定于GCC的
2 Linux
下面的C++ Linux示例使用与我的其他答案相同的信号处理程序,并演示了如何c++filt
使用它来解码符号.
代码:
class foo { public: foo() { foo1(); } private: void foo1() { foo2(); } void foo2() { foo3(); } void foo3() { foo4(); } void foo4() { crash(); } void crash() { char * p = NULL; *p = 0; } }; int main(int argc, char ** argv) { // Setup signal handler for SIGSEGV ... foo * f = new foo(); return 0; }
输出(./test
):
signal 11 (Segmentation fault), address is (nil) from 0x8048e07 [bt]: (1) ./test(crash__3foo+0x13) [0x8048e07] [bt]: (2) ./test(foo4__3foo+0x12) [0x8048dee] [bt]: (3) ./test(foo3__3foo+0x12) [0x8048dd6] [bt]: (4) ./test(foo2__3foo+0x12) [0x8048dbe] [bt]: (5) ./test(foo1__3foo+0x12) [0x8048da6] [bt]: (6) ./test(__3foo+0x12) [0x8048d8e] [bt]: (7) ./test(main+0xe0) [0x8048d18] [bt]: (8) ./test(__libc_start_main+0x95) [0x42017589] [bt]: (9) ./test(__register_frame_info+0x3d) [0x8048981]
Demangled Output(./test 2>&1 | c++filt
):
signal 11 (Segmentation fault), address is (nil) from 0x8048e07 [bt]: (1) ./test(foo::crash(void)+0x13) [0x8048e07] [bt]: (2) ./test(foo::foo4(void)+0x12) [0x8048dee] [bt]: (3) ./test(foo::foo3(void)+0x12) [0x8048dd6] [bt]: (4) ./test(foo::foo2(void)+0x12) [0x8048dbe] [bt]: (5) ./test(foo::foo1(void)+0x12) [0x8048da6] [bt]: (6) ./test(foo::foo(void)+0x12) [0x8048d8e] [bt]: (7) ./test(main+0xe0) [0x8048d18] [bt]: (8) ./test(__libc_start_main+0x95) [0x42017589] [bt]: (9) ./test(__register_frame_info+0x3d) [0x8048981]
以下内容基于我原始答案中的信号处理程序,可以替换上例中的信号处理程序,以演示如何abi::__cxa_demangle
使用它来解码符号.该信号处理程序产生与上例相同的去格式输出.
代码:
void crit_err_hdlr(int sig_num, siginfo_t * info, void * ucontext) { sig_ucontext_t * uc = (sig_ucontext_t *)ucontext; void * caller_address = (void *) uc->uc_mcontext.eip; // x86 specific std::cerr << "signal " << sig_num << " (" << strsignal(sig_num) << "), address is " << info->si_addr << " from " << caller_address << std::endl << std::endl; void * array[50]; int size = backtrace(array, 50); array[1] = caller_address; char ** messages = backtrace_symbols(array, size); // skip first stack frame (points here) for (int i = 1; i < size && messages != NULL; ++i) { char *mangled_name = 0, *offset_begin = 0, *offset_end = 0; // find parantheses and +address offset surrounding mangled name for (char *p = messages[i]; *p; ++p) { if (*p == '(') { mangled_name = p; } else if (*p == '+') { offset_begin = p; } else if (*p == ')') { offset_end = p; break; } } // if the line could be processed, attempt to demangle the symbol if (mangled_name && offset_begin && offset_end && mangled_name < offset_begin) { *mangled_name++ = '\0'; *offset_begin++ = '\0'; *offset_end++ = '\0'; int status; char * real_name = abi::__cxa_demangle(mangled_name, 0, 0, &status); // if demangling is successful, output the demangled function name if (status == 0) { std::cerr << "[bt]: (" << i << ") " << messages[i] << " : " << real_name << "+" << offset_begin << offset_end << std::endl; } // otherwise, output the mangled function name else { std::cerr << "[bt]: (" << i << ") " << messages[i] << " : " << mangled_name << "+" << offset_begin << offset_end << std::endl; } free(real_name); } // otherwise, print the whole line else { std::cerr << "[bt]: (" << i << ") " << messages[i] << std::endl; } } std::cerr << std::endl; free(messages); exit(EXIT_FAILURE); }
可能值得一看的是Google Breakpad,一个跨平台的崩溃转储生成器和处理转储的工具.
您没有指定您的操作系统,因此很难回答.如果您使用的是基于gnu libc的系统,则可以使用libc函数backtrace()
.
GCC还有两个可以帮助你的内置版本,但是你的架构可能会或者可能不会完全实现,而且那些是__builtin_frame_address
和__builtin_return_address
.两者都想要一个立即的整数级别(通过立即,我的意思是它不能是一个变量).如果__builtin_frame_address
给定级别非零,则应该可以安全地获取相同级别的返回地址.
ulimit -c
在unix上设置核心文件大小限制.默认情况下,核心文件大小限制为0.您可以使用以下内容查看ulimit
值ulimit -a
.
另外,如果你从gdb中运行你的程序,它将停止你的程序"分段违规"(SIGSEGV
通常当你访问一块你没有分配的内存时)或者你可以设置断点.
ddd和nemiver是gdb的前端,这使新手更容易使用它.
感谢热心人士将我的注意力吸引到addr2line实用程序.
我写了一个快速而又脏的脚本来处理这里提供的答案的输出:(非常感谢jschmier!)使用addr2line实用程序.
该脚本接受一个参数:包含jschmier实用程序输出的文件名.
对于每个级别的跟踪,输出应该打印如下内容:
BACKTRACE: testExe 0x8A5db6b FILE: pathToFile/testExe.C:110 FUNCTION: testFunction(int) 107 108 109 int* i = 0x0; *110 *i = 5; 111 112 } 113 return i;
码:
#!/bin/bash LOGFILE=$1 NUM_SRC_CONTEXT_LINES=3 old_IFS=$IFS # save the field separator IFS=$'\n' # new field separator, the end of line for bt in `cat $LOGFILE | grep '\[bt\]'`; do IFS=$old_IFS # restore default field separator printf '\n' EXEC=`echo $bt | cut -d' ' -f3 | cut -d'(' -f1` ADDR=`echo $bt | cut -d'[' -f3 | cut -d']' -f1` echo "BACKTRACE: $EXEC $ADDR" A2L=`addr2line -a $ADDR -e $EXEC -pfC` #echo "A2L: $A2L" FUNCTION=`echo $A2L | sed 's/\.*//' | cut -d' ' -f2-99` FILE_AND_LINE=`echo $A2L | sed 's/.* at //'` echo "FILE: $FILE_AND_LINE" echo "FUNCTION: $FUNCTION" # print offending source code SRCFILE=`echo $FILE_AND_LINE | cut -d':' -f1` LINENUM=`echo $FILE_AND_LINE | cut -d':' -f2` if ([ -f $SRCFILE ]); then cat -n $SRCFILE | grep -C $NUM_SRC_CONTEXT_LINES "^ *$LINENUM\>" | sed "s/ $LINENUM/*$LINENUM/" else echo "File not found: $SRCFILE" fi IFS=$'\n' # new field separator, the end of line done IFS=$old_IFS # restore default field separator
重要的是要注意,一旦生成核心文件,您将需要使用gdb工具来查看它.要让gdb理解你的核心文件,你必须告诉gcc使用调试符号来检测二进制文件:为此,使用-g标志进行编译:
$ g++ -g prog.cpp -o prog
然后,你可以设置"ulimit -c unlimited"让它转储核心,或者只是在gdb中运行你的程序.我更喜欢第二种方法:
$ gdb ./prog ... gdb startup output ... (gdb) run ... program runs and crashes ... (gdb) where ... gdb outputs your stack trace ...
我希望这有帮助.
我一直在看这个问题.
并深深埋藏在Google Performance Tools自述文件中
http://code.google.com/p/google-perftools/source/browse/trunk/README
谈论libunwind
http://www.nongnu.org/libunwind/
很想听听这个图书馆的意见.
-rdynamic的问题在于它可以在某些情况下相对显着地增加二进制文件的大小
某些版本的libc包含处理堆栈跟踪的函数; 你可以使用它们:
http://www.gnu.org/software/libc/manual/html_node/Backtraces.html
我记得很久以前使用libunwind来获取堆栈跟踪,但是你的平台可能不支持它.
您可以使用DeathHandler - 小型C++类,它可以为您提供一切可靠的服务.
忘记改变你的来源并使用backtrace()函数或宏来做一些黑客 - 这些只是糟糕的解决方案.
作为一个正常工作的解决方案,我建议:
使用"-g"标志编译程序,以便将调试符号嵌入到二进制文件中(不要担心这不会影响您的性能).
在linux上运行下一个命令:"ulimit -c unlimited" - 允许系统进行大型崩溃转储.
当您的程序崩溃时,在工作目录中您将看到文件"core".
运行下一个命令将backtrace打印到stdout:gdb -batch -ex"backtrace"./ your_program_exe ./core
这将以人类可读的方式打印程序的正确可读回溯(具有源文件名和行号).此外,这种方法可让您自由地自动化系统:使用一个简短的脚本来检查进程是否创建了核心转储,然后通过电子邮件向开发人员发送回溯,或将其记录到某些日志记录系统中.
ulimit -c unlimited
是一个系统变量,它将允许在应用程序崩溃后创建核心转储.在这种情况下无限量.在同一目录中查找名为core的文件.确保在启用调试信息的情况下编译代码!
问候
看着:
男人3回溯
和:
#includeint backtrace(void **buffer, int size);
这些是GNU扩展.
请参阅ACE中的堆栈跟踪工具(自适应通信环境).它已经编写为涵盖所有主要平台(以及更多).该库是BSD风格的许可证,因此如果您不想使用ACE,您甚至可以复制/粘贴代码.
我可以帮助Linux版本:可以使用函数backtrace,backtrace_symbols和backtrace_symbols_fd.请参阅相应的手册页.