我使用GCC编译器在Linux上工作。当我的c++程序崩溃时,我希望它自动生成一个堆栈跟踪。
我的程序由许多不同的用户运行,它也可以在Linux、Windows和Macintosh上运行(所有版本都使用gcc编译)。
我希望我的程序能够在崩溃时生成堆栈跟踪,下次用户运行它时,它会问他们是否可以将堆栈跟踪发送给我,这样我就可以跟踪到问题。我可以处理发送信息给我,但我不知道如何生成跟踪字符串。什么好主意吗?
我使用GCC编译器在Linux上工作。当我的c++程序崩溃时,我希望它自动生成一个堆栈跟踪。
我的程序由许多不同的用户运行,它也可以在Linux、Windows和Macintosh上运行(所有版本都使用gcc编译)。
我希望我的程序能够在崩溃时生成堆栈跟踪,下次用户运行它时,它会问他们是否可以将堆栈跟踪发送给我,这样我就可以跟踪到问题。我可以处理发送信息给我,但我不知道如何生成跟踪字符串。什么好主意吗?
当前回答
对于Linux和Mac OS X,如果使用gcc或任何使用glibc的编译器,可以使用execinfo.h中的backtrace()函数打印堆栈跟踪,并在出现分段错误时优雅地退出。文档可以在libc手册中找到。
下面是一个示例程序,它安装了一个SIGSEGV处理程序,并在发生段错误时将堆栈跟踪输出到stderr。这里的baz()函数会导致触发处理程序的段错误:
#include <stdio.h>
#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
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可以使用它来创建一个漂亮的stacktrace:
$ 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]
这显示了堆栈中每个帧的加载模块、偏移量和函数。在这里,您可以看到信号处理程序位于堆栈顶部,除了main、foo、bar和baz之外,还在main之前有libc函数。
其他回答
我研究这个问题有一段时间了。
深埋在谷歌性能工具README
http://code.google.com/p/google-perftools/source/browse/trunk/README
谈到libunwind
http://www.nongnu.org/libunwind/
很乐意听听对这个图书馆的意见。
使用-rdynamic的问题是,在某些情况下,它会相对显著地增加二进制文件的大小
尽管已经提供了描述如何使用GNU libc backtrace()函数1的正确答案,并且我提供了自己的答案,描述了如何确保从信号处理程序的回溯指向fault2的实际位置,但我没有看到任何从回溯输出的要求c++符号的提及。
当从c++程序获得回溯时,可以通过c++filt1运行输出来要求符号,或者直接使用abi::__cxa_demangle1。
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;
}
输出(. /测试):
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]
需求输出(。/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);
}
作为一种仅适用于Windows的解决方案,您可以使用Windows错误报告(Error Reporting)获得相当于堆栈跟踪的信息(包含多得多的信息)。只需几个注册表项,就可以设置为收集用户模式转储:
从Windows Server 2008和Windows Vista with Service Pack 1 (SP1)开始,可以配置Windows错误报告(WER),以便在用户模式应用程序崩溃后收集完整的用户模式转储并存储在本地。[…] 默认情况下不启用此特性。启用该特性需要管理员权限。要启用和配置该功能,请在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps键下使用以下注册表值。
您可以从具有所需权限的安装程序中设置注册表项。
创建用户模式转储比在客户端生成堆栈跟踪有以下优点:
It's already implemented in the system. You can either use WER as outlined above, or call MiniDumpWriteDump yourself, if you need more fine-grained control over the amount of information to dump. (Make sure to call it from a different process.) Way more complete than a stack trace. Among others it can contain local variables, function arguments, stacks for other threads, loaded modules, and so on. The amount of data (and consequently size) is highly customizable. No need to ship debug symbols. This both drastically decreases the size of your deployment, as well as makes it harder to reverse-engineer your application. Largely independent of the compiler you use. Using WER does not even require any code. Either way, having a way to get a symbol database (PDB) is very useful for offline analysis. I believe GCC can either generate PDB's, or there are tools to convert the symbol database to the PDB format.
请注意,WER只能由应用程序崩溃触发(即系统由于未处理的异常而终止进程)。MiniDumpWriteDump可以在任何时候调用。如果您需要转储当前状态以诊断崩溃以外的问题,这可能会很有帮助。
如果你想评估mini dump的适用性,必读:
有效minidumps 有效的小排量(第二部分)
我在这里见过很多答案执行一个信号处理程序,然后退出。 这就是方法,但是请记住一个非常重要的事实:如果您想获得生成错误的核心转储,则不能调用exit(status)。而是调用abort() !
忘记改变你的源代码,用backtrace()函数或宏来做一些hack -这些只是糟糕的解决方案。
作为一个有效的解决方案,我的建议是:
用"-g"标志编译程序,用于将调试符号嵌入二进制(不用担心,这不会影响您的性能)。 在linux上运行下一个命令:"ulimit -c unlimited" -允许系统进行大崩溃转储。 当你的程序崩溃时,在工作目录中你会看到文件“core”。 gdb -batch -ex "backtrace" ./your_program_exe ./core . exe .执行下一个命令,将backtrace输出到标准输出
这将以人类可读的方式(包含源文件名和行号)打印程序的正确可读回溯。 此外,这种方法将给你自由自动化你的系统: 编写一个简短的脚本,检查进程是否创建了核心转储,然后通过电子邮件向开发人员发送回溯信息,或将其记录到一些日志系统中。