iOS 开源库源码分析之KSCrash
KSCrash_MonitorContext
这是捕获crash时定义的数据结构
首先由一个KSCrashMonitorType表示crash的类型
/** 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;
crash最终的处理都是在kscm_handleException函数
Objective-C Exception
这里看一下KSCrashMonitor_NSException的实现,可以通过NSSetUncaughtExceptionHandler函数注册崩溃的回调,这个回调会在崩溃的时候调用
在回调捕捉最简单的方式
void uncaughtExceptionHandler(NSException *exception){
// 异常的堆栈信息
NSArray *stackArray = [exception callStackSymbols];
// 出现异常的原因
NSString *reason = [exception reason];
// 异常名称
NSString *name = [exception name];
NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
NSLog(@"%@", exceptionInfo);
NSMutableArray *tmpArr = [NSMutableArray arrayWithArray:stackArray];
[tmpArr insertObject:reason atIndex:0];
//保存到本地 -- 当然你可以在下次启动的时候,上传这个log
[exceptionInfo writeToFile:[NSString stringWithFormat:@"%@/Documents/microFinanceCrashError.txt",NSHomeDirectory()] atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
主线程死锁
死锁的处理在KSCrashDeadlockMonitor类里面
- 新建一个线程,在运行方法里面实现一个do while循环,里面加上autoreleasepool,让其能够一直运行。
- 定义一个awaitingResponse的属性,以及一个watchdog方法,watchdog方法里面主要是,首先置awaitingResponse为YES,然后异步切换到主线程,awaitingResponse再置为NO
- 在前面新建线程的循环里面,通过线程sleep,然其每隔一定的时间调用watchdog方法,如果发现awaitingResponse为YES,则说明死锁了,然后调用kscm_handleException方法处理崩溃
fatal signal
通过sigaction()函数设置信号的回调。
int sigaction(int sig, const struct sigaction *restrict act,
struct sigaction *restrict oact);
sig参数表示信号的类型
这里会捕捉这些信号
SIGABRT, /* abort() */
SIGBUS, /* bus error */
SIGFPE, /* floating point exception */
SIGILL, /* illegal instruction (not reset when caught) */
SIGPIPE, /* write on a pipe with no one to read it */
SIGSEGV, /* segmentation violation */
SIGSYS, /* bad argument to system call */
SIGTRAP, /* trace trap (not reset when caught) */
SIGPIPE:管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
SIGSEGV:由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
SIGSEGV是当SEGV发生的时候,让代码终止的标识。这是在iOS中最为常见导致崩溃的原因。当App视图去访问没有被开辟的内存或者已经被释放的内存时,这样异常就会产生。
把Mach exception 和 UNIX(BSD) signal 的转换制表后,如下
SIGPIPE
当服务器close一个连接时,若client端接着发数据。根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。而根据信号的默认处理规则,SIGPIPE信号的默认执行动作是terminate(终止、退出),所以client会退出。
场景:长连接socket或重定向管道进入后台,没有关闭
解决办法1:切换到后台时,关闭长连接和管道,回到前台再重建;
解决办法2:使用signal(SIGPIPE,SIG_IGN),将SIGPIPE交给了系统处理。这么做将SIGPIPE设为SIG_IGN,使得客户端不执行默认动作,即不退出。
abort
iOS独有,Jetsam(即低内存事件的常驻监控线程)
之所以会发生这么JetsamEvent,主要还是由于iOS设备不存在交换区导致的内存受限,所以iOS内核不得不把一些优先级不高或者占用内存过大的杀掉。这些JetsamEvent就是系统在杀掉App后记录的一些数据信息。
从某种程度来说,JetsamEvent是一种另类的Crash事件,但是在常规的Crash捕获工具中,由于iOS上能捕获的信号量的限制,所以因为内存导致App被杀掉是无法被捕获的。
用户态的应用程序的线程不可能高于操作系统和内核。而且,在用户态的应用程序间的线程优先级分配也有区别,前台活动的应用程序优先级高于后台的应用程序。iOS上大名鼎鼎的SpringBoard是应用程序中优先级最高的程序。
当App内存占用超过阈值时,系统就会发出MemoryWarning的通知,若App不处理,就会进入memorystatus_kill_hiwat_proc被杀掉;
EXC_BAD_ACCESS
在访问一个已经释放的对象或向它发送消息时,EXC_BAD_ACCESS就会出现。造成EXC_BAD_ACCESS最常见的原因是,在初始化方法中初始化变量时用错了所有权修饰符,这会导致对象过早地被释放。举个例子,在viewDidLoad方法中为UIViewController创建了一个包含元素的NSArray,却将该数组的所有权修饰符设成了assign而不是strong。现在在viewWillAppear中,若要访问已经释放掉的对象时,就会得到名为EXC_BAD_ACCESS的崩溃。
这个崩溃发生时,查看崩溃日志,却往往得不到有用的栈信息。还好,有一个方法用来解决这个问题:NSZombieEnabled。
对于Exception Type,苹果给出了一些常见的错误编码,具体如下:
Bad Memory Access [EXC_BAD_ACCESS ( SIGSEGV | SIGBUS)]
Abnormal Exit [EXC_CRASH (SIGABRT)]
Trace Trap [EXC_BREAKPOINT (SIGTRAP)]
Illegal Instruction [EXC_BAD_INSTRUCTION (SIGILL)]
Quit [SIGQUIT]
Killed [SIGKILL]
Guarded Resource Violation [EXC_GUARD]
Resource Limit [EXC_RESOURCE]
mach kernel exceptions
这里主要是创建一个线程,无限for循环mach_msg函数,接收mach消息,如果返回KERN_SUCCESS则进入崩溃处理流程
c++ crash
调用
terminate_handler set_terminate(terminate_handler)
这个函数是c++发生崩溃时的处理函数
KSCrash只有在非debug executable的情况下捕捉
这里是根据当前进程有没有被追踪来判断是xcode调试的包(不管release 还是debug)还是用户安装的包,true就是xcode调试包,false就是用户安装包
/** Check if the current process is being traced or not.
*
* @return true if we're being traced.
*/
bool ksdebug_isBeingTraced(void)
{
struct kinfo_proc procInfo;
size_t structSize = sizeof(procInfo);
int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
if(sysctl(mib, sizeof(mib)/sizeof(*mib), &procInfo, &structSize, NULL, 0) != 0)
{
KSLOG_ERROR("sysctl: %s", strerror(errno));
return false;
}
return (procInfo.kp_proc.p_flag & P_TRACED) != 0;
}
在捕获Objective-C异常时,使用Xcode进行调试可以清晰地看到调用流程。先调用了导致Crash的测试代码,然后进入异常处理函数捕获Crash日志。 但是,在调试Unix信号的捕获时会发现没有进入异常处理函数。这是怎么回事呢?难道是我们对于Unix信号的捕获没有生效么?其实并不是这样的。主要是由于Xcode调试器的优先级会高于我们对于Unix信号的捕获,系统抛出的信号被Xcode调试器给捕获了,就不会再往上抛给我们的异常处理函数了。 因此,如果我们要调试Unix信号的捕获时,不能直接在Xcode调试器里进行调试,一般使用的调试方式是:
通过Xcode查看设备的Device Logs,从中得到我们打印的日志。 直接将Crash保存到沙盒中,然后进行查看。
mach kernel exceptions 分类
Mach异常是指最底层的内核级异常。
Mach 异常是指最底层的内核级异常。用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常。
Unix 信号又称BSD 信号,如果开发者没有捕获Mach异常,则会被host层的方法ux_exception()将异常转换为对应的UNIX信号,并通过方法threadsignal()将信号投递到出错线程。可以通过方法signal(x, SignalHandler)来捕获single。
NSException:应用级异常,开发者通过try catch来捕获NSException,或者通过NSSetUncaughtExceptionHandler()来处理一下crash前的事情。
无论设置了NSSetUncaughtExceptionHandler与否,最终都会被转成Unix信号,只要该NSException没有被try catch。
日志上报格式
上报的日志我认为最好兼容苹果的 symbolicatecrash 工具, 这就需要我们去理解 symbolicate 工作的原理,随后我们可以加上自己的内容,方便自己进行更多的功能扩展。
symbolicatecrash 的原理
- 解析头部信息是否符合规范
- 解析堆栈信息,符号表 信息
- 在文件中查找符号表的路径
- 根据堆栈信息去匹配相应符号表
- 使用atosl工具进行符号化
- 文本替换成符号化后的日志
总体来说,iOS符号化我们需要了解两部分知识, 1 Dwarf调试格式 2 Macho文件格式
我们经常可以接触到的dsym文件是一个目录,其中包含了一个格式为Dwarf的调试信息文件。
调试信息是在编译器生成机器码的时候一起产生的。它代表着可执行程序和源代码之间的关系。这个信息以预定义的格式进行编码,并同机器码一起存储。
在DWARF里基本的描述项是调试信息项(DebuggingInformation Entry——DIE)。一个DIE有一个标签,它指明了这个DIE描述什么及一个填入了细节并进一步描述该项的属性列表。一个DIE(除了最顶层的)被一个父DIE包含(或者说拥有),并可能有兄弟DIE或子DIE。
通过提取Dwarf文件中的调试信息和对应的堆栈进行匹配,就可以解出一些符号信息,包含调用行号文件名称等信息。
符号文件,是Macho文件格式的。
Macho文件中包含 SymbolTable 可以提取出符号的名称,对于系统库和外部符号(例如你的静态库中的符号),我们可以从SymbolTable中提取符号名称。
符号化需要符号表,符号表是二进制中的指令码与源码的对应关系,在debug模式下,符号表存在于二进制文件中,mach-o文件中的Symbol table存储了这些信息,在release模式下,符号表会存储在一个叫做dSYM的文件中,这样可以减小二进制文件的大小,同时也可以保障app的安全,因为符号表中有大量的方法名称这类信息,如果放在二进制文件中将会随着appstore进行分发。