前言
Crash的主要原因是你的应用收到了未处理的信号。
未处理信号可能来源于三个地方:kernel、其他进程、以及App本身。
因此,crash异常也分为三种:
- Mach异常:是指最底层的内核级异常。用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常。
- Unix信号:又称BSD 信号,如果开发者没有捕获Mach异常,则会被host层的方法ux_exception()将异常转换为对应的UNIX信号,并通过方法threadsignal()将信号投递到出错线程。可以通过方法signal(x, SignalHandler)来捕获single。
- NSException:应用级异常,它是未被捕获的Objective-C异常,导致程序向自身发送了SIGABRT信号而崩溃,对于未捕获的Objective-C异常,是可以通过try catch来捕获的,或者通过NSSetUncaughtExceptionHandler()机制来捕获。
Mach异常与Unix信号
Mach异常是什么?它又是如何与Unix信号建立联系的? Mach是一个XNU的微内核核心,Mach异常是指最底层的内核级异常 。每个thread,task,host都有一个异常端口数组,Mach的部分API暴露给了用户态,用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常,抓取Crash事件。
所有Mach异常未处理,它将在host层被ux_exception转换为相应的Unix信号,并通过threadsignal将信号投递到出错的线程。iOS中的 POSIX API 就是通过 Mach 之上的 BSD 层实现的。
看看Matt大神的回答:
EXC_BAD_ACCESS is a Mach exception sent by the kernel to your application when you try to access memory that is not mapped for your application. If not handled at the Mach level, it will be translated into a SIGBUS or SIGSEGV BSD signal.
大概意思就是:EXC_BAD_ACCESS异常主要是访问了不属于本进程或者已经释放的内存地址。如果未在mach层捕获,它将在host层被转换成SIGSEGV信号投递到出错的线程。
捕获Mach异常或者Unix信号都可以抓到crash事件,这两种方式哪个更好呢?
优选Mach异常,因为Mach异常处理会先于Unix信号处理发生,如果Mach异常的handler让程序exit了,那么Unix信号就永远不会到达这个进程了。
如果优选Mach来捕获异常,为什么还要转化为unix信号呢?
转换Unix信号是为了兼容更为流行的POSIX标准(SUS规范),这样不必了解Mach内核也可以通过Unix信号的方式来兼容开发。
为什么第三方库PLCrashReporter即使在优选捕获Mach异常的情况下,也放弃捕获Mach异常EXC_CRASH,而选择捕获与之对应的SIGABRT信号?
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.
注意:
因为硬件产生的信号(通过CPU陷阱)被Mach层捕获,然后才转换为对应的Unix信号;苹果为了统一机制,于是操作系统和用户产生的信号(通过调用kill和pthread_kill)也首先沉下来被转换为Mach异常,再转换为Unix信号。
也就是说整个流程是这样的:
硬件产生信号或者kill或pthread_kill信号 --> Mach异常 --> Unix信号(SIGABRT)
因此,捕获crash的流程是这样的
Crash收集方式
通过UncaughtExceptionHandler机制收集
这种收集方式只适合收集应用级异常,我们要做的就是用自定义的函数替代该ExceptionHandler即可。
// 记录之前的崩溃回调函数
static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler = NULL;
@implementation NWUncaughtExceptionHandler
#pragma mark - Register
+ (void)registerHandler {
//将先前别人注册的handler取出并备份
previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}
#pragma mark - Private
// 崩溃时的回调函数
static void UncaughtExceptionHandler(NSException * exception) {
// 异常的堆栈信息
NSArray * stackArray = [exception callStackSymbols];
// 出现异常的原因
NSString * reason = [exception reason];
// 异常名称
NSString * name = [exception name];
NSString * exceptionInfo = [NSString stringWithFormat:@"========uncaughtException异常错误报告========\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@", name, reason, [stackArray componentsJoinedByString:@"\n"]];
// 保存崩溃日志到沙盒cache目录
[NWCrashTool saveCrashLog:exceptionInfo fileName:@"Crash(Uncaught)"];
//在自己handler处理完后自觉把别人的handler注册回去,规规矩矩的传递
if (previousUncaughtExceptionHandler) {
previousUncaughtExceptionHandler(exception);
}
// 杀掉程序,这样可以防止同时抛出的SIGABRT被SignalException捕获
kill(getpid(), SIGKILL);
}
@end
注意:
在自己的程序里集成多个Crash日志收集服务实是否是明智之举?
通常情况下,第三方功能性SDK都会集成一个Crash收集服务,以及时发现自己SDK的问题。当各家的服务都以保证自己的Crash统计正确完整为目的时,难免出现时序手脚,强行覆盖等等的恶意竞争,就会导致在其之前注册过的日志收集服务写出的Crash日志因为取不到NSException而丢失Last Exception Backtrace等信息。
因此,如果同时有多方通过NSSetUncaughtExceptionHandler注册异常处理程序,正确的作法是:后注册者通过NSGetUncaughtExceptionHandler将先前别人注册的handler取出并备份,在自己handler处理完后自觉把别人的handler注册回去,防止handle覆盖导致其他日志收集框架用不了的问题。
未设置NSSetUncaughtExceptionHandler的NSException最后会转成Unix信号吗?
无论设置NSSetUncaughtExceptionHandler与否,只要未被try catch,最终都会被转成Unix信号,只不过设置了无法在其ExceptionHandler中无法获得最终发送的Unix信号类型
Mach异常方式
这种基本没用过。
Unix信号
Unix信号:signal(SIGSEGV,signalHandler);
SIGABRT is a BSD signal sent by an application to itself when an NSException or obj_exception_throw is not caught.
大概意思就是:
当NSException或obj_exception_throw未被捕获时,应用程序会给它本身发送一个SIGABRT信号。
但是,这并不能代表SIGABRT就是 NSException导致,因为SIGABRT是调用abort()生成的信号。
若程序因NSException而Crash,系统日志中的Last Exception Backtrace信息是完整准确的。
#import "NWCrashSignalExceptionHandler.h"
#import <execinfo.h>
#import "NWCrashTool.h"
typedef void(*SignalHandler)(int signal, siginfo_t *info, void *context);
static SignalHandler previousABRTSignalHandler = NULL;
static SignalHandler previousBUSSignalHandler = NULL;
static SignalHandler previousFPESignalHandler = NULL;
static SignalHandler previousILLSignalHandler = NULL;
static SignalHandler previousPIPESignalHandler = NULL;
static SignalHandler previousSEGVSignalHandler = NULL;
static SignalHandler previousSYSSignalHandler = NULL;
static SignalHandler previousTRAPSignalHandler = NULL;
@implementation NWCrashSignalExceptionHandler
+ (void)registerHandler {
// 将先前别人注册的handler取出并备份
[self backupOriginalHandler];
[self signalRegister];
}
+ (void)backupOriginalHandler {
struct sigaction old_action_abrt;
sigaction(SIGABRT, NULL, &old_action_abrt);
if (old_action_abrt.sa_sigaction) {
previousABRTSignalHandler = old_action_abrt.sa_sigaction;
}
struct sigaction old_action_bus;
sigaction(SIGBUS, NULL, &old_action_bus);
if (old_action_bus.sa_sigaction) {
previousBUSSignalHandler = old_action_bus.sa_sigaction;
}
struct sigaction old_action_fpe;
sigaction(SIGFPE, NULL, &old_action_fpe);
if (old_action_fpe.sa_sigaction) {
previousFPESignalHandler = old_action_fpe.sa_sigaction;
}
struct sigaction old_action_ill;
sigaction(SIGILL, NULL, &old_action_ill);
if (old_action_ill.sa_sigaction) {
previousILLSignalHandler = old_action_ill.sa_sigaction;
}
struct sigaction old_action_pipe;
sigaction(SIGPIPE, NULL, &old_action_pipe);
if (old_action_pipe.sa_sigaction) {
previousPIPESignalHandler = old_action_pipe.sa_sigaction;
}
struct sigaction old_action_segv;
sigaction(SIGSEGV, NULL, &old_action_segv);
if (old_action_segv.sa_sigaction) {
previousSEGVSignalHandler = old_action_segv.sa_sigaction;
}
struct sigaction old_action_sys;
sigaction(SIGSYS, NULL, &old_action_sys);
if (old_action_sys.sa_sigaction) {
previousSYSSignalHandler = old_action_sys.sa_sigaction;
}
struct sigaction old_action_trap;
sigaction(SIGTRAP, NULL, &old_action_trap);
if (old_action_trap.sa_sigaction) {
previousTRAPSignalHandler = old_action_trap.sa_sigaction;
}
}
+ (void)signalRegister {
NWSignalRegister(SIGABRT);
NWSignalRegister(SIGBUS);
NWSignalRegister(SIGFPE);
NWSignalRegister(SIGILL);
NWSignalRegister(SIGPIPE);
NWSignalRegister(SIGSEGV);
NWSignalRegister(SIGSYS);
NWSignalRegister(SIGTRAP);
}
#pragma mark - Private
#pragma mark Register Signal
static void NWSignalRegister(int signal) {
struct sigaction action;
action.sa_sigaction = NWSignalHandler;
action.sa_flags = SA_NODEFER | SA_SIGINFO;
sigemptyset(&action.sa_mask);
sigaction(signal, &action, 0);
}
#pragma mark SignalCrash Handler
static void NWSignalHandler(int signal, siginfo_t* info, void* context) {
NSMutableString *mstr = [[NSMutableString alloc] init];
[mstr appendString:@"Signal Exception:\n"];
[mstr appendString:[NSString stringWithFormat:@"Signal %@ was raised.\n", signalName(signal)]];
[mstr appendString:@"Call Stack:\n"];
// 这里过滤掉第一行日志
// 因为注册了信号崩溃回调方法,系统会来调用,将记录在调用堆栈上,因此此行日志需要过滤掉
for (NSUInteger index = 1; index < NSThread.callStackSymbols.count; index++) {
NSString *str = [NSThread.callStackSymbols objectAtIndex:index];
[mstr appendString:[str stringByAppendingString:@"\n"]];
}
[mstr appendString:@"threadInfo:\n"];
[mstr appendString:[[NSThread currentThread] description]];
// 保存崩溃日志到沙盒cache目录
[NWCrashTool saveCrashLog:[NSString stringWithString:mstr] fileName:@"Crash(Signal)"];
NWClearSignalRegister();
// 调用之前崩溃的回调函数
// 在自己handler处理完后自觉把别人的handler注册回去,规规矩矩的传递
previousSignalHandler(signal, info, context);
kill(getpid(), SIGKILL);
}
#pragma mark Signal To Name
static NSString *signalName(int signal) {
NSString *signalName;
switch (signal) {
case SIGABRT:
signalName = @"SIGABRT";
break;
case SIGBUS:
signalName = @"SIGBUS";
break;
case SIGFPE:
signalName = @"SIGFPE";
break;
case SIGILL:
signalName = @"SIGILL";
break;
case SIGPIPE:
signalName = @"SIGPIPE";
break;
case SIGSEGV:
signalName = @"SIGSEGV";
break;
case SIGSYS:
signalName = @"SIGSYS";
break;
case SIGTRAP:
signalName = @"SIGTRAP";
break;
default:
break;
}
return signalName;
}
#pragma mark Previous Signal
static void previousSignalHandler(int signal, siginfo_t *info, void *context) {
SignalHandler previousSignalHandler = NULL;
switch (signal) {
case SIGABRT:
previousSignalHandler = previousABRTSignalHandler;
break;
case SIGBUS:
previousSignalHandler = previousBUSSignalHandler;
break;
case SIGFPE:
previousSignalHandler = previousFPESignalHandler;
break;
case SIGILL:
previousSignalHandler = previousILLSignalHandler;
break;
case SIGPIPE:
previousSignalHandler = previousPIPESignalHandler;
break;
case SIGSEGV:
previousSignalHandler = previousSEGVSignalHandler;
break;
case SIGSYS:
previousSignalHandler = previousSYSSignalHandler;
break;
case SIGTRAP:
previousSignalHandler = previousTRAPSignalHandler;
break;
default:
break;
}
if (previousSignalHandler) {
previousSignalHandler(signal, info, context);
}
}
#pragma mark Clear
static void NWClearSignalRegister() {
signal(SIGSEGV,SIG_DFL);
signal(SIGFPE,SIG_DFL);
signal(SIGBUS,SIG_DFL);
signal(SIGTRAP,SIG_DFL);
signal(SIGABRT,SIG_DFL);
signal(SIGILL,SIG_DFL);
signal(SIGPIPE,SIG_DFL);
signal(SIGSYS,SIG_DFL);
}
@end
常用的Unix信号
1.SIGABRT是调用abort()生成的信号,有可能是NSException也有可能是Mach异常
2.SIGBUS:非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。比如:
char *s = "hello world";
*s = 'H';
3.SIGSEGV:试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。比如:给已经release的对象发送消息
4.SIGILL:执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。
5.SIGPIPE:管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
6.SIGSEGV:试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
7.SIGSYS:非法的系统调用。
8.SIGTRAP:由断点指令或其它trap指令产生. 由debugger使用。
参考链接
Handling unhandled exceptions and signals
Mach异常
漫谈iOS Crash收集框架
iOS异常捕获