iOS Crash 的監(jiān)聽

沒想到都2021年迷扇,我還得寫篇文章來講講 Crash 監(jiān)聽的一些事情。雖然蠻多文章講 Crash 監(jiān)聽這塊,但總是講的不夠深入或者說不夠全面骗村。于是我想分享一下最近我對這方面知識的一些理解和整理。我計劃講以下幾個主題:

Crash 的類型

根據(jù)Crash 的不同來源呀枢,Crash 分為以下三類:

  • Mach 異常

    最底層的內(nèi)核級異常胚股。用戶態(tài)的開發(fā)者可以直接通過Mach API設(shè)置thread,task裙秋,host的異常端口琅拌,來捕獲Mach異常。

  • Unix 信號

    又稱BSD 信號摘刑,如果開發(fā)者沒有捕獲Mach異常进宝,則會被host層的方法ux_exception()將異常轉(zhuǎn)換為對應(yīng)的UNIX信號,并通過方法threadsignal()將信號投遞到出錯線程枷恕〉辰可以通過方法signal(x, SignalHandler)來捕獲signal。

  • NSException

    應(yīng)用級異常徐块,它是未被捕獲的Objective-C異常未玻,導(dǎo)致程序向自身發(fā)送了SIGABRT信號而崩潰,是app自己可控的蛹锰,對于未捕獲的Objective-C異常深胳,是可以通過try catch來捕獲的,或者通過NSSetUncaughtExceptionHandler()機(jī)制來捕獲铜犬。

Mach 異常

Mach異常是內(nèi)核級異常舞终,在系統(tǒng)的位置如下圖所示:

image
image

Mach相關(guān)知識

Mach內(nèi)核作為系統(tǒng)一個底層的基礎(chǔ)轻庆,僅與驅(qū)動操作系統(tǒng)所需的最低需要有關(guān)。 其他所有內(nèi)容都由操作系統(tǒng)的更高層來實(shí)現(xiàn)敛劝,然后再利用Mach并以其認(rèn)為合適的任何方式對其進(jìn)行操作余爆。

Mach提供了一小部分內(nèi)核抽象,這些內(nèi)核抽象被設(shè)計為既簡單又強(qiáng)大夸盟。與Mach異常相關(guān)的內(nèi)核抽象有:

  • tasks

資源所有權(quán)單位蛾方; 每個任務(wù)由一個虛擬地址空間、一個端口權(quán)限名稱空間和一個或多個線程組成上陕。 (類似于進(jìn)程)

  • threads

任務(wù)中CPU執(zhí)行的單位桩砰。

  • ports

安全的單工通信通道,只能通過發(fā)送和接收功能(稱為端口權(quán)限)進(jìn)行訪問释簿。

這些內(nèi)核對象亚隅,對于Mach來說都是一個個的Object,這些Objects基于Mach實(shí)現(xiàn)自己的功能庶溶,并通過Mach Message來進(jìn)行通信煮纵,Mach提供了相關(guān)的應(yīng)用層的API來操作。與Mach異常相關(guān)的幾個API有:

  • task_get_exception_ports:獲取task的異常端口
  • task_set_exception_ports:設(shè)置task的異常端口
  • mach_port_allocate:創(chuàng)建調(diào)用者指定的端口權(quán)限類型
  • mach_port_insert_right:將指定的端口插入目標(biāo)task

如何捕捉 Mach 異常

image

參考上圖偏螺,主要的流程是:新建一個監(jiān)控線程行疏,在監(jiān)控線程中監(jiān)聽 Mach 異常并處理異常信息。主要的步奏如下圖:

image

具體代碼如下:

static mach_port_t server_port;
static void *exc_handler(void *ignored);

//判斷是否 Xcode 聯(lián)調(diào)
bool ksdebug_isBeingTraced(void)
{
    struct kinfo_proc procInfo;
    size_t structSize = sizeof(procInfo);
    int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};

    if(sysctl(mib, sizeof(mib)/sizeof(*mib), &procInfo, &structSize, NULL, 0) != 0)
    {
        return false;
    }

    return (procInfo.kp_proc.p_flag & P_TRACED) != 0;
}

#define EXC_UNIX_BAD_SYSCALL 0x10000 /* SIGSYS */
#define EXC_UNIX_BAD_PIPE    0x10001 /* SIGPIPE */
#define EXC_UNIX_ABORT       0x10002 /* SIGABRT */
static int signalForMachException(exception_type_t exception, mach_exception_code_t code)
{
    switch(exception)
    {
        case EXC_ARITHMETIC:
            return SIGFPE;
        case EXC_BAD_ACCESS:
            return code == KERN_INVALID_ADDRESS ? SIGSEGV : SIGBUS;
        case EXC_BAD_INSTRUCTION:
            return SIGILL;
        case EXC_BREAKPOINT:
            return SIGTRAP;
        case EXC_EMULATION:
            return SIGEMT;
        case EXC_SOFTWARE:
        {
            switch (code)
            {
                case EXC_UNIX_BAD_SYSCALL:
                    return SIGSYS;
                case EXC_UNIX_BAD_PIPE:
                    return SIGPIPE;
                case EXC_UNIX_ABORT:
                    return SIGABRT;
                case EXC_SOFT_SIGNAL:
                    return SIGKILL;
            }
            break;
        }
    }
    return 0;
}

