如何定位Obj-C野指针随机Crash(二):让非必现Crash变成必现

时间:2022-05-05
本文章向大家介绍如何定位Obj-C野指针随机Crash(二):让非必现Crash变成必现,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

上一篇如何定位Obj-C野指针随机Crash介绍了思路后,这次我们继续看,如何让非必现Crash变为必现。

陈其锋,腾讯SNG即通产品部音视频技术中心软件工程师,主要负责iOS平台音视频功能开发,热衷于移动开发,以及各类APP体验

注:本文主要介绍一种延迟内在释放的技术,继续上一篇提到的如何提高野指针Crash的概率(可以文章底部点击“阅读原文”,查看上一篇文章)。另外,本文探讨的环境是在非arc情况下。

只有小概率Crash肿么办?

之前介绍了一种在内存释放后填充0x55使野指针后数据不能访问,从而使某些野指针从不必现Crash变成了必现。然而,我们早就看穿了一切,这个事情不会那么顺利的。

加上上次的代码之后,再试试下面的代码:

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)] autorelease];
    [self.view addSubview:testView];
    [[NSRunLoop mainRunLoop]runMode:NSDefaultRunLoopMode beforeDate:nil];
}

[testObj setNeedsLayout];

依然有大概率不会Crash!难道是我们的实现有问题?我试了一下xcode的Enable Scribble,但一样是大概率不Crash!

其实这就是上一篇文中留下了几个问题之一,如果我们填充0x55后内存又被别的内存覆盖了,最终还是会出现随机Crash。而在真实环境中,这种情况是非常常见的。

我们再梳理一下这个过程:

1. 我们在即将要释放的填了0x55,之后调用了free真正释放,内存被系统回收。

2. 这个时候系统随时可能把这片内存给别的代码使用,也就是说我们的0x55被再次写上随机的数据(在这里再强调一下,访问野指针是不会Crash的,只有野指针指向的地址被写上了有问题的数据才会引发Crash)。

3. 假如释放的内存上又填上了另一个对象的指针,而那个对象也有同样的一个方法,那很可能只是逻辑上有问题,并不会直接Crash,甚至悄无声息地像什么事情都没发生一样。(这个地方可能会发生多种情况,可以参考之上一篇文章中的图)

没有发生Crash可不是好事,因为这种情况如果后续再Crash,问题就非常难查,因为你看到的Crash栈很可能和出错的代码完全没有关联。既然这个问题这么棘手,最好还是和之前一样,让这个Crash提前暴露。

继续提高Crash率

沿着上次的思路,首先,我们要解决的问题就是怎么让系统不再往这片释放的内存上乱放东西。

要控制底层内存管理机制让它不使用这些内存可能很困难。但是,我们变通一下,简单粗暴地,我们干脆就不释放这片内存了。也就是当free被调用的时候我们不真的调用free,而是自己保留着内存,这样系统不知道这片内存已经不需要用了,自然就不会被再次写上别的数据(偷笑)。

为了防止系统内存过快耗尽,还需要额外多做几件事:

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. 在safe_free以及它调用的函数里面尽量不要再用带锁的函数,不然很容易导致死锁。

2. 加上这个代码之后APP的内存占用会增大不少,拿过来测试可以,但万万不能放在正式的发布版本中

3. 关于性能问题,我的机器是iPhone5,跑在App里面运行,还算流畅(不同App性能可能会有些不同)。

4. 可能由于锁的存在,会使cpu线程切换变得频繁,这样多线程的问题Crash率也可能会提升(最近遇到一个多线程引起的Crash很难重现,但我加了这个代码后就变成了必现Crash)

做完这些之后拿到项目中实际验证一下,验证的版本可以是经过测试,且遗留Crash问题已经很少,但还没有对外灰度或发布的版本。

现在来看一下效果:

终于出现了我们熟悉的Crash了!并且,我们做了更多的尝试之后,Crash还是以高概率重现!

但以上代码只是雏形,其实还有很多地方可以优化,大家在试用时可以参考着优化:

1. 最好是根据机器的情况来决定偷偷保留内存的数量。

2. 由于内存申请太过频繁,其实我们保留的内存很快就会耗尽,对于大片的内存,可以适当放过,这样可以提高保存指针的数量,防止消耗的内存过多。

3. 有的APP自己写的都是Obj-C代码,想忽略c、c++对象的话可以过滤掉(会有办法判断的)。

4. 如果觉得某些Obj-C类有问题,可以只保留指定的类对象,如果数量不是特别大,甚至可以干脆不释放。

5. ……

总结一下

理论上,机器的内存越大,我们就可以瞒着系统不释放更多内存,野指针Crash的概率也就越大。

小编有话说

提前暴露问题并解决,避免事后再补,是一个很好的习惯,希望大家都能试试。

不总结哪来经验,不分享经验何用?

在此小编号召大家多总结,互分享,踊跃给我们投稿,把自己踩过并爬出来的坑树个指示牌警醒后人,让猿们的开发生活更加美好!

投稿方式:将文章和个人介绍邮件到 bugly@tencent.com,字数不限。

本文系腾讯Bugly特邀文章,转载请注明作者和出处“腾讯Bugly(http://bugly.qq.com)”