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

前言

Crash的主要原因是你的應(yīng)用收到了未處理的信號(hào)。

未處理信號(hào)可能來源于三個(gè)地方:kernel、其他進(jìn)程、以及App本身。

因此,crash異常也分為三種:

  • Mach異常:是指最底層的內(nèi)核級(jí)異常。用戶態(tài)的開發(fā)者可以直接通過Mach API設(shè)置thread,task,host的異常端口,來捕獲Mach異常。
  • Unix信號(hào):又稱BSD 信號(hào),如果開發(fā)者沒有捕獲Mach異常,則會(huì)被host層的方法ux_exception()將異常轉(zhuǎn)換為對應(yīng)的UNIX信號(hào),并通過方法threadsignal()將信號(hào)投遞到出錯(cuò)線程??梢酝ㄟ^方法signal(x, SignalHandler)來捕獲single。
  • NSException:應(yīng)用級(jí)異常,它是未被捕獲的Objective-C異常,導(dǎo)致程序向自身發(fā)送了SIGABRT信號(hào)而崩潰,對于未捕獲的Objective-C異常,是可以通過try catch來捕獲的,或者通過NSSetUncaughtExceptionHandler()機(jī)制來捕獲。

Mach異常與Unix信號(hào)

Mach異常是什么?它又是如何與Unix信號(hào)建立聯(lián)系的? Mach是一個(gè)XNU的微內(nèi)核核心,Mach異常是指最底層的內(nèi)核級(jí)異常 。每個(gè)thread,task,host都有一個(gè)異常端口數(shù)組,Mach的部分API暴露給了用戶態(tài),用戶態(tài)的開發(fā)者可以直接通過Mach API設(shè)置thread,task,host的異常端口,來捕獲Mach異常,抓取Crash事件。

794763-145fdfe57b332b2e.png

所有Mach異常未處理,它將在host層被ux_exception轉(zhuǎn)換為相應(yīng)的Unix信號(hào),并通過threadsignal將信號(hào)投遞到出錯(cuò)的線程。iOS中的 POSIX API 就是通過 Mach 之上的 BSD 層實(shí)現(xiàn)的。

看看Matt大神的回答

EXC_BAD_ACCESS is a Mach exception sent by the kernel to your application when you try to access memory that is not mapped for your application. If not handled at the Mach level, it will be translated into a SIGBUS or SIGSEGV BSD signal.

大概意思就是:EXC_BAD_ACCESS異常主要是訪問了不屬于本進(jìn)程或者已經(jīng)釋放的內(nèi)存地址。如果未在mach層捕獲,它將在host層被轉(zhuǎn)換成SIGSEGV信號(hào)投遞到出錯(cuò)的線程。

捕獲Mach異?;蛘遀nix信號(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)程了。

如果優(yōu)選Mach來捕獲異常,為什么還要轉(zhuǎn)化為unix信號(hào)呢?

轉(zhuǎn)換Unix信號(hào)是為了兼容更為流行的POSIX標(biāo)準(zhǔn)(SUS規(guī)范),這樣不必了解Mach內(nèi)核也可以通過Unix信號(hào)的方式來兼容開發(fā)。

為什么第三方庫PLCrashReporter即使在優(yōu)選捕獲Mach異常的情況下,也放棄捕獲Mach異常EXC_CRASH,而選擇捕獲與之對應(yīng)的SIGABRT信號(hào)?

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.

注意:
因?yàn)橛布a(chǎn)生的信號(hào)(通過CPU陷阱)被Mach層捕獲,然后才轉(zhuǎn)換為對應(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)。

也就是說整個(gè)流程是這樣的:
硬件產(chǎn)生信號(hào)或者kill或pthread_kill信號(hào) --> Mach異常 --> Unix信號(hào)(SIGABRT)

因此,捕獲crash的流程是這樣的


mach2.png

Crash收集方式

通過UncaughtExceptionHandler機(jī)制收集

這種手機(jī)方式只適合收集應(yīng)用級(jí)異常,我們要做的就是用自定義的函數(shù)替代該ExceptionHandler即可。

// 記錄之前的崩潰回調(diào)函數(shù)
static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler = NULL;

@implementation NWUncaughtExceptionHandler

#pragma mark - Register

+ (void)registerHandler {
    //將先前別人注冊的handler取出并備份
    previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
    
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}

#pragma mark - Private

