野指针是指指向一个已删除的对象或未申请访问受限内存区域的指针。本文说的Obj-C野指针,说的是Obj-C对象释放之后指针未置空,导致的野指针(Obj-C里面一般不会出现未初始化对象的常识性错误)。
既然是访问已经释放的对象为什么不是必现Crash呢?
因为dealloc执行后只是告诉系统,这片内存我不用了,而系统并没有就让这片内存不能访问。
现实大概是下面几种可能的情况:
1.对象释放后内存没被改动过,原来的内存保存完好,可能不Crash或者出现逻辑错误(随机Crash)。
2.对象释放后内存没被改动过,但是它自己析构的时候已经删掉某些必要的东西,可能不Crash、Crash在访问依赖的对象比如类成员上、出现逻辑错误(随机Crash)。
3.对象释放后内存被改动过,写上了不可访问的数据,直接就出错了很可能Crash在objc_msgSend上面(必现Crash,常见)。
4.对象释放后内存被改动过,写上了可以访问的数据,可能不Crash、出现逻辑错误、间接访问到不可访问的数据(随机Crash)。
5.对象释放后内存被改动过,写上了可以访问的数据,但是再次访问的时候执行的代码把别的数据写坏了,遇到这种Crash只能哭了(随机Crash,难度大,概率低)!!
6.对象释放后再次release(几乎是必现Crash,但也有例外,很常见)。
仔细看看上面的关键路径只有出现被随机填入的数据是不可访问的时候才会必现Crash。
所以把这一随机的过程变成不随机的过程。对象释放后在内存上填上不可访问的数据,其实这种技术其实一直都有,xcode的Enable Scribble就是这个作用。
但是有个问题:这个方法不能放在测试那边用!因为总不能让测试装了xcode来测试吧?
于是我们自己动手实现一个,这个过程中我们要解决几个问题:
1.怎么在内存释放后填上不可访问的数据?
内存释放很可能不在我们的代码中。为此我们需要hook对象释放的接口,内存时候之后马上执行我们的破坏工作。
2.我们要重写对象释放的接口,重写哪个呢?
NSObject的dealloc、runtime的 object_dispose,C的free应该都是可以,但是各有优点,我选择的是覆盖面最广的free,free是C的函数,重写了它之后还可以顺带解决一部分C的野指针问题。
3.怎么重写?
重写C的接口场景的有两种:
a.替换系统动态库
b.hook
替换动态库太麻烦,还不知道行不行得通;hook我们就找现成的fishhook,github里面找的,但现成的代码需要防止代码冲突。
4.填充的不可访问的数据的长度怎么确定?
获取内存长度的接口不在标准库中,好在在Mac和iOS中可以用malloc_size就可以。
5.填什么? 和xcode一样,填0x55。
上hook后的free代码:
[size=0.85em]void safe_free(void* p){ size_t memSiziee=malloc_size(p); memset(p, 0x55, memSiziee); orig_free(p); return;}
测试一下,出现了和Enable Scribble一样的Crash!
以上就是一种在内存释放后填充0x55使野指针后数据不能访问,从而使某些野指针从不必现Crash变成了必现;
其实这就是上一篇文中留下了几个问题之一,如果我们填充0x55后内存又被别的内存覆盖了,最终还是会出现随机Crash。而在真实环境中,这种情况是非常常见的。
我们再梳理一下这个过程:
1.我们在即将要释放的填了0x55,之后调用了free真正释放,内存被系统回收。
2.这个时候系统随时可能把这片内存给别的代码使用,也就是说我们的0x55被再次写上随机的数据(在这里再强调一下,访问野指针是不会Crash的,只有野指针指向的地址被写上了有问题的数据才会引发Crash)。
3.假如释放的内存上又填上了另一个对象的指针,而那个对象也有同样的一个方法,那很可能只是逻辑上有问题,并不会直接Crash,甚至悄无声息地像什么事情都没发生一样。(这个地方可能会发生多种情况,可以参考之上一篇文章中的图)
没有发生Crash可不是好事,因为这种情况如果后续再Crash,问题就非常难查,因为你看到的Crash栈很可能和出错的代码完全没有关联。既然这个问题这么棘手,最好还是和之前一样,让这个Crash提前暴露。
首先,我们要解决的问题就是怎么让系统不再往这片释放的内存上乱放东西。
要控制底层内存管理机制让它不使用这些内存可能很困难。但是,我们变通一下,简单粗暴地,我们干脆就不释放这片内存了。也就是当free被调用的时候我们不真的调用free,而是自己保留着内存,这样系统不知道这片内存已经不需要用了,自然就不会被再次写上别的数据
为了防止系统内存过快耗尽,还需要额外多做几件事:
1.自己保留的内存大于一定值的时候就释放一部分,防止被系统杀死。
2.系统内存警告的时候,也要释放一部分内存。
在safe_free以及它调用的函数里面尽量不要再用带锁的函数,不然很容易导致死锁。
加上这个代码之后APP的内存占用会增大不少,拿过来测试可以,但万万不能放在正式的发布版本中。
关于性能问题,我的机器是iPhone5,跑在App里面运行,还算流畅(不同App性能可能会有些不同)。
可能由于锁的存在,会使cpu线程切换变得频繁,这样多线程的问题Crash率也可能会提升(最近遇到一个多线程引起的Crash很难重现,但我加了这个代码后就变成了必现Crash)