在Linux上编写运行C语言程序,经常会遇到程序崩溃、卡死等异常的情况。程序崩溃时最常见的就是程序运行终止,报告Segmentation fault (core dumped)
错误。而程序卡死一般来源于代码逻辑的缺陷,导致了死循环、死锁等问题。总的来看,常见的程序异常问题一般可以分为非法内存访问和资源访问冲突两大类。
1.非法内存访问(读/写):非法指针、多线程共享数据访问冲突、内存访问越界、缓冲区溢出等。
2.资源访问冲突:栈内存溢出、堆内存溢出、死锁等。
一、非法内存访问
非法内存访问是最常见的程序异样原因,可能开发者看的“表象”不尽相同,但是很多情况下都是由于非法内存访问引起的。
1. 非法指针
非法指针是最典型的非法内存访问案例,空指针、指向非法地址的指针是代码中最常出现的错误。
示例代码如下:
long *ptr;
*ptr = 0; // 空指针
ptr = (long *)0x12345678;
*ptr = 100; // 非法地址访问
无论是访问地址为0
的空指针,还是用户态无效的地址,都会导致非法指针访问错误。
实际编程过程中,强制类型转换一不小心就会产生非法指针,因此做强制类型转换时要格外注意,最好事先做好类型检查,以避免该问题。
2. 多线程共享数据访问冲突
在多线程程序中,非法指针的产生可能就没那么容易发现了。
一般情况下,多个线程对共享的数据同时写,或者一写多读时,如果不加锁保证共享数据的同步访问,则会很容易导致数据访问冲突,继而引发非法指针、产生错误数据,甚至影响执行逻辑。
示例代码如下:
// 全局变量
long *ptr = (long *)malloc(sizeof(long));
// 线程1
if (ptr) {
*ptr = 100; // 潜在的非法地址访问
}
// 线程2
free(ptr);
ptr = NULL;
上述代码中,全局初始化了指针ptr
,线程1
会判断该指针不为NULL
时进行写100
操作,
而线程2
会释放ptr
指向的内存,并将ptr
置为NULL
。
虽然线程1
做了判断处理,但是多线程环境下,则会出现线程2
刚调用完free
操作,还未来得及将ptr
设为NULL
时,
发生线程上下文切换,转而执行线程1
的写100
操作,从而引发非法地址访问。
解决并发数据访问冲突的方案是使用锁同步线程。
针对图中的线程同步问题,只需要在线程1
和线程2
的处理逻辑前,使用读写锁同步即可。
操作系统或者gcc
的库函数内也存在很多线程不安全的API
,在使用这些API
时,一定要仔细阅读相关的API
文档,使用线程锁进行同步访问。
3. 内存访问越界
内存访问越界经常出现在对数组处理的过程中。本身C
语言并未有对数组边界的检查机制,因此在越界访问数组内存时并不一定会产生运行时错误,但是因为越界访问继而引发的连锁反应就无法避免了。
示例代码如下:
void out_of_bound() {
long *ptr;
long buffer[] = {0};
ptr = buffer;
buffer[1] = 0; // 越界访问导致ptr被覆盖
ptr[0]++;
}
示例代码在函数out_of_bound
内定义了两个变量:指针ptr
和数组buffer
。
指针ptr
指向buffer
其实地址,正常情况下使用ptr[0]
可以访问访问到buffer
的第一个元素。
然而对buffer[1]
的越界写操作会直接覆盖ptr
的值为0
,从而导致ptr
为空指针。
了解该问题的原因需要清楚局部变量在栈内的存储机制。
在函数调用时,会将调用信息、局部变量等保存在进程的栈内。
栈是从高地址到低地址增长的,因此先定义的局部变量的地址一般大于后定义的局部变量地址。
上述代码中,buffer
和ptr
的大小都是8Byte
,因此buffer[1]
实际就是ptr
所在的内存。
这样对buffer[1]
的写操作会覆盖ptr
的值就不足为怪了。总之,对数组访问的时候,做好边界检查是重中之重。
类似的问题也出现在对字符串的操作中,包括gcc
提供的字符串库函数也存在该问题,使用时需要尤其注意。
说到边界检查,这里引申出一个话题。在对数组处理时