iOS異常淺析

異常簡(jiǎn)介

處理器和系統(tǒng)內(nèi)核中有設(shè)計(jì)標(biāo)識(shí)不同事件的狀態(tài)碼,這些狀態(tài)被編碼為不同的位和信號(hào)。每次處理器和內(nèi)核檢測(cè)到狀態(tài)的變化時(shí),便會(huì)觸發(fā)一個(gè)事件,該事件稱為異常。

系統(tǒng)中可能的每種類型的異常都分配了一個(gè)唯一的非負(fù)整數(shù)的異常號(hào)。這些異常號(hào)由處理器和操作系統(tǒng)的內(nèi)核設(shè)計(jì)者分配。

當(dāng)處理器檢測(cè)到事件時(shí),會(huì)通過(guò)一張被稱為異常表的跳轉(zhuǎn)表,進(jìn)行間接過(guò)程調(diào)用到一個(gè)專門設(shè)計(jì)用來(lái)處理這類事件的操作系統(tǒng)的子程序,稱為異常處理程序。

在系統(tǒng)啟動(dòng)時(shí)(計(jì)算機(jī)啟動(dòng)),操作系統(tǒng)會(huì)分配和初始化一張異常表,該表的起始地址存放在一個(gè)稱為異常表基寄存器exception table base register)的特殊CPU寄存器中。異常號(hào)是異常表中的索引。通過(guò)起始地址與異常號(hào),找到異常程序的調(diào)用地址,最終執(zhí)行異常程序。

異常的類別

異常一般分四類:中斷(interrupt),陷阱(trap),故障(fault),終止(abort)。

image.png

中斷:是來(lái)自處理器外部的I/O異常信號(hào)導(dǎo)致的,不是由任何一條專門的指令造成的,從這個(gè)層面上來(lái)講,它是異步的。硬件中斷的處理程序通常被稱為中斷處理程序。比如:拔插U盤。

陷阱:是有意的異常,是執(zhí)行一條指令的結(jié)果。和中斷一樣,陷阱處理程序?qū)⒖刂品祷氐较乱粭l指令。陷阱最終的用途,是在用戶程序和內(nèi)核之間提供一個(gè)像過(guò)程一樣的接口,稱為系統(tǒng)調(diào)用。比如用戶程序經(jīng)常需要向內(nèi)核請(qǐng)求服務(wù),比如讀一個(gè)文件(read)、創(chuàng)建一個(gè)進(jìn)程(fork)、加載一個(gè)新的程序(execve)或者終止當(dāng)前進(jìn)程(exit),這些操作都需要通過(guò)觸發(fā)陷阱異常,來(lái)執(zhí)行系統(tǒng)內(nèi)核的程序來(lái)實(shí)現(xiàn)。

故障:是由錯(cuò)誤情況引起的,它可能被故障處理程序修正。當(dāng)故障發(fā)生時(shí),處理器將控制轉(zhuǎn)移給故障處理程序。如果故障處理程序可以修正這個(gè)錯(cuò)誤情況,便會(huì)將控制放回到故障指令,從而重新執(zhí)行它,如果不能修正,故障處理程序便會(huì)將控制轉(zhuǎn)移到系統(tǒng)內(nèi)核的abort()函數(shù),abort()最終會(huì)終止引起故障的應(yīng)用程序。

一般保護(hù)性故障,Unix不會(huì)嘗試恢復(fù),而是將這種保護(hù)性故障報(bào)告為段違規(guī)(segmentation violation)對(duì)應(yīng)信號(hào)為SIGSEGV,然后終止程序。比如:除零、程序嘗試寫(xiě)入只讀的文本段等。

故障的經(jīng)典示例便是缺頁(yè)異常。
終止:是由不可恢復(fù)的致命錯(cuò)誤造成的結(jié)果。終止從不將控制返回給應(yīng)用程序。如:奇偶校驗(yàn)錯(cuò)誤(機(jī)器硬件錯(cuò)誤檢測(cè))

