1. 程式人生 > 其它 >如何定位Obj-C野指標隨機Crash(一):先提高野指標Crash率

如何定位Obj-C野指標隨機Crash(一):先提高野指標Crash率

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

本文主要介紹如何利用OC Runtime的特性,讓OC野指標物件主動丟擲自己的資訊,秒殺某些全系統棧Crash。

陳其鋒,騰訊SNG即通產品部音視訊技術中心軟體工程師,主要負責iOS平臺音視訊功能開發,熱衷於移動開發,以及各類APP體驗。

是的,你沒有看錯,現在要說的就是提高Crash率!

欲讓其滅亡先讓其瘋狂,我們當然不是人為製造Crash,準確地說,是使隱藏的隨機性Crash暴露出來,提高測試時的Crash率,從而降低版本釋出後的Crash率。

寫C、C++程式碼的同學應該都清楚,Crash最多的原因通常有兩種,一種是多執行緒,一種是野指標。這兩種Crash都帶隨機性,而且這兩種Crash有相當一部分都很難區分,甚至大量的Crash只有系統棧,如果不能根據日誌重現,幾乎是無解,讓人非常蛋疼。

本文主要討論的方向是Obj-C的野指標。Obj-C的野指標最常見的一種棧是objc_msgSend,從Bugly上報的Crash資料來看,objc_msgSend的量佔了五分之一,這其中大多數是Obj-C野指標。當然也有相當多的Obj-C野指標不是這種表現,所以野指標的Crash體量非常驚人。

為什麼Obj-C野指標的Crash那麼多?

我們有這麼多自動化和人工測試流程,而且還有幾輪的灰度過程,其實很多Crash場景都應該已經覆蓋到了,但隨機性意味著,測試的時候它沒有問題,等使用者用了才有問題,這種情況該怎麼辦?!

我覺得關鍵在於它的隨機性,隨機性問題我初略地分為兩類:

第一類是跑不進出錯的邏輯,執行不到出錯的程式碼,這種可以提高測試場景覆蓋度來解決。 第二類

是跑進了有問題的邏輯,但是野指標指向的地址並不一定會導致Crash,這好像要看人品了?

一說到人品就頭疼啊有木有,由於上輩子做了太多善事,人品太好每次自測的時候根本不Crash有木有!

先來分析分析

野指標是指指向一個已刪除的物件或未申請訪問受限記憶體區域的指標。本文說的Obj-C野指標,說的是Obj-C物件釋放之後指標未置空,導致的野指標(Obj-C裡面一般不會出現為初始化物件的常識性錯誤)。

既然是訪問已經釋放的物件為什麼不是必現Crash呢?

因為dealloc執行後只是告訴系統,這片記憶體我不用了,而系統並沒有就讓這片記憶體不能訪問。

現實大概是下面幾種可能的情況:

  1. 物件釋放後記憶體沒被改動過,原來的記憶體儲存完好,可能不Crash或者出現邏輯錯誤(隨機Crash)。
  2. 物件釋放後記憶體沒被改動過,但是它自己析構的時候已經刪掉某些必要的東西,可能不Crash、Crash在訪問依賴的物件比如類成員上、出現邏輯錯誤(隨機Crash)。
  3. 物件釋放後記憶體被改動過,寫上了不可訪問的資料,直接就出錯了很可能Crash在objc_msgSend上面(必現Crash,常見)。
  4. 物件釋放後記憶體被改動過,寫上了可以訪問的資料,可能不Crash、出現邏輯錯誤、間接訪問到不可訪問的資料(隨機Crash)。
  5. 物件釋放後記憶體被改動過,寫上了可以訪問的資料,但是再次訪問的時候執行的程式碼把別的資料寫壞了,遇到這種Crash只能哭了(隨機Crash,難度大,概率低)!!
  6. 物件釋放後再次release(幾乎是必現Crash,但也有例外,很常見)。

參考下面的這張圖:

看看下面的程式碼,明顯有問題,但是大部分時候是不會Crash的。

UIView* testObj=[[UIView alloc] init];
[testObj release];
[testObj setNeedsLayout];

但是這個放在使用者那邊或者不是UIView這個類就不好說了,Crash率可能颼颼就上去了!

讓隨機變成不隨機

從上面列的情況來看,出現隨機Crash的情況有很多種!這是得多蛋疼呢!或許最好的辦法讓他們全都立馬Crash,然後把野指標都找出來!

仔細看看上面的關鍵路徑只有出現被隨機填入的資料是不可訪問的時候才會必現Crash。

這個地方我們可以做一下手腳,把這一隨機的過程變成不隨機的過程。物件釋放後在記憶體上填上不可訪問的資料,其實這種技術其實一直都有,xcode的Enable Scribble就是這個作用。

下面我們就拿剛剛的程式碼試一下。

scheme=>diagnostics=>Enable Scribble

果然,必現了,0x5555561!!

但是有個問題:這貨不能放在測試同學那邊用!因為總不能讓測試同學裝了xcode來測試吧?

於是我們自己動手實現一個,這個過程中我們要解決幾個問題:

  1. 怎麼在記憶體釋放後填上不可訪問的資料?記憶體釋放很可能不在我們的程式碼中。為此我們需要hook物件釋放的介面,記憶體時候之後馬上執行我們的破壞工作。
  2. 我們要重寫物件釋放的介面,重寫哪個呢?NSObject的dealloc、runtime的 object_dispose,C的free應該都是可以,但是各有優點,我選擇的是覆蓋面最廣的free,free是C的函式,重寫了它之後還可以順帶解決一部分C的野指標問題。
  3. 怎麼重寫?重寫C的介面場景的有兩種: a.替換系統動態庫 b.hook 替換動態庫太麻煩,還不知道行不行得通;hook我們就找現成的fishhook,github裡面找的,但現成的程式碼需要防止程式碼衝突。
  4. 填充的不可訪問的資料的長度怎麼確定?獲取記憶體長度的介面不在標準庫中,好在在Mac和iOS中可以用malloc_size就可以。
  5. 填什麼?和xcode一樣,填0x55。

上hook後的free程式碼:

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

測試一下,出現了和Enable Scribble一樣的Crash!

重複造了這個xcode的輪子之後,以後編包給測試,終於在某些情況下不需要那麼拼人品了。但是這僅僅覆蓋了眾多野指標中的一部分,還有大量的疑問等著繼續解答。比如:

1、由於記憶體已經被釋放了,很可能我們的0x55又被別的資料覆蓋,這種情況還是無能為力。 2、為什麼我們的0x55555555變成了0x55555561。 3、如果釋放後訪問野指標的是系統程式碼,雖然提前發現了Crash,但是離解決問題還是很遠。 4、如果野指標指向的資料沒有被當成指標使用,還是可能不立即Crash。

欲知後續問題如何解決,請聽下回分解。

小編有話說

筆者的經驗告訴我們:正視問題,才有機會把它解決。

開發者在開發過程中,如果能夠秉持不規避問題的心態,儘可能多的暴露問題、解決問題。那這個產品正在走向優秀的路途上。

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

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

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

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