[译]理解 iOS 异常类型

13,345 阅读9分钟

原文链接:Understanding iOS Exception Types (PS.由于未知原因已失效,经检查,文章中引用的链接都还有效 :)

翻译:CoderWangx

当你的iOS应用崩溃的时候,我们需要去分析异常日志以定位根本原因。崩溃可能是 “低内存崩溃 Low Memory Crash” 或者 “普通异常崩溃”。当碰到“异常”时,更好的理解“不同类型的异常”能够真正帮助我们快速定位问题所在。

在这篇文章中,我们将研究 iOS 应用可能碰到的不同类型的“异常”,例如EXC_CRASHEXC_BAD_ACCESSEXC_RESOURCE00000020 等。

崩溃日志中的“异常”

“异常”这个词在“崩溃日志”语境下更多与“Mach 异常”(以“EXC_为前缀”)和 “UNIX 信号”(如: SIGSEGV, SIGBUS等)相关。在某些情况下(应该是有对应的dSYM符号文件时)系统会通过映射将底层的 Mach 异常 翻译为 UNIX 信号。这就是为什么你能log中看到有用 “EXC_CRASH(SIGABRT)”“EXC_BAC_ACCESS(SIGSEGV)” 作为 异常类型(Exception Type)

对于某些异常,还会附带一个关联的 处理器定制异常码(processor-specific Exception Code) 或者 异常子类型(Exception Subtype),用以包含更多问题相关信息。举例来说, “EXC_BAC_ACCESS” 类型异常可能有一行如“KERN_INVALID_ADDRESS at 0x80000010”作为“异常码”; “EXC_RESOURCE” 可能有一行"WAKEUPS"作为"异常子类别"。

UNIX 信号

iOS开发者常见的 UNIX 信号 如下:

UNIX 信号 注释
SIGSEGV 访问无效的内存地址。地址存在,但是应用程序无法访问。
SIGABRT 程序崩溃。由 C函数 abort() 初始化。通常意味着系统检测到某些事务出错,例如 assert() 或者 NSAssert() 校验失败。
SIGBUS 访问无效的内存地址。地址不存在,或对齐无效。(The address does not exist, or the alignment is invalid.)
SIGTRAP 调试器相关
SIGILL 尝试执行非法的、有缺陷、未知的或者需要权限的指令。

更多 UNIX 信号 可以参考这里:Unix_signal

Mach 异常

Mach 异常 描述 注释
EXC_BAD_ACCESS 错误内存访问 访问“错误”内存地址。“错误”可能指“地址不存在”或者“应用没有权限访问”。因此通常与 SIGBUSSIGSEGV 相关联。
EXC_CRASH 异常跳出 通常与 SIGABRT 相关联,意思是由于检测到代码抛出的未捕获异常而使应用程序异常退出。
EXC_BREAKPOINT 跟踪/断点捕获 通用与 SIGTRAP 相关联。可以由你自己的代码或者 NSExceptions 抛出时触发。
EXC_GUARD 违反了受保护资源的防护(Violated Guarded Resource Protection) 由违背受保护资源防护触发,例如‘某些文件描述符’。
EXC_BAD_INSTRUCTION 非法指令 通常与特定非法或未定义指令/操作数相关。
EXC_RESOURCE 资源限制 应用由于达到资源消耗限制而退出。
00000020 十六进制异常类型 非 'OS Kernel' 异常。

查看完整 Mach 异常列表请参考 这里 (sys/osfmk/mach/exception_types.h)的源码文件。

异常

EXC_BAD_ACCESS(错误内存访问)

“EXC_BAD_ACCESS” 是APP崩溃时最常见的异常之一。不幸的是,调试起来却不容易。

一般有两种可能性:

  • 访问某些尚未初始化的对象。(SIGBUS)
  • 访问已经被 ARC 释放(导致地址变为不可访问)的对象。如果是这个情况,你通常可以在崩溃日志中的 “Backtrace” 顶部附近看到 objc_release

示例如:

Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x6d783f44
...
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Codes: KERN_PROTECTION_FAILURE at 0x00000011

“EXC_BAD_ACCESS”也有关联的“异常码”以帮助提供额外信息。举例来说,KERN_PROTECTION_FAILURE 表示内存有效,但是不允许当前形式的访问,KERN_INVALID_ADDRESS 意思是地址当前无效。

