APM - iOS Crash 异常捕获原理

本篇总结了一下 iOS 中常见的异常的种类,以及通过常用的框架 KSCrash 分析了异常捕获的原理和方法。

一、系统层面的异常

软件异常

软件异常的主要来源是两个 API 的调用:kill()、pthread_kill(),iOS 中我们经常遇到的 NSException未捕获、abort() 函数调用等,都属于这种情况。比如在 Crash 堆栈中经常看到有 pthread_kill 方法的调用。

硬件异常

硬件异常的信号始于处理器 trap,处理器 trap 是平台相关的。比如我们遇到的"野指针崩溃" 大部分是硬件异常。

Mach 异常与 UNIX 信号

  • Mach 异常

    Mach是一个XNU的微内核核心,Mach异常是指最底层的内核级异常。 所有Mach异常未处理,它将在host层被转换为相应的Unix信号,并将信号投递到出错的线程。iOS中的 POSIX API 就是通过 Mach 之上的 BSD 层实现的。

下面通过苹果系统的架构图,了解一下上述各个名词的作用。我们使用的 iOS/ macOS 系统,是建立在名 Darwin 的底层操作系统上的,如图:

Darwin 的内核是 XNU,XNU是兼具宏内核和微内核特性的混合内核,架构分层如下:

什么是微内核?

XNU 的核心是 Mach 微内核,Mach微内核把系统服务,单独包装为一个个的模块,比如虚拟内存管理、设备 IO。 宏内核正相反,把所有系统服务放在一起。

微内核作为底层使用进程间通信收发消息,Mach 可以通过申请 port,然后利用 IPC 机制(进程间通信机制)想这个 post 发送消息。

Mach 微内核的几个概念:

  • 中断(Interrupt)

中断是一个硬件或软件发出的请求,要求 CPU 暂停当前的工作去处理更重的事情。是来自处理器外部的I/O异常信号导致的,不是由任何一条专门的指令造成的,从这个层面上来讲,它是异步的。硬件中断的处理程序通常被称为中断处理程序。比如:插拔U盘、键盘输入。

  • 陷阱 (Trap)

UNIX 下,触发到内核态需要通过系统调用触发陷阱。陷阱是指有意的异常,是执行一条指令的结果。和中断一样,陷阱处理程序将控制返回到下一条指令。陷阱最终的用途,是在用户程序和内核之间提供一个像过程一样的接口,称为系统调用。比如用户程序经常需要向内核请求服务,比如读一个文件(read)、创建一个进程(fork)、加载一个新的程序(execve)或者终止当前进程(exit),这些操作都需要通过触发陷进异常,来执行系统内核的程序来实现。

  • 故障

是由错误情况引起的,它可能被故障处理程序修正。当故障发生时,处理器将控制转移个故障处理程序。一般保护性故障,Unix不尝试修复,而是将这种保护性故障报告为段违规(Segmentation Violation)对应信号为SIGSEGV,然后终止程序。

  • 终止

由不可恢复的致命错误造成的结果。如:奇偶校验错误。

  • UNIX 信号

UNIX 信号(Signals)是 UNIX 系统中用于进程间通信(IPC)和进程管理的一种机制。信号允许一个进程通知另一个进程某个事件已经发生。我们常见的信号如 SIGBUS SIGSEGV SIGABRT SIGKILL 等。

小结

  • UNIX 信号可以看做是对硬件异常和软件异常的封装。

二、语言层面的异常

1. Objective-C Exception

如开发中经常遇见的 OC 异常有:

  • NSInvalidArgumentException

给方法传入了非法参数抛出的异常,如:给NSMutableDictionary、NSMutableArray的添加了 nil 元素时则会抛出此异常。

  • NSRangeException

尝试访问某些数据范围之外的数据时抛出的异常,如 NSArry 访问的索引越界了。

  • NSFileHandleOperationException

文件处理操作的异常,如空间不足、没有读写权限、读文件失败等等。

所有的OC异常可以参见: 苹果开发者文档

注意:

  1. 在OC层如果有对应的NSException(OC异常),就转换成OC异常,OC异常可以在OC层得到处理;如果OC异常一直得不到处理,程序会强行发送SIGABRT信号中断程序。

  2. 在OC层如果没有对应的NSException,就只能让Unix标准的signal机制来处理了。

2. C++异常

苹果开发同时支持OC和C++语法,其底层也基本上是由C++编写的,所以APP在IOS或OSX中运行时可能会抛C++异常。系统在捕获C++异常后的处理有两种情况:

1、如果此C++异常可以转换为OC异常,则抛给OC异常处理机制;

2、如果此C++异常不能转换为OC异常,则使用_cxa_rethrow()再次抛出