static NSString *stringForMachException(exception_type_t exception) {
    switch(exception)
    {
        case EXC_ARITHMETIC:
            return @"EXC_ARITHMETIC";
        case EXC_BAD_ACCESS:
            return @"EXC_BAD_ACCESS";
        case EXC_BAD_INSTRUCTION:
            return @"EXC_BAD_INSTRUCTION";
        case EXC_BREAKPOINT:
            return @"EXC_BREAKPOINT";
        case EXC_EMULATION:
            return @"EXC_EMULATION";
        case EXC_SOFTWARE:
        {
            return @"EXC_SOFTWARE";
            break;
        }
    }
    return 0;
}

void installExceptionHandler() {
    if (ksdebug_isBeingTraced()) {
        // 當(dāng)前正在調(diào)試狀態(tài), 不啟動 mach 監(jiān)聽
        return ;
    }
    kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
    assert(kr == KERN_SUCCESS);

    kern_return_t rc = 0;
    exception_mask_t excMask = EXC_MASK_BAD_ACCESS |
    EXC_MASK_BAD_INSTRUCTION |
    EXC_MASK_ARITHMETIC |
    EXC_MASK_SOFTWARE |
    EXC_MASK_BREAKPOINT;

    rc = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
    if (rc != KERN_SUCCESS) {
        fprintf(stderr, "------->Fail to allocate exception port\\\\\\\\n");
        return;
    }

    rc = mach_port_insert_right(mach_task_self(), server_port, server_port, MACH_MSG_TYPE_MAKE_SEND);
    if (rc != KERN_SUCCESS) {
        fprintf(stderr, "-------->Fail to insert right");
        return;
    }

    rc = thread_set_exception_ports(mach_thread_self(), excMask, server_port, EXCEPTION_DEFAULT, MACHINE_THREAD_STATE);
    if (rc != KERN_SUCCESS) {
        fprintf(stderr, "-------->Fail to  set exception\\\\\\\\n");
        return;
    }

    //建立監(jiān)聽線程
    pthread_t thread;
    pthread_create(&thread, NULL, exc_handler, NULL);
}

static void *exc_handler(void *ignored) {
    // Exception handler – runs a message loop. Refactored into a standalone function
    // so as to allow easy insertion into a thread (can be in same program or different)
    mach_msg_return_t rc;
    fprintf(stderr, "Exc handler listening\\\\\\\\n");
    // The exception message, straight from mach/exc.defs (following MIG processing) // copied here for ease of reference.
    typedef struct {
        mach_msg_header_t Head;
        /* start of the kernel processed data */
        mach_msg_body_t msgh_body;
        mach_msg_port_descriptor_t thread;
        mach_msg_port_descriptor_t task;
        /* end of the kernel processed data */
        NDR_record_t NDR;
        exception_type_t exception;
        mach_msg_type_number_t codeCnt;
        integer_t code[2];
        int flavor;
        mach_msg_type_number_t old_stateCnt;
        natural_t old_state[144];
    } Request;

    Request exc;

    struct rep_msg {
        mach_msg_header_t Head;
        NDR_record_t NDR;
        kern_return_t RetCode;
    } rep_msg;

    for(;;) {
        // Message Loop: Block indefinitely until we get a message, which has to be
        // 這里會阻塞套像,直到接收到exception message酿联,或者線程被中斷。
        // an exception message (nothing else arrives on an exception port)
        rc = mach_msg( &exc.Head,
                      MACH_RCV_MSG|MACH_RCV_LARGE,
                      0,
                      sizeof(Request),
                      server_port, // Remember this was global – that's why.
                      MACH_MSG_TIMEOUT_NONE,
                      MACH_PORT_NULL);

        if(rc != MACH_MSG_SUCCESS) {
            /*... */
            break ;
        };

        //Mach Exception 類型
        NSMutableString *crashInfo = [NSMutableString stringWithFormat:@"mach exception:%@ %@\n\n",stringForMachException(exc.exception), stringForSignal(signalForMachException(exc.exception, exc.code[0]))];

        rep_msg.Head = exc.Head;
        rep_msg.NDR = exc.NDR;
        rep_msg.RetCode = KERN_FAILURE;

        kern_return_t result;
        if (rc == MACH_MSG_SUCCESS) {
            result = mach_msg(&rep_msg.Head,
                              MACH_SEND_MSG,
                              sizeof (rep_msg),
                              0,
                              MACH_PORT_NULL,
                              MACH_MSG_TIMEOUT_NONE,
                              MACH_PORT_NULL);
        }
        //移除其他 Crash 監(jiān)聽, 防止死鎖
        NSSetUncaughtExceptionHandler(NULL);
        signal(SIGHUP, SIG_DFL);
        signal(SIGINT, SIG_DFL);
        signal(SIGQUIT, SIG_DFL);
        signal(SIGABRT, SIG_DFL);
        signal(SIGILL, SIG_DFL);
        signal(SIGSEGV, SIG_DFL);
        signal(SIGFPE, SIG_DFL);
        signal(SIGBUS, SIG_DFL);
        signal(SIGPIPE, SIG_DFL);
    }

    return  NULL;
}

