一、什么是野指针
所指向的对象内存被回收,但是指向该对象内存的指针没有设置为空,依然可以访问,这时这个指针就是野指针
二、野指针分类
- 内存没被覆盖
- 内存被覆盖
为什么OC野指针的crash这么多? 我们一般在app发版前,都会经过多轮的自测、内侧、灰度测试等,按照常理来说,大部分的crash应该都被覆盖了,但是由于野指针的随机性,使得经常在测试时不会出现crash,而是在线上出现crash,这对app体验来说是非常致命的
而野指针的随机性问题大致可以分为两类:
- 跑不进出错的逻辑,执行不到出错的代码,这种可以通过提高测试场景覆盖率来解决
- 跑进有问题的逻辑,但是野指针指向的地址并不一定会导致crash,原因是因为:野指针其本质是一个指向已经删除的对象或受限内存区域的指针。这里说的OC野指针,是指OC对象释放后指针未置空而导致的野指针。这里不必现的原因是因为dealloc执行后只是告诉系统,这片内存我不用了,而系统并没有让这片内存不能访问
三、快速复现野指针
我们关注重点放在随机Crash的情况上做功夫,因为Crash的场景随机,才让开发头疼。随机 Crash 的场景如果能变成必现场景 ,就能让我们更好的找到野指针场景。
那怎么将随机 Crash 变成必现的呢? 其实我们头疼的事,苹果已经提供相应的工具供我们使用。
方案1: Malloc Scribble
其官方解释如下:申请内存 alloc 时在内存上填0xAA,释放内存 dealloc 在内存上填0x55,当访问这两种内存就会crash 总的来说分两种
- 对象创建后,没有调用init方法,直接进行调用
- 对象已经释放,仍然给它发消息、读写等操作的
开启Malloc Scribble步骤:
方案2: Zombie Objects
其官方解释如下:一个对象已经解除了它的引用,已经被释放掉,但是此时仍然是可以接受消息,这个对象就叫做Zombie Objects(僵尸对象)。这种方案的重点就是将释放的对象,全都转为僵尸对象
简单说就是用生成僵尸对象来替换delloc的实现,当对象引用计数为0的时候,将需要delloc的对象转化为僵尸对象。如果之后再给这个僵尸对象发消息,则抛出异常,并打印出相应的信息,调试者可以很轻松的找到异常发生位置。
开启Zombie Objects步骤:
四、业界常用工具
方案1: 利用Malloc Scribble原理使野指针必现
思路:当访问到对象内存中填充的是0xAA、0x55时,程序就会出现异常
- 申请内存 alloc 时在内存上填0xAA,
- 释放内存 dealloc 在内存上填 0x55。
所以综上所述,针对野指针,我们的解决办法是:在对象释放时做数据填充0x55即可,当访问0x55内存就必现出现crash
实现流程
- 通过fishhook替换C函数的free方法为自定义的safe_free,类似于Method Swizzling
js
void safe_free(void* p){
size_tmemSiziee=malloc_size(p);
memset(p,0x55, memSiziee);
orig_free(p);
return;
}
- 在safe_free方法中对已经释放变量的内存,填充0x55,使已经释放变量不能访问,从而使某些野指针的crash从不必现安变成必现。
- 为了防止填充0x55的内存被新的数据内容填充,使野指针crash变成不必现,在这里采用的策略是,safe_free不释放这片内存,而是自己保留着,即safe_free方法中不会真的调用free。
- 同时为了防止系统内存过快消耗(因为要保留内存),需要在保留的内存大于一定值时释放一部分,防止被系统杀死,同时,在收到系统内存警告时,也需要释放一部分内存
- 发生crash时,得到的崩溃信息有限,不利于问题排查,所以这里采用代理类(即继承自NSProxy的子类),重写消息转发的三个方法,以及NSObject的实例方法,来获取异常信息。但是这的话,还有一个问题,注意NSProxy只能做OC对象的代理.
具体验证
js
//viewController 设置MRC
UIView* testObj = [[UIView alloc] init];
[testObj release];
for (int i = 0; i < 10; i++) {
UIView* testView = [[UIView alloc] initWithFrame:CGRectMake(0,200,CGRectGetWidth(self.view.bounds), 60)]
[self.view addSubview:testView];
}
[testObj setNeedsLayout];
运行
方案2: 利用Zombie Objects原理使野指针必现
具体操作
第一步:xcode开启 Zombie enable
第二步:NSZombie_[类名]生成过程
对象释放时,如果当前开启了zombie 设置,会进入__dealloc_zombie函数,内部会生成[_NSZombie_类名]的类,将当前类isa指向[_NSZombie_类名]类
我们给__dealloc_zombie函数添加一个符号断点,看下具体实现
当对象释放后会进入__dealloc_zombie的实现
js
CoreFoundation`-[NSObject(NSObject) __dealloc_zombie]:
0x7fff3fa2dee7 <+23>: leaq 0x5a59c4a2(%rip), %rax ; __CFZombieEnabled
0x7fff3fa2defa <+42>: callq 0x7fff3fa7d930 ; symbol stub for: object_getClass
0x7fff3fa2df0a <+58>: callq 0x7fff3fa7d486 ; symbol stub for: class_getName
0x7fff3fa2df12 <+66>: leaq 0x237d1b(%rip), %rsi ; "_NSZombie_%s"
0x7fff3fa2df2b <+91>: callq 0x7fff3fa7d8b8 ; symbol stub for: objc_lookUpClass
0x7fff3fa2df38 <+104>: leaq 0x2376a9(%rip), %rdi ; "_NSZombie_"
0x7fff3fa2df3f <+111>: callq 0x7fff3fa7d8b8 ; symbol stub for: objc_lookUpClass
0x7fff3fa2df4d <+125>: callq 0x7fff3fa7d870 ; symbol stub for: objc_duplicateClass
0x7fff3fa2df61 <+145>: callq 0x7fff3fa7d86a ; symbol stub for: objc_destructInstance
0x7fff3fa2df6c <+156>: callq 0x7fff3fa7d948 ; symbol stub for: object_setClass
从此处断点可以大概看出 Zombie Object 的生成过程,调用objc_destructInstance在没有释放原来类的内存情况下释放依赖的变量、关联对象等数据
objc源码看下objc_destructInstance的实现
js
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor(); //释放存在c++变量
bool assoc = obj->hasAssociatedObjects();//释放存在关联对象
// This order is important.
if (cxx) object_cxxDestruct(obj);//如果存在c++变量进行销毁
if (assoc) _object_remove_associations(obj, /*deallocating*/true); //如果存在关联对象,进行销毁
obj->clearDeallocating(); //清除弱引用、引用计数等数据
}
return obj;
}
从上面创建_NSZombie的汇编代码,可以还原对应的伪代码:
js
// 获取到即将deallocted对象所属类(Class)
Class cls = object_getClass(self);
// 获取类名
const char *clsName = class_getName(cls)
// 生成僵尸对象类名
const char *zombieClsName = "_NSZombie_" + clsName;
// 查看是否存在相同的僵尸对象类名,不存在则创建
Class zombieCls = objc_lookUpClass(zombieClsName);
if (!zombieCls) {
// 获取僵尸对象类 _NSZombie_
Class baseZombieCls = objc_lookUpClass("_NSZombie_");
// 创建 zombieClsName 类
zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
// 在对象内存未被释放的情况下销毁对象的成员变量及关联引用。
objc_destructInstance(self);
// 修改对象的 isa 指针,令其指向特殊的僵尸类
objc_setClass(self, zombieCls);
第三步:触发NSZombie
再次对一个释放的对象发送消息,[_NSZombie_类名]会接收到,但是没有实现任何方法,这里会进行消息转发,可以看到程序断在 forwarding ,从此处的汇编代码中可以看到关键字 NSZombie ,在调用 abort( ) 函数退出进程时会有对应的信息输出@"*** -[%s %s]: message sent to deallocated instance %p"。
js
CoreFoundation`___forwarding___:
0x7fff3f90b1cd <+269>: leaq 0x35a414(%rip), %rsi ; "_NSZombie_"
大概总结的流程如下:
js
// 获取对象class
Class cls = object_getClass(self);
// 获取对象类名
const char *clsName = class_getName(cls);
// 检测是否带有前缀_NSZombie_
if (string_has_prefix(clsName, "_NSZombie_")) {
// 获取被野指针对象类名
const char *originalClsName = substring_from(clsName, 10);
// 获取当前调用方法名 _cmd获取当前方法签名
const char *selectorName = sel_getName(_cmd);
// 输出日志
Log(''*** - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self);
// 结束进程
abort();
}
五、总结
前面我们介绍了Malloc Scribble和Zombie Objects,然后大家可能云里雾里,感觉都是检测通过检测对释放的对象发送消息检测出野指针的。
- Malloc Scribble:内存涂鸦,内存释放后在释放的内存上填 0x55;
- 再就是说如果内存未被初始化就被访问。
- 或者释放后被访问,就会引发异常,这样就可以使问题尽快暴露出来。
- Zombie Objects:Zombie的原理是用生成僵尸对象来替换 delloc的实现,当对象引用计数为0的时候,将需要delloc的对象转化为僵尸对象。如果之后再给这个僵尸对象发消息,则抛出异常,并打印出相应的信息,调试者可以很轻松的找到异常发生位置。
- Malloc Guard Edges和Guard Malloc可以帮助你发现内存溢出,并在通过对申请的大块内存保护和延迟释放来使你的程序在误用内存时产生更明确地崩溃。
xcode的工具虽然比较好用,但是只能调试时使用,并且有些野指针复现时间很长,更多时候我们产生的野指针问题都是线上环境,而且非常非常难以复现。下一篇我们再探讨开发一个可以检测野指针的工具...
到这里就完毕了,感谢您的阅读,欢迎阅读我的其他文章!