野指针扑获理论篇

一、什么是野指针

所指向的目标内存被收回,可是指向该目标内存的指针没有设置为空,依然能够拜访,这时这个指针便是野指针

二、野指针分类

  • 内存没被覆盖
  • 内存被覆盖
    野指针扑获理论篇

为什么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
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目标的署理.

详细demo

详细会证

//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的完成

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的完成

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的汇编代码,能够还原对应的伪代码:

// 获取到行将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”。

CoreFoundation`___forwarding___:
    0x7fff3f90b1cd <+269>:  leaq   0x35a414(%rip), %rsi      ; "_NSZombie_"

大约总结的流程如下:

// 获取目标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的东西尽管比较好用,可是只能调试时使用,而且有些野指针复现时间很长,更多时分咱们发生的野指针问题都是线上环境,而且十分十分难以复现。下一篇咱们再探讨开发一个能够检测野指针的东西…

到这儿就完毕了,感谢您的阅览,欢迎阅览我的其他文章!