iOS crash分类,Mach异常Unix 信号和NSException 异常
Posted 想名真难
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS crash分类,Mach异常Unix 信号和NSException 异常相关的知识,希望对你有一定的参考价值。
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异常。如果不在Mach级进行处理,将转换为SIGBUS或SIGSEGV BSD信号。
捕获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.
我们仍然需要使用信号处理程序来捕获进程中的SIGABRT。内核发送一个EXC_CRASH mach异常来表示SIGABRT终止。在这种情况下,在进程中捕获Mach异常会导致进程在不间断等待中死锁。因此,对于SIGABRT,我们依赖BSD信号处理程序,而不注册EXC_崩溃。
注意:
因为硬件产生的信号(通过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注册回去,规规矩矩的传递
未设置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.
SIGABRT是应用程序在未捕获NSException或obj_exception_抛出时向自身发送的BSD信号。
但是,这并不能代表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使用。
以上是关于iOS crash分类,Mach异常Unix 信号和NSException 异常的主要内容,如果未能解决你的问题,请参考以下文章
避免SIGPIPE导致的iOS应用闪退/Avoiding SIGPIPE signal crash in iOS(mach_msg_trapSIGPIPE信号)