前一篇看了如何捕获各种类型的异常,这一篇将探究如何解析异常。这一篇的理解有一定的难度,你需要提前对 Mach-O 的结构有一个大体上的了解。其中涉及到如何将 crash 的地址 symbolicate 的过程,步骤类似于 fishhook,可以翻阅我之前对 fishhook 的解析。
Crash 回调
在接收到各种类型的 Crash 信息之后,会统一回调一个处理函数,它就是 KSCrashC.c
中的 onCrash()
方法:
1 | /// 接收到 crash 信息的处理函数 |
其中重要的就是讲 context 写入文件的 kscrashreport_writeStandardReport()
方法:
1 | /// 将 context 转为 report 写入 path |
写入方法
先来看怎么写入的。不用管那些初始化方法,直接看 beginObject()
,它在兜兜转转后调用了如下方法:
1 | /// 将 data 保存到 buffer 或者文件系统中 |
KSBufferedWriter
打开了一个写入流,同时维护了一个 1024 大小的 buffer。当 buffer 中的数据超出之后,就会将 buffer 写入文件中。
写入 image 信息
写入 image 信息通过 writeBinaryImages()
实现:
1 | static void writeBinaryImages(const KSCrashReportWriter* const writer, const char* const key) |
通过 dyld 提供的 _dyld_image_count()
方法获取加载的 image 数量。具体的 image 中的信息在 writeBinaryImage()
中获取:
1 | static void writeBinaryImage(const KSCrashReportWriter* const writer, |
总的来说,通过 _dyld_get_image_header
获取 image 的 header 部分。然后通过 header 位置定位到 load command,遍历 load command 的信息
写入线程信息
前一篇讲到过,在 crash 的时候,会 suspend 所有的线程,并且获取它们的基本信息。现在就要对获取到的线程进行解析:
1 | static void writeThread(const KSCrashReportWriter* const writer, |
其中比较重要的是符号化调用堆栈,写入寄存器的值,以及写入 zombie 记录的信息。
符号化调用堆栈
符号化的过程其实在 fishhock 中有介绍。就是通过实际的地址找到符号表中相应的符号,再到字符串表中找到对应的字符串:
1 | static void writeBacktrace(const KSCrashReportWriter* const writer, |
其核心逻辑在以下方法中:
1 | /// 根据 address 获取符号名 |
代码中已经做了详尽的注释,对于原理我就不再做详尽的阐述了。如果对这部分不明白,也可以参考我对于 fishhook 的解析。总之,在这个过程中就完成了从地址到 image 中符号的转化。
写入寄存器
在通过 crash 获取的 context 信息中,我们可以拿到寄存器相关的信息:
1 | static void writeBasicRegisters(const KSCrashReportWriter* const writer, |
获取到的寄存器有以下所示:
1 | static const char* g_registerNames[] = |
根据寄存器的序号通过如下方法在 context 中获取寄存器中存储的值:
1 | uint64_t kscpu_registerValue(const KSMachineContext* const context, const int regNumber) |
记录调用堆栈的部分信息
分析 crash 的原因,怎么能少了出错线程堆栈的解析呢?KSCrash 提供了方法将出错线程的堆栈上的部分信息拷贝出来:
1 | static void writeStackContents(const KSCrashReportWriter* const writer, |
拿到 stack pointer 是直接通过 sp 寄存器:
1 | uintptr_t kscpu_stackPointer(const KSMachineContext* const context) |
拷贝栈上的信息的范围是:
1 |
这个是作者设置的,也就说是,拷贝栈内 20 个对象,以及刚刚出栈的 10 个对象的地址。在拿到 sp 和范围之后,就可以通过 c 的方法获取:
1 | static inline int copySafely(const void* restrict const src, void* restrict const dst, const int byteCount) |
取出地址上的对象
上面无论是寄存器还是堆栈,取出的都是地址。但是我们其实更需要的是对象的信息。因此,我们还需要到地址上去解析对象信息:
1 | static void writeNotableAddresses(const KSCrashReportWriter* const writer, |
我们以解析寄存器上的对象为例:
1 | static void writeMemoryContents(const KSCrashReportWriter* const writer, |
writeObjCObject()
将地址转化为对象:
1 | static bool writeObjCObject(const KSCrashReportWriter* const writer, |
这一段代码有点长,先把 address 强转为一个 object,然后判断 object 到底是 tagged pointer 还是 block 还是 OC 类型,还是自建的 Class。根据类型写入相应的信息。
野指针的监控
上面的获取地址对象方法中,我们要注意到一个方法 writeZombieIfPresent()
。这是 KSCrash 提供的 Zombie,用于监控野指针。我们来看一下它的实现:
1 | static void writeZombieIfPresent(const KSCrashReportWriter* const writer, |
它的方法非常简洁。就是判断当前 address 上的 object 是否是记录过的对象。这是怎么做到的呢?g_zombieCache
又是什么呢?这就需要回到 KSCrashMonitor_Zombie.c
中查看。
前一篇我们说到,每一个 monitor 会被调用其 setEnabled()
方法启动。Zombie 也不例外。它在 setEnabled()
中调用了 install
方法:
1 | static void install() |
根据代码,我们可以知道,它 hook 了 NSObject
以及 NSProxy
的 dealloc 方法。先调用自己的处理方法 handleDealloc()
然后再调用原来的 dealloc 方法。注意这里调用原来 dealloc 方法的实现,这样做的原因我在 JSPatch 中有分析过,这里不做赘述了:
1 | typedef void (*fn)(id,SEL); \ |
再来看 handleDealloc()
方法:
1 | static inline void handleDealloc(const void* self) |
在 install
的时候的时候创建了一个空的 Cache:
1 | g_zombieCache = calloc(cacheSize, sizeof(*g_zombieCache)); |
在处理 dealloc 的时候,会把对象的地址放到这个 Zombie 的 cache 中。这样的作用就是对于已经销毁的对象,我们记录了一份它们的地址信息,这样以后出现野指针 crash 的时候,如果发现是 zombie 中指向的对象,那么就可以说明它被提前释放了。当然,这并不是非常准确的,因为 hash 获取 index 的方式总是会产生一定的碰撞导致对象被覆盖。当然不可否认这是一种经济有效的测试野指针的方式。作者自己都在注释中说明这是一种 Poor man’s Zombie tracking XD
1 | /* Poor man's zombie tracking. |
总结
到这里,KSCrash 的中篇结束了。这一篇因为涉及的方法和 API 太多,而且很多都是之前研究 fishhook 的时候解释过的,所以我对这部分内容的解释会显得比较少。当然,对于一般的开发者来说,掌握其中的基本原理就已经足够了,对于其中很多 c 相关的 API 以及写法没有必要纠结太深。
KSCrash 分析的部分应该就是这样了。在 KSCrash 下中,我打算去了解一下腾讯开源的 Matrix 中内存预警相关的原理。