[史上最全] iOS Crash/崩溃/异常 捕获

没想到都2021年,我还得写篇文章来讲讲 Crash 监听的一些事情。虽然蛮多文章讲 Crash 监听这块,但总是讲的不够深入或者说不够全面。于是我想分享一下最近我对这方面知识的一些理解和整理。我计划讲以下几个主题:

Crash 的类型

根据Crash 的不同来源,Crash 分为以下三类:

  • Mach 异常

    最底层的内核级异常。用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常。

  • Unix 信号

    又称BSD 信号,如果开发者没有捕获Mach异常,则会被host层的方法ux_exception()将异常转换为对应的UNIX信号,并通过方法threadsignal()将信号投递到出错线程。可以通过方法signal(x, SignalHandler)来捕获signal。

  • NSException

    应用级异常,它是未被捕获的Objective-C异常,导致程序向自身发送了SIGABRT信号而崩溃,是app自己可控的,对于未捕获的Objective-C异常,是可以通过try catch来捕获的,或者通过NSSetUncaughtExceptionHandler()机制来捕获。

Mach 异常

Mach异常是内核级异常,在系统的位置如下图所示:

OS X kernel architecture

Mach相关知识

Mach内核作为系统一个底层的基础,仅与驱动操作系统所需的最低需要有关。 其他所有内容都由操作系统的更高层来实现,然后再利用Mach并以其认为合适的任何方式对其进行操作。

Mach提供了一小部分内核抽象,这些内核抽象被设计为既简单又强大。与Mach异常相关的内核抽象有:

  • tasks

资源所有权单位; 每个任务由一个虚拟地址空间、一个端口权限名称空间和一个或多个线程组成。 (类似于进程)

  • threads

任务中CPU执行的单位。

  • ports

安全的单工通信通道,只能通过发送和接收功能(称为端口权限)进行访问。

这些内核对象,对于Mach来说都是一个个的Object,这些Objects基于Mach实现自己的功能,并通过Mach Message来进行通信,Mach提供了相关的应用层的API来操作。与Mach异常相关的几个API有:

  • task_get_exception_ports:获取task的异常端口
  • task_set_exception_ports:设置task的异常端口
  • mach_port_allocate:创建调用者指定的端口权限类型
  • mach_port_insert_right:将指定的端口插入目标task

如何捕捉 Mach 异常

image

参考上图,主要的流程是:新建一个监控线程,在监控线程中监听 Mach 异常并处理异常信息。主要的步奏如下图:

内核 crash 手机的流程

具体代码如下:

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

//判断是否 Xcode 联调
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()) {
        // 当前正在调试状态, 不启动 mach 监听
        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;
    }
    
    //建立监听线程
    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 监听, 防止死锁
        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;
}

监听 Mach 异常需要注意:

  • 避免在 Xcode 联调时监听

    原因是我们监听了EXC_BREAKPOINT这类型的Exception,一旦启动 app 联调后, 会立即触发EXC_BREAKPOINT。而这段代码处理完后,会进入下一个循环等待,可主线程这是还等着消息处理结果,这就造成等待死锁。

关于代码中其他分析异常原因的代码,我会在下一篇讲获取堆栈的文章中详细解读。

Unix 信号(Signal)

Mach已经通过异常机制提供了底层的异常处理,但为了兼容更为流行的POSIX标准,BSD在Mach异常机制之上构建的UNIX信号处理机制。异常信号首先被转换为Mach异常,如果没有被外界捕捉,则会被默认的异常处理ux_exception()转换为UNIX信号。

我们可以把信号看做是对硬件异常跟软件异常的封装。

Unix 信号列表

Unix Signal 其实是由 Mach port 抛出的信号转化的,那么都有哪些信号呢?

  • SIGHUP
    本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。

  • SIGINT
    程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

  • SIGQUIT
    和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

  • SIGABRT
    调用abort函数生成的信号。
    SIGABRT is a BSD signal sent by an application to itself when an NSException or obj_exception_throw is not caught.

  • SIGBUS
    非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

  • SIGFPE
    在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

  • SIGKILL
    用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

  • SIGSEGV
    试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

  • SIGPIPE
    管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。

  • SIGSYS

    非法的系统调用。

  • SIGTRAP

    由断点指令或其它 trap 指令产生. 由d ebugger 使用。

  • SIGILL

    执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

其他未列出的信号可以参照这篇文章:linux 各个SIG信号含义