Unix信號(hào)(signal)

更高層的軟件形式的異常,一個(gè)信號(hào)就是一條消息,它通知進(jìn)程某種類型的消息已經(jīng)在系統(tǒng)中發(fā)生了。信號(hào)提供了一種向用戶進(jìn)程通知這些異常發(fā)生的機(jī)制,也允許進(jìn)程中斷其他進(jìn)程。

LinuxmacOS都是類Unix系統(tǒng),下表列舉了Linux系統(tǒng)支持的30多種不同類型的信號(hào),有許多Linux信號(hào)在macOS同樣適用:

號(hào)碼 名字 默認(rèn)行為 相應(yīng)事件 號(hào)碼 名字 默認(rèn)行為 相應(yīng)事件
1 SIGHUP 終止 終端線掛起 16 SIGSTKFLT 終止 協(xié)處理器上的棧故障
2 SIGINT 終止 來(lái)自鍵盤的中斷 17 SIGCHLD 忽略 一個(gè)子進(jìn)程暫?;蛘呓K止
3 SIGQUIT 終止 來(lái)自鍵盤的退出 18 SIGCONT 忽略 若進(jìn)程暫停則繼續(xù)進(jìn)程
4 SIGILL 終止 非法指令 19 SIGSTOP 停止直到下一個(gè)SIGCONT 不來(lái)自終端的暫停信號(hào)
5 SIGTRAP 終止并轉(zhuǎn)儲(chǔ)存儲(chǔ)器 跟蹤陷阱 20 SIGTSTP 停止直到下一個(gè)SIGCONT 來(lái)自終端的暫停信號(hào)
6 SIGABRT 終止并轉(zhuǎn)儲(chǔ)存儲(chǔ)器 來(lái)自abort函數(shù)的終止信號(hào) 21 SIGTTIN 停止直到下一個(gè)SIGCONT 后臺(tái)進(jìn)程從終端讀
7 SIGBUS 終止 總線錯(cuò)誤 22 SIGTTOU 停止直到下一個(gè)SIGCONT 后臺(tái)進(jìn)程向終端寫(xiě)
8 SIGFPE 終止并轉(zhuǎn)儲(chǔ)存儲(chǔ)器 浮點(diǎn)異常 23 SIGURG 忽略 套接字上的緊急情況
9 SIGKILL 終止 殺死程序 24 SIGXCPU 終止 CPU時(shí)間超出限制
10 SIGUSER1 終止 用戶定義的信號(hào)1 25 SIGXFSZ 終止 文件大小超出限制
11 SIGSEGV 終止 無(wú)效的存儲(chǔ)器引用(段故障) 26 SIGVTALRM 終止 虛擬定時(shí)器期滿
12 SIGUSER2 終止 用戶定義的信號(hào)2 27 SIGPROF 終止 剖析定時(shí)器期滿
13 SIGPIPE 終止 向一個(gè)沒(méi)有用戶讀的管道做寫(xiě)操作 28 SIGWINCH 忽略 窗口大小變化
14 SIGALRM 終止 來(lái)自alarm函數(shù)的定時(shí)器信號(hào) 29 SIGIO 終止 在某個(gè)描述符上執(zhí)行I/O操作
15 SIGTERM 終止 軟件終止信號(hào) 30 SIGPWR 終止 電源故障

系統(tǒng)內(nèi)核

Mac OS X&iOS&iPad OS系統(tǒng)內(nèi)核都是Darwin。Darwin包含了開(kāi)放源代碼的XNU混合內(nèi)核,它包含了Mach/BSD,BSD是建立在Mach之上提供標(biāo)準(zhǔn)化(POSIX)的API,XNU的核心是Mach。下圖為OS X 內(nèi)核架構(gòu),查看來(lái)源。

image.png

