iOS

iOS Crash 的学习

Posted by Puqin Chen on 2018-08-04

iOS Crash 的知识梳理

[TOC]

iOS 上的 APP 会遇到各种各样的原因,第三方库不兼容、响应超时、内存等都可能造成 Crash 。但是更多的情况是 程序本身的代码逻辑产生了错误。比如,NSArray等集合容器的越界问题、调用不存在的方法,调用函数参数不符合要求等等。

在开发的过程中,我们可以在 Xcode 上通过一些方法可以捕获到异常,从而定位到出错代码。但是对于已经发布的 APP 来说,想要通过这种方式定位到代码就比较困难了。因此,在 crash 发现之前就去预测到问题代码,这是件非常有意义的事情。

最近,也了解一些这部分的知识,也将看到的知识作为一个梳理,对于后续的项目中 crash 数据的预处理或多或少的提供一种帮助。

1. 常见的 crash 类型

1. 1 OC Exception

由 iOS 库或者各种第三方库或者 OC Runtime 验证出错误而抛出异常。
常见的 OC 异常,包括:

NSInvalidArgumentException // 非法参数异常是 OC 代码中最常出现的错误,比如集合数据的参数传递、一些API的错误使用以及未实现一些方法等。
NSRangeException // 越界异常,常见的场景包括数组最大下标处理错误 | 下标的值是个不确定的变量 | 使用空数组
NSGenericException // 这个异常经常出现在 for in 中,当你 add 或 remove 遍历的数组时,就会出错
NSInternalInconsistencyException // 不一致导致的出错,比如你把 NSDictionary 当作 可变的 NSMutableDictionary 来使用,就会产生这样的错误。还有我们界面使用不当的时候,也会抱这种错误。
NSFileHandleOperationException // 内存空间不足,无法分配足够的内存空间,就会报此类问题。

下图,就是一个 NSRangeException 的异常。
PwTSmT.png

在debug环境下,OC异常导致崩溃时Xcode控制台会输出完整的异常信息。
非debug环境下,可以通过注册 NSUncaughtExceptionHandler 捕获异常信息。

1.2 Mach Exception

