在测试中,有时候开机正常,有时候跑几分钟死机。测试bug中经常遇到正常跑某个流程,是正常的,但是在特定条件下,总是遇到程序崩溃,业务中断。也会遇到同样的代码,在这次运行正常,下次就会崩溃。今天涉及的空指针就会造成问题现象稳定复现,野指针会出现BUG飘忽不定。为了减少这种烦恼,本文将介绍什么是空指针和野指针,他们有哪些危害,平时我们又该如何预防呢?
一、定义
1、空指针(NULL Pointer)--是合法的指针状态
指针变量被赋值为NULL(本质是宏,一般等价于(void*)0),代表指针不指向任何合法有效的内存地址(明确表明当前指针没有指向有效地址 )。
NULL是C标准规定的特殊常量地址0,C++新增nullptr
2、野指针(Wild Pointer)
指针变量保存了一个随机、未知、不受控制的非法内存地址,没有初始化,或者指向的内存已经被释放。
c
int *p;//局部变量,未初始化,内存残留随机值---》野指针
int *p=malloc(4);
free(p);//free之后不会自动变为NULL,p依旧保存原来地址---》野指针
二、危害
1、空指针带来的困扰
(1)程序直接崩溃(确定性故障)
Linux产生错误(SIGSEGV),Windows程序闪退。服务端程序一旦崩溃,业务中断;嵌入式设备直接死机、重启。
(2)崩溃堆栈清晰,但是容易忽略边界场景
很多场景只是在特定条件下指针才变成NULL,正常流程跑不出来。
(3)增加防御代码负担
所有接收指针的函数都要加if(p==NULL)判断,如果漏了,就会埋下崩溃隐患。
空指针造成的故障稳定,只要充分测试,大多可以提前发现。
2、野指针带来的困扰
(1)Bug难以复现
未定义行为(UB):同样的代码,这次运行正常,下次崩溃。
野指针保存的随机地址,每次运行不一样。
刚好访问只读内存,程序直接崩溃;
刚好访问进程合法内存:静默运行,无任何报错。
(2)隐性数据纂改(无声的破坏性BUG)
野指针碰巧指向程序其他变量、数组、栈上数据,进行写操作---不会崩溃,但是悄悄修改其他变量的值。
eg:变量值莫名被篡改,日志看不出异常,业务逻辑错乱,计算结果错误。
(3)破坏栈、堆内存,引发连锁崩溃
eg:A函数报错,根源在完全不相干的B函数野指针。
改写堆内存管理块:后续调用malloc/free直接崩溃
破坏栈上函数返回地址:程序跑飞,出现诡异的非法指令崩溃--崩溃点不是野指针出错的地方,崩溃堆栈完全误导人。
(4)无法通过简单判空防御
判空只能防御空指针,防御不了野指针。
(5)重复free释放
重复free触发堆破坏,程序直接崩溃。
(6)嵌入式环境危害大
嵌入式没有完善内存保护,野指针可能改写寄存器、外设地址,导致硬件异常,设备失控
三、出现后现象表现
1、空指针操作现象--不能对空指针引用*p
Linux:段错误Segmentation fault(SIGSEGV),程序直接崩溃。
Windows触发访问异常,程序闪退,如下

单纯if(p==NULL)判断空指针完全没有问题,只有解引用才报错。
2、野指针操作现象
很多人以为出错是空指针,实际大量线上崩溃、诡异逻辑错误都是野指针导致,有如下几种可能情况
直接崩溃 :访问操作系统保护地址,报段错误
静默运行,没有报错 :刚好指向程序自己可读内存,读写成功,潜伏bug
破坏其他变量参数 :改写了别的合法变量的值,造成莫名奇妙的数据错乱
间歇性崩溃:有时候跑正常,有时候崩溃。
四、野指针产生的根源
1、指针未初始化
局部指针变量不赋初值,栈内存残留随机垃圾值(野指针)
c
int *p;
*p=5;//给未知地址赋值了5
2、内存free后,指针没有NULL
c
#include<stdio.h>
#include<windows.h>
#include<math.h>
int main(){
SetConsoleOutputCP(65001);
SetConsoleCP(65001);
int *p=malloc(4);
free(p);//p仍然保存原来的地址,变为野指针
printf("p的地址%p\n",p);
if(p!=NULL)
{
*p = 10;//访问已经释放的内存
}
}

3、指针越界、指向临时变量(栈内存失效)
c
#include<stdio.h>
#include<windows.h>
#include<math.h>
int* func()
{
int a=10;
return &a;
}
int main(){
SetConsoleOutputCP(65001);
SetConsoleCP(65001);
int *p;
p=func();
printf("p的地址%p\n",p);
if(p!=NULL)
{
*p = 10;//访问已经释放的内存
}
}
func中的a是局部变量,函数结束栈回收,返回的指针变为野指针,编译只是警告,仍可编译成功

五、预防
1、编码规范
指针一律初始化,使用指针之前先确定位置,free后置NULL
使用指针解引用之前,先判断是否为NULL
函数接收指针参数,优先做空指针防御检查
禁止返回局部栈变量的地址
2、尽量减少裸指针,有条件使用封装
3、工具辅助
linux:valgrind检测内存泄漏、野指针、重复free
gcc编译加警告:gcc -wall -wextra,尽可能捕获未初始化指针
使用地址消毒器AddressSanitizer:-fsanitize=address,快速定位野指针、越界访问。
总结:空指针明确指向"无地址",可控;
野指针指向随机未知地址,极度危险。