1. 崩溃信号处理
当程序崩溃时,操作系统会发送一个信号给程序,通知它发生了异常。在 C++中,可以通过 signal 函数来注册一个信号处理程序,使程序能够在接收到该信号时执行自定义的代码。
#include <execinfo.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void signal_handler(int sig) {
// print out all the info to a log file
_exit(1); // exit the program
}
int main() {
// register signal handler
signal(SIGSEGV, signal_handler);
// your code here
return 0;
}
有这个回调函数就解决了崩溃是否能够被观测的问题。
接下来就是获取崩溃信息的途径。
2. 打印崩溃信息
一般察看函数运行时堆栈的方法是使用GDB之类的外部调试器。但是,实际运行环境中,我们不可能让程序一直在调试状态下运行。所以,在正常运行期间出错时,打印出函数的调用堆栈是非常有用的。backtrace函数就给我们提供了一个很好的接口。
在头文件"execinfo.h"中声明了三个函数用于获取当前线程的函数调用堆栈。
int backtrace(void **buffer, int size)
该函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针列表。参数 size 用来指定buffer中可以保存多少个void* 元素。函数返回值是实际获取的指针个数,最大不超过size大小,在buffer中的指针实际是从堆栈中获取的返回地址,每一个堆栈框架有一个返回地址,注意某些编译器的优化选项对获取正确的调用堆栈有干扰,另外内联函数没有堆栈框架;删除框架指针也会使无法正确解析堆栈内容。
char ** backtrace_symbols (void *const *buffer, int size)
backtrace_symbols将从backtrace函数获取的信息转化为一个字符串数组。参数buffer应该是从backtrace函数获取的数组指针,size是该数组中的元素个数(backtrace的返回值)。函数返回值是一个指向字符串数组的指针,它的大小同buffer相同。每个字符串包含了一个相对于buffer中对应元素的可打印信息。它包括函数名,函数的偏移地址,和实际的返回地址。
该函数的返回值是通过malloc函数申请的空间,因此调用这必须使用free函数来释放指针.
注意:如果不能为字符串获取足够的空间函数的返回值将会为NULL
void backtrace_symbols_fd (void *const *buffer, int size, int fd)
backtrace_symbols_fd与backtrace_symbols 函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行。它不需要调用malloc函数,因此适用于有可能调用该函数会失败的情况。
下面是一个使用backtrace捕获异常并打印函数调用堆栈的例子:
#include <execinfo.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void signal_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 a log file
FILE* fp = fopen("crash.log", "w");
if (fp != NULL) {
fprintf(fp, "Error: signal %d\n", sig);
backtrace_symbols_fd(array, size, fileno(fp));
fclose(fp);
}
_exit(1); // exit the program
}
int main() {
// register signal handler
signal(SIGSEGV, signal_handler);
// your code here
return 0;
}
崩溃堆栈:
- 函数调用地址:表示该函数在程序内存中的位置。
- 函数名和参数:表示该函数的名称和参数列表。如果无法获取函数名,则显示为"???"。
- 文件名和行号:表示该函数在源代码中的位置。如果无法获取文件名和行号,则显示为"(unknown)"。
Error: signal 11
[0] ./myprogram(+0x1139) [0x563a21a8f139]
[1] ./myprogram(+0x1298) [0x563a21a8f298]
[2] /lib/x86_64-linux-gnu/libc.so.6(+0x3ef20) [0x7f9c1d069f20]
[3] ./myprogram(+0xe75) [0x563a21a8ee75]
[4] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7f9c1cf9e0b3]
[5] ./myprogram(+0x992) [0x563a21a8e992]
符号表(Symbol Table)是一个存储程序函数名、变量名和其他符号信息的数据库,通常包含函数名、函数的起始地址、函数的大小、文件名、行号等信息。
在Linux下,可以使用GNU Binutils提供的addr2line命令来查询符号表,从而将函数地址和行号转换为源代码中的具体位置。
以下是一个使用addr2line命令查询符号表并输出对应代码位置的示例代码:
#include <execinfo.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void signal_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 a log file
FILE* fp = fopen("crash.log", "w");
if (fp != NULL) {
fprintf(fp, "Error: signal %d\n", sig);
for (int i = 0; i < size; i++) {
fprintf(fp, "[%d] ", i);
if (array[i] != NULL) {
char addr2line_cmd[256];
sprintf(addr2line_cmd, "addr2line -f -e ./myprogram %p", array[i]);
FILE* addr2line_fp = popen(addr2line_cmd, "r");
if (addr2line_fp != NULL) {
char line[256];
while (fgets(line, sizeof(line), addr2line_fp) != NULL) {
fprintf(fp, "%s", line);
}
pclose(addr2line_fp);
}
} else {
fprintf(fp, "(unknown)\n");
}
}
fclose(fp);
}
_exit(1); // exit the program
}
int main() {
// register signal handler
signal(SIGSEGV, signal_handler);
// your code here
return 0;
}
通过addr2line转换之后:
Error: signal 11
[0] ./myprogram(foo+0x1d) [0x563a21a8f139]
at example.cpp:27
[1] ./myprogram(bar+0x18) [0x563a21a8f298]
at example.cpp:36
[2] /lib/x86_64-linux-gnu/libc.so.6(__GI___libc_read+0x10) [0x7f9c1d069f20]
at ../sysdeps/unix/syscall-template.S:185
[3] ./myprogram(main+0x25) [0x563a21a8ee75]
at example.cpp:45
[4] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7f9c1cf9e0b3]
at ../csu/libc-start.c:308
[5] ./myprogram(_start+0x2a) [0x563a21a8e992]
at ???:0