監(jiān)聽 Mach 異常需要注意:

  • 避免在 Xcode 聯(lián)調(diào)時監(jiān)聽

    原因是我們監(jiān)聽了EXC_BREAKPOINT這類型的Exception凉夯,一旦啟動 app 聯(lián)調(diào)后货葬, 會立即觸發(fā)EXC_BREAKPOINT。而這段代碼處理完后劲够,會進(jìn)入下一個循環(huán)等待震桶,可主線程這是還等著消息處理結(jié)果,這就造成等待死鎖征绎。

關(guān)于代碼中其他分析異常原因的代碼蹲姐,我會在下一篇講獲取堆棧的文章中詳細(xì)解讀。

Unix 信號(Signal)

Mach已經(jīng)通過異常機(jī)制提供了底層的異常處理人柿,但為了兼容更為流行的POSIX標(biāo)準(zhǔn)柴墩,BSD在Mach異常機(jī)制之上構(gòu)建的UNIX信號處理機(jī)制。異常信號首先被轉(zhuǎn)換為Mach異常凫岖,如果沒有被外界捕捉江咳,則會被默認(rèn)的異常處理ux_exception()轉(zhuǎn)換為UNIX信號。

image

我們可以把信號看做是對硬件異常跟軟件異常的封裝哥放。

Unix 信號列表

Unix Signal 其實(shí)是由 Mach port 拋出的信號轉(zhuǎn)化的歼指,那么都有哪些信號呢爹土?

  • SIGHUP
    本信號在用戶終端連接(正常或非正常)結(jié)束時發(fā)出, 通常是在終端的控制進(jìn)程結(jié)束時, 通知同一session內(nèi)的各個作業(yè), 這時它們與控制終端不再關(guān)聯(lián)踩身。

  • SIGINT
    程序終止(interrupt)信號, 在用戶鍵入INTR字符(通常是Ctrl-C)時發(fā)出胀茵,用于通知前臺進(jìn)程組終止進(jìn)程。

  • SIGQUIT
    和SIGINT類似, 但由QUIT字符(通常是Ctrl-)來控制. 進(jìn)程在因收到SIGQUIT退出時會產(chǎn)生core文件, 在這個意義上類似于一個程序錯誤信號挟阻。

  • SIGABRT
    調(diào)用abort函數(shù)生成的信號琼娘。
    SIGABRT is a BSD signal sent by an application to itself when an NSException or obj_exception_throw is not caught.

  • SIGBUS
    非法地址, 包括內(nèi)存地址對齊(alignment)出錯。比如訪問一個四個字長的整數(shù), 但其地址不是4的倍數(shù)附鸽。它與SIGSEGV的區(qū)別在于后者是由于對合法存儲地址的非法訪問觸發(fā)的(如訪問不屬于自己存儲空間或只讀存儲空間)脱拼。

  • SIGFPE
    在發(fā)生致命的算術(shù)運(yùn)算錯誤時發(fā)出. 不僅包括浮點(diǎn)運(yùn)算錯誤, 還包括溢出及除數(shù)為0等其它所有的算術(shù)的錯誤。

  • SIGKILL
    用來立即結(jié)束程序的運(yùn)行. 本信號不能被阻塞拒炎、處理和忽略挪拟。如果管理員發(fā)現(xiàn)某個進(jìn)程終止不了,可嘗試發(fā)送這個信號击你。

  • SIGSEGV
    試圖訪問未分配給自己的內(nèi)存, 或試圖往沒有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù).

  • SIGPIPE
    管道破裂。這個信號通常在進(jìn)程間通信產(chǎn)生谎柄,比如采用FIFO(管道)通信的兩個進(jìn)程丁侄,讀管道沒打開或者意外終止就往管道寫,寫進(jìn)程會收到SIGPIPE信號朝巫。

  • SIGSYS

    非法的系統(tǒng)調(diào)用鸿摇。

  • SIGTRAP

    由斷點(diǎn)指令或其它 trap 指令產(chǎn)生. 由d ebugger 使用。

  • SIGILL

    執(zhí)行了非法指令. 通常是因?yàn)榭蓤?zhí)行文件本身出現(xiàn)錯誤, 或者試圖執(zhí)行數(shù)據(jù)段. 堆棧溢出時也有可能產(chǎn)生這個信號劈猿。

其他未列出的信號可以參照這篇文章:linux 各個SIG信號含義

如何捕捉 Unix 信號

一般來說我們需要捕捉以下信號:

static const int g_fatalSignals[] =
{
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGPIPE,
    SIGSEGV,
    SIGSYS,
    SIGTRAP,
};

而要捕捉 Unix 信號拙吉,比 Mach 異常容易多了

