本篇总结了一下 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异常可以参见: 苹果开发者文档
注意:
-
在OC层如果有对应的NSException(OC异常),就转换成OC异常,OC异常可以在OC层得到处理;如果OC异常一直得不到处理,程序会强行发送SIGABRT信号中断程序。
-
在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时,以下几种情况也会抛出此异常:
- 一个非可选类型值为nil;
- 强制类型转换失败
-
EXC_ARITHMETIC
执行了无效的算术运算,包括除以0或取余0的情况;
-
EXC_SOFTWARE /EXC_CRASH
- SIGABRT, 发送此信号是因为进程调用了abort 函数,例如,当应用遇到未捕获的 Objective-C 或 C++异常时。
- SIGKILL, 此信号表示系统中止进程,通常是调用函数 exit() 或 kill(9) 产生。
其他:
-
崩溃报告中包含的代表中止原因的编码:
- 0x8badf00d:ate bad food系统监视程序由中止无响应应用。注意在生命周期的不同阶段,触发看门狗机制的超时时间是不一样的。
- 0xc00010ff:cool off,系统由于过热保护中止应用,通常与特定的手机和环境有关。
- 0xdead10cc:dead lock,系统中止在挂起期间一直保持文件锁或SQLite数据库锁的应用。
-
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 异常的大体思路是:
- 先创建一个异常处理的port,并申请权限
- 把原本的异常接收的 port 替换成自己新建port
- 创建一个线程去一直读取新的 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 的过程,并再在此过程中保存调用堆栈。
- 设置异常处理函数
调用 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;
}
}
- 重写__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();
}
- 异常处理函数
之后会进入通过 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 中各种捕获异常的方法,其中异常捕获的思路是比较相似的:
-
首先替换原来的异常捕获方法
-
捕获到异常后,会暂停所有线程,抓取所有线程信息,调用统一的异常处理函数
-
最后,恢复原来的异常捕获方法
参考