我使用GCC编译器在Linux上工作。当我的c++程序崩溃时,我希望它自动生成一个堆栈跟踪。
我的程序由许多不同的用户运行,它也可以在Linux、Windows和Macintosh上运行(所有版本都使用gcc编译)。
我希望我的程序能够在崩溃时生成堆栈跟踪,下次用户运行它时,它会问他们是否可以将堆栈跟踪发送给我,这样我就可以跟踪到问题。我可以处理发送信息给我,但我不知道如何生成跟踪字符串。什么好主意吗?
我使用GCC编译器在Linux上工作。当我的c++程序崩溃时,我希望它自动生成一个堆栈跟踪。
我的程序由许多不同的用户运行,它也可以在Linux、Windows和Macintosh上运行(所有版本都使用gcc编译)。
我希望我的程序能够在崩溃时生成堆栈跟踪,下次用户运行它时,它会问他们是否可以将堆栈跟踪发送给我,这样我就可以跟踪到问题。我可以处理发送信息给我,但我不知道如何生成跟踪字符串。什么好主意吗?
当前回答
它甚至比“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
其他回答
作为一种仅适用于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 有效的小排量(第二部分)
我可以帮助Linux版本:函数backtrace, backtrace_symbols和backtrace_symbols_fd可以使用。请参见相应的手册。
我迄今为止最好的异步信号安全尝试
如果不安全请告诉我。我还没有找到显示行号的方法。
#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
我在这里见过很多答案执行一个信号处理程序,然后退出。 这就是方法,但是请记住一个非常重要的事实:如果您想获得生成错误的核心转储,则不能调用exit(status)。而是调用abort() !