void installSignalHandler() {
        signal(SIGABRT, handleSignalException);
    //...等等其他需要監(jiān)聽的 Signal
}
void handleSignalException(int signal) {
    //打印堆棧
    NSMutableString * crashInfo = [[NSMutableString alloc]init];
    [crashInfo appendString:[NSString stringWithFormat:@"signal:%d\n",signal]];
    [crashInfo appendString:@"Stack:\n"];
    void* callstack[128];
    int i, frames = backtrace(callstack, 128);
    char** strs = backtrace_symbols(callstack, frames);
    for (i = 0; i <frames; ++i) {
        [crashInfo appendFormat:@"%s\n", strs[I]];
    }
    NSLog(@"%@", crashInfo);
    //移除其他 Crash 監(jiān)聽, 防止死鎖
    NSSetUncaughtExceptionHandler(NULL);
    signal(SIGHUP, SIG_DFL);
    signal(SIGINT, SIG_DFL);
    signal(SIGQUIT, SIG_DFL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGILL, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
}

備用信號棧

上面這個方法可以監(jiān)控到大部分的 Signal 異常,但是我們會發(fā)現(xiàn)如果遇到死循環(huán)這類的Crash揪荣,就沒法監(jiān)控了筷黔。原因是一般情況下,信號處理函數(shù)被調(diào)用時仗颈,內(nèi)核會在進(jìn)程的棧上為其創(chuàng)建一個棧幀佛舱。但這里就會有一個問題,如果之前棧的增長達(dá)到了棧的最大長度挨决,或是棧沒有達(dá)到最大長度但也比較接近请祖,那么就會導(dǎo)致信號處理函數(shù)不能得到足夠棧幀分配。

為了解決這個問題脖祈,我們需要設(shè)定一個可選的棧幀

  1. 申請一塊內(nèi)存空間作為可選的信號處理函數(shù)棧使用
  2. 使用 sigaltstack 函數(shù)通知系統(tǒng)可選的信號處理棧幀的存在及其位置
  3. 當(dāng)使用 sigaction 函數(shù)建立一個信號處理函數(shù)時肆捕,通過指定 SA_ONSTACK 標(biāo)志通知系統(tǒng)這個信號處理函數(shù)應(yīng)該在可選的棧幀上面執(zhí)行注冊的信號處理函數(shù)

前面監(jiān)聽 Unix 信號的代碼,改動一下:

void installSignalHandler() {
        stack_t ss;
    struct sigaction sa;
    struct timespec req, rem;
    long ret;

    ss.ss_flags = 0;
    ss.ss_size = SIGSTKSZ;
    ss.ss_sp = malloc(ss.ss_size);
    sigaltstack(&ss, NULL);

    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handleSignalException;
    sa.sa_flags = SA_ONSTACK;
    sigaction(SIGABRT, &sa, NULL);
}

調(diào)試 Signal 信號

有可能為了調(diào)試盖高,你會在處理堆棧的地方打上斷點(diǎn)慎陵,但是 Crash 發(fā)生后卻沒有命中掏秩。這是因?yàn)樵赬code調(diào)試時,Debugger模式會先于我們的代碼catch到所有的crash荆姆,所以需要直接從模擬器中進(jìn)入程序才可以蒙幻。

如果想要在Xcode中調(diào)試,網(wǎng)上我找個方法胆筒。在lldb中輸入以下命令:pro hand -p true -s false SIGABRT邮破。注意:SIGABRT可以替換為你需要的任何signal類型,比如SIGSEGV仆救。

image

但是...我自己試了并不成功抒和,我仍然寫出來供大家坐坐參考,說不定你們就可以了彤蔽。

除此之外還有個辦法就是使用控制臺這個 app摧莽,可以在應(yīng)用程序(Application)里面找到這個 app,打開后你可以看到各種 app 的輸出顿痪。然后通過關(guān)鍵字找到你想要日志:

image

NSException

NSException 是應(yīng)用級異常镊辕,是指 OC 代碼運(yùn)行過程由Objective-C 拋出的異常,基本上是代碼運(yùn)行過程中的邏輯錯誤蚁袭。比如往 NSArray 中插入 nil 對象征懈,或者用nil 初始化 NSURL 等。最簡單區(qū)分一個異常是否 NSException 的方式是看這個異常能否被@trycatch 給捕獲揩悄。

