iOS Crash 的监听

手机游戏开发者 2024-9-23 08:43:47 83 0 来自 中国
没想到都2021年,我还得写篇文章来讲讲 Crash 监听的一些事变。固然蛮多文章讲 Crash 监听这块,但总是讲的不敷深入或者说不敷全面。于是我想分享一下近来我对这方面知识的一些明白和整理。我筹划讲以下几个主题:

  • Crash 的监听
  • 堆栈分析
  • KSCrash 源码分析
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非常是内核级非常,在体系的位置如下图所示:
1.jpg 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 非常

参考上图,紧张的流程是:新建一个监控线程,在监控线程中监听 Mach 非常并处理处罚非常信息。紧张的步奏如下图:
4.png 具体代码如下:
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,就没法监控了。缘故起因是一样寻常环境下,信号处理处罚函数被调用时,内核会在历程的栈上为其创建一个栈帧。但这里就会有一个题目,如果之前栈的增长到达了栈的最大长度,或是栈没有到达最大长度但也比力接近,那么就会导致信号处理处罚函数不能得到充足栈帧分配。
为相识决这个题目,我们必要设定一个可选的栈帧

  • 申请一块内存空间作为可选的信号处理处罚函数栈使用
  • 使用 sigaltstack 函数关照体系可选的信号处理处罚栈帧的存在及其位置
  • 当使用 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 的输出。然后通过关键字找到你想要日记:
7.png 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
    移除未注册的观察者
    重复移除观察者
    添加了观察者但是没有实现-observeValueForKeyPathfObject: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-thread0   libsystem_kernel.dylib          0x00007fff93ef8d46 __kill + 101   libsystem_c.dylib               0x00007fff89968df0 abort + 1772   libc++abi.dylib                 0x00007fff8beb5a17 abort_message + 2573   libc++abi.dylib                 0x00007fff8beb33c6 default_terminate() + 284   libobjc.A.dylib                 0x00007fff8a196887 _objc_terminate() + 1115   libc++abi.dylib                 0x00007fff8beb33f5 safe_handler_caller(void (*)()) + 86   libc++abi.dylib                 0x00007fff8beb3450 std::terminate() + 167   libc++abi.dylib                 0x00007fff8beb45b7 __cxa_throw + 1118   test                            0x0000000102999f3b main + 759   libdyld.dylib                   0x00007fff8e4ab7e1 start + 1如何捕捉C++非常

为了得到C++非常的调用堆栈,我们必要模仿抛出NSException的过程并在此过程中生存调用堆栈。

  • 设置非常处理处罚函数
    g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);调用std::set_terminate设置新的全局停止处理处罚函数并生存旧的函数。
  • 重写__cxa_throw
    void __cxa_throw(void* thrown_exception, std::type_info* tinfo, void (*dest)(void*))在非常发生时,会先辈入此重写函数,应该先获取调用堆栈并存储;再调用原始的__cxa_throw函数。
  • 非常处理处罚函数
    __cxa_throw今后实行,进入set_terminate设置的非常处理处罚函数。判断如果检测是OC非常,则什么也不做,让OC非常机制处理处罚;否则获取非常信息。
差别范例的非常之间的关系

在前面,我们讲了几种非常范例及其细节。但是他们之间的关系我们还相识甚少。下面我就来讲讲差别非常之间大概存在的转换关系,以及优先次序等。
非常处理处罚的次序

起首,我们看一下下图,这个图很紧张
我大抵总结一下:

  • 如果是 NSException 范例的非常
    先看 app 是否 trycatch 了;再看有没有实现 NSSetUncaughtExceptionHandler;末了如果都没处理处罚,则调用 c 的 abort(),kernal 针对 app 发出 _pthread_kill 的信号,转为 Mach 非常。
  • 如果是 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。

  • 既然 Unix 信号都是由 Mach Exception 转化的,为啥还要转Unix 信号呢,直接传 Mach 的非常不就行了?
    这是为了兼容更为盛行的POSIX尺度,BSD在Mach非常机制之上构建的UNIX信号处理处罚机制。
  • 既然 Mach Exception 能转化为 Signal 信号,Signal 信号监听也更简朴,为什么不但监听 Signal 信号?
    不是全部的 "Mach非常” 范例都映射到了 “UNIX信号”。 如 EXC_GUARD 。在苹果开源的 xnu 源码中可以看到这点。
  • 为什么优先监听 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.
我提炼一下上文的要点:

  • EXC_Crash 表现历程优劣正常退出。
  • 当 EXC_Crash 发生的时间,这意味着历程即将被强杀,任何其他任务都不会被实行,以是Mach Exception Handler 不会实行。
  • 雷同 abort()的方法只会触发 signal 信号,根本不会触发hardware trap。
以是,我认为我们必要监听 Signal 的缘故起因是 EXC_Crash 根本不能通过 Mach 监控来捕捉,和死锁无关。即便是和死锁有关,雷同 abort()的场景也必须使用 Signal 监听。
Mach Exception 和 Signal 的转换关系

9.png 上图是网络上找来的一个对应关系,但我以为这个对应关系只得当在 Mach Exception 处理处罚的时间使用。在 Signal 处理处罚的时间,发起使用如下的对应关系:
signalexception typeSIGFPEEXC_ARITHMETICSIGSEGVEXC_BAD_ACCESSSIGBUSEXC_BAD_ACCESSSIGILLEXC_BAD_INSTRUCTIONSIGTRAPEXC_BREAKPOINTSIGEMTEXC_EMULATIONSIGSYSEXC_UNIX_BAD_SYSCALLSIGPIPEEXC_UNIX_BAD_PIPESIGABRTEXC_CRASHSIGKILLEXC_SOFT_SIGNAL关于 Mach 和 Signal 的说明,还可以参考一下官方文档
为何要实现 NSException 监听

按照我们前面所说,通过 Mach/Signal 的方式我们已经可以监听绝大部分瓦解场景了,那为何我们还要实现NSException 监听呢?缘故起因就是未被try catch的NSException会发出kill或pthread_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 语言下的办理方案。
我只管将我在学习这个知识所遇到的全部狐疑以及劳绩都分享在这篇文章。但是依然大概有些遗漏和错误,欢迎各位指正。而撰写此文的过程中,有大量的内容整理自其他文章,其目的是尽大概在一篇文章完备的报告相干内容。
末了,如果此文对你有资助,不求赞赏,只求各人轻轻一个点赞
iOS开发
作者:felix9
链接:https://www.jianshu.com/p/3f6775c02257
泉源:简书
著作权归作者全部。贸易转载请接洽作者得到授权,非贸易转载请注明出处。
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 20:21, Processed in 0.191925 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表