Mach:是一個(gè)微內(nèi)核的操作系統(tǒng)。微內(nèi)核僅處理最核心的任務(wù),其他任務(wù)交給用戶態(tài)的程序,包括文件管理,設(shè)備驅(qū)動(dòng)等服務(wù),這些服務(wù)被分解到不同的地址空間,彼此消息傳遞需要IPC。主要負(fù)責(zé):線程與進(jìn)程管理、虛擬內(nèi)存管理、進(jìn)程通信與消息傳遞、任務(wù)調(diào)度。與之對(duì)應(yīng)的單內(nèi)核則是把所有的服務(wù)放在相同的地址空間下,服務(wù)之間可相互調(diào)用。

BSD:是Unix的衍生系統(tǒng)。主要負(fù)責(zé):Unix進(jìn)程模型、POSIX線程模型以及相關(guān)原語(yǔ)、文件系統(tǒng)訪問(wèn)、設(shè)備訪問(wèn)、網(wǎng)絡(luò)協(xié)議棧、Unix用戶與群組。

異常來(lái)源

iOS中異常主要來(lái)源于硬件異常、軟件異常、Mach異常、Signal異常,它們之間的關(guān)系如下圖:

image.png

Mach異常

Mach異常是系統(tǒng)內(nèi)核級(jí)異常,是由CPU觸發(fā)一個(gè)陷阱引發(fā),調(diào)用到Mach的異常處理程序,將來(lái)自硬件的異常轉(zhuǎn)換為Mach異常,然后將Mach異常傳遞到相應(yīng)的threadtask、host,若無(wú)結(jié)果返回,任務(wù)便會(huì)被終止。

Mach異常傳遞涉及到的內(nèi)核函數(shù)如下圖:

image.png

依據(jù)上圖提供信息,查閱蘋(píng)果開(kāi)源資料,找到對(duì)應(yīng)的函數(shù)信息,簡(jiǎn)單列舉如下(詳細(xì)查閱請(qǐng)前往此處):

struct ppc_saved_state *trap(int trapno,
                 struct ppc_saved_state *ssp,
                 unsigned int dsisr,
                 unsigned int dar) {
      //... 
      doexception(exception, code, subcode);
      //...
}
void doexception(
        int exc,
        int code,
        int sub) {
    exception_data_type_t   codes[EXCEPTION_CODE_MAX];

    codes[0] = code;    
    codes[1] = sub;
    exception(exc, codes, 2);
}
// Des:The current thread caught an exception.
// We make an up-call to the thread's exception server.
void exception(
    exception_type_t    exception,
    exception_data_t    code,
    mach_msg_type_number_t  codeCnt)
{
    thread_act_t        thr_act;
    task_t          task;
    host_priv_t     host_priv;
    struct exception_action *excp;
    mutex_t         *mutex;

    assert(exception != EXC_RPC_ALERT);
    if (exception == KERN_SUCCESS)
        panic("exception");
    /*
     * Try to raise the exception at the activation level.線程級(jí)別
     */
    thr_act = current_act();
    mutex = mutex_addr(thr_act->lock);
    excp = &thr_act->exc_actions[exception];
    exception_deliver(exception, code, codeCnt, excp, mutex);
    /*
     * Maybe the task level will handle it. 任務(wù)級(jí)別
     */
    task = current_task();
    mutex = mutex_addr(task->lock);
    excp = &task->exc_actions[exception];
    exception_deliver(exception, code, codeCnt, excp, mutex);
    /*
     * How about at the host level? 主機(jī)級(jí)別
     */
    host_priv = host_priv_self();
    mutex = mutex_addr(host_priv->lock);
    excp = &host_priv->exc_actions[exception];
    exception_deliver(exception, code, codeCnt, excp, mutex);
    /*
     * Nobody handled it, terminate the task. 沒(méi)有處理終止
     */
         // ...
    (void) task_terminate(task);
    thread_exception_return();
    /*NOTREACHED*/
}
// Make an upcall to the exception server provided.
void exception_deliver(
    exception_type_t    exception,
    exception_data_t    code,
    mach_msg_type_number_t  codeCnt,
    struct exception_action *excp,
    mutex_t         *mutex)
{
        ///...
    
    int behavior = excp->behavior;

    switch (behavior) {
    case EXCEPTION_STATE: {
        ///EXCEPTION_STATE:Send a `catch_exception_raise_state` message 
        ///including the thread state.
        //..
        kr = exception_raise_state(exc_port, exception,
                           code, codeCnt,
                           &flavor,
                           state, state_cnt,
                           state, &state_cnt);
                //..                                
        return;
    }

    case EXCEPTION_DEFAULT:
        ///EXCEPTION_DEFAULT表示:Send a `catch_exception_raise` message 
        ///including the thread identity.
        //..
        kr = exception_raise(exc_port,
                retrieve_act_self_fast(a_self),
                retrieve_task_self_fast(a_self->task),
                exception,
                code, codeCnt);
                //..                
        return;

    case EXCEPTION_STATE_IDENTITY: {
        /// EXCEPTION_STATE_IDENTITY:表示Send a `catch_exception_raise_state_identity` message 
        ///including the thread identity and state.
        //..
            kr = exception_raise_state_identity(exc_port,
                retrieve_act_self_fast(a_self),
                retrieve_task_self_fast(a_self->task),
                exception,
                code, codeCnt,
                &flavor,
                state, state_cnt,
                state, &state_cnt);
         //..
         return;
    }
    
    default:
        panic ("bad exception behavior!");
    }
}

