C/C++捕获段错误,打印出错的具体位置(精确到哪一行)

修订:2013-02-16

其实还可以使用 glibc 的 backtrace_symbols 函数,把栈帧各返回地址里面的数字地址翻译成符号描述的

修订:2011-06-11

背景知识:

· 在linux/unix中的信号处理机制,知道signal函数与sigaction的区别

· 段错误的概念,CPU中断处理的步骤,中断向量表的分类

· 知道CPU Exception分为Fault、trap和abort,了解他们的基本区别

· 段错误和浮点错误属于Fault,产生Fault时会将出错指令的地址入栈,而不是下一条将执行指令的地址

· 在linux/unix里可以通过调用backstrace来获取栈帧的信息

· 文中用到的几个头文件和函数,都属于glibc,所以不用担心出现找不到头文件和链接错误的情况

· addr2line是个系统自带的小工具,用来转换编译出来的地址和源码行号

背景知识大家可以看书,google,看手册(建议可以简单阅读一下本文列出来的参考资料)…,这里不想粘贴大量的背景知识,本文主要介绍在 linux / unix 里面,如何捕获段错误并输出发生错误时的代码执行路径,最后还提供了一个封装好的头文件。

OK,下面直奔主题:

——先要抓住段错误,别让它跑了

捕获段错误的方式很简单,针对段错误的信号调用 sigaction 注册一个处理函数就可以了。

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,将这些打印出来的指令地址转换为行号、函数名。

执行情况的一个示例:

[[email protected] 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

[[email protected] 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上哪里去找行号。否则只能得到函数名称,无法得到行号信息。

——头疼,想直接用行不行,能来个直接可以用的代码么

这里提供一个头文件(见附件segvCatch.rar),但是不保证没有bug哦。使用方法很简单,只需要在main函数所在源文件包含该头文件即可。

该头文件捕获了浮点错误和段错误,像上面示例所说的,在出错时会向 STDOUT 输出一系列地址后退出程序,再使用 addr2line 对输出的地址进行转换,bingo,调用路径一目了然展示在你眼前啦!

标注:

[f1]调用函数时,会将函数返回地址入栈,此返回地址为返回后将执行的指令地址。

[f2]事实上,test()翻译成若干汇编指令,指向这些指令对应区域的所有地址将被addr2line转换为调用test()对应的行号。

[f3]权限不够、ulimit未打开、core文件太大…等情况

参考资料:

中断与异常

http://blog.csdn.net/shaohaigod1981/archive/2009/11/04/4767915.aspx

http://hi.baidu.com/hilyjiang/blog/item/cdd7ebb417f8be728bd4b2a1.html

http://www.logix.cz/michal/doc/i386/chp09-08.htm

ucontext_t说明 http://www.gnu.org/s/libc/manual/html_node/System-V-contexts.html

信号概述 http://www.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html?ca=drs-

Backstrace http://hi.baidu.com/sunkang_2/blog/item/e7dd68df51db585c94ee378f.html

man 7 signal

时间: 2024-10-19 07:26:31

C/C++捕获段错误,打印出错的具体位置(精确到哪一行)的相关文章

linux/unix 段错误捕获【续】

本文为“在C/C++中捕获段错误,打印出错的具体位置”的续篇,进一步解决涉及动态链接库的情况. 背景知识: ·linux/unix下动态链接库的基本原理 ·/proc/pid/maps文件的基本格式 ·动态链接库:在进程执行过程中动态加载,进程间可以共享代码,可用在发布升级包等场合 概述: 用户自己编写的代码均编译进了可执行文件里的时候,“在C/C++中捕获段错误,打印出错的具体位置”里给出了在发生段错误(或其他错误,读者可以修改附件里面的头文件,增加捕获的错误类型)的情况下,输出代码执行路径的

用gdb调试程序笔记: 以段错误(Segmental fault)为例

用gdb调试程序笔记: 以段错误(Segmental fault)为例[转] 1.背景介绍2.程序中常见的bug分类3.程序调试器(如gdb)有什么用4.段错误(Segmental fault)介绍5.gdb调试入门 一.背景介绍这个笔记主要介绍开源的程序调试器(gdb)的入门知识,目的是使unix/linux环境的编程新手能够快速学会使用gdb调试程序的方法,同时也是对我使用gdb的一个经验总结.本文假设你能使用简单的unix/linux命令并能用gcc(GNU C Compiler, GNU

段错误以及调试方式

dummy_function(void) { unsigned char * ptr=0x00; *ptr=0x00; } int main() { dummy_function(); return 0; }作为一名熟练的c/c++程序员,以上代码的bug应该是很清楚的,因为它尝试操作地址为0的内存区域,而这个地址区域通常是不可访问的禁区,当然会出错了. 方法1 :利用gdb逐步查找段错误 这种方法也是被大众所熟知并广泛采用的方法,首先我们需要一个带有调试 信息的可执行程序,所以我们加上"-g

Linux环境下段错误的产生原因及调试方法小结(转)

最近在Linux环境下做C语言项目,由于是在一个原有项目基础之上进行二次开发,而且 项目工程庞大复杂,出现了不少问题,其中遇到最多.花费时间最长的问题就是著名的“段错误”(Segmentation Fault).借此机会系统学习了一下,这里对Linux环境下的段错误做个小结,方便以后同类问题的排查与解决. 1. 段错误是什么 一句话来说,段错误是指访问的内存超出了系统给这个程序所设定的内存空间,例如访问了不存在的内存地址.访问了系统保护的内存地址.访问了只读的内存地址等等情况.这里贴一个对于“段

Linux 下的段错误(Segmentation fault)调试方法

我们在用C/C++语言写程序的时侯,内存管理的绝大部分工作都是需要我们来做的.实际上,内存管理是一个比较繁琐的工作,无论你多高明,经验多丰富,难免会在此处犯些小错误,而通常这些错误又是那么的浅显而易于消除.但是手工“除虫”(debug),往往是效率低下且让人厌烦的,本文将就"段错误"这个内存访问越界的错误谈谈如何快速定位这些"段错误"的语句. 下面将就以下的一个存在段错误的程序介绍几种调试方法: 1 dummy_function (void) 2 { 3 unsig

常见的Linux下的段错误 及解决办法

一.段错误 所谓的段错误就是指访问的内存超出了系统所给这个程序的内存空间,通常这个值是由gdtr来保存的,他是一个48位的寄存器,其中的32位是保存由它指向的gdt表,后13位保存相应于gdt的下标,最后3位包括了程序是否在内存中以及程序的在cpu中的运行级别,指向的gdt是由以64位为一个单位的表,在这张表中就保存着程序运行的代码段以及数据段的起始地址以及与此相应的段限和页面交换还有程序运行级别还有内存粒度等等的信息.一旦一个程序发生了越界访问,cpu就会产生相应的异常保护,于是segment

段错误bug的调试

我们在用C/C++语言写程序的时侯,内存管理的绝大部分工作都是需要我们来做的.实际上,内存管理是一个比较繁琐的工作,无论你多高明,经验多丰富,难 免会在此处犯些小错误,而通常这些错误又是那么的浅显而易于消除.但是手工“除虫”(debug),往往是效率低下且让人厌烦的,本文将就"段错误"这个 内存访问越界的错误谈谈如何快速定位这些"段错误"的语句.下面将就以下的一个存在段错误的程序介绍几种调试方法:      1  dummy_function (void)    

Linux环境下段错误的产生原因及调试方法小结

最近在Linux环境下做C语言项目,由于是在一个原有项目基础之上进行二次开发,而且项目工程庞大复杂,出现了不少问题,其中遇到最多.花费时间最长的问题就是著名的“段错误”(Segmentation Fault).借此机会系统学习了一下,这里对Linux环境下的段错误做个小结,方便以后同类问题的排查与解决. 1. 段错误是什么 一句话来说,段错误是指访问的内存超出了系统给这个程序所设定的内存空间,例如访问了不存在的内存地址.访问了系统保护的内存地址.访问了只读的内存地址等等情况.这里贴一个对于“段错

【转】【调试技巧】Linux环境下段错误的产生原因及调试方法小结

本文转自:http://www.cnblogs.com/panfeng412/archive/2011/11/06/segmentation-fault-in-linux.html 最近在Linux环境下做C语言项目,由于是在一个原有项目基础之上进行二次开发,而且项目工程庞大复杂,出现了不少问题,其中遇到最多.花费时间最长的问题就是著名的“段错误”(Segmentation Fault).借此机会系统学习了一下,这里对Linux环境下的段错误做个小结,方便以后同类问题的排查与解决. 1. 段错误