linux/unix段错误捕获
// 段错误捕获,并打印栈信息 static void segvhandler(int sig) { #define BACKTRACE_MAX_FRAMES 100 static bool in_handler = false; printf("capture a signal: %d", sig); if (!in_handler){ int j, nptrs; void* buffer[BACKTRACE_MAX_FRAMES]; char** symbols; in_handler = true; nptrs = backtrace(buffer, BACKTRACE_MAX_FRAMES); printf("SIGSEGV captured, stack trace(%d):", nptrs); symbols = backtrace_symbols(buffer, nptrs); if (symbols != NULL){ for (j = 0; j < nptrs; j++) printf("%s", symbols[j]); free(symbols); } in_handler = false; } // do with all necessary things before exit process_before_exit(); exit(-1); }
打印出地址信息,再通过addr2line输出函数信息。
backtrace是库函数引入的应用自调试函数。
系列里的三个函数可以缓冲或输出栈帧。
#include <execinfo.h>
int backtrace(void **buffer, int size);
char **backtrace_symbols(void *const *buffer, int size);
void backtrace_symbols_fd(void *const *buffer, int size, int fd);
参考:backtrace、backtrace_symbols、backtrace_symbols_fd-support for application self-debugging
背景知识:
· 在linux/unix中的信号处理机制,知道signal函数与sigaction的区别
· 段错误的概念,CPU中断处理的步骤,中断向量表的分类
· 知道CPU Exception分为Fault、trap和abort,了解他们的基本区别
· 段错误和浮点错误属于Fault,产生Fault时会将出错指令的地址入栈,而不是下一条将执行指令的地址
· 在linux/unix里可以通过调用backstrace来获取栈帧的信息
· 文中用到的几个头文件和函数,都属于glibc,所以不用担心出现找不到头文件和链接错误的情况
· addr2line是个系统自带的小工具,用来转换编译出来的地址和源码行号
一、捕获段错误
struct sigaction act; int sig = SIGSEGV; sigemptyset(&act.sa_mask); act.sa_sigaction = OnSIGSEGV; act.sa_flags = SA_SIGINFO; if(sigaction(sig, &act, NULL)<0) { perror("sigaction:"); } void OnSIGSEGV(int signum, siginfo_t *info, void *ptr) { //TO DO: 输出堆栈信息 abort(); }
发生段错误时的函数调用关系体现在栈帧上,可以通过在信号处理函数中调用 backstrace 来获取栈帧信息,backstrace 的具体描述可google之/阅读头文件execinfo.h。
void OnSIGSEGV(int signum, siginfo_t *info, void *ptr) { void * array[25]; /* 25 层,太够了 : ),你也可以自己设定个其他值 */ int nSize = backtrace(array, sizeof(array)/sizeof(array[0])); for (int i=nSize-3; i>=2; i--){ /* 头尾几个地址不必输出,看官要是好奇,输出来看看就知道了 */ /* 修正array使其指向正在执行的代码 */[f1] printf("SIGSEGV catched when running code at %x\n", (char*)array[i] - 1); } abort(); }
要想输出出错的具体位置,必须用到信号处理函数的第三个参数,在linux/unix环境下,该指针指向一个ucontext_t结构。这个结构的具体情况,可以通过阅读头文件ucontext.h得知。此结构体里面包含了发生段错误时的寄存器现场,其中就包含EIP寄存器,该寄存器的内容正是段错误时的指令地址(因为段错误是一种Fault)。
void OnSIGSEGV(int signum, siginfo_t *info, void *ptr) { void * array[25]; int nSize = backtrace(array, sizeof(array)/sizeof(array[0])); for (int i=nSize-3; i>2; i--){ /* 头尾几个地址不必输出 */ /* 对array修正一下,使地址指向正在执行的代码 */ printf("signal[%d] catched when running code at %x\n", signum, (char*)array[i] - 1); } if (NULL != ptr){ ucontext_t* ptrUC = (ucontext_t*)ptr; int *pgregs = (int*)(&(ptrUC->uc_mcontext.gregs)); int eip = pgregs[REG_EIP]; if (eip != array[i]){ /* 有些处理器会将出错时的 EIP 也入栈 */ printf("signal[%d] catched when running code at %x\n", signum, (char*)array[i] - 1); } printf("signal[%d] catched when running code at %x\n", signum, eip); /* 出错地址 */ }else{ printf("signal[%d] catched when running code at unknown address\n", signum); } abort(); }
好了,现在栈帧里面的地址和出错位置的地址都已经以十六进制的形式输出了,但是这是编译后的地址,而不是源码的行号,你能看懂么?所以还需要借助一个linux/unix自带的小工具addr2line,将这些打印出来的指令地址转换为行号、函数名。
[root@suse tcpBreak]# ./a.out signal[11] catched when running code at 804861d signal[11] catched when running code at 8048578 signal[11] catched when running code at 804855a [root@ suse tcpBreak]# addr2line 804861d 8048578 804855a -s -C -f -e a.out main newsig.cpp:55 oops() newsig.cpp:32 error(int) newsig.cpp:27
捕获的信号序号是 11 (SIGSEGV)
执行路径是第52行--第32行--第27行
调用关系是main--oops--error,在error函数内部,即文件的第27行发生了段错误。
——一点讨论
· 你可能已经阅读了 execinfo.h,发现其中有一个 backtrace_symbols,想通过调用这个函数来输出stack frame上面的函数名…你不妨试一下
· 将 backtrace 得到的 array 地址元素减 1 就能得到调用地点么?的确是这样的,减 1 不保证地址落到函数调用时跳转指令的起始处,但可以保证指向了该指令的最后一个字节,而该指令地址经addr2line转换后[f2] ,就对应了发生函数调用的行号。
· 可不可以不调用 backstrace 来得到栈帧中的内容?可以的,因为这些内容都在栈里,你要是明确地知道偏移,就可以得知函数调用栈,但是要费很多心思,而且估计你自己写的模仿 backstrace 的代码,可移植性成了问题。
· 通过 gdb 调试 core文件 不是直接看得到内存映像么,还有必要搞得这么复杂么?一般情况下当然不必要,上面所列解决方法的优点在无法正常产生 core 文件的情况[f3] 下才得以体现。
· 需要在编译时添加选项 -g 么?当然需要了,不在可执行文件中记录行号信息,addr2line上哪里去找行号。否则只能得到函数名称,无法得到行号信息。
二、出错代码在三方库中
signal[8] catched when running code at 8048ab3 signal[8] catched when running code at 4001771b signal[8] catched when running code at 400176fd # addr2line 8048ab3 4001771b 400176fd -s -C -f -e a.out main test.cpp:15 ?? ??:0 ?? ??:0
[root@redhat tcpBreak]# addr2line 4001771b 400176fd -s -C -f -e libtest.so ?? ??:0 ?? ??:0
还是翻译不出来。当然出不来了,因为进程挂掉时输出的地址,和动态链接库文件内的静态偏移地址根本就不是一回事。所以我们需要知道出错时,所输出的代码地址与动态链接库偏移地址之间的关系。
-------------------------- 进程挂掉时的MAPS文件 -------------------------- 08048000-08049000 r-xp 00000000 00:09 17256 /mnt/hgfs/share/net/tcpBreak/a.out 08049000-0804a000 rw-p 00001000 00:09 17256 /mnt/hgfs/share/net/tcpBreak/a.out 0804a000-0804b000 rwxp 00000000 00:00 0 40000000-40015000 r-xp 00000000 08:02 271023 /lib/ld-2.3.2.so 40015000-40016000 rw-p 00014000 08:02 271023 /lib/ld-2.3.2.so 40016000-40017000 rw-p 00000000 00:00 0 40017000-40018000 r-xp 00000000 00:09 17255 /mnt/hgfs/share/net/tcpBreak/libtest.so 40018000-40019000 rw-p 00000000 00:09 17255 /mnt/hgfs/share/net/tcpBreak/libtest.so 40019000-4001b000 rw-p 00000000 00:00 0 40026000-400cf000 r-xp 00000000 08:02 350892 /usr/lib/libstdc++.so.5.0.3 400cf000-400d4000 rw-p 000a9000 08:02 350892 /usr/lib/libstdc++.so.5.0.3 400d4000-400d9000 rw-p 00000000 00:00 0 400d9000-400fa000 r-xp 00000000 08:02 286922 /lib/tls/libm-2.3.2.so 400fa000-400fb000 rw-p 00020000 08:02 286922 /lib/tls/libm-2.3.2.so 400fb000-40102000 r-xp 00000000 08:02 271272 /lib/libgcc_s-3.2.2-20030225.so.1 40102000-40103000 rw-p 00007000 08:02 271272 /lib/libgcc_s-3.2.2-20030225.so.1 40103000-40104000 rw-p 00000000 00:00 0 42000000-4212e000 r-xp 00000000 08:02 286920 /lib/tls/libc-2.3.2.so 4212e000-42131000 rw-p 0012e000 08:02 286920 /lib/tls/libc-2.3.2.so 42131000-42133000 rw-p 00000000 00:00 0 bfffd000-c0000000 rwxp ffffe000 00:00 0 ------------------------------------------------------------------------- --------------------------- 进程挂掉时的栈帧 -------------------------- signal[8] catched when running code at 8048ab3 signal[8] catched when running code at 4001771b signal[8] catched when running code at 400176fd -------------------------------------------------------------------------
# addr2line 71b 6fd -s -C -f -e libtest.so a() lib.cpp:14 b() lib.cpp:10
# addr2line 8048ab3 -s -C -f -e a.out main test.cpp:15 # addr2line 71b 6fd -s -C -f -e libtest.so a() lib.cpp:14 b() lib.cpp:10