3. Mach 异常 与 UNIX 信号

二者的关系

Mach 异常与 Unix 信号亮着几乎是一一对应的:硬件产生的信号被 Mach 层捕获(也就是 Mach 异常),BSD 将其转换为 Unix 信号。

  • EXC_BAD_ACCESS

由于内存访问问题而导致崩溃。 内存访问问题有许多原因,例如,解引用指向无效内存地址的指针,写入只读内存,或跳转到无效地址处的指令。

diff 复制代码
- SIGSEGV,Signal Segmentation Violation

非法地址。访问未分配内存、写入没有写权限的内存等

  • SIGBUS

一般是由于地址未对齐导致的,例如内存地址对齐出错,或者试图执行没有权限的代码地址。子码有以下几种情况:

  • EXC_BREAKPOINT

    当使用swift时,以下几种情况也会抛出此异常:

  1. 一个非可选类型值为nil;
  2. 强制类型转换失败
  • EXC_ARITHMETIC

    执行了无效的算术运算,包括除以0或取余0的情况;

  • EXC_SOFTWARE /EXC_CRASH

    • SIGABRT, 发送此信号是因为进程调用了abort 函数,例如,当应用遇到未捕获的 Objective-C 或 C++异常时。
    • SIGKILL, 此信号表示系统中止进程,通常是调用函数 exit() 或 kill(9) 产生。

其他:

  1. 崩溃报告中包含的代表中止原因的编码:

    • 0x8badf00d:ate bad food系统监视程序由中止无响应应用。注意在生命周期的不同阶段,触发看门狗机制的超时时间是不一样的。
    • 0xc00010ff:cool off,系统由于过热保护中止应用,通常与特定的手机和环境有关。
    • 0xdead10cc:dead lock,系统中止在挂起期间一直保持文件锁或SQLite数据库锁的应用。
  2. SIGABRT 和 SIGKILL区别:

    两者都是发送给进程的中止信号。

    SIGKILL等价于"kill -9",它是用来杀死僵尸进程;而SIGABRT等价于"kill -6",它是用来杀死正在运行的进程。

    SIGKILL不能被捕获或忽略,接受进程也不能在收到此信号后做任何清理操作;而SIGABRT可以被捕获,但不能阻塞。

Mach 异常为什么还要转化为UNIX信号呢?

转换UNIX信号是为了兼容更为流行的POSIX标准(SUS规范),这样不必了解Mach内核也可以通过UNIX信号的方式来兼容开发。

Mach 异常如何转换成 UNIX 信号?

  • 异常处理线程

    当 BSD 进程(用户态进程)被 bsdinit_task 函数启动时,会初始化一个名为 ux_handle 的 Mach 内核线程,用于监听 Mach 异常消息,并将Mach 异常转换为信号。

  • 异常线程

    硬件产生的信号始于处理器陷阱。会将 Mach 异常消息抛给"异常处理线程" 去处理。

如图所示:

是否直接捕捉 UNIX 信号就可以,还需要捕捉 Mach 异常吗?

答案是需要的。首先 Mach 异常和 UNIX 信号都可以捕获,也几乎是一一对应的。但我们需要优先处理 Mach 异常,因为 Mach 异常更接近底层,在Mach 异常的处理函数中存在直接退出的情况,而无法生生 UNIX 信号。

如异常 EXC_CRASH ,在 Mach 异常阶段没有被捕获,而是放到了 UNIX 信号中捕获的。其中的解释在PLCrashReporter的注释中有详细的解释:(大概意思是会发生死锁,所以没处理)

css 复制代码
/* 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. */

小结

  • 整个异常机制是建立在 Mach 异常之上的,所有软件/硬件异常都会先转换成 Mach 异常,进而转为UNIX信号。

  • 整个流程是这样的:硬件产生信号或者kill或pthread_kill信号 --> Mach异常 --> Unix信号(SIGABRT)。

三、异常的捕获 (KSCrash)

KSCrash 是 iOS 中异常捕获的一个知名的开源框架,KSCrash 功能齐全,可以捕获如下类型的 Crash

  • Objective-C exceptions
  • Mach kernel exceptions
  • Fatal signals
  • C++ exceptions
  • Main thread deadlock (experimental)
  • Custom crashes (e.g. from scripting languages)

下面主要分析一下 KSCrash 中关于不同种类异常的收集方案。

1. Objective-C 异常的捕获

对于OC 层面的异常处理比较简单,通过注册 NSUncaughtExceptionHandler 回调函数来捕获异常信息,通过 NSException 参数对 Crash 信息的封装,从而再进行上报。

KSCrash 中的源码:(KSCrashMonitor_NSException):

