iOS app崩潰捕獲

1、信號(hào)的理解

信號(hào)的概念:
信號(hào)(本人關(guān)于signal的一篇博客) http://www.itdecent.cn/p/cfd8e9824f54

2、Mach異常和Unix信號(hào)

Exception Type項(xiàng)通常會(huì)包含兩個(gè)元素: Mach異常 和 Unix信號(hào)。

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

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

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

posix-bsd-mach層級(jí).png

其他 : POSIX 可移植接口,Mach異常轉(zhuǎn)化成UNIX信號(hào)的原因是為了兼容更為流行的POSIX標(biāo)準(zhǔn),讓不了解Mach內(nèi)核的人也可以通過UNIX信號(hào)的方式來兼容開發(fā)。(即:可以通過注冊(cè)signalHandler來捕獲信號(hào):

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異常如何轉(zhuǎn)化為UNIX信號(hào)這篇文章寫的較為詳細(xì):
http://www.itdecent.cn/p/725e7d69272c

Mach異常和UNIX信號(hào)都可以抓取crash事件,這兩種方式哪個(gè)更好?
優(yōu)選Mach異常,因?yàn)镸ach異常處理會(huì)先于Unix信號(hào)處理發(fā)生,如果Mach異常的handler讓程序exit了,那么Unix信號(hào)就永遠(yuǎn)不會(huì)到達(dá)這個(gè)進(jìn)程了。

另外有一點(diǎn)需要注意的是:

因?yàn)?code>硬件產(chǎn)生的信號(hào)(通過CPU陷阱)被Mach層捕獲后,先產(chǎn)生Mach異常,然后才轉(zhuǎn)換為對(duì)應(yīng)的Unix信號(hào);所以蘋果為了統(tǒng)一機(jī)制,將操作系統(tǒng)用戶產(chǎn)生的信號(hào)(通過調(diào)用kill和pthread_kill) 也先沉下來被轉(zhuǎn)換為Mach異常,再轉(zhuǎn)換為Unix信號(hào)。
關(guān)于 調(diào)用kill和pthread_kill在實(shí)際的操作中我們會(huì)用到,用來讓用戶自己產(chǎn)生Unix信號(hào)。

3、Crash收集的實(shí)現(xiàn)思路

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

1)Mach異常方式

基于Mach內(nèi)核編程,需要對(duì)內(nèi)核有一定了解

內(nèi)核crash收集的流程.png

具體代碼看下圖
代碼實(shí)現(xiàn).png

關(guān)于內(nèi)核異常的捕獲這里有一點(diǎn)需要提的是:在我們debug時(shí)內(nèi)核異常(例如:EXC_BAD_ACESS)出現(xiàn)時(shí),xcode就會(huì)停止運(yùn)行,假如我們通過注冊(cè)了signal()來捕獲相應(yīng)的crash,但是在debug狀態(tài)下,發(fā)生crash并不能執(zhí)行到通過signal注冊(cè)的回調(diào)函數(shù)中,因?yàn)椋菏謾C(jī)的debugserver會(huì)直接接收到內(nèi)核拋出的異常,接收到異常后沒有將mach異常轉(zhuǎn)換成相應(yīng)的signal。最后把內(nèi)核異常直接拋給了xcode,所以我們通過signal()注冊(cè)的回調(diào)函數(shù)無法被回調(diào),因?yàn)闆]有signal產(chǎn)生。(還有一種說法我們?cè)谡{(diào)試手機(jī)上的應(yīng)用時(shí),并不是mac上的lldb直接加載上了手機(jī)中的應(yīng)用,而是通過手機(jī)上的debugserver中轉(zhuǎn)一次加載的,而debugserver可以接收到內(nèi)核拋出的異常,在接到異常后直接捕獲住而并沒有透明地傳給lldb,就導(dǎo)致我們mac上的lldb什么都干不了了。個(gè)人覺得好像不太成立)。
參考上面捕獲mach異常的方式,可以通過設(shè)置手機(jī)debugserver的異常捕獲端口來把原有的覆蓋掉。

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

