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>
下 。
每個thread
,task
,host
都有一個異常端口數組,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 可移植接口,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內核編程,需要對內核有一定了解
具體代碼看下圖:
關于內核異常的捕獲這里有一點需要提的是:在我們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 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 forEXC_CRASH
.
應用級的異常NSException,需要特殊處理,需要去獲取它的reason
,name
,callStackSymbols
信息才能確定出問題的程序位置。因為出現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
參考:http://www.lxweimin.com/p/133fd6f20563
轉載注明出處:http://www.lxweimin.com/p/b5304d3412e4