Contents
  1. 1. 前言
  2. 2. Crash 日志的简单解读
    1. 2.1. 基础信息
    2. 2.2. 调用栈记录
    3. 2.3. 寄存器状态
    4. 2.4. 二进制信息
  3. 3. 没有符号化的 crash 如何处理?
  4. 4. 最后

前言

iOS 开发过程中对遇到 crash 要想解决必须借助 crash 日志。一份 crash 日志包含解决该 crash 的绝大多数信息,理想状况下只要有日志就能解决该 crash。

Crash 日志的简单解读

一份 Crash 日志包括基础信息、调用栈记录、寄存器状态、二进制映像

基础信息

我从手机中的 crash 记录中随便找了一个,找到支付宝的 crash 记录贴在这里。

Incident Identifier: 8B9A6105-3201-4BF2-9022-328E50570C30
CrashReporter Key:   6fb28a6fa7acb035c6613c3aeb838a1df765f7af
Hardware Model:      iPhone9,1
Process:             AlipayWallet [2266]
Path:                /private/var/containers/Bundle/Application/4B231389-FADA-4CE7-9C79-69DB4BB1A5CB/AlipayWallet.app/AlipayWallet
Identifier:          com.alipay.iphoneclient
Version:             10.1.0.090133 (10.1.0)
Code Type:           ARM-64 (Native)
Role:                Non UI
Parent Process:      launchd [1]
Coalition:           com.alipay.iphoneclient [731]


Date/Time:           2017-09-19 01:39:10.5103 +0800
Launch Time:         2017-09-19 01:39:03.8429 +0800
OS Version:          iPhone OS 10.3.3 (14G60)
Report Version:      104

Exception Type:  EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note:  EXC_CORPSE_NOTIFY
Termination Reason: Namespace SPRINGBOARD, Code 0x8badf00d
Triggered by Thread:  0

其中
Incident Identifier 是 crash 的唯一 ID,但不是用户 ID
Process 是该进程的名字,这里是支付宝的进程名。放括号内应该是 pid。
Path 是沙盒在 iPhone、iPad 中的文件路径
Identifier 是包名
Code Type 这里可以看到是 32 位还是 64 位系统,当然其他地方也可以推测,待会儿讲
Parent Process 父进程,同 OS X 一样,iOS 的应用的父进程都是 launchd[1]

下面几个很重要
Exception Type 表明 crash 的类型

  • SIGABRT Obj-C 层面异常,比如向数组中插入 nil
  • SIGSEGV (segment fault)内存权限错误,包括:数组越界、引用已经 release 的内存、写被标记为“写保护”的内存块儿(多线程写)
  • SIGBUS 内存地址错误,如未对齐,而 SIGSEGV 地址是对的。

Exception Codes 用十六进制表达的语言 https://en.wikipedia.org/wiki/Hexspeak 很有意思。
如 0xc00010ff,意义为 0xCoolOff cool off 烫烫烫而被系统干掉了

Triggered by Thread 发生 crash 所在的线程标号,方便查找对应的调用栈。

调用栈记录

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libobjc.A.dylib                   0x00000001811f2468 0x1811dc000 + 91240
1   libobjc.A.dylib                   0x00000001811ed84c 0x1811dc000 + 71756
2   libobjc.A.dylib                   0x00000001811ed540 0x1811dc000 + 70976
3   libobjc.A.dylib                   0x00000001811f8478 0x1811dc000 + 115832
4   UIKit                             0x000000018912a8dc 0x1888a5000 + 8935644
5   UIKit                             0x00000001888af254 0x1888a5000 + 41556
6   UIKit                             0x0000000188a2b628 0x1888a5000 + 1599016
7   AlipayWallet                      0x0000000101cec490 0x100074000 + 29852816
8   AlipayWallet                      0x0000000101cea530 0x100074000 + 29844784
9   AlipayWallet                      0x000000010314ed70 0x100074000 + 51228016
10  AlipayWallet                      0x000000010314d740 0x100074000 + 51222336
11  AlipayWallet                      0x000000010138c034 0x100074000 + 20021300
12  AlipayWallet                      0x0000000101cea3cc 0x100074000 + 29844428
13  AlipayWallet                      0x0000000101ceabfc 0x100074000 + 29846524
14  AlipayWallet                      0x0000000101d0b304 0x100074000 + 29979396
15  AlipayWallet                      0x0000000101cea99c 0x100074000 + 29845916
16  AlipayWallet                      0x0000000101ce7fec 0x100074000 + 29835244

截取了主线程(Thread 0)部分调用栈记录,可以看到主线程的名字叫 com.apple.main-thread
第一列标号为栈帧标号,数字越大越旧。
第二列是对应的二进制包的名字
第三列是函数的地址,这里就能看出设备的比特位数,这是一个有 16 个十六进制位的处理器,也就是 64 位。
第四列是对应二进制包在整个内存中的起始位置
第五列是个加号
第六列是一串十进制数,待会儿讲它的意义

寄存器状态

