iOS 野指crash定位

2020-02-18  本文已影响0人  TaoGeNet

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函数捕获野指针异常

上一篇下一篇

猜你喜欢

热点阅读