Objc 复制代码
static void setEnabled(bool isEnabled)
{
    if(isEnabled != g_isEnabled)
    {
        g_isEnabled = isEnabled;
        if(isEnabled)
        {
            KSLOG_DEBUG(@"Backing up original handler.");
            // 记录之前的 OC 异常处理函数
            g_previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
            
            KSLOG_DEBUG(@"Setting new handler.");
            // 设置新的 OC 异常处理函数
            NSSetUncaughtExceptionHandler(&handleException);
            KSCrash.sharedInstance.uncaughtExceptionHandler = &handleException;
        }
        else
        {
            KSLOG_DEBUG(@"Restoring original handler.");
            NSSetUncaughtExceptionHandler(g_previousUncaughtExceptionHandler);
        }
    }
}

2. Mach 异常的捕获

在介绍代码之前,介绍几个 Mach 微内核的基础概念:

  • Tasks,拥有一组系统资源的对象,允许 thread 在其中执行。每一个 BSD 进程都在底层关联了一个 Mach 任务对象。(BSD 在 Mach 之上,提供更高层次的功能,如 UNIX 进程模型等)

  • Threads,执行的基本单位,拥有 task 的上下文,并共享其资源。

  • Ports,task 之间通讯的一组消息队列;task 可以对任何 port 发送、接收数据。

  • Message,有类型的数据对象集合,只可以发送到 port。

2.1 开启捕获

捕获 Mach 异常的大体思路是:

  1. 先创建一个异常处理的port,并申请权限
  2. 把原本的异常接收的 port 替换成自己新建port
  3. 创建一个线程去一直读取新的 port 上的消息

KSCrash 中的源码:(KSCrashMonitor_MachException)

Objc 复制代码
static bool installExceptionHandler()
{
    KSLOG_DEBUG("Installing mach exception handler.");

    bool attributes_created = false;
    pthread_attr_t attr;

    kern_return_t kr;
    int error;
    // 拿到当前进程
    const task_t thisTask = mach_task_self();
    exception_mask_t mask = EXC_MASK_BAD_ACCESS |
    EXC_MASK_BAD_INSTRUCTION |
    EXC_MASK_ARITHMETIC |
    EXC_MASK_SOFTWARE |
    EXC_MASK_BREAKPOINT;

    KSLOG_DEBUG("Backing up original exception ports.");
    // 获取该 Task 上的注册好的异常端口
    kr = task_get_exception_ports(thisTask,
                                  mask,
                                  g_previousExceptionPorts.masks,
                                  &g_previousExceptionPorts.count,
                                  g_previousExceptionPorts.ports,
                                  g_previousExceptionPorts.behaviors,
                                  g_previousExceptionPorts.flavors);
    // 获取失败走 failed 逻辑
    if(kr != KERN_SUCCESS)
    {
        KSLOG_ERROR("task_get_exception_ports: %s", mach_error_string(kr));
        goto failed;
    }
    // KSCrash 的异常为空则走执行逻辑
    if(g_exceptionPort == MACH_PORT_NULL)
    {
        KSLOG_DEBUG("Allocating new port with receive rights.");
        // 申请异常处理端口
        kr = mach_port_allocate(thisTask,
                                MACH_PORT_RIGHT_RECEIVE,
                                &g_exceptionPort);
        if(kr != KERN_SUCCESS)
        {
            KSLOG_ERROR("mach_port_allocate: %s", mach_error_string(kr));
            goto failed;
        }

        KSLOG_DEBUG("Adding send rights to port.");
        // 为异常处理端口申请权限:MACH_MSG_TYPE_MAKE_SEND
        kr = mach_port_insert_right(thisTask,
                                    g_exceptionPort,
                                    g_exceptionPort,
                                    MACH_MSG_TYPE_MAKE_SEND);
        if(kr != KERN_SUCCESS)
        {
            KSLOG_ERROR("mach_port_insert_right: %s", mach_error_string(kr));
            goto failed;
        }
    }

    KSLOG_DEBUG("Installing port as exception handler.");
    // 为该 Task 设置异常处理端口
    kr = task_set_exception_ports(thisTask,
                                  mask,
                                  g_exceptionPort,
                                  EXCEPTION_DEFAULT,
                                  THREAD_STATE_NONE);
    if(kr != KERN_SUCCESS)
    {
        KSLOG_ERROR("task_set_exception_ports: %s", mach_error_string(kr));
        goto failed;
    }

    KSLOG_DEBUG("Creating secondary exception thread (suspended).");
    pthread_attr_init(&attr);
    attributes_created = true;
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    // 设置监控线程
    error = pthread_create(&g_secondaryPThread,
                           &attr,
                           &handleExceptions,
                           kThreadSecondary);
    if(error != 0)
    {
        KSLOG_ERROR("pthread_create_suspended_np: %s", strerror(error));
        goto failed;
    }
    // 转换为 Mach 内核线程
    g_secondaryMachThread = pthread_mach_thread_np(g_secondaryPThread);
    ksmc_addReservedThread(g_secondaryMachThread);

    KSLOG_DEBUG("Creating primary exception thread.");
    error = pthread_create(&g_primaryPThread,
                           &attr,
                           &handleExceptions,
                           kThreadPrimary);
    if(error != 0)
    {
        KSLOG_ERROR("pthread_create: %s", strerror(error));
        goto failed;
    }
    pthread_attr_destroy(&attr);
    g_primaryMachThread = pthread_mach_thread_np(g_primaryPThread);
    ksmc_addReservedThread(g_primaryMachThread);
    
    KSLOG_DEBUG("Mach exception handler installed.");
    return true;


failed:
    KSLOG_DEBUG("Failed to install mach exception handler.");
    if(attributes_created)
    {
        pthread_attr_destroy(&attr);
    }
    // 还原之前的异常注册端口,将控制权还原
    uninstallExceptionHandler();
    return false;
}
 