MachXNU 的微内核,Mach 异常是指最底层的内核级异常。在 iOS 系统中,底层 Crash 先触发 Mach 异常,然后再转换为对应的 signal 信号。

  • 两种常见的 Mach 异常:
    1. EXC_BAD_ACCESS(Bad Memory Access) :内存访问错误,分为:访问对象未初始化(SIGBUS 信号) 和访问的东西被回收掉(SIGSEGV 信号)
    2. EXC_BAD_INSTRUCTION(Illegal Instruction) :通过 SIGILL信号触发的。它是在说运行了一条非法的指令。错误格式:`XC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
  • 其他类型的异常
    1. EXC_RESOURCE是指的程序到达资源上限,比如cpu占用过高,内存不足之类的
    2. EXC_GUARD是一些c层函数访问错误导致的异常,比如fopen文件访问错误之类的都会爆出这个
    3. 0x00000020这种错误基本上都是在 FreeBSD 中。比较常见的就是 看门狗机制。

1.3 Unix Signal Exceptions

从 Mach 异常最终会转化成 Unix 信号投递到出错的线程,如果想要具体了解的可以参考《漫谈iOS Crash收集框架》
注意:

OC异常并不是真正的异常,但是当一个OC异常被抛出到最外层还没被捕获,程序会强行发送SIGABRT信号中断程序。
Mach异常没有比较便利的捕获方式,既然它最终会转化成信号,我们也可以通过捕获信号,来捕获 Crash 事件。

同时, iOS 提供了 signal 方法来注册一个处理函数,在处理函数中,使用execinfo中的 backtrace_symbols取出汇编层程序的堆栈信息。

void InstallSignalHandler(void) {
    signal(SIGHUP, handleSignalException);
    signal(SIGINT, handleSignalException);
    signal(SIGQUIT, handleSignalException);
 
    signal(SIGABRT, handleSignalException);
    signal(SIGILL, handleSignalException);
    signal(SIGSEGV, handleSignalException);
    signal(SIGFPE, handleSignalException);
    signal(SIGBUS, handleSignalException);
    signal(SIGPIPE, handleSignalException);
}
void handleSignalException(int signal) {
    NSMutableString * crashInfo = [[NSMutableString alloc]init];
    [crashInfo appendString:[NSString stringWithFormat:@"signal:%d\n",signal]];
    [crashInfo appendString:@"Stack:\n"];
    void* callstack[128];
    int i, frames = backtrace(callstack, 128);
    char** strs = backtrace_symbols(callstack, frames);
    for (i = 0; i <frames; ++i) {         
		    [crashinfo appendformat:@"%s\n", strs[i]];    
	}
	[wzcrashreporter savecrash:crashinfo];
}

想要了解各种信号的含义可以学习《iOS异常捕获》

以上,基本上就是我们所能遇到的异常类型了。

2. iOS Crash 日志

2.1 日志结构

一份 iOS crash 日志都包含以下信息:
P0PCM8.png
P0P3dJ.png

  • 进程信息:闪退进程的相关报告
    • Incident Identifier : 崩溃报告的唯一标识符。
    • CrashReporter Key:与设备标识相对应的唯一键值
    • Hardware Model: 标识设备类型
    • Process :对项目的操作权限
    • Path:崩溃文件的路径
    • 还有一些其他崩溃消息
  • 一些基本信息,闪退的时间什么的
  • 异常类型:
    • Exception Type:异常的类型。
    • Exception Codes :异常错误码
    • Termination Reason:闪退的原因,比如常见的数组越界啊,什么的。
    • Triggered by Thread:出现问题在哪个线程,这个比较重要,首先确定在哪个线程中出了问题,然后再去定位
  • 线程回溯:提供应用中所有线程的回溯日志。线程调用的一些堆栈信息。 它包括四列:
    • 帧编号
    • 调用库以及一些静态二进制库的名称
    • 调用方法的地址
    • 分为两个子列:基地址和偏移量。这里的方法名是已经被符号化后的状态。
  • 线程状态:闪退时寄存器中的值。一般不需要这部分的信息。
  • 二进制映像:闪退时已经加载的二进制文件。

2.2 符号化

所谓符号化,就是将 crash 文件线程回溯部分中的基地址+偏移地址转变成 方法名 + 行数。有几种符号化的方法:

  1. symbolicatecrash
    需要获取到 .dsym文件或 .app 文件 ,和 crash 文件放在同一目录下,执行命令。

  2. 命令行工具 atos
    symbolicatecrash 可以帮助我们很好的分析 crash 日志,但是有它的局限性 — 不够灵活。我们需要 symbolicatecrash、.crash 及 .dSYM 三个文件才能解析。
    相对于 symbolicatecrash, atos 命令更加灵活,特别是你需要对不同渠道的 crash 文件,写一个自动化的分析脚本的时候,这个方法优势很大。
    但是这种方式也有个不方便的地方:一个线上的 App,用户使用的版本存在差异,而每个版本所对应的 .dSYM 都是不同的。必须确保 .crash 和 .dSYM 文件是匹配的,才能正确符号化,匹配的条件就是它们的 UUID 一致。
    有人已经收集完了不同机型的符号文件,可以了解一下

  3. 使用集成的工具

具体内容,可以参考iOS Crash 捕获及堆栈符号化思路解析

3.实际应用

通过对上面两部分内容的理解,我们也可以大致的理解和尝试着去定位出错代码。
在项目中,在只能拿到 crash 文件的情况下,去通过上述方式定位到异常位置,找到 crash 的偏移地址,在 Mach-O 文件中进行截取上下文一部分内容,做后续的处理,放入到 CNN 网络中进行学习。

4. 后续

接下来,会去收集更多的 crash 数据,然后试着在 mach-o 文件中进行截取。