常見的 NSException 場景

  • 非主線程刷新UI

  • NSInvalidArgumentException
    非法參數(shù)異常(NSInvalidArgumentException)是 Objective – C 代碼最常出現(xiàn)的錯誤卖哎,所以平時在寫代碼的時候,需要多加注意删性,加強(qiáng)對參數(shù)的檢查亏娜,避免傳入非法參數(shù)導(dǎo)致異常,其中尤以nil參數(shù)為甚蹬挺。

  • NSRangeException
    越界異常(NSRangeException)也是比較常出現(xiàn)的異常维贺。

  • NSGenericException
    NSGenericException這個異常最容易出現(xiàn)在foreach操作中,在for in循環(huán)中如果修改所遍歷的數(shù)組汗侵,無論你是add或remove幸缕,都會出錯 “for in”,它的內(nèi)部遍歷使用了類似 Iterator進(jìn)行迭代遍歷,一旦元素變動晰韵,之前的元素全部被失效发乔,所以在foreach的循環(huán)當(dāng)中,最好不要去進(jìn)行元素的修改動作雪猪,若需要修改栏尚,循環(huán)改為for遍歷,由于內(nèi)部機(jī)制不同只恨,不會產(chǎn)生修改后結(jié)果失效的問題译仗。

  • NSInternalInconsistencyException
    不一致導(dǎo)致出現(xiàn)的異常
    比如NSDictionary當(dāng)做NSMutableDictionary來使用抬虽,從他們內(nèi)部的機(jī)理來說,就會產(chǎn)生一些錯誤
    NSMutableDictionary *info = method return to NSDictionary type;
    [info setObject:@“sxm” forKey:@”name”];
    比如xib界面使用或者約束設(shè)置不當(dāng)

  • NSFileHandleOperationException
    處理文件時的一些異常纵菌,最常見的還是存儲空間不足的問題阐污,比如應(yīng)用頻繁的保存文檔,緩存資料或者處理比較大的數(shù)據(jù):
    所以在文件處理里咱圆,需要考慮到手機(jī)存儲空間的問題笛辟。

  • NSMallocException
    這也是內(nèi)存不足的問題,無法分配足夠的內(nèi)存空間
    此外還有

  • KVO Crash
    移除未注冊的觀察者
    重復(fù)移除觀察者
    添加了觀察者但是沒有實(shí)現(xiàn)-observeValueForKeyPath:ofObject:change:context:方法
    添加移除keypath=nil
    添加移除observer=nil

  • unrecognized selector send to instance

監(jiān)聽 NSException 異常

NSException的監(jiān)聽也十分簡單:

void InstallUncaughtExceptionHandler(void) {
    NSSetUncaughtExceptionHandler( &handleUncaughtException );
}

void handleUncaughtException(NSException *exception) {
    NSString * crashInfo = [NSString stringWithFormat:@"yyyy Exception name:%@\nException reason:%@\nException stack:%@",[exception name], [exception reason], [exception callStackSymbols]];
    NSLog(@"%@", crashInfo);
}

需要注意的是序苏,在監(jiān)聽處理的方法中手幢,是無法直接采集錯誤到堆棧的。詳情我同樣會在下一篇的崩潰堆棧收集的文章中介紹忱详。

C++ 異常

有朋友看到這里可能就會好奇围来,前面說了三種異常,為何這里又多出一種異常匈睁。實(shí)質(zhì)上C++異常也可以通過 Mach 異常的方式處理监透。只是在細(xì)節(jié)處理上仍多有區(qū)別。

以下部分內(nèi)容轉(zhuǎn)載自文章:iOS/OSX Crash:捕捉異常软舌,具體內(nèi)容需要讀者自行驗(yàn)證才漆。

為什么要捕捉 C++異常

在OSX中,會通過對話框展示異常給用戶佛点,但在iOS中,只是重新拋出異常黎比。系統(tǒng)在捕捉到C++異常后超营,如果能夠?qū)⒋薈++異常轉(zhuǎn)換為OC異常,則拋出OC異常處理機(jī)制阅虫;如果不能轉(zhuǎn)換演闭,則會立刻調(diào)用__cxa_throw重新拋出異常。

當(dāng)系統(tǒng)在RunLoop捕捉到的C++異常時颓帝,此時的調(diào)用堆棧是異常發(fā)生時的堆棧米碰,但當(dāng)系統(tǒng)在不能轉(zhuǎn)換為OC異常時調(diào)用__cxa_throw時,上層捕捉此再拋出的異常獲取到的調(diào)用堆棧是RunLoop異常處理函數(shù)的堆棧购城,導(dǎo)致原始異常調(diào)用堆棧丟失吕座。

Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0   libsystem_kernel.dylib          0x00007fff93ef8d46 __kill + 10
1   libsystem_c.dylib               0x00007fff89968df0 abort + 177
2   libc++abi.dylib                 0x00007fff8beb5a17 abort_message + 257
3   libc++abi.dylib                 0x00007fff8beb33c6 default_terminate() + 28
4   libobjc.A.dylib                 0x00007fff8a196887 _objc_terminate() + 111
5   libc++abi.dylib                 0x00007fff8beb33f5 safe_handler_caller(void (*)()) + 8
6   libc++abi.dylib                 0x00007fff8beb3450 std::terminate() + 16
7   libc++abi.dylib                 0x00007fff8beb45b7 __cxa_throw + 111
8   test                            0x0000000102999f3b main + 75
9   libdyld.dylib                   0x00007fff8e4ab7e1 start + 1

如何捕捉C++異常