查看这里的源码文件获取完整的可能值列表。

为了辅助调试 “EXC_BAD_ACCESS” 类型异常,你可以勾选 Xcode 中的 “Enable Zombie Objects” 后再尝试。

EXC_CRASH(异常跳出)

相较于 “EXC_BAD_ACCESS”,“EXC_CRASH" 更容易遇到。它通常发生在对象接收到未实现的消息时,如 Xcode 调试器中显示的 “unrecognized selector sent to instance 0x6a33840”。

一般情况里这个异常会与调试器一起发挥作用,因为调试器可以中断进程。如果没有附加调试器,会生成一个崩溃日志。

崩溃日志中展示的信息示例:

Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
...
## Usually you will see a similar line in the "backtrace" part
2 CoreFoundation 0x36c02e02 -[NSObject(NSObject) doesNotRecognizeSelector:] + 166

可能存在某些与“unrecognized selector”无关的特殊情况。如果碰到了,请注意到处都有可能发生这种事情。

另一个常见的“EXC_CRASH”情况是关于“应用扩展(App Extensions)”。应用扩展如果“花了太长时间来初始化”则会被系统终止。在这种情况下,异常子类型(Exception Subtype)显示为 LAUNCH_HANG,附带一个得体的异常消息(Exception Message)

Exception Type: EXC_CRASH (SIGABRT)
Exception Subtype: LAUNCH_HANG
Exception Message: The extension took too much time to initialize

EXC_BREAKPOINT(跟踪捕获)

与“EXC_CRASH”非常相似,EXC_BREAKPOINT 也往往与调试器一起发挥作用,在测试阶段被捕获。 当使用 Swift 时,在以下情况这个异常会在运行时抛出:

  • 一个非可选类型值为nil
  • 强制类型转换失败

示例信息如:

Exception Type: EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000002, 0x0000000000000000

你可以在代码中手动调用 __builtin_trap() 来触发这个异常。

EXC_GUARD(违反了受保护资源的防护)

与其他所有“EXC_”前缀的异常不同,这个异常不是一个“原生”的 Mach 异常。事实上,它是为 XNU - 一个苹果开发的衍生操作系统内核 而添加的。

"XNU" 代表 "X 不是 Unix"(X is Not Unix)。 “EXC_GUARD”的定义可以在这里-osfmk/mach/exception_types.h找到。

这个异常的一个较好例子是应用程序在 Core Data 访问 SQLite 文件时关闭了它的“文件描述符(file descriptor)”。

在 iOS7 之前,这个异常会附带一部分“异常码(Exception Codes)”以帮助理解情况。异常码包含“两个”位域代码(如:0x400000010000005e)及subcode(如:0x00007f8254a019c0)。

位域代码部分可分解为“3”个区:

  • Guard Type - 这个时候只有一种类型 - 受保护的文件描述符(guarded file descriptor(GUARD_TYPE_FD))。值为0x2。如果你看到时 0x4 作为代码的前缀,则这个崩溃与“文件描述符”相关。
  • Flavor - 当违反“受保护的文件描述符”时的不同条件: 如果设置了“第1”([32]: "1 << 0")位(kGUARD_EXC_CLOSE),则它曾试图在“受保护的文件描述”上调用 close()。 如果设置了“第2”([33]: "1 << 1")位(kGUARD_EXC_DUP),则它试图在“受保护的文件描述符”上使用 F_DUPFDF_DUPFD_CLOEXEC 调用 dup(2),dup2(2),fcntl(2)。还包含了尝试使用 /dev/fd/打开“文件描述符”。 如果设置了“第3”([34]: "1 << 2")位(kGUARD_EXC_NOCLOEXEC),则它试图关闭“文件描述符”上的“close-on-exec”标志。 如果设置了“第4”([35]: "1 << 3")位(kGUARD_EXC_SOCKET_IPC),则它试图通过 套接字(socket)发送“受保护的文件描述符”。 如果设置了“第5”([36]: "1 << 4")位(GUARD_FILEPORT),则它曾试图通过 套接字(socket)从“受保护的文件描述符”创建一个文件端口。 如果设置了“第6”([37]: "1 << 5")位(kGUARD_EXC_MISMATCH),说明“受保护的文件描述符”与“守卫”不相符。 如果设置了“第7”([38]: << 6)位(kGUARD_EXC_WRITE),则它曾试图通过 套接字(socket)写入一个“受保护的文件描述符”。
  • File Descriptor - 应用尝试操作的受保护的文件描述符。- subcode部分包含“受保护的值”。