// 崩潰時(shí)的回調(diào)函數(shù)
static void UncaughtExceptionHandler(NSException * exception) {
    // 異常的堆棧信息
    NSArray * stackArray = [exception callStackSymbols];
    // 出現(xiàn)異常的原因
    NSString * reason = [exception reason];
    // 異常名稱
    NSString * name = [exception name];
    
    NSString * exceptionInfo = [NSString stringWithFormat:@"========uncaughtException異常錯(cuò)誤報(bào)告========\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@", name, reason, [stackArray componentsJoinedByString:@"\n"]];
    
    // 保存崩潰日志到沙盒cache目錄
    [NWCrashTool saveCrashLog:exceptionInfo fileName:@"Crash(Uncaught)"];
    
    //在自己handler處理完后自覺把別人的handler注冊回去,規(guī)規(guī)矩矩的傳遞
    if (previousUncaughtExceptionHandler) {
        previousUncaughtExceptionHandler(exception);
    }
    
    // 殺掉程序,這樣可以防止同時(shí)拋出的SIGABRT被SignalException捕獲
    kill(getpid(), SIGKILL);
}

@end

注意:

在自己的程序里集成多個(gè)Crash日志收集服務(wù)實(shí)是否是明智之舉?

通常情況下,第三方功能性SDK都會(huì)集成一個(gè)Crash收集服務(wù),以及時(shí)發(fā)現(xiàn)自己SDK的問題。當(dāng)各家的服務(wù)都以保證自己的Crash統(tǒng)計(jì)正確完整為目的時(shí),難免出現(xiàn)時(shí)序手腳,強(qiáng)行覆蓋等等的惡意競爭,就會(huì)導(dǎo)致在其之前注冊過的日志收集服務(wù)寫出的Crash日志因?yàn)槿〔坏絅SException而丟失Last Exception Backtrace等信息。

因此,如果同時(shí)有多方通過NSSetUncaughtExceptionHandler注冊異常處理程序,正確的作法是:后注冊者通過NSGetUncaughtExceptionHandler將先前別人注冊的handler取出并備份,在自己handler處理完后自覺把別人的handler注冊回去,規(guī)規(guī)矩矩的傳遞

未設(shè)置NSSetUncaughtExceptionHandler的NSException最后會(huì)轉(zhuǎn)成Unix信號(hào)嗎?

無論設(shè)置NSSetUncaughtExceptionHandler與否,只要未被try catch,最終都會(huì)被轉(zhuǎn)成Unix信號(hào),只不過設(shè)置了無法在其ExceptionHandler中獲得最終發(fā)送的Unix信號(hào)類型

Mach異常方式

794763-34781a7c81338146.png

這種基本沒用過。

Unix信號(hào)

Unix信號(hào):signal(SIGSEGV,signalHandler);

SIGABRT is a BSD signal sent by an application to itself when an NSException or obj_exception_throw is not caught.

大概意思就是:
當(dāng)NSException或obj_exception_throw未被捕獲時(shí),應(yīng)用程序會(huì)給它本身發(fā)送一個(gè)SIGABRT信號(hào)。
但是,這并不能代表SIGABRT就是 NSException導(dǎo)致,因?yàn)镾IGABRT是調(diào)用abort()生成的信號(hào)。
若程序因NSException而Crash,系統(tǒng)日志中的Last Exception Backtrace信息是完整準(zhǔn)確的。

#import "NWCrashSignalExceptionHandler.h"
#import <execinfo.h>
#import "NWCrashTool.h"

typedef void(*SignalHandler)(int signal, siginfo_t *info, void *context);

static SignalHandler previousABRTSignalHandler = NULL;
static SignalHandler previousBUSSignalHandler = NULL;
static SignalHandler previousFPESignalHandler = NULL;
static SignalHandler previousILLSignalHandler = NULL;
static SignalHandler previousPIPESignalHandler = NULL;
static SignalHandler previousSEGVSignalHandler = NULL;
static SignalHandler previousSYSSignalHandler = NULL;
static SignalHandler previousTRAPSignalHandler = NULL;

@implementation NWCrashSignalExceptionHandler

+ (void)registerHandler {
    // 將先前別人注冊的handler取出并備份
    [self backupOriginalHandler];
    
    [self signalRegister];
}

