在LIinux 下C/C++中,出现段错误很多都是有指针造成的,指针声明后没有内容的存储空间,当你不指向指定的内存空间时,就会出现segmentation fault(段错误),这种情况往往能编译通过的,但是运行时就会出现在段错误。
段错误segmentation fault,信号SIGSEGV,是由于访问内存管理单元MMU异常所致,通常由于无效内存引用,如指针引用了一个不属于当前进程地址空间中的地址,操作系统便会进行干涉引发SIGSEGV信号产生段错误。
段错误产生的原因
空指针
野指针
堆栈越界
…
空指针
由于空指针访问导致段错误是较为常见和简单的一种,空指针访问即尝试操作地址为0的内存区域,由于该区域内存是禁止访问的区域,所以当发生空指针访问时进程就会收到SIGSEGV信号发生Segmentation fault例如:
#include <>
int main(int argc, char* argv[])
{
int* ptr = NULL;
*ptr = 0;
return 0;
}
编译运行:
恭喜,出现段错误了!
通常在项目中的遇到的空指针大多在对象访问的时候出现,一般情况下是对对象的方法进行访问,所以即使对象指针为空也不会马上出现段错误,只有当访问对象数据成员是才可能发生段错误。
野指针
野指针通常有两种情况,一种是指针未初始化,指针定义后是不会自动初始化为NULL指针,它的缺省值是随机的,所以指针定义的时候就应该初始化为NULL或者合法内存;第二种是指向的内存已经释放,使用free或者delete操作并不会对指针本身进行清除,释放完后应该对指针进行重置,或者指向的对象已经超出了对象作用域范围,离开作用域的时候也应该对指针进行重置;
#include <>
#include <>
int main(int argc , char *argv[])
{
char *source1 = "abc";
char *source2;
printf("first source2 is : %u\n",source2);
strcpy(source2,source1);
printf("second source2 is : %s\n");
return 0;
}
编译输出:
这段程序定义了一个指向字符的指针source2,但是没有给它一个初始值。由于source2在定义时没有给初值,程序运行时系统会默认给source2一个值(732373280),而732373280是一个内存的地址,至于是哪段内存地址,谁也不知道,可能是操作系统本身所在的内存地址,也可能是一个空的内存地址。
野指针是危险的因为它不像空指针一样可以通过判断来识别是否指向合法内存,它可能指向一块不存在的内存页,也可能是指向一块没有访问权限的内存区域,如果是这样你应该感谢segmentation fault段错误,因为问题很快就会暴露出来而不会被蔓延,否则如果指向了一块合法内存,那对内存的破坏将会有无法预测的事情发生,可能只是纂改了你的数据,也可能是破坏了内存结构,这个时候错误可能被蔓延到一个无法预测的时刻。
堆栈越界
堆栈越界通常分为堆越界和栈越界。
堆越界破坏的是堆内存空间,堆空间通常由malloc/new分配,free/delete进行回收,由于堆内存空间分配的时候并不一定是连续的,所以如果发生堆越界可能破坏的内存属于一个毫无关系的对象,堆越界通常都会破坏堆内存结构,导致后续分配或者回收内存的时候出现异常。
#include <>
#include <>
#include <>
int main(int argc ,char *argv[])
{
char *p=(char *)malloc(sizeof(char)*10);
char *q=strcpy(p,"This is an example of a heap error");
free(p);
return 0;
}
编译运行:
分配的堆空间为10个byte。多出来的字符被放置在10之后的内存中。一不小心发生了越界。才出现了这个堆被破坏的错误。
栈越界破坏的栈内存空间,栈空间是由系统自动分配和回收,栈空间分配是从高地址象低地址连续分配的,所以当出现栈越界破坏的都是相邻的数据块,栈越界通常修改当前函数返回地址,参数或者局部变量,如果返回地址被修改可能会产生指令错误或者执行非预期的代码,所以黑客通常通过栈越界来插入后门。
#include <>
#include <>
#include <>
bool fun1()
{
char passwd[10];
printf("please input your password :\n");
scanf("%s", passwd);
printf("your password is %s\n", passwd);
return true;
}
int main()
{
if(fun1())
{
printf("ok!\n");
return 0;
}
return 0;
}
编译运行:
如果程序崩溃出现栈破坏的情况,请检查数组越界、返回局部变量地址等等问题。在开发软件时一定要养成好习惯,不要犯这种错误,一般这种错误时很难定位的。
段错误信息的获取
dmesg命令
‘dmesg’命令显示linux内核的环形缓冲区信息,我们可以从中获得诸如系统架构、cpu、挂载的硬件,RAM等多个运行级别的大量的系统信息。
当计算机启动时,系统内核(操作系统的核心部分)将会被加载到内存中。在加载的过程中会显示很多的信息,在这些信息中我们可以看到内核检测硬件设备。
可以看出,发生段错误的地址:0, 和指令指针地址:00000000004004e4
catchsegv工具
这里通过catchsegv 脚本测试段错误的方法,其他方法见后面相关资料。
catchsegv 利用系统动态链接的 PRELOAD 机制(请参考man ld-linux)
man ld-linux
把库 /lib/ 提前 load 到内存中,然后通过它检查程序运行过程中的段错误。
gdb + core
这种方式也很常用,关于gdb是linux下一个强大的debug调试程序,不熟的朋友可以参考这一篇<Linux 下Coredump分析与配置>,这里就不多说了
总结
我们在用C/C++语言写程序的时侯,内存管理的绝大部分工作都是需要我们来做的。实际上,内存管理是一个比较繁琐的工作,无论你多高明,经验多丰富,难免会在此处犯些小错误,而通常这些错误又是那么的浅显而易于消除。
有条件的情况尽量借助工具对问题进行排查定位包括用于单元测试,Linux平台推荐valgrind、catchsegv等。