漫談iOS Crash收集架構

來源:互聯網
上載者:User

標籤:顯示   rup   unix   開源項目   .net   陷阱   blog   詳細   也有   

漫談iOS Crash收集架構

為了能夠第一時間發現程式問題,應用程式需要實現自己的崩潰日誌收集服務,成熟的開源項目很多,如 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訊號。

Exception Type:         EXC_BAD_ACCESS (SIGSEGV)Exception Subtype:      KERN_INVALID_ADDRESS at 0x041a6f3

Mach異常是什嗎?它又是如何與Unix訊號建立聯絡的? Mach是一個XNU的微核心核心,Mach異常是指最底層的核心級異常,被定義在 <mach/exception_types.h>下 。每個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訊號;蘋果為了統一機制,於是作業系統和使用者產生的訊號(通過調用killpthread_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 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日誌,但是函數棧裡面沒有你的代碼:

Thread 0 Crashed:0       libsystem_kernel.dylib          0x3a61757c   __semwait_signal_nocancel + 0x181       libsystem_c.dylib               0x3a592a7c   nanosleep$NOCANCEL + 0xa02       libsystem_c.dylib               0x3a5adede   usleep$NOCANCEL + 0x2e3       libsystem_c.dylib               0x3a5c7fe0   abort + 0x504       libc++abi.dylib                 0x398f6cd2   abort_message + 0x465       libc++abi.dylib                 0x3990f6e0   default_terminate_handler() + 0xf86       libobjc.A.dylib                 0x3a054f62   _objc_terminate() + 0xbe7       libc++abi.dylib                 0x3990d1c4   std::__terminate(void (*)()) + 0x4c8       libc++abi.dylib                 0x3990cd28   __cxa_rethrow + 0x609       libobjc.A.dylib                 0x3a054e12   objc_exception_rethrow + 0x2610      CoreFoundation                  0x2f7d7f30   CFRunLoopRunSpecific + 0x27c11      CoreFoundation                  0x2f7d7c9e   CFRunLoopRunInMode + 0x6612      GraphicsServices                0x346dd65e   GSEventRunModal + 0x8613      UIKit                           0x32124148   UIApplicationMain + 0x46c14      XXXXXX                          0x0003b1f2   main + 0x1f215      libdyld.dylib                   0x3a561ab4   start + 0x0

可以看出是因為某個NSException導致程式Crash的,只有拿到這個NSException,擷取它的reasonnamecallStackSymbols資訊才能確定出問題的程式位置。

/* 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 + 0x7e1 libobjc.A.dylib 0x3a054cc     objc_exception_throw + 0x222 CoreFoundation 0x2f8a3c94     -[NSException raise] + 0x43 Foundation 0x301e8f1e         -[NSObject(NSKeyValueCoding) setValue:forKey:] + 0xc64 DemoCrash 0x00085306          -[ViewController crashMethod] + 0x6e5 DemoCrash 0x00084ecc          main + 0x1cc6 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率
  • 如何定位Obj-C野指標隨機Crash(二):讓非必現Crash變成必現
  • 如何定位Obj-C野指標隨機Crash(三):加點黑科技讓Crash自報家門
  • 分析objc_msgSend()處崩潰的小技巧

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

漫談iOS Crash收集架構

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.