美文网首页
iOS 野指crash定位

iOS 野指crash定位

作者: TaoGeNet | 来源:发表于2020-02-18 12:35 被阅读0次

    PLCrashReporter
    PLCrashReporter 源码分析
    获取任意线程调用栈的那些事
    Scribble& NSZombieEnabled
    通过hook OC -delloc函数捕获野指针异常

    1、每个xxx.app 和 xxx.app.dSYM 文件都有对应的UUID,crash文件(指的的通过工具转换过的文件)也有自己的UUID,只要这三个文件的UUID,只要这三个文件的UUID一致,我们就可以通过他们解析正确的错误函数信息

    查看XXX.app 文件的UUID : dwarfdump --uuid xxx.app/xxx
    查看xxx.app.dSYM文件的UUID:dwarfdump --uuid xxx.app.dSYM
    crash 文件内第一行 Incident Identifier 就是该crash文件的uuid

    什么是 UUID ?
    UUID 是由一组 32 位数的十六进制数字所构成。每一个可执行程序都有一个 build UUID 唯一标识。.crash日志包含发生 crash 的这个应用的 build UUID 以及 crash 发生时,应用加载的所有库文件的 build UUID。

    iOS crash 日志堆栈解析

    一、如何定位Obj-C 野指针随机Crash
    先提高野指针Crash率
    访问已经释放的对象为什么不是必现Crash?
    显示大概以下几种可能:
    1、对象释放后内存没被改动过,原来的内存保存完好,可能不crash或者出现逻辑错误(随机crash)
    2、对象释放后内存没被改动过,但是它自己析构的时候已经删掉某些必要的东西,可能不Crash, Crash在访问依赖的对象比如类成员上,出现逻辑错误。
    3、对象释放内存后被改动过,写上了不可访问的数据,直接就出错了很可能Crash在Objc_msgSend上面。
    4、对象释放后内存被改动过,写上了可以访问的数据,可能不Crash、出现逻辑错误、间接访问到不可访问数据的数据
    5、对象释放后内存被改动过,写上了可以访问的数据,但是在此访问的时候执行的代码把别的数据写坏了,遇到这种Crash只能哭了。
    6、对象释放后再次release

    目标:提前暴露这类Crash
    这一随机的过程变成不随机的过程。对象释放后在内存上填上不可访问的数据,其实这种技术,其实一直都有,xcode的Enable Scribble就是这个作用


    scribble.png

    这只能设置XCode,但给测试人员使用怎么办。
    通过hook C语言free函数
    上hook 后的free 代码:

    void safe_free(void* p){
        size_t memSiziee=malloc_size(p);
        memset(p, 0x55, memSiziee);
        orig_free(p);
        return;
    } 
    

    内存填充为0X55 和xcode保持一致。

    二、如何定位Obj-C 野指针随机Crash

    1、我们在即将释放的填了0x55,之后调用了free真正释放,内存被系统回收。
    2、这个时候系统随时可能把这片内存给别的代码使用,也就是说我们的0X55被再次写上随机的数据。(在这里再强调下,访问野指针是不会Crash的,只有野指针指向的地址被写上了有问题的数据才会Crash)
    3、假如释放的内存又填上了另一个对象的指针,而那个对象也有同样的方法,那很可能只是逻辑问题,并不会直接Crash,甚至悄无声息像什么事情都没发生过一样。

    继续提高Crash率
    简单粗暴,直接不释放内存。
    需要额外多做几种事:
    1、自己保留的内存大一一定值的时候就释放一部分,防止被系统杀死。
    2、系统内存警告的时候,也要释放一部分内存。

    实现的代码:

    DSQueue* _unfreeQueue=NULL;//用来保存自己偷偷保留的内存:1这个队列要线程安全或者自己加锁;2这个队列内部应该尽量少申请和释放堆内存。
    int unfreeSize=0;//用来记录我们偷偷保存的内存的大小
    
    #define MAX_STEAL_MEM_SIZE 1024*1024*100//最多存这么多内存,大于这个值就释放一部分
    #define MAX_STEAL_MEM_NUM 1024*1024*10//最多保留这么多个指针,再多就释放一部分
    #define BATCH_FREE_NUM 100//每次释放的时候释放指针数量
    
    //系统内存警告的时候调用这个函数释放一些内存
    void free_some_mem(size_t freeNum){
        size_t count=ds_queue_length(_unfreeQueue);
        freeNum=freeNum>count?count:freeNum;
        for (int i=0; i<freeNum; i++) {
            void* unfreePoint=ds_queue_get(_unfreeQueue);
            size_t memSiziee=malloc_size(unfreePoint);
            __sync_fetch_and_sub(&unfreeSize,memSiziee);
            orig_free(unfreePoint);
        }
    }
    
    void safe_free(void* p){
    #if 0//之前的代码我们先注释掉
        size_t memSiziee=malloc_size(p);
        memset(p, 0x55, memSiziee);
        orig_free(p);
    #else
        int unFreeCount=ds_queue_length(_unfreeQueue);
        if (unFreeCount>MAX_STEAL_MEM_NUM*0.9 || unfreeSize>MAX_STEAL_MEM_SIZE) {
            free_some_mem(BATCH_FREE_NUM);
        }else{
            size_t memSiziee=malloc_size(p);
            memset(p, 0x55, memSiziee);
            __sync_fetch_and_add(&unfreeSize,memSiziee);
            ds_queue_put(_unfreeQueue, p);
        }
    #endif
    
        return;
    }
    bool init_safe_free()
    {
        _unfreeQueue=ds_queue_create(MAX_STEAL_MEM_NUM);
    
        orig_free=(void(*)(void*))dlsym(RTLD_DEFAULT, "free");
        rebind_symbols1((struct rebinding[]){{"free", (void*)safe_free}}, 1);
    
        return true;
    }
    - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
    {
        free_some_mem(1024*1024);}
    }
    

    可以进行进一步的优化:

    1. 最好是根据机器的情况来决定偷偷保留内存的数量。
    2. 由于内存申请太过频繁,其实我们保留的内存很快就会耗尽,对于大片的内存,可以适当放过,这样可以提高保存指针的数量,防止消耗的内存过多。
    3. 有的APP自己写的都是Obj-C代码,想忽略c、c++对象的话可以过滤掉(会有办法判断的)。
    4. 如果觉得某些Obj-C类有问题,可以只保留指定的类对象,如果数量不是特别大,甚至可以干脆不释放。
    5. ……

    三、如何定位Objc-C野指针随机Crash
    怎么获取野指针的更多异常数据?
    既然0x55555555是被当成了类的指针使用,那假如我们用指定的类覆盖这个指针,是不是就可以执行我们指定类的方法呢?
    进一步说就是在发生野指针调用的时候,我们是不是可以控制CPU的行为?说起来有点像溢出攻击,利用shellcode覆盖函数返回值,一旦我们在出错的时候控制了CPU就可以获取更多异常信息,比如是哪个类,调了什么方法,对象的地址之类。

    先解决几个关键问题:

    1. 覆盖成什么?我们需要自己写一个类,用它的isa来替换已经释放的对象的isa。如果不出我们所料,我们用自己的类覆盖之后,之前调用的sel就换成了调用我们自己的类的某个sel。这样,只要我们指定的类也实现这个方法,就可以执行我们需要执行的代码,然后在里面获取我们需要的信息。当然,我们无法预料野指针对象会在调用哪个函数时发生Crash,好在我们可以利用runtime的重定向特性了转到我们自己的代码里面去。
    2. 怎么覆盖isa?object_setClass可以替换一个类的isa,但是试了一下,发生死锁!根据Obj-C对象的内存布局,对象的第一个数据就是isa,这里我们可以直接用自己的类指针替换它,反正是已经释放的内存,随便我们怎么玩。总之,还是很简单,这个类就是下面这样:
    @interface DPCatcher : NSObject
    @property (readwrite,assign,nonatomic) Class origClass;
    @end
    
    @implementation DPCatcher
    - (id)forwardingTargetForSelector:(SEL)aSelector{
     NSLog(@"发现objc野指针:%s::%p=>%@",class_getName(self.origClass),self,NSStringFromSelector(aSelector));
     abort();
     return nil;
    }
    -(void)dealloc{
     NSLog(@"发现objc野指针:%s::%p=>%@",class_getName(self.origClass),self,@"dealloc");
     abort();
    }
    
    -(oneway void)release{
     NSLog(@"发现objc野指针:%s::%p=>%@",class_getName(self.origClass),self,@"release");
     abort();
    }
    - (instancetype)autorelease{
     NSLog(@"发现objc野指针:%s::%p=>%@",class_getName(self.origClass),self,@"autorelease");
     abort();
    }
    @end
    
    1. 我们打印出了野指针对象的名字和地址,当这个类的对象比较少时,对查找问题有很大的用处(如果是自定义的类出现野指针,一般还是比较容易找到问题),但是如果是一些经常出现的类,比如nsarray,定位起来还是比较麻烦。这个时候建议试一下xcode的malloc history工具,或者可以自己实现一个类似记录内存使用记录的工具,因为有内存申请和释放的记录,只要重现一次就可以精确定位野指针。
    2. 如果出现dealloc的使用错误,例如先[super dealloc],然后release成员变量,那么就会出现崩溃的现象,且此时对象的地址为0x55555555。这是因为[super dealloc]只会释放对应的内存,但其成员的内存不会被release而变成了0x555555。 这种问题场景比较简单,一旦发生绝对是必现的,修复也比较容易。

    上述实现,最好使用代理:参考中会提供代理如何去实现

    参考:
    GCC 内置原子操作 _sync系列函数简书及历程
    Scribble& NSZombieEnabled
    通过hook OC -delloc函数捕获野指针异常

    相关文章

      网友评论

          本文标题:iOS 野指crash定位

          本文链接:https://www.haomeiwen.com/subject/lsoifhtx.html