+ (void)backupOriginalHandler {
    struct sigaction old_action_abrt;
    sigaction(SIGABRT, NULL, &old_action_abrt);
    if (old_action_abrt.sa_sigaction) {
        previousABRTSignalHandler = old_action_abrt.sa_sigaction;
    }
    
    struct sigaction old_action_bus;
    sigaction(SIGBUS, NULL, &old_action_bus);
    if (old_action_bus.sa_sigaction) {
        previousBUSSignalHandler = old_action_bus.sa_sigaction;
    }
    
    struct sigaction old_action_fpe;
    sigaction(SIGFPE, NULL, &old_action_fpe);
    if (old_action_fpe.sa_sigaction) {
        previousFPESignalHandler = old_action_fpe.sa_sigaction;
    }
    
    struct sigaction old_action_ill;
    sigaction(SIGILL, NULL, &old_action_ill);
    if (old_action_ill.sa_sigaction) {
        previousILLSignalHandler = old_action_ill.sa_sigaction;
    }
    
    struct sigaction old_action_pipe;
    sigaction(SIGPIPE, NULL, &old_action_pipe);
    if (old_action_pipe.sa_sigaction) {
        previousPIPESignalHandler = old_action_pipe.sa_sigaction;
    }
    
    struct sigaction old_action_segv;
    sigaction(SIGSEGV, NULL, &old_action_segv);
    if (old_action_segv.sa_sigaction) {
        previousSEGVSignalHandler = old_action_segv.sa_sigaction;
    }
    
    struct sigaction old_action_sys;
    sigaction(SIGSYS, NULL, &old_action_sys);
    if (old_action_sys.sa_sigaction) {
        previousSYSSignalHandler = old_action_sys.sa_sigaction;
    }
    
    struct sigaction old_action_trap;
    sigaction(SIGTRAP, NULL, &old_action_trap);
    if (old_action_trap.sa_sigaction) {
        previousTRAPSignalHandler = old_action_trap.sa_sigaction;
    }
}

+ (void)signalRegister {
    NWSignalRegister(SIGABRT);
    NWSignalRegister(SIGBUS);
    NWSignalRegister(SIGFPE);
    NWSignalRegister(SIGILL);
    NWSignalRegister(SIGPIPE);
    NWSignalRegister(SIGSEGV);
    NWSignalRegister(SIGSYS);
    NWSignalRegister(SIGTRAP);
}

#pragma mark - Private

#pragma mark Register Signal

static void NWSignalRegister(int signal) {
    struct sigaction action;
    action.sa_sigaction = NWSignalHandler;
    action.sa_flags = SA_NODEFER | SA_SIGINFO;
    sigemptyset(&action.sa_mask);
    sigaction(signal, &action, 0);
}

#pragma mark SignalCrash Handler

static void NWSignalHandler(int signal, siginfo_t* info, void* context) {
    NSMutableString *mstr = [[NSMutableString alloc] init];
    [mstr appendString:@"Signal Exception:\n"];
    [mstr appendString:[NSString stringWithFormat:@"Signal %@ was raised.\n", signalName(signal)]];
    [mstr appendString:@"Call Stack:\n"];
    
    // 這里過濾掉第一行日志
    // 因?yàn)樽粤诵盘?hào)崩潰回調(diào)方法,系統(tǒng)會(huì)來調(diào)用,將記錄在調(diào)用堆棧上,因此此行日志需要過濾掉
    for (NSUInteger index = 1; index < NSThread.callStackSymbols.count; index++) {
        NSString *str = [NSThread.callStackSymbols objectAtIndex:index];
        [mstr appendString:[str stringByAppendingString:@"\n"]];
    }
    
    [mstr appendString:@"threadInfo:\n"];
    [mstr appendString:[[NSThread currentThread] description]];
    
    // 保存崩潰日志到沙盒cache目錄
    [NWCrashTool saveCrashLog:[NSString stringWithString:mstr] fileName:@"Crash(Signal)"];
    
    NWClearSignalRegister();
    
    // 調(diào)用之前崩潰的回調(diào)函數(shù)
    // 在自己handler處理完后自覺把別人的handler注冊回去,規(guī)規(guī)矩矩的傳遞
    previousSignalHandler(signal, info, context);
    
    kill(getpid(), SIGKILL);
}

#pragma mark Signal To Name

static NSString *signalName(int signal) {
    NSString *signalName;
    switch (signal) {
        case SIGABRT:
            signalName = @"SIGABRT";
            break;
        case SIGBUS:
            signalName = @"SIGBUS";
            break;
        case SIGFPE:
            signalName = @"SIGFPE";
            break;
        case SIGILL:
            signalName = @"SIGILL";
            break;
        case SIGPIPE:
            signalName = @"SIGPIPE";
            break;
        case SIGSEGV:
            signalName = @"SIGSEGV";
            break;
        case SIGSYS:
            signalName = @"SIGSYS";
            break;
        case SIGTRAP:
            signalName = @"SIGTRAP";
            break;
        default:
            break;
    }
    return signalName;
}

#pragma mark Previous Signal

