我使用GCC编译器在Linux上工作。当我的c++程序崩溃时,我希望它自动生成一个堆栈跟踪。
我的程序由许多不同的用户运行,它也可以在Linux、Windows和Macintosh上运行(所有版本都使用gcc编译)。
我希望我的程序能够在崩溃时生成堆栈跟踪,下次用户运行它时,它会问他们是否可以将堆栈跟踪发送给我,这样我就可以跟踪到问题。我可以处理发送信息给我,但我不知道如何生成跟踪字符串。什么好主意吗?
我使用GCC编译器在Linux上工作。当我的c++程序崩溃时,我希望它自动生成一个堆栈跟踪。
我的程序由许多不同的用户运行,它也可以在Linux、Windows和Macintosh上运行(所有版本都使用gcc编译)。
我希望我的程序能够在崩溃时生成堆栈跟踪,下次用户运行它时,它会问他们是否可以将堆栈跟踪发送给我,这样我就可以跟踪到问题。我可以处理发送信息给我,但我不知道如何生成跟踪字符串。什么好主意吗?
当前回答
在Linux/unix/MacOSX上使用核心文件(您可以通过ulimit或兼容的系统调用启用它们)。在Windows上使用Microsoft错误报告(您可以成为合作伙伴并访问您的应用程序崩溃数据)。
其他回答
需要注意的是,一旦生成了核心文件,就需要使用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 ...
我希望这能有所帮助。
一些版本的libc包含处理堆栈跟踪的函数;你可能会用到它们:
http://www.gnu.org/software/libc/manual/html_node/Backtraces.html
我记得很久以前使用过libunwind来获取堆栈跟踪,但您的平台可能不支持它。
我忘记了GNOME的“apport”技术,但我不太了解如何使用它。它用于生成堆栈跟踪和其他用于处理的诊断,并可以自动归档错误。这当然值得一看。
我迄今为止最好的异步信号安全尝试
如果不安全请告诉我。我还没有找到显示行号的方法。
#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
它甚至比“man backtrace”更简单,有一个很少有文档的库(GNU专用)作为libSegFault与glibc一起分发。所以,我相信这是由Ulrich Drepper写的,以支持程序catchsegv(见“man catchsegv”)。
这给了我们3种可能性。而不是运行“program -o hai”:
在catchsegv中运行: $ catchsegv程序-o hai 在运行时使用libSegFault链接: LD_PRELOAD = / lib / libSegFault美元。所以编程-o hai 在编译时使用libSegFault链接: $ gcc -g1 -lSegFault -o program program.cc $ program -o hai
在这三种情况下,您将获得更清晰的回溯,并减少优化(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