漫談iOS Crash收集框架

比較好的轉載:http://www.cocoachina.com/ios/20151218/14748.html
轉載 http://www.cocoachina.com/ios/20150701/12301.html

為了能夠第一時間發現程序問題,應用程序需要實現自己的崩潰日志收集服務,成熟的開源項目很多,如 KSCrash,plcrashreporter,CrashKit 等。追求方便省心,對于保密性要求不高的程序來說,也可以選擇各種一條龍Crash統計產品,如 Crashlytics,Hockeyapp ,友盟,Bugly 等等。

是否集成越多的Crash日志收集服務就越保險?
自己收集的Crash日志和系統生成的Crash日志有分歧,應該相信誰?
為什么有大量Crash日志顯示崩在main函數里,但函數棧中卻沒有一行自己的代碼?
野指針類的Crash難定位,有何妙招來應對?
想解釋清這些問題,必須從Mach異常說起。

Mach異常與Unix信號

iOS系統自帶的 Apple’s Crash Reporter 記錄在設備中的Crash日志,Exception Type項通常會包含兩個元素: Mach異常 和 Unix信號。

1
2
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x041a6f3
Mach異常是什么?它又是如何與Unix信號建立聯系的?

Mach是一個XNU的微內核核心,Mach異常是指最底層的內核級異常,被定義在下 。每個thread,task,host都有一個異常端口數組,Mach的部分API暴露給了用戶態,用戶態的開發者可以直接通過Mach API設置thread,task,host的異常端口,來捕獲Mach異常,抓取Crash事件。

所有Mach異常都在host層被ux_exception轉換為相應的Unix信號,并通過threadsignal將信號投遞到出錯的線程。iOS中的 POSIX API 就是通過 Mach 之上的 BSD 層實現的。

blob.png

因此,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach層的EXC_BAD_ACCESS異常,在host層被轉換成SIGSEGV信號投遞到出錯的線程。既然最終以信號的方式投遞到出錯的線程,那么就可以通過注冊signalHandler來捕獲信號:

1
signal(SIGSEGV,signalHandler);
捕獲Mach異常或者Unix信號都可以抓到crash事件,這兩種方式哪個更好呢?

優選Mach異常,因為Mach異常處理會先于Unix信號處理發生,如果Mach異常的handler讓程序exit了,那么Unix信號就永遠不會到達這個進程了。轉換Unix信號是為了兼容更為流行的POSIX標準(SUS規范),這樣不必了解Mach內核也可以通過Unix信號的方式來兼容開發。

小貼士:

因為硬件產生的信號(通過CPU陷阱)被Mach層捕獲,然后才轉換為對應的Unix信號;蘋果為了統一機制,于是操作系統和用戶產生的信號(通過調用kill和pthread_kill)也首先沉下來被轉換為Mach異常,再轉換為Unix信號。

Crash收集的實現思路

正如上述所說,可以通過捕獲Mach異常、或Unix信號兩種方式來抓取crash事件,于是總結起來實現方案就一共有3種。

1)Mach異常方式

blob.png

2)Unix信號方式

1
signal(SIGSEGV,signalHandler);
3)Mach異常+Unix信號方式

Github上多數開源項目都采用的這種方式,即使在優選捕獲Mach異常的情況下,也放棄捕獲EXC_CRASH異常,而選擇捕獲與之對應的SIGABRT信號。著名開源項目plcrashreporter在代碼注釋中給出了詳細的解釋:

1
We still need to use signal handlers to catch SIGABRT in-process. The kernel sends an EXC_CRASH mach exception to denote SIGABRT termination. In that case, catching the Mach exception in-process leads to process deadlock in an uninterruptable wait. Thus, we fall back on BSD signal handlers for SIGABRT, and do not register for EXC_CRASH.
另外,需要重點說明的是:對于應用級異常NSException,還需要特殊處理。