如何捕捉 Unix 信号

一般来说我们需要捕捉以下信号:

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

而要捕捉 Unix 信号,比 Mach 异常容易多了

void installSignalHandler() {
        signal(SIGABRT, handleSignalException);
    //...等等其他需要监听的 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 监听, 防止死锁
    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);
}

备用信号栈

上面这个方法可以监控到大部分的 Signal 异常,但是我们会发现如果遇到死循环这类的Crash,就没法监控了。原因是一般情况下,信号处理函数被调用时,内核会在进程的栈上为其创建一个栈帧。但这里就会有一个问题,如果之前栈的增长达到了栈的最大长度,或是栈没有达到最大长度但也比较接近,那么就会导致信号处理函数不能得到足够栈帧分配。

为了解决这个问题,我们需要设定一个可选的栈帧

  1. 申请一块内存空间作为可选的信号处理函数栈使用
  2. 使用 sigaltstack 函数通知系统可选的信号处理栈帧的存在及其位置
  3. 当使用 sigaction 函数建立一个信号处理函数时,通过指定 SA_ONSTACK 标志通知系统这个信号处理函数应该在可选的栈帧上面执行注册的信号处理函数

前面监听 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);
}

调试 Signal 信号

有可能为了调试,你会在处理堆栈的地方打上断点,但是 Crash 发生后却没有命中。这是因为在Xcode调试时,Debugger模式会先于我们的代码catch到所有的crash,所以需要直接从模拟器中进入程序才可以。

如果想要在Xcode中调试,网上我找个方法。在lldb中输入以下命令:pro hand -p true -s false SIGABRT。注意:SIGABRT可以替换为你需要的任何signal类型,比如SIGSEGV。

但是...我自己试了并不成功,我仍然写出来供大家坐坐参考,说不定你们就可以了。

除此之外还有个办法就是使用控制台这个 app,可以在应用程序(Application)里面找到这个 app,打开后你可以看到各种 app 的输出。然后通过关键字找到你想要日志:

NSException

NSException 是应用级异常,是指 OC 代码运行过程由Objective-C 抛出的异常,基本上是代码运行过程中的逻辑错误。比如往 NSArray 中插入 nil 对象,或者用nil 初始化 NSURL 等。最简单区分一个异常是否 NSException 的方式是看这个异常能否被@trycatch 给捕获。

常见的 NSException 场景

  • 非主线程刷新UI

  • NSInvalidArgumentException
    非法参数异常(NSInvalidArgumentException)是 Objective – C 代码最常出现的错误,所以平时在写代码的时候,需要多加注意,加强对参数的检查,避免传入非法参数导致异常,其中尤以nil参数为甚。

  • NSRangeException
    越界异常(NSRangeException)也是比较常出现的异常。

  • NSGenericException
    NSGenericException这个异常最容易出现在foreach操作中,在for in循环中如果修改所遍历的数组,无论你是add或remove,都会出错 “for in”,它的内部遍历使用了类似 Iterator进行迭代遍历,一旦元素变动,之前的元素全部被失效,所以在foreach的循环当中,最好不要去进行元素的修改动作,若需要修改,循环改为for遍历,由于内部机制不同,不会产生修改后结果失效的问题。

  • NSInternalInconsistencyException
    不一致导致出现的异常
    比如NSDictionary当做NSMutableDictionary来使用,从他们内部的机理来说,就会产生一些错误
    NSMutableDictionary *info = method return to NSDictionary type;
    [info setObject:@“sxm” forKey:@”name”];
    比如xib界面使用或者约束设置不当

  • NSFileHandleOperationException
    处理文件时的一些异常,最常见的还是存储空间不足的问题,比如应用频繁的保存文档,缓存资料或者处理比较大的数据:
    所以在文件处理里,需要考虑到手机存储空间的问题。

  • NSMallocException
    这也是内存不足的问题,无法分配足够的内存空间
    此外还有

  • KVO Crash
    移除未注册的观察者
    重复移除观察者
    添加了观察者但是没有实现-observeValueForKeyPath:ofObject:change:context:方法
    添加移除keypath=nil
    添加移除observer=nil

  • unrecognized selector send to instance

监听 NSException 异常

NSException的监听也十分简单:

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);
}

需要注意的是,在监听处理的方法中,是无法直接采集错误到堆栈的。详情我同样会在下一篇的崩溃堆栈收集的文章中介绍。

C++ 异常