static void previousSignalHandler(int signal, siginfo_t *info, void *context) {
    SignalHandler previousSignalHandler = NULL;
    switch (signal) {
        case SIGABRT:
            previousSignalHandler = previousABRTSignalHandler;
            break;
        case SIGBUS:
            previousSignalHandler = previousBUSSignalHandler;
            break;
        case SIGFPE:
            previousSignalHandler = previousFPESignalHandler;
            break;
        case SIGILL:
            previousSignalHandler = previousILLSignalHandler;
            break;
        case SIGPIPE:
            previousSignalHandler = previousPIPESignalHandler;
            break;
        case SIGSEGV:
            previousSignalHandler = previousSEGVSignalHandler;
            break;
        case SIGSYS:
            previousSignalHandler = previousSYSSignalHandler;
            break;
        case SIGTRAP:
            previousSignalHandler = previousTRAPSignalHandler;
            break;
        default:
            break;
    }
    
    if (previousSignalHandler) {
        previousSignalHandler(signal, info, context);
    }
}

#pragma mark Clear

static void NWClearSignalRegister() {
    signal(SIGSEGV,SIG_DFL);
    signal(SIGFPE,SIG_DFL);
    signal(SIGBUS,SIG_DFL);
    signal(SIGTRAP,SIG_DFL);
    signal(SIGABRT,SIG_DFL);
    signal(SIGILL,SIG_DFL);
    signal(SIGPIPE,SIG_DFL);
    signal(SIGSYS,SIG_DFL);
}

@end
常用的Unix信號(hào)

1.SIGABRT是調(diào)用abort()生成的信號(hào),有可能是NSException也有可能是Mach異常
2.SIGBUS:非法地址, 包括內(nèi)存地址對齊(alignment)出錯(cuò)或者沒有遵守權(quán)限分配內(nèi)存(向一個(gè)只讀權(quán)限的內(nèi)存寫數(shù)據(jù))。比如訪問一個(gè)四個(gè)字長的整數(shù), 但其地址不是4的倍數(shù)。比如:

char *s = "hello world";
*s = 'H';

3.SIGSEGV:試圖訪問未分配給自己的內(nèi)存, 或試圖往沒有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù)。比如:給已經(jīng)release的對象發(fā)送消息
4.SIGILL:執(zhí)行了非法指令. 通常是因?yàn)榭蓤?zhí)行文件本身出現(xiàn)錯(cuò)誤, 或者試圖執(zhí)行數(shù)據(jù)段. 堆棧溢出時(shí)也有可能產(chǎn)生這個(gè)信號(hào)。
5.SIGPIPE:管道破裂。這個(gè)信號(hào)通常在進(jìn)程間通信產(chǎn)生,比如采用FIFO(管道)通信的兩個(gè)進(jìn)程,讀管道沒打開或者意外終止就往管道寫,寫進(jìn)程會(huì)收到SIGPIPE信號(hào)。此外用Socket通信的兩個(gè)進(jìn)程,寫進(jìn)程在寫Socket的時(shí)候,讀進(jìn)程已經(jīng)終止。
6.SIGSEGV:試圖訪問未分配給自己的內(nèi)存, 或試圖往沒有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù).
7.SIGSYS:非法的系統(tǒng)調(diào)用。
8.SIGTRAP:由斷點(diǎn)指令或其它trap指令產(chǎn)生. 由debugger使用。


參考鏈接
Handling unhandled exceptions and signals
Mach異常
漫談iOS Crash收集框架
iOS異常捕獲

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

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

  • 來源:程序媛念茜的博客 Crash日志收集 為了能夠第一時(shí)間發(fā)現(xiàn)程序問題,應(yīng)用程序需要實(shí)現(xiàn)自己的崩潰日志收集服務(wù),...
    幸福的魚閱讀 1,260評(píng)論 0 2
  • 以下為文章正文,如果覺得有用,歡迎給她打賞。 為了能夠第一時(shí)間發(fā)現(xiàn)程序問題,應(yīng)用程序需要實(shí)現(xiàn)自己的崩潰日志收集服務(wù)...
    赤色追風(fēng)閱讀 2,614評(píng)論 1 11
  • 轉(zhuǎn)載(漫談 iOS Crash 收集框架) 前言 很早以前就和念茜認(rèn)識(shí),念茜不但技術(shù)功底扎實(shí),而且長得很漂亮,說她...
    狂風(fēng)無跡閱讀 3,584評(píng)論 1 11
  • [這是第14篇] 序: iOS Crash問題是iOS開發(fā)中難以忽視的存在,本文就捕獲iOS Crash、Cras...
    南華coder閱讀 10,108評(píng)論 21 116
  • 比較好的轉(zhuǎn)載:http://www.cocoachina.com/ios/20151218/14748.html轉(zhuǎn)...
    liudhkk閱讀 988評(píng)論 0 2

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