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进行分发。

results matching ""

    No results matching ""