為了獲得C++異常的調(diào)用堆棧,我們需要模擬拋出NSException的過程并在此過程中保存調(diào)用堆棧瘪板。

  1. 設(shè)置異常處理函數(shù)

    g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);
    
    

    調(diào)用std::set_terminate設(shè)置新的全局終止處理函數(shù)并保存舊的函數(shù)吴趴。

  2. 重寫__cxa_throw

    void __cxa_throw(void* thrown_exception, std::type_info* tinfo, void (*dest)(void*))
    
    

    在異常發(fā)生時,會先進(jìn)入此重寫函數(shù)侮攀,應(yīng)該先獲取調(diào)用堆棧并存儲锣枝;再調(diào)用原始的__cxa_throw函數(shù)厢拭。

  3. 異常處理函數(shù)

    __cxa_throw往后執(zhí)行,進(jìn)入set_terminate設(shè)置的異常處理函數(shù)撇叁。判斷如果檢測是OC異常供鸠,則什么也不做,讓OC異常機(jī)制處理陨闹;否則獲取異常信息楞捂。

不同類型的異常之間的關(guān)系

在前面,我們講了幾種異常類型及其細(xì)節(jié)正林。但是他們之間的關(guān)系我們還了解甚少泡一。下面我就來講講不同異常之間可能存在的轉(zhuǎn)換關(guān)系,以及優(yōu)先順序等觅廓。

異常處理的順序

首先鼻忠,我們看一下下圖,這個圖很重要

image

我大致總結(jié)一下:

  1. 如果是 NSException 類型的異常

    先看 app 是否 trycatch 了杈绸;再看有沒有實(shí)現(xiàn) NSSetUncaughtExceptionHandler帖蔓;最后如果都沒處理,則調(diào)用 c 的 abort()瞳脓,kernal 針對 app 發(fā)出 _pthread_kill 的信號塑娇,轉(zhuǎn)為 Mach 異常。

  2. 如果是 Mach 異常

    如果 app 處理了 Mach 異常則進(jìn)入處理流程劫侧;否則 mach 異常會被轉(zhuǎn)為 Unix/BSD signal 信號埋酬,并進(jìn)入 Signal 的處理流程。

簡單的說就是:NSException->Mach->Signal

不同類型異常的關(guān)系和處理決策

首先要明確的一點(diǎn)是烧栋,Mach異常和UNIX信號都可以被捕獲写妥,他們也幾乎一一對應(yīng)。那為什么幾乎所有 Crash 監(jiān)控框架都會捕捉 Mach 異常审姓、Unix 信號以及 NSException 呢珍特?

Mach 異常 和 Unix 信號

所有Mach異常未處理,它將在host層被ux_exception轉(zhuǎn)換為相應(yīng)的Unix信號魔吐,并通過threadsignal將信號投遞到出錯的線程扎筒。所以其實(shí)所有我們看到的 Unix信號異常,都是從 Mach 傳過來的酬姆,只是在 Mach 沒有catch嗜桌,所以轉(zhuǎn)成Unix給我們處理。比如 Bad Access轴踱。

  1. 既然 Unix 信號都是由 Mach Exception 轉(zhuǎn)化的症脂,為啥還要轉(zhuǎn)Unix 信號呢,直接傳 Mach 的異常不就行了?
    這是為了兼容更為流行的POSIX標(biāo)準(zhǔn)诱篷,BSD在Mach異常機(jī)制之上構(gòu)建的UNIX信號處理機(jī)制壶唤。
  2. 既然 Mach Exception 能轉(zhuǎn)化為 Signal 信號,Signal 信號監(jiān)聽也更簡單棕所,為什么不只監(jiān)聽 Signal 信號闸盔?
    不是所有的 "Mach異常” 類型都映射到了 “UNIX信號”琳省。 如 EXC_GUARD 迎吵。在蘋果開源的 xnu 源碼中可以看到這點(diǎn)。
  3. 為什么優(yōu)先監(jiān)聽 Mach Exception针贬?
    這是因?yàn)?Mach 異常會更早的拋出來击费,而且如果Mach異常的handler讓程序exit了,那么Unix信號就永遠(yuǎn)不會到達(dá)這個進(jìn)程了桦他。

為什么不能只監(jiān)聽 Mach Exception蔫巩?

網(wǎng)上所說的原因都是因?yàn)?EXC_CRASH 不能通過 Mach 監(jiān)控來抓捕。那為什么不能呢快压?網(wǎng)上我所有能找到的中文資料圆仔,都是如此解釋(來源開源項目 plcrashreporter):

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.

大致意思是說我們是在進(jìn)程中監(jiān)聽 Mach Exception,在 EXC_Crash 發(fā)生的時候蔫劣,會發(fā)生進(jìn)程死鎖坪郭。但是我一直沒明白為啥會死鎖。于是我又搜索了一下外文資料脉幢。發(fā)現(xiàn)有一篇文章大量的講述了EXC_Crash.

If you’re messing with EXC_CRASH, you probably know that a major drawback of this scheme is that it can only respond to crashes that originated as genuine hardware traps. abort() and all of the things that wind up calling abort() are not, they’re generated entirely in software. This is important for a crash reporter because lots of interesting crashes arise through this mechanism, such as assertion failures and runtime (C++ and Objective-C) exceptions. abort() is implemented in Libc-825.26/stdlib/FreeBSD/abort.c abort, and it raises SIGABRT all on its own, without ever triggering a hardware trap. That means that your program can catch these crashes in-process via the POSIX signal interface, but because it was never a Mach exception to begin with, there’s no opportunity to catch one.