關(guān)于如何捕獲Mach異常,蘋(píng)果文檔描述很少,也沒(méi)有提供可用的API,具體的Mach內(nèi)核的API介紹,可從此處查閱。

Sigal信號(hào)

BSD派生自Unix操作系統(tǒng),屬于類Unix系統(tǒng),基于Mach內(nèi)核進(jìn)程任務(wù),提供POSIX應(yīng)用程序接口。詳見(jiàn)維基百科-XNU?;诖耍?code>Unix Signal機(jī)制同樣適用蘋(píng)果操作系統(tǒng)。蘋(píng)果系統(tǒng)對(duì)于Unix Signal的定義,可通過(guò)#import <sys/signal.h>跳轉(zhuǎn)查看。

Mach異常-> Signal信號(hào)

蘋(píng)果操作系統(tǒng)Mach異常與Signal信號(hào)共存。Mach將操作系統(tǒng)的核心部分當(dāng)做獨(dú)立進(jìn)程運(yùn)行,與BSD服務(wù)進(jìn)程之間通過(guò)IPC機(jī)制實(shí)現(xiàn)消息傳遞。同理Mach內(nèi)核態(tài)的異常也是基于IPC將異常消息發(fā)送到BSDBSD將消息轉(zhuǎn)換為用戶態(tài)的Signal信號(hào)。具體流程如下:

  1. 蘋(píng)果內(nèi)核啟動(dòng)時(shí)會(huì)執(zhí)行bsdinit_task()并最終調(diào)用ux_handler_init()方法。
void bsdinit_task(void)
{
    proc_t p = current_proc();
    struct uthread *ut;
    thread_t thread;

    process_name("init", p);

    ux_handler_init();

    thread = current_thread();
    (void) host_set_exception_ports(host_priv_self(),
                    EXC_MASK_ALL & ~(EXC_MASK_RPC_ALERT),//pilotfish (shark) needs this port
                    (mach_port_t) ux_exception_port,
                    EXCEPTION_DEFAULT| MACH_EXCEPTION_CODES,
                    0);

    ut = (uthread_t)get_bsdthread_info(thread);

    bsd_init_task = get_threadtask(thread);
    init_task_failure_data[0] = 0;

#if CONFIG_MACF
    mac_cred_label_associate_user(p->p_ucred);
    mac_task_label_update_cred (p->p_ucred, (struct task *) p->task);
#endif
    load_init_program(p);
    lock_trace = 1;
}
  1. ux_handler_init()初始化一個(gè)ux_handler()方法,并創(chuàng)建線程執(zhí)行它。
