未符号化的 crash debug 经验谈
前言
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,但不是用户 IDProcess
是该进程的名字,这里是支付宝的进程名。放括号内应该是 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 日志让面试者解读,哈哈。