iOS Crash 流程化0:概览

Ref:iOS Crash 捕获及堆栈符号化思路剖析

  • iOS Crash 流程化:概览
    • 崩溃捕获
      • Mach 异常捕获
      • Unix 信号捕获
      • NSException 捕获
    • 冲突
    • 堆栈收集
    • 堆栈符号解析
    • UUID
    • 系统库符号化

一、崩溃捕获

对于崩溃的情况,一般是由 Mach异常或 Objective-C 异常(NSException)引起的。可以针对这两种情况抓取对应的 Crash 事件。

对于 Mach 异常,到了 BSD 层会转换为对应的 Signal 信号,那么我们也可以通过捕获信号,来捕获 Crash 事件。

针对 NSException 可以通过注册 NSUncaughtExceptionHandler 捕获异常信息。

image

Mach 异常捕获

如果想要做 Mach 异常捕获,需要注册一个异常端口,这个异常端口会对当前任务的所有线程有效,如果想要针对单个线程,可以通过 thread_set_exception_ports注册自己的异常端口,发生异常时,首先会将异常抛给线程的异常端口,然后尝试抛给任务的异常端口,当我们捕获异常时,就可以做一些自己的工作,比如,当前堆栈收集等。

对于如何注册一个异常端口,这里有示意图和 PLCrashReporter 可以参考

image

Unix 信号捕获

对于 Mach 异常,操作系统会将其转换为对应的 Unix 信号,所以如果你对Mach不熟悉的话,也可以通过注册signalHandler的方式来做信号异常。对于实例,你可以参考这里

signal(SIGHUP, signalHandler);
signal(SIGINT, signalHandler);
signal(SIGQUIT, signalHandler);

signal(SIGABRT, signalHandler);
signal(SIGILL, signalHandler);
signal(SIGSEGV, signalHandler);
signal(SIGFPE, signalHandler);
signal(SIGBUS, signalHandler);
signal(SIGPIPE, signalHandler);

NSException 捕获

对于NSException异常,也比较容易处理,通过注册NSUncaughtExceptionHandler捕获异常信息即可,将拿到的NSException细节写入Crash日志,上传到后台做数据分析

 // register the uncaught exception handler
 NSSetUncaughtExceptionHandler(&handler);

冲突

在我们自己研发 Crash 收集框架之前,最早肯定都会接入网易云捕、腾讯 Bugly、Fabric 等第三方日志框架来进行崩溃的收集和分析。如果多个 Crash 收集框架存在时,往往会存在冲突。

不管是对于 Signal 捕获还是 NSException 捕获都会存在 handler 覆盖的问题,正确的做法应该是先判断是否有前者已经注册了 handler,如果有则应该把这个 handler 保存下来,在自己处理完自己的 handler 之后,再把这个 handler 抛出去,供前面的注册者处理。这里给出相应的 Demo,Demo 由@zerygao提供。

typedef void (*SignalHandler)(int signo, siginfo_t *info, void *context);

static SignalHandler previousSignalHandler = NULL;

+ (void)installSignalHandler {
    struct sigaction old_action;
    sigaction(SIGABRT, NULL, &old_action);
    if (old_action.sa_flags & SA_SIGINFO) {
        previousSignalHandler = old_action.sa_sigaction;
    }

    LDAPMSignalRegister(SIGABRT);
    // .......

}
static void LDAPMSignalRegister(int signal) {
    struct sigaction action;
    action.sa_sigaction = LDAPMSignalHandler;
    action.sa_flags = SA_NODEFER | SA_SIGINFO;
    sigemptyset(&action.sa_mask);
    sigaction(signal, &action, 0);
}
static void LDAPMSignalHandler(int signal, siginfo_t* info, void* context) {
    //  获取堆栈,收集堆栈
    ........

    LDAPMClearSignalRigister();

    // 处理前者注册的 handler
    if (previousSignalHandler) {
        previousSignalHandler(signal, info, context);
    }
}

上面的是一个处理 Signal handler 冲突的大概代码思路,下面是 NSException handler 的处理思路,两者大同小异。

static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler;

static void LDAPMUncaughtExceptionHandler(NSException *exception) {
    // 获取堆栈,收集堆栈
    // ......
    //  处理前者注册的 handler
    if (previousUncaughtExceptionHandler) {
        previousUncaughtExceptionHandler(exception);
    }
}

+ (void)installExceptionHandler {
    previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
    NSSetUncaughtExceptionHandler(&LDAPMUncaughtExceptionHandler);
}

SignalHandler不要在debug环境下测试。因为系统的debug会优先去拦截。我们要运行一次后,关闭debug状态。应该直接在模拟器上点击我们build上去的App去运行。而UncaughtExceptionHandler可以在调试状态下捕捉

堆栈收集

你可以直接用系统的方法获取当前线程堆栈,也可以使用 PLCrashRepoter 获取所有线程堆栈,也可以参考 BSBacktraceLogger 自己写一套轻量级的堆栈采集框架。

堆栈符号解析

堆栈符号化还原有四种常见的方法:

  • symbolicatecrash

  • mac 下的 atos 工具

  • linux 下的 atos 的替代品 atosl

  • 通过 dSYM 文件提取地址和符号的对应关系,进行符号还原

以上方案都有对应的应用场景,对于线上的 Crash 堆栈符号还原,主要采用的还是后三种方案。atos 和 atosl 的使用方法很类似,以下是 atos 的一个示例。