void ux_handler_init(void)
{
    thread_t thread = THREAD_NULL;

    ux_exception_port = MACH_PORT_NULL;
    (void) kernel_thread_start((thread_continue_t)ux_handler, NULL, &thread);
    thread_deallocate(thread);
    proc_list_lock();
    if (ux_exception_port == MACH_PORT_NULL)  {
        (void)msleep(&ux_exception_port, proc_list_mlock, 0, "ux_handler_wait", 0);
    }
    proc_list_unlock();
}
  1. ux_handler()申請(qǐng)用于接收Mach內(nèi)核消息的端口(port)集合,接收來(lái)自Mach的異常消息
static void ux_handler(void)
{
    task_t      self = current_task();
    mach_port_name_t    exc_port_name;
    mach_port_name_t    exc_set_name;

    /*
     *  Allocate a port set that we will receive on.
     */
    if (mach_port_allocate(get_task_ipcspace(ux_handler_self), MACH_PORT_RIGHT_PORT_SET,  &exc_set_name) != MACH_MSG_SUCCESS)
        panic("ux_handler: port_set_allocate failed");

    /*
     *  Allocate an exception port and use object_copyin to
     *  translate it to the global name.  Put it into the set.
     */
    if (mach_port_allocate(get_task_ipcspace(ux_handler_self), MACH_PORT_RIGHT_RECEIVE, &exc_port_name) != MACH_MSG_SUCCESS)
    panic("ux_handler: port_allocate failed");
    if (mach_port_move_member(get_task_ipcspace(ux_handler_self),
                exc_port_name,  exc_set_name) != MACH_MSG_SUCCESS)
    panic("ux_handler: port_set_add failed");

    if (ipc_object_copyin(get_task_ipcspace(self), exc_port_name,
            MACH_MSG_TYPE_MAKE_SEND, 
            (void *) &ux_exception_port) != MACH_MSG_SUCCESS)
        panic("ux_handler: object_copyin(ux_exception_port) failed");

    proc_list_lock();
    thread_wakeup(&ux_exception_port);
    proc_list_unlock();

    /* Message handling loop. */

    for (;;) {
    struct rep_msg {
        mach_msg_header_t Head;
        NDR_record_t NDR;
        kern_return_t RetCode;
    } rep_msg;
    struct exc_msg {
        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;
        mach_exception_data_t code;
        /* some times RCV_TO_LARGE probs */
        char pad[512];
    } exc_msg;
    mach_port_name_t    reply_port;
    kern_return_t    result;

    exc_msg.Head.msgh_local_port = CAST_MACH_NAME_TO_PORT(exc_set_name);
    exc_msg.Head.msgh_size = sizeof (exc_msg);
#if 0
    result = mach_msg_receive(&exc_msg.Head);
#else
    result = mach_msg_receive(&exc_msg.Head, MACH_RCV_MSG,
                 sizeof (exc_msg), exc_set_name,
                 MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL,
                 0);
#endif
    if (result == MACH_MSG_SUCCESS) {
        reply_port = CAST_MACH_PORT_TO_NAME(exc_msg.Head.msgh_remote_port);
            ///收到消息后調(diào)用 mach_exc_server() 
        if (mach_exc_server(&exc_msg.Head, &rep_msg.Head)) {
                ///收到消息,回復(fù)消息
        result = mach_msg_send(&rep_msg.Head, MACH_SEND_MSG,
            sizeof (rep_msg),MACH_MSG_TIMEOUT_NONE,MACH_PORT_NULL);
        if (reply_port != 0 && result != MACH_MSG_SUCCESS)
            mach_port_deallocate(get_task_ipcspace(ux_handler_self), reply_port);
        }

    }
    else if (result == MACH_RCV_TOO_LARGE)
        /* ignore oversized messages */;
    else
        panic("exception_handler");
    }
}
  1. ux_handler(void)收到Mach內(nèi)核消息后,便會(huì)調(diào)用mach_exc_server函數(shù),這個(gè)函數(shù)會(huì)根據(jù)異常的行為調(diào)用對(duì)應(yīng)的catch_mach_exception_raise(), catch_mach_exception_raise_state(), 和 catch_mach_exception_raise_state_identity(),catch_mach_exception_raise()會(huì)觸發(fā)Mach異常消息到Unix信號(hào)的轉(zhuǎn)換。而關(guān)于mach_exc_server()的實(shí)現(xiàn),并未像其他函數(shù)直接給出,具體請(qǐng)見(jiàn)此處。

  2. 調(diào)用catch_mach_exception_raise()Mach異常轉(zhuǎn)換為Unix信號(hào),最終發(fā)送到對(duì)應(yīng)線程。