This is where EXC_CRASH comes in. EXC_CRASH is a new (as of Mac OS X 10.5) exception type that’s only generated in one place: when a process is dying an abnormal death. In xnu-2050.24.15/bsd/kern/kern_exit.c proc_prepareexit, the logic says that if the process is exiting due to a signal that’s considered a crash (one that might generate a “ core” file, identified by the presence of SA_CORE in xnu-2050.24.15/bsd/sys/signalvar.h sigprop), an EXC_CRASH Mach exception will be raised for the task. Along with several other signals, the SIGILL, SIGSEGV, SIGBUS, and SIGABRT examples above are all core-generating, so they qualify for this treatment. By the time a process is exiting due to an unhandled signal, it’s a goner. It’s not going to be scheduled any more. That includes any Mach exception handler that was running on a thread in the process. This is why you can’t catch EXC_CRASHexceptions in the process itself: by the time an EXC_CRASH is generated, your process is no longer running. Indeed, in the bug report, you can see the abort() as an “upstream” caller of in-kernel process teardown code, passing through proc_prepareexit, exception_triage, and ultimately getting blocked waiting for a response to mach_exception_raise that will never come.

我提煉一下上文的要點(diǎn):

  1. EXC_Crash 表示進(jìn)程是非正常退出歪沃。
  2. 當(dāng) EXC_Crash 發(fā)生的時候,這意味著進(jìn)程即將被強(qiáng)殺嫌松,任何其他任務(wù)都不會被執(zhí)行绸罗,所以Mach Exception Handler 不會執(zhí)行。
  3. 類似 abort()的方法只會觸發(fā) signal 信號豆瘫,根本不會觸發(fā)hardware trap。

所以菊值,我認(rèn)為我們需要監(jiān)聽 Signal 的原因是 EXC_Crash 根本不能通過 Mach 監(jiān)控來捕捉外驱,和死鎖無關(guān)。即便是和死鎖有關(guān)腻窒,類似 abort()的場景也必須使用 Signal 監(jiān)聽昵宇。

Mach Exception 和 Signal 的轉(zhuǎn)換關(guān)系

image

上圖是網(wǎng)絡(luò)上找來的一個對應(yīng)關(guān)系,但我覺得這個對應(yīng)關(guān)系只適合在 Mach Exception 處理的時候使用儿子。在 Signal 處理的時候瓦哎,建議使用如下的對應(yīng)關(guān)系:

signal exception type
SIGFPE EXC_ARITHMETIC
SIGSEGV EXC_BAD_ACCESS
SIGBUS EXC_BAD_ACCESS
SIGILL EXC_BAD_INSTRUCTION
SIGTRAP EXC_BREAKPOINT
SIGEMT EXC_EMULATION
SIGSYS EXC_UNIX_BAD_SYSCALL
SIGPIPE EXC_UNIX_BAD_PIPE
SIGABRT EXC_CRASH
SIGKILL EXC_SOFT_SIGNAL

關(guān)于 Mach 和 Signal 的說明,還可以參考一下官方文檔

為何要實(shí)現(xiàn) NSException 監(jiān)聽