大体流程是这样的:

2.2 处理异常

下面是关键的异常处理方法:

Objc 复制代码
/// 处理 Exception
static void* handleExceptions(void* const userData)
{
    MachExceptionMessage exceptionMessage = {{0}};
    MachReplyMessage replyMessage = {{0}};
    char* eventID = g_primaryEventID;

    const char* threadName = (const char*) userData;
    pthread_setname_np(threadName);
    if(threadName == kThreadSecondary)
    {
        KSLOG_DEBUG("This is the secondary thread. Suspending.");
        thread_suspend((thread_t)ksthread_self());
        eventID = g_secondaryEventID;
    }

    for(;;)
    {
        KSLOG_DEBUG("Waiting for mach exception");

        // Wait for a message.
        /// 不断调用 mach_msg 接收消息,从异常端口中读取信息到 exceptionMessage 中
        kern_return_t kr = mach_msg(&exceptionMessage.header,
                                    MACH_RCV_MSG,
                                    0,
                                    sizeof(exceptionMessage),
                                    g_exceptionPort,
                                    MACH_MSG_TIMEOUT_NONE,
                                    MACH_PORT_NULL);
        /// 上面一直循环读取,直到读取成功了,进入后面的处理函数中
        if(kr == KERN_SUCCESS)
        {
            break;
        }

        // Loop and try again on failure.
        KSLOG_ERROR("mach_msg: %s", mach_error_string(kr));
    }

    KSLOG_DEBUG("Trapped mach exception code 0x%llx, subcode 0x%llx",
                exceptionMessage.code[0], exceptionMessage.code[1]);
    if(g_isEnabled)
    {
        thread_act_array_t threads = NULL;
        mach_msg_type_number_t numThreads = 0;
        /// 暂停所有非当前线程以及白名单线程的线程
        ksmc_suspendEnvironment(&threads, &numThreads);
        g_isHandlingCrash = true;
        /// 捕捉到异常之后清除所有的 monitor
        kscm_notifyFatalExceptionCaptured(true);

        KSLOG_DEBUG("Exception handler is installed. Continuing exception handling.");


        // Switch to the secondary thread if necessary, or uninstall the handler
        // to avoid a death loop.
        /// 捕捉到 exception 后,恢复原来的 port
        if(ksthread_self() == g_primaryMachThread)
        {
            KSLOG_DEBUG("This is the primary exception thread. Activating secondary thread.");
// TODO: This was put here to avoid a freeze. Does secondary thread ever fire?
            restoreExceptionPorts();
            if(thread_resume(g_secondaryMachThread) != KERN_SUCCESS)
            {
                KSLOG_DEBUG("Could not activate secondary thread. Restoring original exception ports.");
            }
        }
        else
        {
            KSLOG_DEBUG("This is the secondary exception thread.");// Restoring original exception ports.");
//            restoreExceptionPorts();
        }

        // Fill out crash information
        /// 设置 crash 信息的 context
        KSLOG_DEBUG("Fetching machine state.");
        /// 创建一个 machineContext 用来保存异常信息
        KSMC_NEW_CONTEXT(machineContext);
        KSCrash_MonitorContext* crashContext = &g_monitorContext;
        crashContext->offendingMachineContext = machineContext;
        /// 创建一个遍历调用栈的 cursor
        kssc_initCursor(&g_stackCursor, NULL, NULL);
        /// 把线程信息附加到 machineContext 上
        if(ksmc_getContextForThread(exceptionMessage.thread.name, machineContext, true))
        {
            kssc_initWithMachineContext(&g_stackCursor, 100, machineContext);
            KSLOG_TRACE("Fault address %p, instruction address %p",
                        kscpu_faultAddress(machineContext), kscpu_instructionAddress(machineContext));
            if(exceptionMessage.exception == EXC_BAD_ACCESS)
            {
                crashContext->faultAddress = kscpu_faultAddress(machineContext);
            }
            else
            {
                crashContext->faultAddress = kscpu_instructionAddress(machineContext);
            }
        }

        KSLOG_DEBUG("Filling out context.");
        crashContext->crashType = KSCrashMonitorTypeMachException;
        crashContext->eventID = eventID;
        crashContext->registersAreValid = true;
        crashContext->mach.type = exceptionMessage.exception;
        crashContext->mach.code = exceptionMessage.code[0] & (int64_t)MACH_ERROR_CODE_MASK;
        crashContext->mach.subcode = exceptionMessage.code[1] & (int64_t)MACH_ERROR_CODE_MASK;
        if(crashContext->mach.code == KERN_PROTECTION_FAILURE && crashContext->isStackOverflow)
        {
            // A stack overflow should return KERN_INVALID_ADDRESS, but
            // when a stack blasts through the guard pages at the top of the stack,
            // it generates KERN_PROTECTION_FAILURE. Correct for this.
            crashContext->mach.code = KERN_INVALID_ADDRESS;
        }
        /// 将 mach 异常转为对应的 signal
        crashContext->signal.signum = signalForMachException(crashContext->mach.type, crashContext->mach.code);
        crashContext->stackCursor = &g_stackCursor;

        /// context 交给 kscrashmonitor 处理
        kscm_handleException(crashContext);

        KSLOG_DEBUG("Crash handling complete. Restoring original handlers.");
        g_isHandlingCrash = false;
        /// 结束了捕获恢复所有线程
        ksmc_resumeEnvironment(threads, numThreads);
    }

    KSLOG_DEBUG("Replying to mach exception message.");
    // Send a reply saying "I didn't handle this exception".
    replyMessage.header = exceptionMessage.header;
    replyMessage.NDR = exceptionMessage.NDR;
    replyMessage.returnCode = KERN_FAILURE;

    /// 发消息告知没有处理这个异常
    mach_msg(&replyMessage.header,
             MACH_SEND_MSG,
             sizeof(replyMessage),
             0,
             MACH_PORT_NULL,
             MACH_MSG_TIMEOUT_NONE,
             MACH_PORT_NULL);

    return NULL;
}