你是否見過崩潰在main函數的crash日志,但是函數棧里面沒有你的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Thread 0 Crashed:
0 libsystem_kernel.dylib 0x3a61757c __semwait_signal_nocancel + 0x18
1 libsystem_c.dylib 0x3a592a7c nanosleep$NOCANCEL + 0xa0
2 libsystem_c.dylib 0x3a5adede usleep$NOCANCEL + 0x2e
3 libsystem_c.dylib 0x3a5c7fe0 abort + 0x50
4 libc++abi.dylib 0x398f6cd2 abort_message + 0x46
5 libc++abi.dylib 0x3990f6e0 default_terminate_handler() + 0xf8
6 libobjc.A.dylib 0x3a054f62 _objc_terminate() + 0xbe
7 libc++abi.dylib 0x3990d1c4 std::__terminate(void (*)()) + 0x4c
8 libc++abi.dylib 0x3990cd28 __cxa_rethrow + 0x60
9 libobjc.A.dylib 0x3a054e12 objc_exception_rethrow + 0x26
10 CoreFoundation 0x2f7d7f30 CFRunLoopRunSpecific + 0x27c
11 CoreFoundation 0x2f7d7c9e CFRunLoopRunInMode + 0x66
12 GraphicsServices 0x346dd65e GSEventRunModal + 0x86
13 UIKit 0x32124148 UIApplicationMain + 0x46c
14 XXXXXX 0x0003b1f2 main + 0x1f2
15 libdyld.dylib 0x3a561ab4 start + 0x0
可以看出是因為某個NSException導致程序Crash的,只有拿到這個NSException,獲取它的reason,name,callStackSymbols信息才能確定出問題的程序位置。

1
2
3
4
5
/* NSException Class Reference */
@property(readonly, copy) NSString *name;
@property(readonly, copy) NSString *reason;
@property(readonly, copy) NSArray *callStackSymbols;
@property(readonly, copy) NSArray *callStackReturnAddresses;
方法很簡單,可通過注冊NSUncaughtExceptionHandler捕獲異常信息:

1
2
3
4
static void my_uncaught_exception_handler (NSException *exception) {
//這里可以取到 NSException 信息
}
NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler);
將拿到的NSException細節寫入Crash日志,精準的定位出錯程序位置:

1
2
3
4
5
6
7
8
9
10
Application Specific Information:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key key.'
Last Exception Backtrace:
0 CoreFoundation 0x2f8a3f7e __exceptionPreprocess + 0x7e
1 libobjc.A.dylib 0x3a054cc objc_exception_throw + 0x22
2 CoreFoundation 0x2f8a3c94 -[NSException raise] + 0x4
3 Foundation 0x301e8f1e -[NSObject(NSKeyValueCoding) setValue:forKey:] + 0xc6
4 DemoCrash 0x00085306 -[ViewController crashMethod] + 0x6e
5 DemoCrash 0x00084ecc main + 0x1cc
6 DemoCrash 0x00084cf8 start + 0x24
那么,是不是收到了大量crash在main函數卻沒有NSException信息的日志,就代表自己集成的Crash日志收集服務沒有注冊NSUncaughtExceptionHandler呢?不一定,還有另外一種可能,就是被同時存在的其他Crash日志收集服務給坑了。

多個Crash日志收集服務共存的坑

是的,在自己的程序里集成多個Crash日志收集服務實在不是明智之舉。通常情況下,第三方功能性SDK都會集成一個Crash收集服務,以及時發現自己SDK的問題。當各家的服務都以保證自己的Crash統計正確完整為目的時,難免出現時序手腳,強行覆蓋等等的惡意競爭,總會有人默默被坑。

1)拒絕傳遞 UncaughtExceptionHandler

如果同時有多方通過NSSetUncaughtExceptionHandler注冊異常處理程序,和平的作法是:后注冊者通過NSGetUncaughtExceptionHandler將先前別人注冊的handler取出并備份,在自己handler處理完后自覺把別人的handler注冊回去,規規矩矩的傳遞。不傳遞強行覆蓋的后果是,在其之前注冊過的日志收集服務寫出的Crash日志就會因為取不到NSException而丟失Last Exception Backtrace等信息。(P.S. iOS系統自帶的Crash Reporter不受影響)

