漫談 iOS Crash 收集框架

轉載(漫談 iOS Crash 收集框架

前言

很早以前就和念茜認識,念茜不但技術功底扎實,而且長得很漂亮,說她是 iOS 界的女神都一點不為過。雖然網上和她常常交流,但是線下和念茜只見過兩次,可惜都沒能合影,實在遺憾。

念茜也是一個愛分享的人,她很早就開始寫博客,所以她在 iOS 圈的知名度很高,從她的微博粉絲數上就能看出來。她喜歡研究安全,博客上有很多安全方面的文章。

有一次我們在交流技術中涉及到了一些 Crash 收集的知識,當時感覺她研究得很深入。前幾天了解到她做打算寫一篇相關的文章,我趕緊就拿一個 Sublime Text 的 License 的條件換得了文章在微信公眾平臺的獨家發表權。

念茜是一個善良的女孩,她一直在資助一位貧困學生上學。在本文中,念茜提出的一些問題其實在某些著名的 Crash 收集服務中都沒有處理好。我問念茜為啥不指明呢?念茜說:“不廣告不詆毀”。我想,這可能就是一個專心做技術人的處事之道吧。

以下為文章正文,如果覺得有用,歡迎給她打賞。

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

是否集成越多的 Crash 日志收集服務就越保險?

自己收集的 Crash 日志和系統生成的 Crash 日志有分歧,應該相信誰?

為什么有大量 Crash 日志顯示崩在 main 函數里,但函數棧中卻沒有一行自己的代碼?

野指針類的 Crash 難定位,有何妙招來應對?

想解釋清這些問題,必須從 Mach 異常說起。

Mach 異常與 Unix 信號

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

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 層實現的。

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

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 異常方式

2)Unix 信號方式

signal(SIGSEGV,signalHandler);

3)Mach 異常 +Unix 信號方式

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

We still need to use signal handlers to catch SIGABRT in-process. The kernel sends anEXC_CRASHmach 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 forEXC_CRASH.

另外,需要重點說明的是:對于應用級異常 NSException,還需要特殊處理。

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

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信息才能確定出問題的程序位置。

/* NSException Class Reference */

@property(readonly, copy) NSString *name;

@property(readonly, copy) NSString *reason;

@property(readonly, copy) NSArray *callStackSymbols;

@property(readonly, copy) NSArray *callStackReturnAddresses;

方法很簡單,可通過注冊NSUncaughtExceptionHandler捕獲異常信息:

static void my_uncaught_exception_handler (NSException *exception) {

// 這里可以取到 NSException 信息

}

NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler);

將拿到的 NSException 細節寫入 Crash 日志,精準的定位出錯程序位置:

Application Specific Information:

*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSDictionaryI 0x14554d00> 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 收集框架在調試狀態下是不工作的。

檢測代碼示例:

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 率

http://bugly.qq.com/blog/?p=200

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

http://bugly.qq.com/blog/?p=308

如何定位 Obj-C 野指針隨機 Crash(三):加點黑科技讓 Crash 自報家門

http://bugly.qq.com/blog/?p=335

分析 objc_msgSend() 處崩潰的小技巧

http://www.sealiesoftware.com/blog/archive/2008/09/22/objc_explain_So_you_crashed_in_objc_msgSend.html

除此之外,在 Crash 日志中補充記錄一些額外信息可以輔助定位,如切面標記線程出處、隊列出處,記錄用戶操作軌跡等等……

感謝閱讀

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

推薦閱讀更多精彩內容

  • 以下為文章正文,如果覺得有用,歡迎給她打賞。 為了能夠第一時間發現程序問題,應用程序需要實現自己的崩潰日志收集服務...
    赤色追風閱讀 2,564評論 1 11
  • 比較好的轉載:http://www.cocoachina.com/ios/20151218/14748.html轉...
    liudhkk閱讀 935評論 0 2
  • 來源:程序媛念茜的博客 Crash日志收集 為了能夠第一時間發現程序問題,應用程序需要實現自己的崩潰日志收集服務,...
    幸福的魚閱讀 1,176評論 0 2
  • 本文就捕獲iOS Crash、Crash日志組成、Crash日志符號化、異常信息解讀、常見的Crash五部分介紹。...
    xukuangbo_閱讀 1,591評論 0 0
  • 第九周里感覺還是沒把力氣用盡。但這個問題在這周里被充分認識到了,這些認識上的長進都是長足的。很多方面結束徘徊,下周...
    5779cc3e3627閱讀 232評論 0 1