异常处理的大概过程是:

3. UNIX 信号的捕获

Mach 异常会在 BSD 层转换为相应的 UNIX 信号,投递到相应的线程中,我们同样也可以捕获相应的 Signal。大体逻辑如图:

3.1 信号类型

KSCrash 中捕获的信号有:(KSCrashMonitor_Signal)

Objc 复制代码
static const int g_fatalSignals[] =
{
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGPIPE,
    SIGSEGV,
    SIGSYS,
    SIGTRAP,
};

3.2 开启捕获

主要是通过 sigaction() 方法为每个 signal 设置对应的异常处理方法,同时会保存之前的处理方法。

Objc 复制代码
static bool installSignalHandler()
{
    KSLOG_DEBUG("Installing signal handler.");

#if KSCRASH_HAS_SIGNAL_STACK

    if(g_signalStack.ss_size == 0)
    {
        KSLOG_DEBUG("Allocating signal stack area.");
        g_signalStack.ss_size = SIGSTKSZ;
        g_signalStack.ss_sp = malloc(g_signalStack.ss_size);
    }

    KSLOG_DEBUG("Setting signal stack area.");
    if(sigaltstack(&g_signalStack, NULL) != 0)
    {
        KSLOG_ERROR("signalstack: %s", strerror(errno));
        goto failed;
    }
#endif

    /// 需要监听的 signal 数组
    const int* fatalSignals = kssignal_fatalSignals();
    /// 需要监听的 signal 数组大小
    int fatalSignalsCount = kssignal_numFatalSignals();

    if(g_previousSignalHandlers == NULL)
    {
        KSLOG_DEBUG("Allocating memory to store previous signal handlers.");
        g_previousSignalHandlers = malloc(sizeof(*g_previousSignalHandlers)
                                          * (unsigned)fatalSignalsCount);
    }

    struct sigaction action = {{0}};
    action.sa_flags = SA_SIGINFO | SA_ONSTACK;
#if KSCRASH_HOST_APPLE && defined(__LP64__)
    action.sa_flags |= SA_64REGSET;
#endif
    sigemptyset(&action.sa_mask);
    action.sa_sigaction = &handleSignal;

    for(int i = 0; i < fatalSignalsCount; i++)
    {
        KSLOG_DEBUG("Assigning handler for signal %d", fatalSignals[i]);
        // 将每个信号的处理函数绑定到上面声明的 action 去,另外用 g_previousSignalHandlers 保存当前信号的处理函数
        if(sigaction(fatalSignals[i], &action, &g_previousSignalHandlers[i]) != 0)
        {
            /// 设置失败的时候走下面的方法
            char sigNameBuff[30];
            const char* sigName = kssignal_signalName(fatalSignals[i]);
            if(sigName == NULL)
            {
                snprintf(sigNameBuff, sizeof(sigNameBuff), "%d", fatalSignals[i]);
                sigName = sigNameBuff;
            }
            KSLOG_ERROR("sigaction (%s): %s", sigName, strerror(errno));
            // Try to reverse the damage
            for(i--;i >= 0; i--)
            {
                sigaction(fatalSignals[i], &g_previousSignalHandlers[i], NULL);
            }
            goto failed;
        }
    }
    KSLOG_DEBUG("Signal handlers installed.");
    return true;

failed:
    KSLOG_DEBUG("Failed to install signal handlers.");
    return false;
}