在開發測試階段,可以利用 fishhook 框架去hookNSSetUncaughtExceptionHandler方法,這樣就可以清晰的看到handler的傳遞流程斷在哪里,快速定位污染環境者。不推薦利用調試器添加符號斷點來檢查,原因是一些Crash收集框架在調試狀態下是不工作的。

檢測代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static NSUncaughtExceptionHandler g_vaildUncaughtExceptionHandler;
static void (
ori_NSSetUncaughtExceptionHandler)( NSUncaughtExceptionHandler * );
void my_NSSetUncaughtExceptionHandler( NSUncaughtExceptionHandler * handler)
{
g_vaildUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
if (g_vaildUncaughtExceptionHandler != NULL) {
NSLog(@"UncaughtExceptionHandler=%p",g_vaildUncaughtExceptionHandler);
}

ori_NSSetUncaughtExceptionHandler(handler);
NSLog(@"%@",[NSThread callStackSymbols]);
 
g_vaildUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
NSLog(@"UncaughtExceptionHandler=%p",g_vaildUncaughtExceptionHandler);

}
對于越獄插件注入應用進程內部,惡意覆蓋NSSetUncaughtExceptionHandler的情況,應用程序本身處理起來比較弱勢,因為越獄環境下操作時序的玩法比較多權利比較大。

2)Mach異常端口換出+信號處理Handler覆蓋

和NSSetUncaughtExceptionHandler的情況類似,設置過的Mach異常端口和信號處理程序也有可能被干掉,導致無法捕獲Crash事件。

3)影響系統崩潰日志準確性

應用層參與收集Crash日志的服務方越多,越有可能影響iOS系統自帶的Crash Reporter。由于進程內線程數組的變動,可能會導致系統日志中線程的Crashed 標簽標記錯位,可以搜索abort()等關鍵字來復查系統日志的準確性。

若程序因NSException而Crash,系統日志中的Last Exception Backtrace信息是完整準確的,不會受應用層的胡來而影響,可作為排查問題的參考線索。

ObjC野指針類的Crash

收集Crash日志這個步驟沒有問題的情況下,還是有很多全系統棧的日志的情況,沒有自己一行代碼,分析起來十分棘手,ObjC野指針類的Crash正是如此,這里推薦幾篇好文章:

如何定位Obj-C野指針隨機Crash(一):先提高野指針Crash率
如何定位Obj-C野指針隨機Crash(二):讓非必現Crash變成必現
如何定位Obj-C野指針隨機Crash(三):加點黑科技讓Crash自報家門
分析objc_msgSend()處崩潰的小技巧
除此之外,在Crash日志中補充記錄一些額外信息可以輔助定位,如切面標記線程出處、隊列出處,記錄用戶操作軌跡等等……

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內容

  • 轉載(漫談 iOS Crash 收集框架) 前言 很早以前就和念茜認識,念茜不但技術功底扎實,而且長得很漂亮,說她...
    狂風無跡閱讀 3,365評論 1 11
  • 以下為文章正文,如果覺得有用,歡迎給她打賞。 為了能夠第一時間發現程序問題,應用程序需要實現自己的崩潰日志收集服務...
    赤色追風閱讀 2,566評論 1 11
  • 來源:程序媛念茜的博客 Crash日志收集 為了能夠第一時間發現程序問題,應用程序需要實現自己的崩潰日志收集服務,...
    幸福的魚閱讀 1,179評論 0 2
  • 本文就捕獲iOS Crash、Crash日志組成、Crash日志符號化、異常信息解讀、常見的Crash五部分介紹。...
    xukuangbo_閱讀 1,593評論 0 0
  • 今天的晨讀材料是讓創意更有黏性,我們每個人都需要傳遞信息,并希望自己的言語能產生更持久的影響,這就是黏性,簡單來說...
    鎖恨閱讀 249評論 0 0