iOS app崩潰捕獲

1、信號的理解

信號的概念:
信號(本人關于signal的一篇博客) http://www.lxweimin.com/p/cfd8e9824f54

2、Mach異常和Unix信號

Exception Type項通常會包含兩個元素: Mach異常 和 Unix信號。

Exception Type:         EXC_BAD_ACCESS (SIGSEGV)     //EXC_BAD_ACCESS是Mach異常 ,SIGSEGV是UNIX信號

Mach異常是什么?它又是如何與Unix信號建立聯系的?我們接下來一一解答:
Mach異常: Mach異常是指最底層的內核級異常,被定義在 <mach/exception_types.h>下 。
每個threadtaskhost都有一個異常端口數組,Mach的部分API暴露給了 用戶態 ,用戶態的開發者可以直接通過Mach API設置thread,task,host的異常端口,來捕獲Mach異常,抓取Crash事件。
UNIX信號: 所有的Mach異常都在host層被轉換成相應的UNIX信號,并通過threadsignal將信號投遞到出錯的線程。(iOS中的 POSIX API 就是通過 Mach 之上的 BSD 層實現的。)

因此: EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach層的EXC_BAD_ACCESS異常在host層被轉換成了SIGSEGV信號,并投遞到出錯的線程。

posix-bsd-mach層級.png

其他 : POSIX 可移植接口,Mach異常轉化成UNIX信號的原因是為了兼容更為流行的POSIX標準,讓不了解Mach內核的人也可以通過UNIX信號的方式來兼容開發。(即:可以通過注冊signalHandler來捕獲信號:

signal(SIGABRT, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);

Mach異常如何轉化為UNIX信號這篇文章寫的較為詳細:
http://www.lxweimin.com/p/725e7d69272c

Mach異常和UNIX信號都可以抓取crash事件,這兩種方式哪個更好?
優選Mach異常,因為Mach異常處理會先于Unix信號處理發生,如果Mach異常的handler讓程序exit了,那么Unix信號就永遠不會到達這個進程了。

另外有一點需要注意的是:

因為硬件產生的信號(通過CPU陷阱)被Mach層捕獲后,先產生Mach異常,然后才轉換為對應的Unix信號;所以蘋果為了統一機制,將操作系統用戶產生的信號(通過調用kill和pthread_kill) 也先沉下來被轉換為Mach異常,再轉換為Unix信號。
關于 調用kill和pthread_kill在實際的操作中我們會用到,用來讓用戶自己產生Unix信號。

3、Crash收集的實現思路

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

1)Mach異常方式

基于Mach內核編程,需要對內核有一定了解

內核crash收集的流程.png

具體代碼看下圖
代碼實現.png

關于內核異常的捕獲這里有一點需要提的是:在我們debug時內核異常(例如:EXC_BAD_ACESS)出現時,xcode就會停止運行,假如我們通過注冊了signal()來捕獲相應的crash,但是在debug狀態下,發生crash并不能執行到通過signal注冊的回調函數中,因為:手機的debugserver會直接接收到內核拋出的異常,接收到異常后沒有將mach異常轉換成相應的signal。最后把內核異常直接拋給了xcode,所以我們通過signal()注冊的回調函數無法被回調,因為沒有signal產生。(還有一種說法我們在調試手機上的應用時,并不是mac上的lldb直接加載上了手機中的應用,而是通過手機上的debugserver中轉一次加載的,而debugserver可以接收到內核拋出的異常,在接到異常后直接捕獲住而并沒有透明地傳給lldb,就導致我們mac上的lldb什么都干不了了。個人覺得好像不太成立)。
參考上面捕獲mach異常的方式,可以通過設置手機debugserver的異常捕獲端口來把原有的覆蓋掉。

#include <mach/task.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
//+load函數
int ret = task_set_exception_ports(
                                       mach_task_self(),
                                       EXC_MASK_BAD_ACCESS,
                                       MACH_PORT_NULL,//m_exception_port,
                                       EXCEPTION_DEFAULT,
                                       0);