注意:

先从堆上分配一块内存区域,被称为"可替换信号栈",目的是将信号处理函数的栈干掉,用堆上的内存区域代替,而不和进程共用一块栈区。

为什么这么做?
一个进程可能有 n 个线程,每个线程都有自己的任务,假如某个线程执行出错,这样就会导致整个进程的崩溃。所以为了信号处理函数正常运行,需要为信号处理函数设置单独的运行空间。另一种情况是递归函数将系统默认的栈空间用尽了,但是信号处理函数使用的栈是它实现在堆中分配的空间,而不是系统默认的栈,所以它仍旧可以正常工作。

3.3 处理异常

与 Mach 异常的处理流程类似,先暂停线程,然后读取线程信息,再把 signal、以及线程信息保存在 context 中,然后传递给外部函数,最后恢复环境。

Objc 复制代码
static void handleSignal(int sigNum, siginfo_t* signalInfo, void* userContext)
{
    KSLOG_DEBUG("Trapped signal %d", sigNum);
    if(g_isEnabled)
    {
        thread_act_array_t threads = NULL;
        mach_msg_type_number_t numThreads = 0;
        /// 暂停线程
        ksmc_suspendEnvironment(&threads, &numThreads);
        /// 通知已经捕获到异常了
        kscm_notifyFatalExceptionCaptured(false);

        KSLOG_DEBUG("Filling out context.");
        KSMC_NEW_CONTEXT(machineContext);
        /// 保存 context 到 machineContext 中,并且获取 thread 信息
        ksmc_getContextForSignal(userContext, machineContext);
        /// 把 machineContext 放到  g_stackCursor 中
        kssc_initWithMachineContext(&g_stackCursor, 100, machineContext);

        /// 生成真正的 context
        KSCrash_MonitorContext* crashContext = &g_monitorContext;
        memset(crashContext, 0, sizeof(*crashContext));
        crashContext->crashType = KSCrashMonitorTypeSignal;
        crashContext->eventID = g_eventID;
        crashContext->offendingMachineContext = machineContext;
        crashContext->registersAreValid = true;
        crashContext->faultAddress = (uintptr_t)signalInfo->si_addr;
        crashContext->signal.userContext = userContext;
        crashContext->signal.signum = signalInfo->si_signo;
        crashContext->signal.sigcode = signalInfo->si_code;
        crashContext->stackCursor = &g_stackCursor;

        /// 把 context 传给外部处理函数
        kscm_handleException(crashContext);
        /// 恢复原来的环境
        ksmc_resumeEnvironment(threads, numThreads);
    }

    KSLOG_DEBUG("Re-raising signal for regular handlers to catch.");
    // This is technically not allowed, but it works in OSX and iOS.
    /// 重新抛出 signal
    raise(sigNum);
}

4. C++异常的捕获

4.1 为什么要捕获 C++异常?

答案是无法获得原始异常发生的堆栈,此时的调用堆栈是异常发生时的堆栈。

Objc 复制代码
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++ 抛出异常的流程:

iOS 工程中的某些功能或者库的实现使用了 C、C++等。当抛出 C++ 异常后,系统会尝试将该异常转换为 NSException,走 OC 的异常处理机制。如果不能转换,则继续走 C++ 异常流程,最后会触发一个 abort 调用,产生一个 SIGABRT 信号。

