iOS解决崩溃和阅读崩溃日志(iOS Carsh日志,以及多线程Carsh))

1,461 阅读8分钟

截屏2021-02-28 下午3.00.43.png

Understanding Crashes and Crash Logs 这个Session主要介绍iOS Crash相关的知识:如何分析crash logs,怎么调试和修复crash问题,比如难以重现的内存问题和多线程问题。 WWDC18 解读崩溃和崩溃日志 本篇也是学习笔记系列,如果允许请配上加冰的快乐水阅读, (适量的糖分会引起兴奋,让你更加享受学习,我就是这么做的,hhhhh)

什么是Crash,为什么会产生crash?

当App发生crash时, attached的debugger会暂停App的运行,并定位的crash位置。

详细看一下crash栈,这里是App启动入口 image.png 这里是crash的具体位置 image.png 发生crash时,debugger会收到signal,然后暂停App的运行,显示crash的调用栈 image.png 如果当前没有attached的debugger,系统会吧crash堆栈信息dump到一个log文件中 image.png release版本的app发生crash时,log文件的调用栈只有地址信息 image.png xcode符号化后的crash log image.png

Accessing crash logs

线上APP发生崩溃时,系统会把log文件上传到云端,然后可以通过xcode的organizer获取并分析crash, 截屏2021-02-28 下午1.09.23.png

crash organizer中有TestFlight and App Store上App的crash数据。包括了最近crash的统计数据,受影响最大的devices信息,crash具体位置信息等 image.png

“Show In Finder” 按钮将打开一个文本文件,获取Log全文

image.png

文件顶部以一些摘要信息开始 这包含你的app名称、版本号 运行它的操作系统版本 以及崩溃的日期和时间 image.png

如何阅读Carsh?

举例,如图! 在这个例子中 异常类型为EXC_BAD_INSTRUCTION异常 SIGILL信号指的是 非法指令信号,这意味着CPU正在尝试执行 由于某种原因不存在或无效的指令 这就是这个进程终止的原因 截屏2021-02-28 下午1.45.28.png 因为这是带有完整调试信息的 符号化堆栈跟踪 我们可以看到一个文件和代码行号 其指明崩溃发生的地方 Swift 26行 所以我们可以看看那段代码 我们打开该项目 这是RecipeImage.swift 第26行是在崩溃中标记的那一行 截屏2021-02-28 下午1.46.42.png

关于carsh解决的经验

大多数程序员在工作中会遇到特别多的Carsh 当你查看足够多的崩溃日志时 你就能够开始找出错误值的一些模式 举例. 截屏2021-02-28 下午1.51.33.png

这里,这个错误就是使用了被释放的空间. 截屏2021-02-28 下午1.52.53.png 我们有了内存分配器使用的地址范围 我们的无效地址看起来 就在malloc范围内 但它被偏移了4位 它被旋转了4位 所以看起来它是一个经过旋转的 有效malloc地址 这是内存分配器本身提供的线索

为什么这个地址被偏移了4位?

截屏2021-02-28 下午1.54.57.png 一个对象以isa字段开始 isa字段指向对象的类 这就是 Objective-C对象的结构 这也是一些Swift对象的结构.

objc_release函数的作用:

读取isa字段 然后解引用该isa字段 从而可以截屏2021-02-28 下午2.20.21.png 到该类对象中查找其方法 通常这当然是有效的 这是正常情况下所发生的事情

但是如果这个对象已经被释放了, 会怎样呢? 当释放函数删除一个对象时 它将其插入到一个 由其它无效对象构成的释放列表中 它会将一个释放列表指针 写入列表中的下一个对象 写入位置 即以前isa字段所在位置 截屏2021-02-28 下午1.58.22.png

但是以一种稍微扭曲的方式 它不会在该字段中直接写入指针 而是将旋转后的指针写入该字段 它想确保写在那里的值 不是有效的内存地址 这正是错误使用该对象 造成崩溃的原因 截屏2021-02-28 下午1.58.45.png

当objc_release 读取isa字段时 它得到的是一个 旋转后的释放列表指针 当它解引用旋转后的释放列表指针时 它就会崩溃

内存分配器为我们做了这件事 它故意旋转了指针 以确保如果我们再次尝试使用它 就会发生崩溃

这就是我们在此崩溃日志中 看到的签名 我们的无效地址字段看起来 像是malloc区域中的指针 但旋转的方式与malloc 旋转其释放列表指针的方式相同

找到具体的对象

有没有办法知道具体是哪个object被多次release导致的crash呢?日志里面虽然有调用栈信息,但是都是编译器生成的函数,没有跟crash相关的具体信息。下面通过一个具体的例子说明如何找到LoginViewController中被多次release的对象。

image.png

// LoginViewController.swift
class LoginViewController: UIViewController {
    var userName: String
    var database: DatabaseProxy
    var views: [UIView]
    ...
}

使用反汇编来找到定位carsh的具体信息

在命令行或者xcode打开lldb command script import lldb.macosx.crashlog 加载crash log文件

截屏2021-02-28 下午2.20.57.png 找到 __ivar_destroyer函数的地址 并对其进行反汇编

截屏2021-02-28 下午2.19.45.png

反汇编指令

disassemble -a (xxx 对应的函数地址值)

这向我们展示了该函数的汇编代码

我没有时间教你如何阅读汇编代码 但幸运的是 对于崩溃日志 你实际上并不需要 能够完全流利地阅读汇编代码 通常只需要简单浏览汇编代码 并大致了解发生了什么就足够了 你不必理解每一条指令 来从崩溃日志中获取有用的信息 截屏2021-02-28 下午2.23.58.png

现在我们回到崩溃日志中的信息 即__ivar_destroyer函数加42 其调用了objc_release函数, 在+42处有一个指令 但还有一个问题 那就是在堆栈跟踪中 大多数堆栈帧的汇编级别偏移量 都是返回地址 它是函数调用之后的指令 所以调用objc_release 的指令是前面一条指令 即这条指令 如果我们读到这个 就说明它是对 objc_release的调用 这很好 这与我们在崩溃日志的堆栈跟踪中 看到的一致 即在此偏移量的 对objc_release的调用 截屏2021-02-28 下午2.26.11.png 这个释放函数正在释放 database属性

(备注: 这里偏移量,为什么这里会有偏移量,然后是偏移多少,还是知道这个偏移,我们只要定位上一条汇编指令就好了, 作者也不是很清楚,如果有小伙伴知道的, 可以加下微信,不胜感激)

Exception Codes 异常出错的代码(常见代码有以下几种) 0x8badf00d错误码:Watchdog超时,意为“ate bad food”。 0xdeadfa11错误码:用户强制退出,意为“dead fall”。 0xbaaaaaad错误码:用户按住Home键和音量键,获取当前内存状态,不代表崩溃。 0xbad22222错误码:VoIP应用(因为太频繁?)被iOS干掉。 0xc00010ff错误码:因为太烫了被干掉,意为“cool off”。 0xdead10cc错误码:因为在后台时仍然占据系统资源(比如通讯录)被干掉,意为“dead lock”。

Crash Log Analysis Summary

  • Understand the crash reason : 明白 Crash 原因
  • Examine the crashed thread’s stack trace : 检查崩溃线程的堆栈跟踪
  • Look for more clues in bad address and disassembly : 在错误地址和反汇编中查找更多线索

Crash Analysis Tips

  • Look at code other than the line that crashed : 看看除了崩溃的行之外的代码
  • Look at thread stack traces other than the crashed thread : 查看崩溃线程以外的线程堆栈跟踪
  • Look at more than one crash log : 查看多个 crash log
  • Use Address Sanitizer and Zombies to reproduce memory errors : 使用 Address Sanitizer 和僵 Zombies 来重现内存错误 截屏2021-02-28 下午2.10.44.png 常见的内存错误崩溃的log图

多线程的Carsh

多线程特别难以重现 因为它们只是偶尔会导致崩溃 因此你的代码似乎在99%的情况下 都能正常工作 并且这些漏洞在很长一段时间内 都不会被发现

多线程carsh的症状

多线程错误也有一些特有的症状 崩溃的线程通常包含 抱歉 崩溃日志通常包含 多个正在执行彼此相关的代码的线程 所以如果某个特定的类或方法 出现在多个线程的崩溃日志中 这表示可能存在多线程错误

多线程内存carsh的症状

多线程问题导致的内存损坏 通常非常随机 因此你可能会发现崩溃发生在 稍微不同的代码行上 或稍微不同的地址 正如Greg所说 你可以看到它们在Xcode中 显示为不同的崩溃点 尽管它们属于同一个漏洞

使用Thread Sanitizer 解决多线程问题

Symptoms of Multithreading Bugs in Crash Logs One of the hardest bug types to reproduce and diagnose: 最难复制和诊断的错误类型之一 Multithreading bugs often cause memory corruptions: 多线程错误通常会导致内存损坏 Multiple threads currently executing similar code: 当前正在执行类似代码的多个线程 One bug can appear as different crash points : 一个bug可以显示为不同的崩溃点

Edit Scheme → Dignostics → Thread Sanitizer → finding buffer overflows

image.png

Tips

  • Test your app on real devices
  • Try to reproduce crashes
  • Use bug-finding tools on hard-to-reproduce crashes
  • Address Sanitizer for memory corruption bugs : 使用Address Sanitizer 调试内存问题
  • Thread Sanitizer for multithreading problems : 使用Thread Sanitizer调试多线程问题

Summary

  • User Organizer to access crash logs: 关注 Organizer 中的crash
  • Analyze reproducible crahses : 分析重复的crahses
  • Look for signs of memory corruption and threading issues : 查找内存损坏和线程问题的迹象
  • Use bug-finding tools to help reproduce: 利用工具帮助复现问题
  • 给每个线程加个名字,发生崩溃容易定位问题 例如,如果使用了命名.那么崩溃的信息就是截屏2021-02-28 下午2.56.08.png

截屏2021-02-28 下午2.54.46.png 这样去找线程是不是轻松很多?