kern_return_t catch_mach_exception_raise(
        __unused mach_port_t exception_port,
        mach_port_t thread,
        mach_port_t task,
        exception_type_t exception,
        mach_exception_data_t code,
        __unused mach_msg_type_number_t codeCnt
)
{
    ///...

    /*
     * Convert exception to unix signal and code.
     */
    ux_exception(exception, code[0], code[1], &ux_signal, &ucode);
        ///struct uthread *ut
        ///struct proc  *p;
        ut = get_bsdthread_info(th_act);
    p = proc_findthread(th_act);
        ///...
         /*
      * Send signal.
      */
      if (ux_signal != 0) {
        ut->uu_exception = exception;
        //ut->uu_code = code[0]; // filled in by threadsignal
        ut->uu_subcode = code[1];           
        threadsignal(th_act, ux_signal, code[0]);
       }
     if (p != NULL) 
       proc_rele(p);
       thread_deallocate(th_act);
    ///...
}
static void ux_exception(
        int         exception,
        mach_exception_code_t   code,
        mach_exception_subcode_t subcode,
        int         *ux_signal,
        mach_exception_code_t   *ux_code)
{
    /*
     *  Try machine-dependent translation first.
     */
    if (machine_exception(exception, code, subcode, ux_signal, ux_code))
    return;
    
    switch(exception) {

    case EXC_BAD_ACCESS:
        if (code == KERN_INVALID_ADDRESS)
            *ux_signal = SIGSEGV;
        else
            *ux_signal = SIGBUS;
        break;

    case EXC_BAD_INSTRUCTION:
        *ux_signal = SIGILL;
        break;

    case EXC_ARITHMETIC:
        *ux_signal = SIGFPE;
        break;

    case EXC_EMULATION:
        *ux_signal = SIGEMT;
        break;

    case EXC_SOFTWARE:
        switch (code) {

        case EXC_UNIX_BAD_SYSCALL:
        *ux_signal = SIGSYS;
        break;
        case EXC_UNIX_BAD_PIPE:
        *ux_signal = SIGPIPE;
        break;
        case EXC_UNIX_ABORT:
        *ux_signal = SIGABRT;
        break;
        case EXC_SOFT_SIGNAL:
        *ux_signal = SIGKILL;
        break;
        }
        break;

    case EXC_BREAKPOINT:
        *ux_signal = SIGTRAP;
        break;
    }
}

ux_exception()函數(shù),展示了Mach異常與Signal信號(hào)轉(zhuǎn)換關(guān)系。關(guān)于iOSMach異常信號(hào)的定義,可通過(guò)#include <mach/exception_types.h>跳轉(zhuǎn)查看。

硬件異常

硬件異常依據(jù)前文所述,主要為:中斷、缺陷、故障、終止。硬件異常的觸發(fā)流程如下圖:


image.png

軟件異常

應(yīng)用級(jí)別的異常,在iOS中就是NSException。如果NSException異常沒(méi)有捕獲處理(try-catch),系統(tǒng)最終會(huì)調(diào)用abort()函數(shù),向應(yīng)用程序發(fā)送SIGABRT的信號(hào)。