其中,当C++ 异常被抛出后,系统通过 try- catch 来判断该异常是否可以转换为 NSException,再重新抛出 C++ 异常,此时异常的现场堆栈已经消失了,所以上层再通过捕获 SIGABRT 信号也是无法还原发生异常时的场景,即异常堆栈缺失

为什么会缺失?

其中,try- catch 语句内部会调用 __cxa_throw 重新抛出异常, __cxa_throw 内部会调用 unwind ,unwind 可以理解为函数的逆调用,用来清理函数调用过程中每个函数生成的局部变量,一直到 try-catch 所在的函数,这就是 C++异常的堆栈消失的原因。

4.2 开启捕获

自己捕获的目的是获得 C++ 异常的调用堆栈,主要流程就是模拟转换 NSException 的过程,并再在此过程中保存调用堆栈。

  1. 设置异常处理函数

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

KSCrash 中的源码:(KSCrashMonitor_CPPException)

Objc 复制代码
static void setEnabled(bool isEnabled)
{
    if(isEnabled != g_isEnabled)
    {
        g_isEnabled = isEnabled;
        if(isEnabled)
        {
            initialize();

            ksid_generate(g_eventID);
            /// 保存原始的 c++ 处理 handler,设置自己的处理 handler
            g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);
        }
        else
        {
            /// 恢复原始的 c++ 处理 handler
            std::set_terminate(g_originalTerminateHandler);
        }
        g_captureNextStackTrace = isEnabled;
    }
}
  1. 重写__cxa_throw

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

Objc 复制代码
  void __cxa_throw(void* thrown_exception, std::type_info* tinfo, void (*dest)(void*))
    {
        static cxa_throw_type orig_cxa_throw = NULL;
        if (g_cxaSwapEnabled == false)
        {	//获取调用堆栈
            captureStackTrace(NULL, NULL, NULL);
        }
        unlikely_if(orig_cxa_throw == NULL)
        {
            orig_cxa_throw = (cxa_throw_type) dlsym(RTLD_NEXT, "__cxa_throw");
        }
        orig_cxa_throw(thrown_exception, tinfo, dest);
        __builtin_unreachable();
    }
  1. 异常处理函数

之后会进入通过 set_terminate 设置自定义的异常处理函数。如果是 OC 异常,则什么都不做,让 OC 异常机制处理。否则获取异常信息。