這時(shí)在xcode層就會(huì)得到一個(gè)信號(hào)。在lldb中輸入相關(guān)命令(具體方式見下方),就可以執(zhí)行我們注冊(cè)的回調(diào)。從而達(dá)到:‘在debug時(shí)遇到EXC_BAD_ACESS等內(nèi)核異常是,我們的程序能夠繼續(xù)執(zhí)行?!?br> 當(dāng)然這是一個(gè)插曲。通常情況下我們是沒有這種需求的,更多的是如何去定位到產(chǎn)生EXC_BAD_ACESS的位置。

2)Unix信號(hào)方式

signal(SIGSEGV,signalHandler);

3)Mach異常+Unix信號(hào)方式

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

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.

應(yīng)用級(jí)的異常NSException,需要特殊處理,需要去獲取它的reasonname,callStackSymbols信息才能確定出問題的程序位置。因?yàn)槌霈F(xiàn)NSException異常后,函數(shù)的棧中不會(huì)有出錯(cuò)的代碼,例如下面這樣:

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對(duì)象的callStackSymbols我們可以獲取到的精準(zhǔn)的調(diào)用信息,如下:

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

關(guān)于應(yīng)用級(jí)異常NSException的捕獲方法很簡(jiǎn)單,使用NSUncaughtExceptionHandler函數(shù): 具體事例如下:

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

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

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

多個(gè)Crash日志收集服務(wù)共存的坑

1)拒絕傳遞 UncaughtExceptionHandler
如果有多個(gè)服務(wù)都通過NSSetUncaughtExceptionHandler注冊(cè)了異?;卣{(diào)函數(shù),如果在回調(diào)函數(shù)中直接退出了,那后面注冊(cè)的回調(diào)函數(shù)就不會(huì)起作用。所以需要在注冊(cè)異常處理函數(shù)時(shí)需要做兩件事

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

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

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

這里尤其需要提一下:前面我們只說了UncaughtExceptionHandler被覆蓋的情況,實(shí)際上signal的handler也會(huì)被覆蓋。而如何檢測(cè)信號(hào)綁定的回調(diào)的handler很少有文章提到。筆者經(jīng)過思考探索,把突破口定位于POSIX層,從而找到了解決辦法:(這里請(qǐng)?jiān)试S我由衷的感嘆一句:真理往往很簡(jiǎn)單,但是發(fā)現(xiàn)真理的過程卻是艱難的。)

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

3)影響系統(tǒng)崩潰日志準(zhǔn)確性
集成多個(gè)crash收集服務(wù),可能會(huì)導(dǎo)致日志收集的不準(zhǔn)確性升高。

1、由于進(jìn)程內(nèi)線程數(shù)組的變動(dòng),可能會(huì)導(dǎo)致系統(tǒng)日志中線程的Crashed 標(biāo)簽標(biāo)記錯(cuò)位,可以搜索abort()等關(guān)鍵字排查系統(tǒng)日志的準(zhǔn)確性。(該方法未嘗試過,暫時(shí)未遇到出錯(cuò)情況)
2、因NSException而Crash,系統(tǒng)日志中的Last Exception Backtrace信息是完整準(zhǔn)確的。即應(yīng)用級(jí)異常不受影響。

5、 Xcode 如何調(diào)試 signal信號(hào)

關(guān)于signal信號(hào)的捕捉,在Xcode調(diào)試時(shí),Debugger模式會(huì)先于我們的代碼catch到所有的crash,所以需要直接從模擬器中進(jìn)入程序才可以

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


signal調(diào)試.png

參考:http://www.itdecent.cn/p/133fd6f20563
轉(zhuǎn)載注明出處:http://www.itdecent.cn/p/b5304d3412e4

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容