Segmentation Fault的产生原因及调试方法
一. 什么是Segmentation Fault
1.1. 一句话来说,段错误是指访问的内存超出了系统给这个程序所设定的内存空间,例如访问了不存在的内存地址、访问了系统保护的内存地址、访问了只读的内存地址等等情况
二. Segmentation Fault产生示例
2.1. 访问不存在的内存地址
#include<stdio.h> #include<stdlib.h> void main() { int *ptr = NULL; *ptr = 0; }
2.2. 访问系统保护的内存地址
#include<stdio.h> #include<stdlib.h> void main() { int *ptr = (int *)0; *ptr = 100; }
2.3. 访问只读的内存地址
#include<stdio.h> #include<stdlib.h> #include<string.h> void main() { char *ptr = "test"; strcpy(ptr, "TEST"); }
2.4. 栈溢出
#include<stdio.h> #include<stdlib.h> void main() { main(); }
三. 获取段错误相信
3.1. dmesg
dmesg可以在应用程序crash掉时,显示内核中保存的相关信息。通过dmesg命令可以查看发生段错误的程序名称、引起段错误发生的内存地址、指令指针地址、堆栈指针地址、错误代码、错误原因等
下面以2.3. 代码为例
3.2. -g
使用gcc编译程序的源码时,加上-g参数,这样可以使得生成的二进制文件中加入可以用于gdb调试的有用信息。以程序2.3为例
gcc -g segment3.c -o segment3
3.3. nm
使用nm命令列出二进制文件中的符号表,包括符号地址、符号类型、符号名等,这样可以帮助定位在哪里发生了段错误。以程序2.3为例:
3.4. ldd
使用ldd命令查看二进制程序的共享链接库依赖,包括库的名称、起始地址,这样可以确定段错误到底是发生在了自己的程序中还是依赖的共享库中。以程序2.3为例
四. 段错误的调试方法
4.1. 使用printf打印调试信息
4.1.1. 此方法适合在debug阶段使用
4.2. 使用gcc和gdb
4.2.1. 调试过程
a. 为了能够使用gdb调试程序,在编译阶段加上-g参数,以程序2.3为例
gcc -g segment3.c -o segment3
b. 使用gdb工具,参考之前文章:https://www.cnblogs.com/linux-37ge/p/11826729.html
4.2.2. 使用场景
a. 仅当能确定程序一定会发生段错误的情况下使用。
b. 当程序的源码可以获得的情况下,使用-g参数编译程序。
c. 一般用于测试阶段,生产环境下gdb会有副作用:使程序运行减慢,运行不够稳定,等等。
d. 即使在测试阶段,如果程序过于复杂,gdb也不能处理
4.3. 使用core文件加gdb
4.3.1. 段错误会触发SIGSEGV信号,通过man 7 signal,可以看到SIGSEGV默认的handler会打印段错误出错信息,并产生core文件,由此我们可以借助于程序异常退出时生成的core文件中的调试信息,使用gdb工具来调试程序中的段错误
4.3.2. 调试过程
a. 在一些Linux版本下,默认是不产生core文件的,首先可以查看一下系统core文件的大小限制,可以看到默认设置情况下,本机Linux环境下发生段错误时不会自动生成core文件,下面设置下core文件的大小限制(单位为KB)
b. 运行程序2.3,发生段错误生成core文件
c. 加载core文件,使用gdb工具进行调试
4.3.3. 适用场景
a. 适合于在实际生成环境下调试程序的段错误(即在不用重新发生段错误的情况下重现段错误)。
4.4. 使用objdump
4.4.1. 调试步骤
a. 使用dmesg命令,找到最近发生的段错误输出信息:
b. 使用objdump生成二进制的相关信息,重定向到文件中:其中,生成的segfault3Dump文件中包含了二进制文件的segfault3的汇编代码
4.4.2. 适用场景
1. 不需要-g参数编译,不需要借助于core文件,但需要有一定的汇编语言基础
2. 如果使用了gcc编译优化参数(-O1,-O2,-O3)的话,生成的汇编指令将会被优化,使得调试过程有些难度
4.5. 使用catchsegv
4.5.1. catchsegv命令专门用来扑获段错误,它通过动态加载器(ld-linux.so)的预加载机制(PRELOAD)把一个事先写好的库(/lib/libSegFault.so)加载上,用于捕捉断错误的出错信息。
五. 一些注意事项
1、出现段错误时,首先应该想到段错误的定义,从它出发考虑引发错误的原因。
2、在使用指针时,定义了指针后记得初始化指针,在使用的时候记得判断是否为NULL。
3、在使用数组时,注意数组是否被初始化,数组下标是否越界,数组元素是否存在等。
4、在访问变量时,注意变量所占地址空间是否已经被程序释放掉。
5、在处理变量时,注意变量的格式控制是否合理等
参考文献:https://mp.weixin.qq.com/s/XLpjV0TQsyFQJxH5iNQN8A