atos -o MonitorExample 0x0000000100062ac4  ARM-64 -l 0x100058000

// 还原结果
-[GYRootViewController tableView:cellForRowAtIndexPath:] (in GYMonitorExample) (GYRootViewController.m:41)

但是 atos 是Mac上一个工具,需要使用 Mac 或者黑苹果来进行解析工作,如果由后台来做解析工作,往往需要一套基于 Linux 的解析方案,这个时候可以选择 atosl,但是这个库已经有多年没有更新了,同时基于我司的尝试, atosl 好像不太支持 arm64 架构,所以我们放弃了该方案。

最终使用了第四个方案,提取 dSYM 的符号表,可以自己研发工具,也可以直接使用 bugly 和 网易云捕提供的工具,下面是提取出来的符号表。第一列是起始内存地址,第二列是结束地址,第三列是对应的函数名、文件名以及行号。

a840    a854    -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:41
a854    a858    -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:42
a858    a87c    -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:42
a87c    a894    -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:42
a894    a8a0    -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:42
aa3c    aa80    -[GYFilePreviewViewController initWithFilePath:] GYRootViewController.m:21
aa80    aaa8    -[GYFilePreviewViewController initWithFilePath:] GYFilePreviewViewController.m:23
aaa8    aab8    -[GYFilePreviewViewController initWithFilePath:] GYFilePreviewViewController.m:23
aab8    aabc    -[GYFilePreviewViewController initWithFilePath:] GYFilePreviewViewController.m:24
aabc    aac8    -[GYFilePreviewViewController initWithFilePath:] GYFilePreviewViewController.m:24

因为程序每次启动基地址都会变化,所以上面提到的地址是相对偏移地址,在我们获取到崩溃堆栈地址后,可以根据堆栈中的偏移地址来与符号表中的地址来做匹配,进而找到堆栈所对应的函数符号。比如下面的第四行,偏移为 43072 转换为十六进制就是 a840,用 a840 去上面的符号表中找对应关系,会发现对应着 -[GYRootViewController tableView:cellForRowAtIndexPath:],基于这种方式,就可以将堆栈地址完全还原为函数符号啦。

0   libsystem_kernel.dylib              0x0000000186cfd314 0x186cde000 + 127764
1   Foundation                          0x00000001887f5590 0x1886ec000 + 1086864
2   GYMonitorExample                    0x00000001000da4ac 0x1000d0000 + 42156
3   GYMonitorExample                    0x00000001000da840 0x1000d0000 + 43072

UUID

我们的应用存在多个版本,并且支持多种不同的架构,那么如何找到与崩溃日志对应的符号表呢?就是依靠 UUID,只有当崩溃日志的 UUID 与 dSYM 的 UUID 一致时,才能得到正确的解析结果。

dSYM 的 UUID 获取方法:

xcrun dwarfdump --uuid <dSYM文件>

应用内获取 UUID 的方法:

#import <mach-o/ldsyms.h>

NSString *executableUUID()
{
    const uint8_t *command = (const uint8_t *)(&_mh_execute_header + 1);
    for (uint32_t idx = 0; idx < _mh_execute_header.ncmds; ++idx) {
        if (((const struct load_command *)command)->cmd == LC_UUID) {
            command += sizeof(struct load_command);
            return [NSString stringWithFormat:@"%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X",
                    command[0], command[1], command[2], command[3],
                    command[4], command[5],
                    command[6], command[7],
                    command[8], command[9],
                    command[10], command[11], command[12], command[13], command[14], command[15]];
        } else {
            command += ((const struct load_command *)command)->cmdsize;
        }
    }
    return nil;
}

系统库符号化

上面只是提取到了我们应用中 dSYM 中的符号表,对于系统库还是无能为力的,比如 UIKit 就没有办法将其地址符号化,想要将动态库符号化,需要先获取系统库的符号文件。提取系统符号文件可以从 iOS 固件中获取,也可以从 Github 上开源项目中找到对应系统的符号文件。

搜集系统符号化文件非常困难,国内有一个非常敬业的iOS同行,搜集总结了iOS7 - iOS10的很多符号化文件,而且作者对文件做了优化,下载下来的文件也不会很大,非常感谢他!

网盘🔗
密码: 79m8

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 本文就捕获iOS Crash、Crash日志组成、Crash日志符号化、异常信息解读、常见的Crash五部分介绍。...
    xukuangbo_阅读 1,499评论 0 0
  • [这是第14篇] 序: iOS Crash问题是iOS开发中难以忽视的存在,本文就捕获iOS Crash、Cras...
    南华coder阅读 9,757评论 21 115
  • 最近在做 Crash 分析方面的工作,发现 iOS 的崩溃捕获和堆栈符号化虽然已经有很多资料可以参考,但是没有比较...
    Joy___阅读 14,980评论 15 142
  • 今天,火箭主场130-123力克鹈鹕拿到十连胜,因伤缺席了14场比赛的保罗在11月16日这天复出之后,火箭一路高奏...
    篮球行为大赏阅读 145评论 0 0
  • 骐骥一跃,不能十步;驽马十驾,功在不舍。 相信小时候大家学习过猴子掰苞米的故事,那个猴子到最后两手空空的回家了。但...
    遇见活在当下的自己阅读 338评论 1 1