我使用GCC编译器在Linux上工作。当我的c++程序崩溃时,我希望它自动生成一个堆栈跟踪。
我的程序由许多不同的用户运行,它也可以在Linux、Windows和Macintosh上运行(所有版本都使用gcc编译)。
我希望我的程序能够在崩溃时生成堆栈跟踪,下次用户运行它时,它会问他们是否可以将堆栈跟踪发送给我,这样我就可以跟踪到问题。我可以处理发送信息给我,但我不知道如何生成跟踪字符串。什么好主意吗?
我使用GCC编译器在Linux上工作。当我的c++程序崩溃时,我希望它自动生成一个堆栈跟踪。
我的程序由许多不同的用户运行,它也可以在Linux、Windows和Macintosh上运行(所有版本都使用gcc编译)。
我希望我的程序能够在崩溃时生成堆栈跟踪,下次用户运行它时,它会问他们是否可以将堆栈跟踪发送给我,这样我就可以跟踪到问题。我可以处理发送信息给我,但我不知道如何生成跟踪字符串。什么好主意吗?
当前回答
我忘记了GNOME的“apport”技术,但我不太了解如何使用它。它用于生成堆栈跟踪和其他用于处理的诊断,并可以自动归档错误。这当然值得一看。
其他回答
在Linux/unix/MacOSX上使用核心文件(您可以通过ulimit或兼容的系统调用启用它们)。在Windows上使用Microsoft错误报告(您可以成为合作伙伴并访问您的应用程序崩溃数据)。
我研究这个问题有一段时间了。
深埋在谷歌性能工具README
http://code.google.com/p/google-perftools/source/browse/trunk/README
谈到libunwind
http://www.nongnu.org/libunwind/
很乐意听听对这个图书馆的意见。
使用-rdynamic的问题是,在某些情况下,它会相对显著地增加二进制文件的大小
对于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函数。
我迄今为止最好的异步信号安全尝试
如果不安全请告诉我。我还没有找到显示行号的方法。
#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#define TRACE_MAX 1024
void handler(int sig) {
(void)sig;
void *array[TRACE_MAX];
size_t size;
const char msg[] = "failed with a signal\n";
size = backtrace(array, TRACE_MAX);
write(STDERR_FILENO, msg, sizeof(msg));
backtrace_symbols_fd(array, size, STDERR_FILENO);
_Exit(1);
}
void my_func_2(void) {
*((int*)0) = 1;
}
void my_func_1(double f) {
(void)f;
my_func_2();
}
void my_func_1(int i) {
(void)i;
my_func_2();
}
int main() {
/* Make a dummy call to `backtrace` to load libgcc because man backrace says:
* * backtrace() and backtrace_symbols_fd() don't call malloc() explicitly, but they are part of libgcc, which gets loaded dynamically when first used. Dynamic loading usually triggers a call to mal‐
* loc(3). If you need certain calls to these two functions to not allocate memory (in signal handlers, for example), you need to make sure libgcc is loaded beforehand.
*/
void *dummy[1];
backtrace(dummy, 1);
signal(SIGSEGV, handler);
my_func_1(1);
}
编译并运行:
g++ -ggdb3 -O2 -std=c++11 -Wall -Wextra -pedantic -rdynamic -o stacktrace_on_signal_safe.out stacktrace_on_signal_safe.cpp
./stacktrace_on_signal_safe.out
需要使用-rdynamic来获取函数名:
failed with a signal
./stacktrace_on_signal_safe.out(_Z7handleri+0x6e)[0x56239398928e]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f04b1459520]
./stacktrace_on_signal_safe.out(main+0x38)[0x562393989118]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f04b1440d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f04b1440e40]
./stacktrace_on_signal_safe.out(_start+0x25)[0x562393989155]
然后,我们可以将它管道到c++filt中以demangle:
./stacktrace_on_signal_safe.out |& c++filt
给:
failed with a signal
/stacktrace_on_signal_safe.out(handler(int)+0x6e)[0x55b6df43f28e]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f40d4167520]
./stacktrace_on_signal_safe.out(main+0x38)[0x55b6df43f118]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f40d414ed90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f40d414ee40]
./stacktrace_on_signal_safe.out(_start+0x25)[0x55b6df43f155]
由于优化,几个级别都丢失了,使用-O0我们得到一个更完整的:
/stacktrace_on_signal_safe.out(handler(int)+0x76)[0x55d39b68325f]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f4d8ffdd520]
./stacktrace_on_signal_safe.out(my_func_2()+0xd)[0x55d39b6832bb]
./stacktrace_on_signal_safe.out(my_func_1(int)+0x14)[0x55d39b6832f1]
./stacktrace_on_signal_safe.out(main+0x4a)[0x55d39b68333e]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f4d8ffc4d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f4d8ffc4e40]
./stacktrace_on_signal_safe.out(_start+0x25)[0x55d39b683125]
行号不存在,但我们可以通过addr2line获取它们。这需要不使用-rdynamic进行构建:
g++ -ggdb3 -O0 -std=c++23 -Wall -Wextra -pedantic -o stacktrace_on_signal_safe.out stacktrace_on_signal_safe.cpp
./stacktrace_on_signal_safe.out |& sed -r 's/.*\(//;s/\).*//' | addr2line -C -e stacktrace_on_signal_safe.out -f
生产:
??
??:0
handler(int)
/home/ciro/stacktrace_on_signal_safe.cpp:14
??
??:0
my_func_2()
/home/ciro/stacktrace_on_signal_safe.cpp:22
my_func_1(i
/home/ciro/stacktrace_on_signal_safe.cpp:33
main
/home/ciro/stacktrace_on_signal_safe.cpp:45
??
??:0
??
??:0
_start
??:?
Awk解析出非-rdynamic输出的+<addr>编号:
./stacktrace_on_signal_safe.out(+0x125f)[0x55984828825f]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f8644a1e520]
./stacktrace_on_signal_safe.out(+0x12bb)[0x5598482882bb]
./stacktrace_on_signal_safe.out(+0x12f1)[0x5598482882f1]
./stacktrace_on_signal_safe.out(+0x133e)[0x55984828833e]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f8644a05d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f8644a05e40]
./stacktrace_on_signal_safe.out(+0x1125)[0x559848288125]
如果你还想将实际的信号数打印到stdout,这里有一个异步信号安全实现int到字符串:使用write或异步安全函数从信号处理程序打印int,因为printf不是。
在Ubuntu 22.04上测试。
C++23 <stacktrace>
与许多其他答案一样,本节忽略了问题的异步信号安全方面,这可能导致代码在崩溃时死锁,这可能会很严重。我们只希望有一天c++标准会添加一个boost::stacktrace::safe_dump_to类函数来一劳永逸地解决这个问题。
这将是一般更优秀的c++堆栈跟踪选项,正如前面提到的:在C或c++中打印调用堆栈,因为它显示行号并自动为我们执行需求。
stacktrace_on_signal.cpp
#include <stacktrace>
#include <iostream>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void handler(int sig) {
(void)sig;
/* De-register this signal in the hope of avoiding infinite loops
* if asyns signal unsafe things fail later on. But can likely still deadlock. */
signal(sig, SIG_DFL);
// std::stacktrace::current
std::cout << std::stacktrace::current();
// C99 async signal safe version of exit().
_Exit(1);
}
void my_func_2(void) {
*((int*)0) = 1;
}
void my_func_1(double f) {
(void)f;
my_func_2();
}
void my_func_1(int i) {
(void)i;
my_func_2();
}
int main() {
signal(SIGSEGV, handler);
my_func_1(1);
}
编译并运行:
g++ -ggdb3 -O2 -std=c++23 -Wall -Wextra -pedantic -o stacktrace_on_signal.out stacktrace_on_signal.cpp -lstdc++_libbacktrace
./stacktrace_on_signal.out
在GCC 12.1上从源代码编译的输出,Ubuntu 22.04:
0# handler(int) at /home/ciro/stacktrace_on_signal.cpp:11
1# at :0
2# my_func_2() at /home/ciro/stacktrace_on_signal.cpp:16
3# at :0
4# at :0
5# at :0
6#
我认为它错过了my_func_1,由于优化被打开,通常没有什么我们可以做的AFAIK。用-O0代替会更好:
0# handler(int) at /home/ciro/stacktrace_on_signal.cpp:11
1# at :0
2# my_func_2() at /home/ciro/stacktrace_on_signal.cpp:16
3# my_func_1(int) at /home/ciro/stacktrace_on_signal.cpp:26
4# at /home/ciro/stacktrace_on_signal.cpp:31
5# at :0
6# at :0
7# at :0
8#
但不知道为什么梅恩没有出现。
backtrace_simple
https://github.com/gcc-mirror/gcc/blob/releases/gcc-12.1.0/libstdc%2B%2B-v3/src/libbacktrace/backtrace-supported.h.in#L45提到backtrace_simple是安全的:
/* BACKTRACE_USES_MALLOC will be #define'd as 1 if the backtrace
library will call malloc as it works, 0 if it will call mmap
instead. This may be used to determine whether it is safe to call
the backtrace functions from a signal handler. In general this
only applies to calls like backtrace and backtrace_pcinfo. It does
not apply to backtrace_simple, which never calls malloc. It does
not apply to backtrace_print, which always calls fprintf and
therefore malloc. */
但它使用起来似乎不太方便,主要是一个内部工具。
标准::basic_stacktrace
这就是std::stacktrace的基础,根据:https://en.cppreference.com/w/cpp/utility/basic_stacktrace
它有一个分配器参数,cppreference将其描述为:
为在热路径或嵌入式环境中使用basic_stacktrace提供了自定义分配器的支持。用户可以在堆栈上或其他适当的地方分配stacktrace_entry对象。
所以我想知道如果basic_stacktrace本身是异步信号安全的,如果它不可能使std::stacktrace的一个版本也与自定义分配器,例如:
写入磁盘上的文件,如boost::stacktrace::safe_dump_to 或者写入某个预先分配的具有最大大小的堆栈缓冲区
https://apolukhin.github.io/papers/stacktrace_r1.html可能是提案,提到:
关于信号安全的注意:本建议并不试图为捕获和解码堆栈跟踪提供信号安全的解决方案。这种功能目前还不能在一些流行的平台上实现。然而,本文试图提供一个可扩展的解决方案,通过提供一个信号安全分配器和改变堆栈跟踪实现细节,有可能使信号安全。
只是得到核心转储吗?
核心转储允许您使用GDB检查内存:当程序的核心转储文件具有命令行参数时,如何使用GDB分析它?所以它比只有痕迹更强大。
只要确保你正确地启用了它,特别是在Ubuntu 22.04上,你需要:
echo 'core' | sudo tee /proc/sys/kernel/core_pattern
或者要学习使用apport,请参见:https://askubuntu.com/questions/1349047/where-do-i-find-core-dump-files-and-how-do-i-view-and-analyze-the-backtrace-st/1442665#1442665
You are probably not going to like this - all I can say in its favour is that it works for me, and I have similar but not identical requirements: I am writing a compiler/transpiler for a 1970's Algol-like language which uses C as it's output and then compiles the C so that as far as the user is concerned, they're generally not aware of C being involved, so although you might call it a transpiler, it's effectively a compiler that uses C as it's intermediate code. The language being compiled has a history of providing good diagnostics and a full backtrace in the original native compilers. I've been able to find gcc compiler flags and libraries etc that allow me to trap most of the runtime errors that the original compilers did (although with one glaring exception - unassigned variable trapping). When a runtime error occurs (eg arithmetic overflow, divide by zero, array index out of bounds, etc) the original compilers output a backtrace to the console listing all variables in the stack frames of every active procedure call. I struggled to get this effect in C, but eventually did so with what can only be described as a hack... When the program is invoked, the wrapper that supplies the C "main" looks at its argv, and if a special option is not present, it restarts itself under gdb with an altered argv containing both gdb options and the 'magic' option string for the program itself. This restarted version then hides those strings from the user's code by restoring the original arguments before calling the main block of the code written in our language. When an error occurs (as long as it is not one explicitly trapped within the program by user code), it exits to gdb which prints the required backtrace.
启动序列中的关键代码行包括:
if ((argc >= 1) && (strcmp(origargv[argc-1], "--restarting-under-gdb")) != 0) {
// initial invocation
// the "--restarting-under-gdb" option is how the copy running under gdb knows
// not to start another gdb process.
and
char *gdb [] = {
"/usr/bin/gdb", "-q", "-batch", "-nx", "-nh", "-return-child-result",
"-ex", "run",
"-ex", "bt full",
"--args"
};
The original arguments are appended to the gdb options above. That should be enough of a hint for you to do something similar for your own system. I did look at other library-supported backtrace options (eg libbacktrace, https://codingrelic.geekhold.com/2010/09/gcc-function-instrumentation.html, etc) but they only output the procedure call stack, not the local variables. However if anyone knows of any cleaner mechanism to get a similar effect, do please let us know. The main downside to this is that the variables are printed in C syntax, not the syntax of the language the user writes in. And (until I add suitable #line directives on every generated line of C :-() the backtrace lists the C source file and line numbers.
G PS我使用的gcc编译选项是:
GCCOPTS=" -Wall -Wno-return-type -Wno-comment -g -fsanitize=undefined
-fsanitize-undefined-trap-on-error -fno-sanitize-recover=all -frecord-gcc-switches
-fsanitize=float-divide-by-zero -fsanitize=float-cast-overflow -ftrapv
-grecord-gcc-switches -O0 -ggdb3 "