Objc 复制代码
static void CPPExceptionTerminate(void)
{
    thread_act_array_t threads = NULL;
    mach_msg_type_number_t numThreads = 0;
    /// 挂起非处理现场和白名单线程的其他所有线程
    ksmc_suspendEnvironment(&threads, &numThreads);
    KSLOG_DEBUG("Trapped c++ exception");
    const char* name = NULL;
    std::type_info* tinfo = __cxxabiv1::__cxa_current_exception_type();
    if(tinfo != NULL)
    {
        name = tinfo->name();
    }
    
    if(name == NULL || strcmp(name, "NSException") != 0)
    {
        /// 捕捉到 crash 后,清空 KSCrash 的所有 monitor
        kscm_notifyFatalExceptionCaptured(false);
        KSCrash_MonitorContext* crashContext = &g_monitorContext;
        memset(crashContext, 0, sizeof(*crashContext));

        char descriptionBuff[DESCRIPTION_BUFFER_LENGTH];
        const char* description = descriptionBuff;
        descriptionBuff[0] = 0;

        KSLOG_DEBUG("Discovering what kind of exception was thrown.");
        g_captureNextStackTrace = false;
        try
        {
            throw;
        }
        catch(std::exception& exc)
        {
            strncpy(descriptionBuff, exc.what(), sizeof(descriptionBuff));
        }
#define CATCH_VALUE(TYPE, PRINTFTYPE) \
catch(TYPE value)\
{ \
    snprintf(descriptionBuff, sizeof(descriptionBuff), "%" #PRINTFTYPE, value); \
}
        CATCH_VALUE(char,                 d)
        CATCH_VALUE(short,                d)
        CATCH_VALUE(int,                  d)
        CATCH_VALUE(long,                ld)
        CATCH_VALUE(long long,          lld)
        CATCH_VALUE(unsigned char,        u)
        CATCH_VALUE(unsigned short,       u)
        CATCH_VALUE(unsigned int,         u)
        CATCH_VALUE(unsigned long,       lu)
        CATCH_VALUE(unsigned long long, llu)
        CATCH_VALUE(float,                f)
        CATCH_VALUE(double,               f)
        CATCH_VALUE(long double,         Lf)
        CATCH_VALUE(char*,                s)
        catch(...)
        {
            description = NULL;
        }
        g_captureNextStackTrace = g_isEnabled;

        // TODO: Should this be done here? Maybe better in the exception handler?
        KSMC_NEW_CONTEXT(machineContext);
        ksmc_getContextForThread(ksthread_self(), machineContext, true);

        KSLOG_DEBUG("Filling out context.");
        crashContext->crashType = KSCrashMonitorTypeCPPException;
        crashContext->eventID = g_eventID;
        crashContext->registersAreValid = false;
        crashContext->stackCursor = &g_stackCursor;
        crashContext->CPPException.name = name;
        crashContext->exceptionName = name;
        crashContext->crashReason = description;
        crashContext->offendingMachineContext = machineContext;

        /// 处理异常
        kscm_handleException(crashContext);
    }
    else
    {
        KSLOG_DEBUG("Detected NSException. Letting the current NSException handler deal with it.");
    }
    /// 恢复线程
    ksmc_resumeEnvironment(threads, numThreads);

    KSLOG_DEBUG("Calling original terminate handler.");
    /// 触发原本的  handler
    g_originalTerminateHandler();
}

5. DeadLock

KSCrash 中主线程死锁的检测和 ANR (Application Not Responding)检测的原理有些类似,大概流程是:

  • 创建一个子线程,在子线程的方法里面通过 do-while 循环保持线程不退出
  • 回到主线程中更改一个变量的值,5s(时间长短可自设)后判断该变量的值是否发生改变。若没有发生改变,则判定主线程发生死锁。否则继续循环该流程。

子线程运行的方法,KSCrash 中的源码:(KSCrashMonitor_NSException):

Objc 复制代码
- (void) runMonitor
{
    BOOL cancelled = NO;
    do
    {
        // Only do a watchdog check if the watchdog interval is > 0.
        // If the interval is <= 0, just idle until the user changes it.
        @autoreleasepool {
            NSTimeInterval sleepInterval = g_watchdogInterval;
            BOOL runWatchdogCheck = sleepInterval > 0;
            if(!runWatchdogCheck)
            {
                sleepInterval = kIdleInterval;//默认 5s
            }
            // 睡眠 
            [NSThread sleepForTimeInterval:sleepInterval];
            cancelled = self.monitorThread.isCancelled;
            if(!cancelled && runWatchdogCheck)
            {
                if(self.awaitingResponse)//判断标志变量是否发生改变
                {
                	// 处理死锁
                    [self handleDeadlock];
                }
                else
                {
                	// 回主线程更改变量
                    [self watchdogPulse];
                }
            }
        }
    } while (!cancelled);
}

回主线程重置变量的方法:

ini 复制代码
- (void) watchdogPulse
{
    __block id blockSelf = self;
    self.awaitingResponse = YES;
    // 回主线程更改变量
    dispatch_async(dispatch_get_main_queue(), ^{
       [blockSelf watchdogAnswer];
  	});
}

// 重置变量
- (void) watchdogAnswer
{
    self.awaitingResponse = NO;
}

6. 其他不能捕获的异常

不能被捕获的异常包括:部分Mach信号(如EXC_RESOURCE),卡死、爆内存等。

6.1 爆内存

当App在前台时,系统在App占用的内存达到了系统对单个App占用的内存上限后,就会杀掉App进程。

当App在后台时,系统如果发现内存压力过大,也会按照一定的优先级规则来杀掉后台进程。

杀掉进程的表现是:通过在BSD层产生SIGKILL信号来杀掉进程,同时产生Jetsam log。

关于爆内存,参考iOS 内存 Jetsam 机制探究

总结

本篇总结了iOS 中异常的类型以及常用框架 KScrash 中各种捕获异常的方法,其中异常捕获的思路是比较相似的:

  1. 首先替换原来的异常捕获方法

  2. 捕获到异常后,会暂停所有线程,抓取所有线程信息,调用统一的异常处理函数

  3. 最后,恢复原来的异常捕获方法


参考

iOS/OSX Crash:异常类型

iOS Crash 收集框架 KSCrash 源码解析

相关推荐
XiaoYu20022 小时前
22.JS高级-ES6之Symbol类型与Set、Map数据结构
前端·javascript·代码规范
小飞猪Jay13 小时前
C++面试速通宝典——13
jvm·c++·面试
yanlele17 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
睡觉然后上课18 小时前
c基础面试题
c语言·开发语言·c++·面试
xgq18 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
邵泽明20 小时前
面试知识储备-多线程
java·面试·职场和发展
missmisslulu21 小时前
电容笔值得买吗?2024精选盘点推荐五大惊艳平替电容笔!
学习·ios·电脑·平板
夜流冰21 小时前
工具方法 - 面试中回答问题的技巧
面试·职场和发展
GEEKVIP1 天前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone