1. 程式人生 > 其它 >如何定位Obj-C野指標隨機Crash(二):讓非必現Crash變成必現

如何定位Obj-C野指標隨機Crash(二):讓非必現Crash變成必現

轉自:https://cloud.tencent.com/developer/article/1070512?from=10680

上一篇如何定位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的概率也就越大。

小編有話說

提前暴露問題並解決,避免事後再補,是一個很好的習慣,希望大家都能試試。

不總結哪來經驗,不分享經驗何用?

在此小編號召大家多總結,互分享,踴躍給我們投稿,把自己踩過並爬出來的坑樹個指示牌警醒後人,讓猿們的開發生活更加美好!

投稿方式:將文章和個人介紹郵件到 [email protected],字數不限。

本文系騰訊Bugly特邀文章,轉載請註明作者和出處“騰訊Bugly(http://bugly.qq.com)”