Thread 0 crashed with ARM Thread State (64-bit):
    x0: 0x00000001041e1218   x1: 0x0000000189415b91   x2: 0x0000000000000000   x3: 0x0000000000000001
    x4: 0x0000000000000000   x5: 0x0000000000000001   x6: 0x000000010fcea2b0   x7: 0x000000006b616e61
    x8: 0x00000001041e11f8   x9: 0x0000000000000002  x10: 0x0000000000000000  x11: 0x00000001041e1200
   x12: 0x0000000000000018  x13: 0x0000000000000001  x14: 0x00000001894b6042  x15: 0x0000000000000000
   x16: 0x00000001a894ba10  x17: 0x0000000000000000  x18: 0x0000000000000000  x19: 0x0000000189415b91
   x20: 0x0000000105702a38  x21: 0x0000000105702c38  x22: 0x00000001a88eafd0  x23: 0x0000000000000000
   x24: 0x00000001a894ba10  x25: 0x0000000000000000  x26: 0x00000000ffed0a92  x27: 0x00000000fffffff0
   x28: 0x0000000000000000   fp: 0x000000016fd87e60   lr: 0x00000001811ed84c
    sp: 0x000000016fd87e40   pc: 0x00000001811f2468 cpsr: 0x80000000

id objc_msgSend(id self, SEL op, ...) 这是 objc_msgSend 的函数原型

其中
x0 保存的是 self 变量
x1 保存的是 SEL 也就是 selector 的值
x2 保存的是第一个参数
x3 保存的是第二个参数
pc 是程序计数器,保存当前指令
sp 是栈指针

前八个参数都会用寄存器保存,再往后的参数是保存在栈上。

二进制信息

0x100074000 - 0x10388ffff AlipayWallet arm64  <c5a47ccd4cb236aa98c8e9ede8b2f5fb> /var/containers/Bundle/Application/4B231389-FADA-4CE7-9C79-69DB4BB1A5CB/AlipayWallet.app/AlipayWallet
0x105554000 - 0x105587fff dyld arm64  <93b6f8d0b0c03d8695fbd178c57cb071> /usr/lib/dyld
0x106394000 - 0x10639ffff GAXClient arm64  <5a808d56007c3dfca12507ae9695ecf4> /System/Library/AccessibilityBundles/GAXClient.bundle/GAXClient
0x181164000 - 0x181165fff libSystem.B.dylib arm64  <ac92dd7e5a81380bba05c91be3a473ec> /usr/lib/libSystem.B.dylib
0x181166000 - 0x1811bbfff libc++.1.dylib arm64  <da0f6a86db853140b2d79e3b36f28795> /usr/lib/libc++.1.dylib
0x1811bc000 - 0x1811d8fff libc++abi.dylib arm64  <5dc5ba28cfa43f838099049d17ba9ec6> /usr/lib/libc++abi.dylib
0x1811dc000 - 0x1815b9fff libobjc.A.dylib arm64  <85f3b59b96243690b138ce96e663bf4b> /usr/lib/libobjc.A.dylib
0x1815ba000 - 0x1815befff libcache.dylib arm64  <ad6aea8120b33622bc51c6b39587c773> /usr/lib/system/libcache.dylib
0x1815bf000 - 0x1815cafff libcommonCrypto.dylib arm64  <468e03d6648137679caa585f8bc8e8e8> /usr/lib/system/libcommonCrypto.dylib
0x1815cb000 - 0x1815cefff libcompiler_rt.dylib arm64  <0ad97345fff23bb99981ac51c558bedf> /usr/lib/system/libcompiler_rt.dylib

没啥说的,记录了各个二进制包起始位置和结束位置

没有符号化的 crash 如何处理?

终于来到了正文,如果遇到一个 crash 却没有符号化如何 debug?看不到对应的函数名怎么办?

7 AlipayWallet 0x0000000101cec490 0x100074000 + 29852816

这里有函数地址 0x0000000101cec490,在 Xcode 中使用命令 image lookup -a 0x0000000101cec490 就可以看到函数名

但是,不能直接直接用 0x0000000101cec490 这个地址值,否则就是刻舟求剑。

1.当前的 App 是 crash 的那一份么?
2.地址空间布局随机化 ASLR 会导致所有的二进制包起始位置每次加载都不一样

所以只能使用 0x0000000101cec490 针对 AlipayWallet 的起始位置计算一个 offset
offset = 0x0000000101cec490 - 0x100074000 = 0x1c78490
其中 0x1c78490 转换成十进制是多少呢?29852816,是不是特别眼熟?就是调用栈记录的最后一列。实际上倒数最后三列应该这样看

函数地址 = 包起始地址 + 偏移

所以,我们现在有了偏移再来一个包起始地址就可以解开这个方程,得到当前函数地址。
使用命令 image list 便可查看所有二进制包的信息。

最后得到实际的加载的函数地址之后使用命令 image lookup -a 就大功告成了。

最后

一份 Crash 日志的信息实在是太多了,对 Crash 日志的深入理解可以考察工程师的方方面面知识。如果我是面试官,我会拿一份 Crash 日志让面试者解读,哈哈。

Contents
  1. 1. 前言
  2. 2. Crash 日志的简单解读
    1. 2.1. 基础信息
    2. 2.2. 调用栈记录
    3. 2.3. 寄存器状态
    4. 2.4. 二进制信息
  3. 3. 没有符号化的 crash 如何处理?
  4. 4. 最后