按照我們前面所說,通過 Mach/Signal 的方式我們已經(jīng)可以監(jiān)聽絕大部分崩潰場景了蒋譬,那為何我們還要實(shí)現(xiàn)NSException 監(jiān)聽呢割岛?原因就是未被try catchNSException會發(fā)出killpthread_kill信號-> Mach異常-> Unix信號(SIGABRT),但是SIGABRT在處理收集信息時,獲取當(dāng)前堆棧時獲取不到犯助,所以采用`NSSetUncaughtExceptionHandler癣漆。具體如何獲取堆棧我們會在下一篇文章講解

捕捉 Swift 崩潰

一開始我以為 Swift 下的 exception 的處理過程和 NSException 類似,但實(shí)踐后發(fā)現(xiàn)根本不是剂买。

  • swift通常都是通過對應(yīng)的signal來捕獲crash惠爽。對于swift的崩潰捕獲,Apple的文檔中有描述說需要通過SIGTRAP信號捕獲強(qiáng)轉(zhuǎn)失敗瞬哼,及非可選的nil值導(dǎo)致的崩潰.具體描述如下:

    Trace Trap[EXC_BREAKPOINT // SIGTRAP]
    類似于異常退出婚肆,此異常旨在使附加的調(diào)試器有機(jī)會在其執(zhí)行中的特定點(diǎn)中斷進(jìn)程。您可以使用該__builtin_trap()函數(shù)從您自己的代碼觸發(fā)此異常坐慰。如果沒有附加調(diào)試器较性,則該過程將終止并生成崩潰報告。
    較低級的庫(例如讨越,libdispatch)會在遇到致命錯誤時捕獲進(jìn)程两残。有關(guān)錯誤的其他信息可以在崩潰報告的“ 附加診斷信息”部分或設(shè)備的控制臺中找到。

    如果在運(yùn)行時遇到意外情況把跨,Swift代碼將以此異常類型終止人弓,例如:
    1.具有nil值的非可選類型
    2.一個失敗的強(qiáng)制類型轉(zhuǎn)換

  • 對于swift還有一種崩潰需要捕獲(Intel處理器,我認(rèn)為應(yīng)該是指在模擬器上的崩潰),為保險起見着逐,也需要將信號SIGILL進(jìn)行注冊崔赌,Apple同樣對其中做了描述

    Illegal Instruction[EXC_BAD_INSTRUCTION // SIGILL]
    該過程嘗試執(zhí)行非法或未定義的指令。該過程可能嘗試通過錯誤配置的函數(shù)指針跳轉(zhuǎn)到無效地址耸别。
    在Intel處理器上健芭,ud2操作碼引起EXC_BAD_INSTRUCTION異常,但通常用于進(jìn)程調(diào)試目的秀姐。如果在運(yùn)行時遇到意外情況慈迈,Intel處理器上的Swift代碼將以此異常類型終止。有關(guān)詳細(xì)信息省有,請參閱Trace Trap痒留。

解除監(jiān)聽

細(xì)心的朋友可能會注意到前面監(jiān)控 Crash 的代碼都包括了類似一下的代碼:

NSSetUncaughtExceptionHandler(NULL);
signal(SIGHUP, SIG_DFL);

這是因?yàn)?1.保證一個 Crash 只會被一個 Handler 處理,避免多次處理蠢沿;2.防止可能出現(xiàn)死鎖導(dǎo)致應(yīng)用不能退出伸头。

多監(jiān)控框架的共存

盡管從技術(shù)上講多 Crash 監(jiān)控框架共存是可能的。但是我并不喜歡這么做舷蟀。當(dāng)發(fā)生 Crash 的時候恤磷,app 已經(jīng)處于一個不穩(wěn)定的狀態(tài)面哼,過長的 Crash 處理鏈條會導(dǎo)致崩潰堆棧不準(zhǔn)確,并且在處理過程中引入新的 Crash扫步。如果你非要在崩潰的時候處理些額外的工作魔策,大部分 Crash 監(jiān)控框架,如 KSCrash 等锌妻,都提供了事件回調(diào)供你使用代乃。

如果你們對這一塊還是很感興趣,可以參考下這篇文章:iOS/OSX Crash:捕捉異常

總結(jié)

雖然本篇總結(jié)的是 iOS 下的 Crash 監(jiān)聽仿粹,但由于 iOS/Mac OS 都是基于 Unix 的搁吓,所以其實(shí)很多內(nèi)容是跨平臺,在寫本文的過程中我也找尋了很多 C 語言下的解決方案吭历。

我盡量將我在學(xué)習(xí)這個知識所遇到的所有困惑以及收獲都分享在這篇文章堕仔。但是依然可能有些遺漏和錯誤,歡迎各位指正晌区。而撰寫此文的過程中摩骨,有大量的內(nèi)容整理自其他文章,其目的是盡可能在一篇文章完整的講述相關(guān)內(nèi)容朗若。

最后恼五,如果此文對你有幫助,不求贊賞哭懈,只求大家輕輕一個點(diǎn)贊灾馒。

iOS開發(fā)

作者:felix9
鏈接:http://www.reibang.com/p/3f6775c02257
來源:簡書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)遣总,非商業(yè)轉(zhuǎn)載請注明出處睬罗。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市旭斥,隨后出現(xiàn)的幾起案子容达,更是在濱河造成了極大的恐慌,老刑警劉巖垂券,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件花盐,死亡現(xiàn)場離奇詭異,居然都是意外死亡菇爪,警方通過查閱死者的電腦和手機(jī)卒暂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來娄帖,“玉大人,你說我怎么就攤上這事昙楚〗伲” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長削葱。 經(jīng)常有香客問我奖亚,道長,這世上最難降的妖魔是什么析砸? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任昔字,我火速辦了婚禮,結(jié)果婚禮上首繁,老公的妹妹穿的比我還像新娘作郭。我一直安慰自己,他們只是感情好弦疮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布夹攒。 她就那樣靜靜地躺著,像睡著了一般胁塞。 火紅的嫁衣襯著肌膚如雪咏尝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天啸罢,我揣著相機(jī)與錄音编检,去河邊找鬼。 笑死扰才,一個胖子當(dāng)著我的面吹牛允懂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播训桶,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼累驮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了舵揭?” 一聲冷哼從身側(cè)響起谤专,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎午绳,沒想到半個月后置侍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拦焚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年蜡坊,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赎败。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡秕衙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出僵刮,到底是詐尸還是另有隱情据忘,我是刑警寧澤鹦牛,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站勇吊,受9級特大地震影響曼追,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜汉规,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一礼殊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧针史,春花似錦晶伦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至射亏,卻和暖如春近忙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背智润。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工及舍, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人窟绷。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓锯玛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親兼蜈。 傳聞我的和親對象是個殘疾皇子攘残,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354