详细定义可以在这里/bsd/sys/guarded.h)找到。

从 iOS 7 开始,“Exception Codes”被提供更详细解释的“Exception Subtype”和“Exception Message”替代。

# iOS 6
Exception Type: EXC_GUARD
Exception Codes: 0x400000010000005e, 0x00007f8254a019c0
# The type is "GUARD_TYPE_FD" (0x4), with "kGUARD_EXC_CLOSE". The FD is "94".
# -------
# iOS 7 and above
Exception Type: EXC_GUARD
Exception Subtype: GUARD_TYPE_FD
Exception Message: CLOSE on file descriptor 81 (guarded with 0x0000000017e6eed0)

EXC_BAD_INSTRUCTION(非法指令)

“EXC_BAD_INSTRUCTION”,通常与“SIGILL”关联,是一个非常容易理解的异常 - 即你正在使用“错误”的指令或操作。然而,有时候也很难去调试。

以下是一些较常见的情况。 由于Xcode提供的调试信息,这个很容易识别 - 它是由于不安全的解包导致的。

## Usually show "EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)" in Xcode.
“fatal error: unexpectedly found nil while unwrapping an Optional value”

但是,像这样这样(均为StackOverflow上的问题)的就不容易了 - 第一个是有关于 GCD 的使用,另一个是苹果的bug。 以下是崩溃日志中的显示格式:

Exception Type: EXC_BAD_INSTRUCTION (SIGILL)
Exception Codes: 0x0000000000000001, 0x000000000000b6d2

EXC_RESOURCE

“EXC_RESOURCE”意思是进程“达到资源消耗上限”。通常,当你的应用在一定时间内持续超出限制时会被触发。 这个异常包含“Exception Subtype”以帮助理解实际情况:

  • CPU - 限制为 50%,时间不超过 180秒
  • WAKEUPS - 表示线程每秒唤醒次数太多。限制为 150次/每秒, 时间不超过 300秒
  • MEMORY - 没有相关文档描述限制信息。

与“EXC_GUARD”类似,它曾使用“位域”来传递信息,现在也使用“Exception SubType”和“Exception Message”。

Exception Type: EXC_RESOURCE
Exception Subtype: CPU
Exception Message: (Limit 50%) Observed 85% over 180 secs
---
Exception Type: EXC_RESOURCE
Exception Subtype: WAKEUPS
Exception Message: (Limit 150/sec) Observed 206/sec over 300 secs
---
Exception Type: EXC_RESOURCE
Exception Subtype: MEMORY
Exception Message: Crossed High Water Mark

00000020

与“EXC_”异常不同,这个“异常类型”实际上不能告诉你任何信息。取而代之,你应该查看“异常代码”获取更多详情。

  • 0x8badf00d(读作 ate bad food)- 表示由于 watchdog 出现超时而导致应用被操作系统终止。通常意味着应用程序花了太长时间启动、关闭或者响应系统事件。一个非常典型的情况是“在主线程上做同步网络请求”。
  • 0xbaaaaaad(读作 “plooookhy”)- 表示日志是整个系统的堆栈,而不是崩溃报告。
  • 0xc00010ff(读作 cool off(冷静))- 表示应用程序被系统关闭以响应热事件。
  • 0xbad22222 - 表示操作系统终止了一个VoIP程序,因为它过于频繁的执行恢复。
  • 0xdead10cc(读作 dead lock(死锁))- 表示应用在后台运行时保持了系统资源。
  • 0xdeadfa11(读作 deadfall)- 表示应用被用户强制关闭了。强制关闭发生于用户先按下电源键直到“滑动来关机”出现然后按住主屏幕按钮。

这些“十六进制”代码实际上是六音词 - 由我们开发者创建作为不容易忘记的魔法数字。

扩展阅读

你可查看这篇文章 Demystifying iOS Application Crash Logs 以了解iOS异常日志结构。