void abort() {
         ///...
    /* <rdar://problem/7397932> abort() should call pthread_kill to deliver a signal to the aborting thread 
     * This helps gdb focus on the thread calling abort()
     */
    if (__is_threaded) {
        //...
        (void)pthread_kill(pthread_self(), SIGABRT);
    } else {
        //...
        (void)kill(getpid(), SIGABRT);
    }
    //...
}

異常捕獲

上文分析可知道硬件異常與軟件異常最終都會(huì)轉(zhuǎn)換為Unix Signal,因此對(duì)于Signal信號(hào)的處理,可以覆蓋大部分的崩潰信息。除此之外系統(tǒng)給我們提供的NSException,可以用來(lái)獲取更詳細(xì)的奔潰信息?;诖?,下文我們將只對(duì)SignalNSException的捕獲進(jìn)行簡(jiǎn)單示例。

Signal捕獲

在進(jìn)行Signal捕獲時(shí),需要注意覆蓋問(wèn)題。因?yàn)槊總€(gè)Signal對(duì)應(yīng)一個(gè)Handler的處理函數(shù),當(dāng)我們通過(guò)綁定我們自己的Hanlder來(lái)收集奔潰信息時(shí),可能會(huì)覆蓋其他三方庫(kù)已經(jīng)綁定的Handler導(dǎo)致他們無(wú)法收集奔潰信息。

核心代碼如下:

//頭文件
#import <sys/signal.h>
#import "execinfo.h"
///1.用以保存舊的handler
static struct sigaction *previous_signalHandlers = NULL;
///2.定義我們要處理的信號(hào)
static int signals[] = {SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGPIPE,SIGSEGV,SIGSYS,SIGTRAP};
///3.注冊(cè)`Handler`
+ (BOOL)registerSignalHandler; {
    ///初始化我們的Sigaction
    struct sigaction action = { 0 };
    ///初始化存放舊的Sigaction數(shù)組
    int count = sizeof(signals) / sizeof(int);
    if (previous_signalHandlers == NULL) {
        previous_signalHandlers = malloc(sizeof(struct sigaction) * count);
    }
    action.sa_flags = SA_SIGINFO;
    sigemptyset(&action.sa_mask);
    /// 綁定我們的處理函數(shù)
    action.sa_sigaction = &_handleSignal;
    for (int i = 0; i < count; i ++) {
        ///遍歷信號(hào)
        int signal = signals[i];///or  *(signals + i)
        ///綁定新的`Sigaction`,存儲(chǔ)舊的`Sigaction`
        int result = sigaction(signal, &action, &previous_signalHandlers[I]);
        /// 綁定失敗
        if (result != 0) {
            NSLog(@"signal:%d,error:%s",signal,strerror(errno));
            for (int j =i--; j >= 0;j--) {
                /// 恢復(fù)舊的Sigaction,此次函數(shù)返回NO
                sigaction(signals[j], &previous_signalHandlers[j], NULL);
            }
            return NO;
        }
    }
    return YES;
}
/// 4. 信號(hào)處理函數(shù)
void _handleSignal(int sigNum,siginfo_t *info,void *ucontext_t) {
    
    /// todo our operation
    NSLog(@"?攔截到崩潰信號(hào):%d,打印堆棧信息:%@",[CrashSignals callStackSymbols]);
    /// 獲取`sigNum`在信號(hào)數(shù)組中對(duì)應(yīng)的`index`
    int index = -1,count = sizeof(signals) / sizeof(int);
    for (int i = 0; i < count; i++) {
        if (*(signals + i) == sigNum) {
            index = I;
            break;
        }
    }
    if (index == -1) return;
    /// 取出舊的`Sigaction`
    struct sigaction previous_action = previous_signalHandlers[index];
    if (previous_action.sa_handler == SIG_IGN) {
        //`SIG_IGN`忽略信號(hào),`SIG_DFL`默認(rèn)方式處理信號(hào)
        return;
    }
    /// 恢復(fù)舊的`Sigaction`與Signal的綁定關(guān)系
    sigaction(sigNum, &previous_action, NULL);
    /// 重新拋出這個(gè)`Signal`,此時(shí)便會(huì)被`previous_action`的處理程序攔截到。
    raise(sigNum);
}
//5. 函數(shù)的調(diào)用棧
+ (NSArray*)callStackSymbols {
    /// ` int backtrace(void ** buffer , int size )`
    /// void ** buffer:在`buffer`指向的數(shù)組返回程序棧楨的回溯,
    /// void ** buffer: Each item in the array pointed to by buffer is of type void *
    void* backtrace_buffer[128];
    /// 返回值可能比 128大,大便截?cái)?,小則全部顯示
    int numberOfReturnAdderss = backtrace(backtrace_buffer, 128);
    ///char **backtrace_symbols(void *const *buffer, int size);
    /// `backtrace_symbols()` translates the addresses into an array of strings that describe the addresses symbolically
    /// The size argument specifies the number of addresses in buffer
    char **symbols = backtrace_symbols(backtrace_buffer, numberOfReturnAdderss);
    /// 提取每個(gè)返回地址對(duì)應(yīng)的符號(hào)信息,棧楨是嵌套的
    NSMutableArray *tempArray = [[NSMutableArray alloc]initWithCapacity:numberOfReturnAdderss];
    for (int i = 0 ; i < numberOfReturnAdderss; i++) {
        char *cstr_item = symbols[I];
        NSString *objc_str = [NSString stringWithUTF8String:cstr_item];
        [tempArray addObject:objc_str];
    }
    return [tempArray copy];
}

