程序发生段错误借助gdb工具去定位是非常好用而精准的。但如果有些段错误是概率性发生,那么有时使用gdb工具去定位就显得那么好用了。如果在代码里去监控段错误,段错误一旦发生,就能很好得记录段错误信息,就能排查问题。所以gdb工具一般只适用于程序调试阶段,而如果想要在程序维护阶段去监控段错误的发生,只能在代码里实现段错误的监控措施,即栈回溯。
int backtrace(void **buffer,int size)
该函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针列表。参数 size 用来指定buffer中可以保存堆栈的个数,如果size 不够,小于栈回溯的长度,怎么将无法完整打印栈回溯。所以size需要保证大于栈回溯的长度。 函数返回值是实际获取的堆栈的个数,最大不超过size大小。在buffer中的指针实际是从堆栈中获取的返回地址,每一个堆栈框架有一个返回地址。
char ** backtrace_symbols (void *const *buffer, int size);
backtrace_symbols将从backtrace函数获取的信息转化为一个字符串数组. 参数buffer应该是从backtrace函数获取的指针数组,size是该数组中的元素个数(backtrace的返回值)。函数返回值是一个指向字符串数组的指针,它的大小同buffer相同。每个字符串包含了一个相对于buffer中对应元素的可打印信息.它包括函数名,函数的偏移地址,和实际的返回地址。
注意:该函数的返回值是通过malloc函数申请的空间,因此调用者必须使用free函数来释放指针.如果不能为字符串获取足够的空间函数的返回值将会为NULL。
段错误例程:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <execinfo.h>
typedef struct
{
const char *name;
int signum;
int flags; //0:default handler, >0:catch, <0:ignore
}SigTable_S;
static SigTable_S sigTable[] =
{
{"zero", 0, 0},
{"SIGHUP", SIGHUP, -1},
{"SIGINT", SIGINT, 1},
{"SIGQUIT", SIGQUIT, -1},
{"SIGILL", SIGILL, 1},
{"SIGTRAP", SIGTRAP, -1},
{"SIGABRT", SIGABRT, 1},
{"SIGBUS", SIGBUS, 1},
{"SIGFPE", SIGFPE, -1},
{"SIGKILL", SIGKILL, 0}, // reserved
{"SIGUSR1", SIGUSR1, 2}, // for light flicker
{"SIGSEGV", SIGSEGV, 1},
{"SIGUSR2", SIGUSR2, -1},
{"SIGPIPE", SIGPIPE, -1},
{"SIGALRM", SIGALRM, -1},
{"SIGTERM", SIGTERM, 1}, // for kill
{"SIGSTKFLT", SIGSTKFLT, -1},
//{"SIGCHLD", SIGCHLD, -1},
{"SIGCONT", SIGCONT, -1},
{"SIGSTOP", SIGSTOP, 0}, // reserved
{"SIGTSTP", SIGTSTP, -1},
{"SIGTTIN", SIGTTIN, -1},
{"SIGTTOU", SIGTTOU, -1},
{"SIGURG", SIGURG, -1},
{"SIGXCPU", SIGXCPU, -1},
{"SIGXFSZ", SIGXFSZ, -1},
{"SIGVTALRM", SIGVTALRM, -1},
{"SIGPROF", SIGPROF, -1},
{"SIGWINCH", SIGWINCH, -1},
{"SIGIO", SIGIO, -1},
{"SIGPWR", SIGPWR, -1},
{"SIGSYS", SIGSYS, -1},
};
void Fun_Print_BackTrace(int signum)
{
if ((signum > 0) && (signum <= SIGSYS))
{
void *array[30]={0};
size_t size=0,i=0;
char** strings=NULL;
char logbuf[256];
char processname[20];
pid_t pid = getpid();
//Fun_GetNameByPid(pid,processname);
sprintf(logbuf, "Signal Catched!process is :%s, signal is %s(%d)\n",processname,sigTable[signum].name,signum);
printf("%s\n",logbuf);
//UserLog_Error_Local1_API(logbuf);
size=backtrace(array,30);
printf("size =%ld\n",size);
strings=backtrace_symbols(array,size);
for(i=0;i<size;i++)
{
sprintf(logbuf,"%s\n",strings[i]);
printf("%s\n",logbuf);
//UserLog_Error_Local1_API(logbuf);
}
free(strings);
strings = NULL;
}
else
{
printf("\n\n\n@@@@@@@@@@@@@@@@@@@@@ signum is :%d line[%d]\n\n\n",signum, __LINE__);
}
exit(0);
}
int SetupSignalHandlers()
{
struct sigaction sigsetup;
unsigned int i;
//set signal handler
for(i=0; i<sizeof(sigTable)/sizeof(sigTable[0]); i++)
{
//exit on system signal
if(sigTable[i].flags == 1)
{
memset(&sigsetup, 0, sizeof(sigsetup));
sigsetup.sa_sigaction = Fun_Print_BackTrace;
sigsetup.sa_flags = SA_SIGINFO;
sigemptyset(&sigsetup.sa_mask);
sigaction(sigTable[i].signum, &sigsetup, NULL);
}
//ignore these
if(sigTable[i].flags < 0)
{
memset(&sigsetup, 0, sizeof(sigsetup));
sigsetup.sa_handler = SIG_IGN;
sigsetup.sa_flags = 0;
sigemptyset(&sigsetup.sa_mask);
sigaction(sigTable[i].signum, &sigsetup, NULL);
}
}
return 0;
}
int fun()
{
int a;
int b=1+5;
return 0;
}
int sub(int c)
{
int a;
char *b2="chen";
*b2='1';
fun();
return c;
}
int aad(int a , int b)
{
sub(a);
return a+b;
}
int add(int a , int b)
{
int t = 2,d,g;
d = aad( a , b);
g= t+d;
return g;
}
int main()
{
SetupSignalHandlers();
int ab = 100;
int abc = 20;
int c;
c = add(ab,abc);
printf("c = %d\n",c);
return 0;
}
编译选项:
编译的时候一定加上-g -rdynamic,-rdynamic可用来通知链接器将所有符号添加到动态符号表中,如果不加rdynamic选项,则发生段错误是,无法将函数名打印出来。
gcc -o strace strace.c -g -rdynamic
测试结果如下:
通过栈回溯,可以将发生段错误的函数名,函数的偏移地址,和实际的返回地址打印出来,并且可以回溯打印。
详细定位吨错误的位置(在文件的哪一行)
唯一不足的地方就是无法打印行号,就无法详细定位段错误的地方,这时可以借助addr2line工具去定位段错误的详细地方。
addr2line
addr2line 0x400f3c -e srace -f
addr2line简介
Addr2line (它是标准的 GNU Binutils 中的一部分)是一个可以将指令的地址和可执行映像转换成文件名、函数名和源代码行数的工具。
用法:addr2line [选项] [地址]
将地址转换成文件名/行号对。
如果没有在命令行中给出地址,就从标准输入中读取它们
选项是:
-a --addresses 显示地址
-b --target= 设置二进位文件格式
-e --exe= 设置输入文件名称(默认为 a.out)
-i --inlines 解开内联函数
-j --section= 读取相对于段的偏移而非地址
-p --pretty-print 让输出对人类更可读
-s --basenames 去除目录名
-f --functions 显示函数名
-C --demangle[=style] 解码函数名
-h --help 显示本帮助
注*static 函数 是无法·导出符号表的,所以栈回溯是无法打印静态函数的函数名,函数的偏移地址,和实际的返回地址。