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 可移植接口,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)核有一定了解

具體代碼看下圖:

關(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 an
EXC_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.
應(yīng)用級(jí)的異常NSException,需要特殊處理,需要去獲取它的reason,name,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

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