用 gdb 配合内核转储文件瞬间定位段错误
前几天在写一个使用Huffman算法的文本压缩程序时被“段错误”折磨了好长时间。因为自己向来对内存的使用保持着“克勤克俭”的作风,所以总是被此类错误折磨的焦头难额。C语言的内存管理本来就是一个繁琐的工作,写代码时略有不慎便会出现诸如“段错误(吐核)”的运行时崩溃。
其实段错误是操作系统的一个内存保护机制,一般情况下某程序尝试访问其许可范围之外的内存空间时便会触发内核的“一般保护性异常”,内核便会向程序发送一个SIGSEGV(11)信号(无效的内存引用),而SIGSEGV信号默认handler的动作便是在终端上打印出名为“段错误”的出错信息,并产生Core(内核转储)文件,最后结束掉当前犯错的程序。
段错误的成因大致有以下几种:
- 程序访问了系统数据区,尤其是往系统保护的内存地址写数据。比如尝试对NULL指针进行解引用或者对其指向的内存写入数据(但是不见得所有的指针越界都会触发“段错误”);
- 内存访问越界(数组越界等);
- 无限的递归(导致栈溢出);
- 对malloc / calloc申请的堆内存二次释放(可能与glibc库版本有关);
- 由于操作系统的段保护机制,如果由于缓冲区溢出等错误导致对某段内存的非法访问也会触发;
另外还有一些大家平时不大注意的地方会导致段错误,例如使用标准库函数fclose对一个打开的文件关闭了多次也会导致段错误,同时终端可能会输出很多关于运行时库错误的信息。因为对使用malloc族函数申请的堆内存释放第二次的时候会触发段错误,所以我猜测fclose触发段错误的原因可能是对文件指针FILE *指向的内存二次释放时触发的段错误。而Valgrind检测的结果基本上证明了我的猜测,fclose引发了堆异常,错误被定位到了free函数。
1 | ==5715== Invalid free() / delete / delete[] / realloc() |
Valgrind是一款用于内存调试、内存泄漏检测以及性能分析的软件开发工具。遗憾的是它只能检测到堆里的内存泄漏和越界访问,对于栈里的内存访问错误爱莫能助(如果你对这里堆栈等概念有疑问,请参阅百度百科词条“堆栈”,至于为什么不推荐维基百科…因为关于这个词条,百度百科的资料更全一些)。关于Valgrind的具体使用方法超出了本文讨论范围,有兴趣的读者请自行Google。另外,关于“段错误”的介绍不再赘述,毕竟我们现在讨论的重点不是“段错误”的前世今生。
言归正传,我们前面提到,当一个程序出现内存异常访问后会触发内核的“一般保护性异常”,内核会向程序发送一个SIGSEGV(11)信号(无效的内存引用),而SIGSEGV信号的默认handler的动作便是在终端上打印出名为“段错误”的出错信息,并产生Core(内核转储)文件,最后结束掉当前犯错的程序。重点在这里,那个所谓的Core(内核转储)文件是什么东西呢?通过查阅man文档(man 5 core)我们得知了在程序崩溃时,它一般会在目录下生成一个core文件。core文件是该程序在内存中的映象(同时还会有一些调试信息包含在内)。而某些系统默认设置是不生成core文件的,我们可以在终端下输入ulimit -a 命令查看设置。
1 | hurley@hurley-fedora ~$ ulimit -a |
可以看到我的当前设置(fedora 16)把core文件大小被限制为0了(不生成core文件)。我们可以在终端下执行ulimit -c 1024修改限制(我的系统在重启后该设置又被重置为0,所以每次调试前都要设置,不知道为什么…)。
设置好了以后我们来制造一个会触发“段错误”的程序吧…
代码很简单:
1 |
|
很显然,编译运行后“段错误(吐核)”
1 | hurley@hurley-fedora test$ gcc test.c -o test - |
我们使用gcc重新编译,这次要加上 -g 和 -rdynamic参数,-g我们都知道是加入调试信息,那 -rdynamic呢?它的作用是用来通知链接器将所有符号添加到动态符号表中(具体请查阅 man文档,关于链接这块的知识除了经典的《Linkers and loaders》之外,国产的《程序员的自我修养——链接、装载与库》也值得一读)。
1 | hurley@hurley-fedora test$ gcc -Wall -g -rdynamic test.c -o test - |
我们看到了程序所在目录下生成了一个名为core.6864的内核转储文件,就是它了。如果你没有找到这个文件,那么请往上翻找找我关于ulimit的说明。
接下来我们用gdb开始调试,命令行如下,注意最后要加上那个内核转储文件。
1 | hurley@hurley-fedora segment-test$ gdb ./test core.6864 |
我们什么也没有做,gdb就定位到了这个*p = ‘a’; 触发了异常,并且明确的告诉了我们这行代码位于test.c的第26行,main函数里,同时gdb告诉我们程序接收到了11号信号退出,原因是段错误。
难不成就这么简单吗?是的,就这么简单。实际的使用中我发现有时候不需要内核转储文件gdb也能直接定位到错误点,这一点比起vc那个调试器来毫不逊色。
这篇文章到这里就告一段落了,貌似关键内容就这么一点。好吧,其实段错误的处理并不复杂,之前的纠结完全是因为自己没有掌握方法罢了…
如果你也被数不尽的“段错误”所纠结着 ,希望这篇文章能帮到你。如果你觉得这篇文章太水了……好吧,我承认,它确实很水…