這時在xcode層就會得到一個信號。在lldb中輸入相關命令(具體方式見下方),就可以執行我們注冊的回調。從而達到:‘在debug時遇到EXC_BAD_ACESS等內核異常是,我們的程序能夠繼續執行。’
當然這是一個插曲。通常情況下我們是沒有這種需求的,更多的是如何去定位到產生EXC_BAD_ACESS的位置。

2)Unix信號方式

signal(SIGSEGV,signalHandler);

3)Mach異常+Unix信號方式

目前已知的開源項目包括同行業內朋友交流,基本上都是采用這種方式(念茜文章中也提到這一點,要是各位有更好的方式,可以一起交流)。
雖然優先捕獲Mach異常,但是對Mach_CRASH異常我們卻不去處理,而是從Unix信號層面,捕獲與之對應的SIGABRT信號。原因如下:著名開源項目plcrashreporter在代碼注釋中給出了詳細的解釋:(捕獲Mach_CRASH時會導致死鎖,所以轉換為捕獲SIGABRT信號)

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

應用級的異常NSException,需要特殊處理,需要去獲取它的reasonnamecallStackSymbols信息才能確定出問題的程序位置。因為出現NSException異常后,函數的棧中不會有出錯的代碼,例如下面這樣:

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

通過讀取NSExcption對象的callStackSymbols我們可以獲取到的精準的調用信息,如下:

*** 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

關于應用級異常NSException的捕獲方法很簡單,使用NSUncaughtExceptionHandler函數: 具體事例如下:

static void customExceptionHandle (NSException *exception) {
    //NOTE:Wang Haoyu - 這里可以取到 NSException 信息,reason、callStack...
}
NSSetUncaughtExceptionHandler(&customExceptionHandle);

4、Crash 日志收集存在的陷阱

主要有兩類:1)多個Crash日志收集服務共存 2)Objc野指針類的Crash。

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

1)拒絕傳遞 UncaughtExceptionHandler
如果有多個服務都通過NSSetUncaughtExceptionHandler注冊了異常回調函數,如果在回調函數中直接退出了,那后面注冊的回調函數就不會起作用。所以需要在注冊異常處理函數時需要做兩件事

1、排查有哪些服務注冊了回調函數,并做了不規范處理(即:在他的回調函數中直接退出了.借助fishHook等工具)。
2、拿到之前的服務注冊的handler,然后備份,等自己的回調函數處理完后再把之前備份的handler注冊回去。

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

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

這里尤其需要提一下:前面我們只說了UncaughtExceptionHandler被覆蓋的情況,實際上signal的handler也會被覆蓋。而如何檢測信號綁定的回調的handler很少有文章提到。筆者經過思考探索,把突破口定位于POSIX層,從而找到了解決辦法:(這里請允許我由衷的感嘆一句:真理往往很簡單,但是發現真理的過程卻是艱難的。)

鏈接:http://www.cnblogs.com/lidabo/p/4581202.html
http://blog.csdn.net/u010944778/article/details/47375299

3)影響系統崩潰日志準確性
集成多個crash收集服務,可能會導致日志收集的不準確性升高。

1、由于進程內線程數組的變動,可能會導致系統日志中線程的Crashed 標簽標記錯位,可以搜索abort()等關鍵字排查系統日志的準確性。(該方法未嘗試過,暫時未遇到出錯情況)
2、因NSException而Crash,系統日志中的Last Exception Backtrace信息是完整準確的。即應用級異常不受影響。

5、 Xcode 如何調試 signal信號

關于signal信號的捕捉,在Xcode調試時,Debugger模式會先于我們的代碼catch到所有的crash,所以需要直接從模擬器中進入程序才可以

如果想要在Xcode中調試怎么辦?(經過探索,已經證明該方法有效,在xcode9中已驗證可行)
Xcode屏蔽了signal的回調,我們需要在lldb中輸入以下命令,signal的回調就可以進來了:
pro hand -p true -s false SIGABRT
注意:SIGABRT可以替換為你需要的任何signal類型,比如SIGSEGV


signal調試.png

參考:http://www.lxweimin.com/p/133fd6f20563
轉載注明出處:http://www.lxweimin.com/p/b5304d3412e4

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容