NSException捕獲

系統(tǒng)提供了對(duì)應(yīng)的處理iOS系統(tǒng)中未被捕獲的NSExceptionAPI,我們只需要按照API進(jìn)行操作即可,但與Signal一樣,需要注意多處注冊(cè)的覆蓋問(wèn)題,避免影響項(xiàng)目中其他收集程序。

核心代碼如下:

///1.聲明用以保存舊的`Hanlder`的靜態(tài)變量
static NSUncaughtExceptionHandler *previous_uncaughtExceptionHandler;
///注冊(cè)處理應(yīng)用級(jí)異常的`handler`
+ (void)registerExceptionHandler; {
    previous_uncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
    NSSetUncaughtExceptionHandler(&_handleException);
}
///我們的異常處理程序
void _handleException(NSException *exception) {
    /// Todo our operation
    NSLog(@"?攔截到異常的堆棧信息:%@",exception.callStackSymbols);
    
    /// 傳遞異常
    if (previous_uncaughtExceptionHandler != NULL) {
        previous_uncaughtExceptionHandler(exception);
    }
    // 殺掉程序,這樣可以防止同時(shí)拋出的SIGABRT被Signal異常捕獲
    /// kill (cannot be caught or ignored)
    kill(getpid(), SIGKILL);
}

調(diào)試驗(yàn)證

XcodeDebug環(huán)境下,Signal異常與NSException異常都會(huì)被Xcode調(diào)試器攔截,不會(huì)走到我們的處理程序。因此代碼的調(diào)試驗(yàn)證,筆者采用模擬器運(yùn)行程序后,停止運(yùn)行,脫離Xcode的調(diào)試環(huán)境,重新在模擬器打開(kāi)程序,開(kāi)啟Mac的控制臺(tái)程序,點(diǎn)擊按鈕觸發(fā)奔潰,查看控制臺(tái)對(duì)應(yīng)模擬器的log記錄,來(lái)驗(yàn)證是否正確捕獲。另:Signal奔潰采用kill(getpid(), SIGBUS);來(lái)觸發(fā)。

參考資料

https://flylib.com/books/en/3.126.1.109/1/

http://shevakuilin.com/ios-crashprotection/

https://minosjy.com/2021/04/10/00/377/

https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/sigaction.2.html

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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