iOS 崩溃分析过程
iOS 崩溃解析
获取崩溃 crash.log
- 真机 Crash 文件目录:var/mobile/Library/Logs/CrashReporter。
- 通过 Xcode:Xcode->Window->Devices,选取设备,View Device Logs。
- 通过 Xcode 直接查看:Xcode -> Window -> Organizer -> Crashes。
- 通过 手机查找:设置->隐私->分析->分析数据,报告命名为 appname-time.ips。
将 XCode 加入环境变量
1 | 查找 XCode 环境 |
获取符号化工具
1 | iOS 15 以下:查找 symbolicatecrash (选对应平台的工具) |
- 错误提示:Error: “DEVELOPER_DIR” is not defined at ./symbolicatecrash line 69。(未配置XCode环境变量)
- 错误提示:No crash report version in file。(iOS 15 crash log 格式做了更新, 需要用到 CrashSymbolicator.py)
确认 crash.log 和 dSYM 的一致性
1 | 获取 app 的 UUID |
还原 crash.log 堆栈信息
方法1:通过符号工具手动还原 crash.log
使用 symbolicatecrash:将 crash 文件、DSYM 文件、symbolicatecrash 工具拷贝到一个目录下,执行:
1 | iOS 15 以下 |
使用 CrashSymbolicator:切换到 CrashSymbolicator.py目录下,执行:
1 | iOS 15 及以上, 使用 Python3 |
方法2:通过 Xcode 还原 crash.log
需要3个文件,放在同一目录下
- crash报告(.crash文件)
- Debug Symbol 符号文件 (.dsym文件)
- 解压 ipa 包后的 .app 文件
操作过程:Xcode -> Devices and Simulators -> 选中设备 -> View Device Logs,
然后把 .crash文件 拖到 Device Logs 或者选择下面的import导入.crash文件。这样你就可以看到crash的详细log了。
方法3:通过 atos 还原单个堆栈信息
1 | xcrun atos -o {executable} -arch {architecture} -l {loadAddress} {stackAddress} |
使用 atos 还原堆栈,需要两个信息:符号的栈地址(stack address)和二进制加载地址(load address)。
iOS 15以下系统的崩溃堆栈,通常包含栈地址(stack address)和符号偏移地址(offset),二进制加载地址(load address)则可能不直接显示,可以通过下面两种方式获得:
- 计算:loadAddress = stackAddress - offset
- 从崩溃日志的 binary Image 部分,找到对应的二进制,获得 loadAddress。
1 | iOS 15以下系统堆栈格式一: [imageName] [stack address] [load address] + [offset] |
iOS 15及以上系统的崩溃堆栈,仅包含 offset,loadAddress 需要根据 imageIndex 从崩溃日志的 usedImages 区域获得,stackAddress 则需要手动计算。
1 | 线程堆栈格式: [imageOffset] [imageIndex] |
方法4:通过 dwarfdump 还原单个堆栈信息
1 | dwarfdump --arch {architecture} {executable} --lookup {dSYM symbol address} |
使用 dwarfdump 还原堆栈,仅需要符号的地址(symbol address),其计算公式:symbol address = start address + offset。start address 为代码段的起始地址,是指 _TEXT 段的内存加载地址,可以通过 otool 命令获得。(也可以通过 MachOView 工具查看)。
1 | 日志格式: [appName] [stack address] [appName] + offset |
通过 otool 命令获得 app 或者 dSYM 的代码段起始地址。
1 | 指令 |
iOS 崩溃分析
旧格式堆栈还原信息(iOS 15 以下)
还原后的 crash 文件信息如下:
1 | Incident Identifier: 668316AC-6AFA-4459-B4BE-6EB53ACDD79B |
注意 Binary Images 下的第一行,包含了 app 主框架的 load address(示例:0x100f74000),CPU 架构(示例:arm64),UUID (示例:f5c7f02ecdbf32cab58577ce6a852e85)。
新格式堆栈还原信息(iOS 15及以上)
还原后的 crash 文件包含两段 json 信息,第一段包含 app 相关信息,第二段包含异常堆栈信息,如下:
1 | { |
- slice_uuid, app 的 Build ID,可以通过此 ID 与对应的 dSYM 符号表进行匹配。
1 | { |
- exception, 崩溃信息。可以获取到崩溃类型等信息。
- faultingThread,崩溃线程序号。从0开始计数。
- threads,线程堆栈信息。其中崩溃线程会带有 “triggered”:true 标识。
- usedImages,二进制信息。可以获取到崩溃二进制段的偏移信息。
常见的 Exception
Mach Exception Type | Signal | 说明 |
---|---|---|
EXC_BAD_ACCESS | SIGSEGV、SIGBUS | 非法访问,通常由于访问了不该访问的内存导致 |
EXC_BAD_INSTRUCTION | SIGILL | 非法指令,通常与特定非法或未定义指令或操作数相关 |
EXC_BREAKPOINT | SIGTRAP | 跟踪陷阱中断进程 |
EXC_ARITHMETIC | SIGFPE | 崩溃的线程执行了无效的算术运算(如除0) |
EXC_CRASH | SIGABRT、SIGKILL、SIGQUIT | 进程收到中止信号 |
SIGSEGV
访问了无效地址。一般是由于内存地址不合法导致,例如访问未申请的虚拟内存地址(空指针,未初始化指针,栈溢出),或者写入没有写权限的内存。子码可能有以下几种:
- KERN_INVALID_FAILURE:试图访问未映射的内存导致的,包括访问数据和取指令。
- KERN_PROTECTION_FAILURE:试图使用受保护的有效内存地址导致的,包括只读内存区域或不可执行内存区域。
SIGBUS
访问了有效地址,但总线访问异常。一般是由于地址未对齐导致的,例如内存地址对齐出错,或者试图执行没有权限的代码地址。子码有以下几种情况:
- KERN_MEMORY_ERROR:试图访问当时无法返回数据的内存,如内存映射文件不可用。
- EXC_ARM_DA_ALIGN:试图访问没有正确对齐的内存。此异常代码很少见,因为64位ARM CPU可处理未对齐的数据。但是,如果内存地址既未对齐又位于未映射的内存区域中,则可能会看到此异常子类型。
SIGABRT
进程调用了abort函数,例如,当应用遇到未捕获的Objective-C或C++异常。SIGABRT等价于”kill -6”,它是用来杀死正在运行的进程,可以被捕获,但不能阻塞。
SIGKILL
此信号表示系统中止进程,通常是调用函数exit()或kill(9)产生。SIGKILL等价于”kill -9”,它是用来杀死僵尸进程;不能被捕获或忽略,接受进程也不能在收到此信号后做任何清理操作。
SIGKILL 时,崩溃报告会包含代表中止原因的编码:
Exception Code | 说明 |
---|---|
0x8badf00d | ate bad food,系统监视程序 watch dog 中止无响应应用。(如主线程进行网络请求) |
0xbaaaaaad | bad,表示这个crash文件是系统的stackshot,并不是crash report,可以通过按住home+voice按键生成 |
0xc00010ff | cool off,系统由于过热保护中止应用,通常与特定的手机和环境有关。 |
0xdead10cc | dead lock,系统中止在后台期间一直保持占用系统资源的应用(进程在suspend期间保持在文件锁或SQLite数据库锁)。 |
0xbaadca11 | bad all,系统由于应用在响应PushKit通知时无法报告CallKit呼叫而中止它。 |
0xbad22222 | 系统由于VoIP应用恢复太频繁而中止程序。(后台调用网络TCP连接被唤醒太多次,例如 300 秒内 15 次,就会导致此崩溃) |
0xbada5e47 | 系统可能由于你启动了过多了后台任务而中止你的应用。 |
0xdeadfa11 | dead fall,程序无响应用户强制关闭(在程序bug造成系统无响应时长按电源键,出现关机确认画面时按下Home键关闭当前程序) |
ILL_ILLTRP:ILL_ILLTRP at 0xxxxx通常是二进制出错,典型比如app升级前后二进制缓存出错。
看门狗 Watchdog
“看门狗”(watchdog)的机制是为了防止一个应用占用过多的系统资源。在不同的场景下,“看门狗”会监测应用的性能。如果超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。
在生命周期的不同阶段,触发看门狗机制的超时时间是不一样的。
生命周期 | 超时时间 |
---|---|
启动 Launch | 20s |
恢复 Resume | 10s |
悬挂 Suspend | 10s |
退出 Quit | 6s |
后台 Background | 10min |
在连接 Xcode 调试时为了便于调试,系统会暂时禁用掉 Watchdog,所以此类问题的发现需要使用正常的启动模式。
扩展:LC_SEGMENT
Mach-O文件的主要功能在于加载命令(load command),加载命令紧跟在文件头之后。
LC_SEGMENT(或LC_SEGMENT_64)命令是最主要的加载命令, 用于将segment直接从Mach-O二进制文件加载到内存中。对于每一个段,将文件中相对应的内容加载到内存中:从偏移量为fileoff处加载 filesize 字节到虚拟内存地址 vmaddr 处的 vmsize 字节。
一条LC_SEGMENT命令都提供了段布局的所有必要细节信息,如下表:
_PAGEZERO段(空指针陷阱)、_TEXT段(程序代码)、_DATA段(程序数据)和_LINKEDIT(链接器使用的符号和其他表)段提供了 LC_SEGMENT 命令。段也可以进一步分解为区(section)。
Mach-O可执行文件中常见的段和区:
常用工具
https://github.com/answer-huang/dSYMTools
https://github.com/Zuikyo/iOS-System-Symbols
参考资料
https://ctinusdev.github.io/2017/08/27/Mach-OBasis_Loadcommand/
https://developer.apple.com/library/archive/qa/qa1592/_index.html
https://developer.apple.com/documentation/xcode/diagnosing-issues-using-crash-reports-and-device-logs
https://developer.apple.com/documentation/xcode/understanding-the-exception-types-in-a-crash-report
https://developer.apple.com/documentation/xcode/adding-identifiable-symbol-names-to-a-crash-report#Symbolicate-the-Crash-Report-in-Xcode