APP Crash相关内容是APP开发者都需要了解的, 这篇文章就针对iOS APP Crash做一个简单小结.
Mach异常与Unix信号
Darwin
是Mac OS
和iOS
的操作系统, 而XNU是Darwin操作系统的内核部分. XNU是混合内核, 兼具宏内核和微内核的特性, 而Mach即为其微内核, Mach微内核中有几个基础概念:
- Tasks, 拥有一组系统资源的对象, 允许
thread
在其中执行。 - Threads, 执行的基本单位, 拥有task的上下文, 并共享其资源。
- Ports, Task之间通讯的一组受保护的消息队列; task可对任何port发送/接收数据。
- Message, 有类型的数据对象集合,只可以发送到port。
如果APP没有进行Crash捕获, 一般来说APP 的Crash(通常的情况是闪退), 是因为the result of an unhandled signal sent to your application
. 这里苹果为了让iOS/Mac OS
系统能支持POSIX
标准, 在Mach
内核上层封装了BSD
层, 因此Mach 异常
会在BSD
层封装成Signal
, 然后发送给APP!!!
这些导致APP Crash的unhandled signal
一般来自3个地方:
- kernel内核
- 其他的进程process
- Application自己
其中最常见的两种Crash关联的signal
是:
EXC_BAD_ACCESS
: APP访问了不属于本进程或者已经释放的内存地址。如果没有在Mach
层捕获, 它将在BSD
层被转化成SIGSEGV
或SIGBUS
BSD信号。SIGABRT
: 是一个标准的BSD signal
. 它是因为NSException
或者obj_exception_throw
没有在应用层catch
.
如果是Objective-C
中的异常, 最常见的Crash原因是:
- sending an unimplemented selector to an object -- 给OC对象发送消息没有IMP实现! (OC是动态语言, OC对象调用方法实际是消息发送)
- sending to an already released -- 给已经release的OC对象继续发送消息!!! (野指针, 不一定100%Crash, 可能运气好没有Crash)
我们常常收到的如下Crash:
EXC_BAD_ACCESS (SIGSEGV)
, 它表示是Mach层
的EXC_BAD_ACCESS异常
, 在BSD
层封装成SIGSEGV signal
捕获异常的方式
为了监控Crash, 我们一般需要捕获异常, 通常一般需要两条线:
- 使用
NSUncaughtExceptionHandler
去捕获Objective-C Exceptions
, 例如OC层中的NSException
的子类异常 - 使用 Linux 的API
sighandler_t signal(int signum, sighandler_t handler);
来捕获BSD signal
信号
在代码中可以用如下方式:
void InstallUncaughtExceptionHandler() {
// 1. OC 层的NSException 异常的捕获
NSSetUncaughtExceptionHandler(&HandleException);
// 2. BSD 层的signal信号捕获
// SIGKILL and SIGSTOP 两个信号是无法捕获的
signal(SIGABRT, SignalHandler);
signal(SIGILL, SignalHandler);
signal(SIGSEGV, SignalHandler);
signal(SIGFPE, SignalHandler);
signal(SIGBUS, SignalHandler);
signal(SIGPIPE, SignalHandler);
}
另外, 第三方框架例如 PLCrashReporter
,KSCrash
等会优先在Mach
层捕获一些Crash, 而不用等到异常到BSD层被封装成signal以后再捕获, :
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.
这里有一个更加完整优秀的Crash生成和捕获的流程图:
KSCrash
中也捕获了Mach kenel Exception
, signal
, C++ Exception
, Objective-C NSException
等异常类型:
/** Various aspects of the system that can be monitored:
* - Mach kernel exception
* - Fatal signal
* - Uncaught C++ exception
* - Uncaught Objective-C NSException
* - Deadlock on the main thread
* - User reported custom exception
*/
typedef enum {
/* Captures and reports Mach exceptions. */
KSCrashMonitorTypeMachException = 0x01,
/* Captures and reports POSIX signals. */
KSCrashMonitorTypeSignal = 0x02,
/* Captures and reports C++ exceptions.
* Note: This will slightly slow down exception processing.
*/
KSCrashMonitorTypeCPPException = 0x04,
/* Captures and reports NSExceptions. */
KSCrashMonitorTypeNSException = 0x08,
/* Detects and reports a deadlock in the main thread. */
KSCrashMonitorTypeMainThreadDeadlock = 0x10,
/* Accepts and reports user-generated exceptions. */
KSCrashMonitorTypeUserReported = 0x20,
/* Keeps track of and injects system information. */
KSCrashMonitorTypeSystem = 0x40,
/* Keeps track of and injects application state. */
KSCrashMonitorTypeApplicationState = 0x80,
/* Keeps track of zombies, and injects the last zombie NSException. */
KSCrashMonitorTypeZombie = 0x100,
} KSCrashMonitorType;
Mach Exception 的异常捕获
对于 Mach 异常, KSCrash
中注册mach exception handler
的下逻辑:
// 注册 mach exception handler
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.");
kr = task_get_exception_ports(thisTask,
mask,
g_previousExceptionPorts.masks,
&g_previousExceptionPorts.count,
g_previousExceptionPorts.ports,
g_previousExceptionPorts.behaviors,
g_previousExceptionPorts.flavors);
if (kr != KERN_SUCCESS) {
KSLOG_ERROR("task_get_exception_ports: %s", mach_error_string(kr));
goto failed;
}
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.");
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.");
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;
}
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;
}
用一个图简单表示核心逻辑:
简单Demo实现可以参考: iOS Mach异常和signal信号
C++ 异常的处理
以下内容部分参考: zhuanlan.zhihu.com/p/271282052
在OSX中,会通过对话框展示异常给用户,但在iOS中,只是重新抛出异常。
系统在捕捉到C++异常后,如果能够将此C++异常转换为OC异常,则抛出OC异常处理机制;如果不能转换,则会立刻调用__cxa_throw
重新抛出异常。
当系统在RunLoop捕捉到的C++异常时, 此时的调用堆栈是异常发生时的堆栈(我们需要的调用栈), 但如果这个异常无法被转换为Objective-C Exception(NSException)
时, 会调用__cxa_throw
, 上层捕捉以后再抛出的异常, 此时获取到的调用堆栈是RunLoop异常处理函数的堆栈,导致原始异常调用堆栈丢失(C++的真实异常函数调用栈丢失!!!)。
这里有一个标准的C++ 异常
(无法转化成Objective-C 异常
), 能看到实际的c++调用堆栈已经丢失:
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
上图中的Runloop
上层对C++异常发生到处理的过程:
- 先调用
__cxa_throw(...)
std::terminate()
set_terminate(...)
设置的异常处理函数safe_handler_caller()
- 调用
abort()
方法, 发送SIGABRT
其中最关键的就是__cxa_throw(...)
函数的调用, 在理解这个内容之前, 建议先理解 C++ 异常是如何实现的, 最终比较完美的捕获C++的异常步骤为如下过程:
- 重写
__cxa_throw(...)
函数!!! 注意使用weak 符号声明, 防止符号冲突!!!
在异常发生时, 会先进入重写函数- 此时, 使用
int backtrace(void**,int)
获取调用堆栈并存储!!! - 使用
dlsym
获取系统原来的__cxa_throw(...)
称为orig_cxa_throw
- 调用
orig_cxa_throw
, 进入std::terminate()
- 此时, 使用
- 设置异常处理函数:
std::set_terminate(...)
, 异常处理回调函数称为CPPExceptionTerminate
CPPExceptionTerminate
回调函数中实现我们自己的处理:ksmc_suspendEnvironment()
, 挂起所有的线程!!! 保护现场!!!- 判断是否是OC Exception, 如果是OC Exception, 啥也不处理(因为会被上层OC Exception Handler 捕获)
- 如果不是OC Exception, 那就是 C++ Exception, 把之前保存的调用栈信息保存起来
KSCrash
中的相关代码如下, 参考我的注释:
// 针对C++的异常捕获 使用 std::set_terminate() 回调
static void setEnabled(bool isEnabled) {
if (isEnabled != g_isEnabled) {
g_isEnabled = isEnabled;
if (isEnabled) {
initialize();
ksid_generate(g_eventID);
// 注册 c++ exception 回调函数
g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);
} else {
std::set_terminate(g_originalTerminateHandler);
}
g_captureNextStackTrace = isEnabled;
}
}
// ============================================================================
#pragma mark - Callbacks -
// ============================================================================
typedef void (*cxa_throw_type)(void *, std::type_info *, void (*)(void *));
extern "C" {
// 声明成 weak 符号!!!!
void __cxa_throw(void *thrown_exception, std::type_info *tinfo, void (*dest)(void *)) __attribute__((weak));
void __cxa_throw(void *thrown_exception, std::type_info *tinfo, void (*dest)(void *)) {
// 缓存 c++ 真实的 thread callstack
if (g_captureNextStackTrace) {
kssc_initSelfThread(&g_stackCursor, 1);
g_capturedStackCursor = true;
}
// 调用原来的方法, 马上回进入 std::set_terminate(...) 设置的 exceptionHandler
static cxa_throw_type orig_cxa_throw = 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();
}
}
// C++ exception 回调函数
static void CPPExceptionTerminate(void) {
// 1. 挂起所有的 thread!!! 保护现场
ksmc_suspendEnvironment();
KSLOG_DEBUG("Trapped c++ exception");
// 2. 获取当前 exception 关键信息 -- name
const char *name = NULL;
std::type_info *tinfo = __cxxabiv1::__cxa_current_exception_type();
if (tinfo != NULL) {
name = tinfo->name();
}
KSLOG_DEBUG("name:%s capured:%d", name, g_capturedStackCursor);
// 3. 判断是否是 `Objective-C Exception, name 非NSException 的就是 c++ exception
if (g_capturedStackCursor && (name == NULL || strcmp(name, "NSException") != 0)) {
// 4.1 c++ exception -> 由于在 __cxa_throw()方法中已经缓存调用栈!!! 把调用栈搞出来
...
} else {
// 4.2 OC Exception -> 恢复, 啥也不处理
KSLOG_DEBUG("Detected NSException. Letting the current NSException handler deal with it.");
ksmc_resumeEnvironment();
}
// 5. 调用原来的 terminateHandler
KSLOG_DEBUG("Calling original terminate handler.");
g_originalTerminateHandler();
}
这里有一个扩展知识深入解构iOS系统下的全局对象和初始化函数 : 理解 C++ 全局static 对象的生命周期
异常处理函数handler的注意点
在APP Debug 环境下, 很多signal会被LLDB Debugger
捕获, 需要使用LLDB调试命令,将指定signal处理抛到用户层处理.
KSCrash
定义了KSCrashMonitorTypeDebuggerUnsafe
的参数用来APP在开发和Debug阶段使用, 这样很多Crash不会被捕获!
#define KSCrashMonitorTypeDebuggerUnsafe \
(KSCrashMonitorTypeMachException | KSCrashMonitorTypeSignal | KSCrashMonitorTypeCPPException | KSCrashMonitorTypeNSException)
此外, 异常发生时, 进程可能执行到代码的任意位置. 因此要确保信号处理程序handler只执行可重入操作:
- 写中断处理程序时, 假定中断进程可能处于不可重入函数中.
- 慎重修改全局数据.
常见的 BSD signal
SIGABRT
是调用abort()
生成的信号,有可能是NSException也有可能是Mach异常SIGBUS
: 非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。比如:
char *s = "hello world";
*s = 'H';
SIGSEGV
: 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。比如:给已经release的对象发送消息SIGILL
: 执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号SIGPIPE
: 管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止SIGTRAP
:由断点指令或其它trap指令产生. 由debugger使用
符号化
如果仅仅获取当前线程的调用栈信息, 可以直接使用[NSThread callStackSymbols]
, 但是如果想随时获取任意线程的调用栈, 可以参考Matrix
中的实现.
参考
developer.aliyun.com/article/499…
www.cocoachina.com/articles/12…