有朋友看到这里可能就会好奇,前面说了三种异常,为何这里又多出一种异常。实质上C++异常也可以通过 Mach 异常的方式处理。只是在细节处理上仍多有区别。

以下部分内容转载自文章:iOS/OSX Crash:捕捉异常,具体内容需要读者自行验证。

为什么要捕捉 C++异常

在OSX中,会通过对话框展示异常给用户,但在iOS中,只是重新抛出异常。系统在捕捉到C++异常后,如果能够将此C++异常转换为OC异常,则抛出OC异常处理机制;如果不能转换,则会立刻调用__cxa_throw重新抛出异常。

当系统在RunLoop捕捉到的C++异常时,此时的调用堆栈是异常发生时的堆栈,但当系统在不能转换为OC异常时调用__cxa_throw时,上层捕捉此再抛出的异常获取到的调用堆栈是RunLoop异常处理函数的堆栈,导致原始异常调用堆栈丢失。

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++异常的调用堆栈,我们需要模拟抛出NSException的过程并在此过程中保存调用堆栈。

  1. 设置异常处理函数

    g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);
    

    调用std::set_terminate设置新的全局终止处理函数并保存旧的函数。

  2. 重写__cxa_throw

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

    在异常发生时,会先进入此重写函数,应该先获取调用堆栈并存储;再调用原始的__cxa_throw函数。

  3. 异常处理函数

    __cxa_throw往后执行,进入set_terminate设置的异常处理函数。判断如果检测是OC异常,则什么也不做,让OC异常机制处理;否则获取异常信息。

不同类型的异常之间的关系

在前面,我们讲了几种异常类型及其细节。但是他们之间的关系我们还了解甚少。下面我就来讲讲不同异常之间可能存在的转换关系,以及优先顺序等。

异常处理的顺序

首先,我们看一下下图,这个图很重要

我大致总结一下:

  1. 如果是 NSException 类型的异常

    先看 app 是否 trycatch 了;再看有没有实现 NSSetUncaughtExceptionHandler;最后如果都没处理,则调用 c 的 abort(),kernal 针对 app 发出 _pthread_kill 的信号,转为 Mach 异常。

  2. 如果是 Mach 异常

    如果 app 处理了 Mach 异常则进入处理流程;否则 mach 异常会被转为 Unix/BSD signal 信号,并进入 Signal 的处理流程。

简单的说就是:NSException->Mach->Signal

不同类型异常的关系和处理决策

首先要明确的一点是,Mach异常和UNIX信号都可以被捕获,他们也几乎一一对应。那为什么几乎所有 Crash 监控框架都会捕捉 Mach 异常、Unix 信号以及 NSException 呢?

Mach 异常 和 Unix 信号

所有Mach异常未处理,它将在host层被ux_exception转换为相应的Unix信号,并通过threadsignal将信号投递到出错的线程。所以其实所有我们看到的 Unix信号异常,都是从 Mach 传过来的,只是在 Mach 没有catch,所以转成Unix给我们处理。比如 Bad Access。

  1. 既然 Unix 信号都是由 Mach Exception 转化的,为啥还要转Unix 信号呢,直接传 Mach 的异常不就行了?
    这是为了兼容更为流行的POSIX标准,BSD在Mach异常机制之上构建的UNIX信号处理机制。
  2. 既然 Mach Exception 能转化为 Signal 信号,Signal 信号监听也更简单,为什么不只监听 Signal 信号?
    不是所有的 "Mach异常” 类型都映射到了 “UNIX信号”。 如 EXC_GUARD 。在苹果开源的 xnu 源码中可以看到这点。
  3. 为什么优先监听 Mach Exception?
    这是因为 Mach 异常会更早的抛出来,而且如果Mach异常的handler让程序exit了,那么Unix信号就永远不会到达这个进程了。

为什么不能只监听 Mach Exception?

网上所说的原因都是因为 EXC_CRASH 不能通过 Mach 监控来抓捕。那为什么不能呢?网上我所有能找到的中文资料,都是如此解释(来源开源项目 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.

大致意思是说我们是在进程中监听 Mach Exception,在 EXC_Crash 发生的时候,会发生进程死锁。但是我一直没明白为啥会死锁。于是我又搜索了一下外文资料。发现有一篇文章大量的讲述了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.

我提炼一下上文的要点:

  1. EXC_Crash 表示进程是非正常退出。
  2. 当 EXC_Crash 发生的时候,这意味着进程即将被强杀,任何其他任务都不会被执行,所以Mach Exception Handler 不会执行。
  3. 类似 abort()的方法只会触发 signal 信号,根本不会触发hardware trap。

所以,我认为我们需要监听 Signal 的原因是 EXC_Crash 根本不能通过 Mach 监控来捕捉,和死锁无关。即便是和死锁有关,类似 abort()的场景也必须使用 Signal 监听。

Mach Exception 和 Signal 的转换关系

上图是网络上找来的一个对应关系,但我觉得这个对应关系只适合在 Mach Exception 处理的时候使用。在 Signal 处理的时候,建议使用如下的对应关系:

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

关于 Mach 和 Signal 的说明,还可以参考一下官方文档

为何要实现 NSException 监听

按照我们前面所说,通过 Mach/Signal 的方式我们已经可以监听绝大部分崩溃场景了,那为何我们还要实现NSException 监听呢?原因就是未被try catchNSException会发出killpthread_kill信号-> Mach异常-> Unix信号(SIGABRT),但是SIGABRT在处理收集信息时,获取当前堆栈时获取不到,所以采用`NSSetUncaughtExceptionHandler。具体如何获取堆栈我们会在下一篇文章讲解

捕捉 Swift 崩溃

一开始我以为 Swift 下的 exception 的处理过程和 NSException 类似,但实践后发现根本不是。

  • swift通常都是通过对应的signal来捕获crash。对于swift的崩溃捕获,Apple的文档中有描述说需要通过SIGTRAP信号捕获强转失败,及非可选的nil值导致的崩溃.具体描述如下:

    Trace Trap[EXC_BREAKPOINT // SIGTRAP]
    类似于异常退出,此异常旨在使附加的调试器有机会在其执行中的特定点中断进程。您可以使用该__builtin_trap()函数从您自己的代码触发此异常。如果没有附加调试器,则该过程将终止并生成崩溃报告。
    较低级的库(例如,libdispatch)会在遇到致命错误时捕获进程。有关错误的其他信息可以在崩溃报告的“ 附加诊断信息”部分或设备的控制台中找到。

    如果在运行时遇到意外情况,Swift代码将以此异常类型终止,例如:
    1.具有nil值的非可选类型
    2.一个失败的强制类型转换

  • 对于swift还有一种崩溃需要捕获(Intel处理器,我认为应该是指在模拟器上的崩溃),为保险起见,也需要将信号SIGILL进行注册,Apple同样对其中做了描述

    Illegal Instruction[EXC_BAD_INSTRUCTION // SIGILL]
    该过程尝试执行非法或未定义的指令。该过程可能尝试通过错误配置的函数指针跳转到无效地址。
    在Intel处理器上,ud2操作码引起EXC_BAD_INSTRUCTION异常,但通常用于进程调试目的。如果在运行时遇到意外情况,Intel处理器上的Swift代码将以此异常类型终止。有关详细信息,请参阅Trace Trap。

解除监听

细心的朋友可能会注意到前面监控 Crash 的代码都包括了类似一下的代码:

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

这是因为:1.保证一个 Crash 只会被一个 Handler 处理,避免多次处理;2.防止可能出现死锁导致应用不能退出。

多监控框架的共存

尽管从技术上讲多 Crash 监控框架共存是可能的。但是我并不喜欢这么做。当发生 Crash 的时候,app 已经处于一个不稳定的状态,过长的 Crash 处理链条会导致崩溃堆栈不准确,并且在处理过程中引入新的 Crash。如果你非要在崩溃的时候处理些额外的工作,大部分 Crash 监控框架,如 KSCrash 等,都提供了事件回调供你使用。

如果你们对这一块还是很感兴趣,可以参考下这篇文章:iOS/OSX Crash:捕捉异常

总结

虽然本篇总结的是 iOS 下的 Crash 监听,但由于 iOS/Mac OS 都是基于 Unix 的,所以其实很多内容是跨平台,在写本文的过程中我也找寻了很多 C 语言下的解决方案。

我尽量将我在学习这个知识所遇到的所有困惑以及收获都分享在这篇文章。但是依然可能有些遗漏和错误,欢迎各位指正。而撰写此文的过程中,有大量的内容整理自其他文章,其目的是尽可能在一篇文章完整的讲述相关内容。

最后,如果此文对你有帮助,不求赞赏,只求大家轻轻一个点赞

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容