iOS_Crash 三:异常类型

文章目录

    • 1. `EXC_BREAKPOINT (SIGTRAP)` 和 `EXC_BAD_INSTRUCTION (SIGILL)`
    • 2. `EXC_BAD_ACCESS` 内存访问问题
      • 2.1. 僵尸对象
      • 2.2. 内存访问问题
        • 2.2.1. 异常子类型
        • 2.2.2. VM Region Info
        • 2.2.3. 内存访问类型
    • 3. `EXC_CRASH(SIGABRT)`
      • 3.1. 语言异常
      • 3.2. 配置错误 - 缺少框架
    • 4. `EXC_CRASH(SIGKILL)`
    • 5. `EXC_CRASH(SIGQUIT)` 应另一个进程的请求而终止
    • 6. `EXC_GUARD` 受保护资源
    • 7. `EXC_RESOURCE` 超出了资源消耗限制


1. EXC_BREAKPOINT (SIGTRAP)EXC_BAD_INSTRUCTION (SIGILL)

断点异常类型表示跟踪陷阱(trace trap)中断了该进程。跟踪陷阱使附加的调试器有机会在进程执行的特定点中断进程。
ARM 处理器上显示为 EXC_BREAKPOINT(SIGTRAP)
x86_64 处理器上显示为 EXC_BAD_INSTRUCTION(SIGILL)

  • Swift 运行时错误
    Swift 使用内存安全技术来及早捕获编程错误。如果 Swift 运行时遇到编程错误,运行时会捕获该错误并故意使程序崩溃,这些崩溃在崩溃报告中具有可识别的异常信息:
  1. 在 ARM 处理器:
Exception Type:  EXC_BREAKPOINT (SIGTRAP)
...
Termination Signal: Trace/BPT trap: 5
Termination Reason: Namespace SIGNAL, Code 0x5
  1. 在 Intel 处理器上:
Exception Type:        EXC_BAD_INSTRUCTION (SIGILL)
...
Exception Note:        EXC_CORPSE_NOTIFY

Termination Signal:    Illegal instruction: 4
Termination Reason:    Namespace SIGNAL, Code 0x4

常见原因如:使用 ! 强行解开可选值 nil,或使用 as! 强制向下转换失败。

一些底层库(如 Dispatch)在遇到不可恢复的错误时,会捕获此异常类型,并在 Additional Diagnostic Information 中记录有关该错误的附加信息。

若想在自己代码中使用相同技术来处理不可恢复的错误,请调用 __builtin_trap() 函数,这将允许系统生成带有线程回溯的崩溃报告,表明代码如何达到不可恢复的错误。


2. EXC_BAD_ACCESS 内存访问问题

2.1. 僵尸对象

当对象被释放后,再给其发送消息,此时是由运行时的僵尸对象接收。向已释放的对象发送消息可能会导致OC运行时的objc_msgSendobjc_retainobjc_release函数崩溃。如:

Thread 0 Crashed:
0   libobjc.A.dylib                   0x00000001a186d190 objc_msgSend + 16
1   Foundation                        0x00000001a1f31238 __NSThreadPerformPerform + 232
2   CoreFoundation                    0x00000001a1ac67e0 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24

另一种可能由于僵尸对象导致的崩溃是 Last Exception Backtrace 包含 doesNotRecognizeSelector(_:)

Last Exception Backtrace:
0   CoreFoundation                    0x1bf596a48 __exceptionPreprocess + 220
1   libobjc.A.dylib                   0x1bf2bdfa4 objc_exception_throw + 55
2   CoreFoundation                    0x1bf49a5a8 -[NSObject+ 193960 (NSObject) doesNotRecognizeSelector:] + 139

如果能复现该崩溃,在控制台的日志如下:

Terminating app due to uncaught exception 'NSInvalidArgumentException',
    reason: '-[NSNumberFormatter playSound]: 
    unrecognized selector sent to instance 0x28360dac0'

该示例给对象发送一条消息未被实现的消息,所以崩溃了。


2.2. 内存访问问题

当程序以意外的方式使用内存时,会导致内存访问问题的崩溃报告。这些报告的异常类型为 EXC_BAD_ACCESSEXC_BAD_ACCESS (SIGBUS) 。如:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000

macOS 下内存访问问题有时只能通过信号来识别,如 SIGSEGVSEGV_MAPERRSEGV_NOOP

Exception Type: SIGSEGV
Exception Codes: SEGV_MAPERR at 0x41e0af0c5ab8

Xcode 调试内存访问的工具有:

  • Address Sanitizer
  • Undefined Behavior Sanitizer
  • Thread Sanitizer

如果程序包含 OC 、C 或 C++ 代码,可使用静态分析器,可识别常见的编程错误。


2.2.1. 异常子类型

Exception Subtype 包含 kern_return_t 描述错误和被错误访问的内存地址,如:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000

macOS 下 Exception Codes 包含 Exception Subtype

Exception Type:        EXC_BAD_ACCESS (SIGBUS)
Exception Codes:       KERN_MEMORY_ERROR at 0x00000001098c1000

异常子类型:

  • KERN_INVALID_ADDRESS:通过访问数据或取指令来访问未映射的内存
  • KERN_PROTECTION_FAILURE:尝试使用受保护的有效内存地址
  • KERN_MEMORY_ERROR:尝试访问但是无法返回数据的内存,如:不可用的内存映射文件
  • EXC_ARM_DA_ALIGN:尝试访问未正确对其的内存,此异常代码很少见,因为 64 位 ARM 的 CPU 会处理为对齐的数据。

arm64e 的 CPU 框架使用加密签名的指针身份验证代码来检测和防止内存中指针的意外更改。由于指针身份验证失败而导致的崩溃会有附加信息:

Exception Type:  EXC_BAD_ACCESS (SIGBUS)
Exception Subtype: KERN_INVALID_ADDRESS at 0x00006f126c1a9aa0 -> 0x000000126c1a9aa0 (possible pointer authentication failure)

有关指针身份验证的更多信息,可参阅 Preparing your app to work with pointer authentication


2.2.2. VM Region Info

VM Region Info 字段显示错误访问的特点内存相对于应用程序地址空间其他部分的位置,如:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
VM Region Info: 0 is not in any region.  Bytes before following region: 4307009536
      REGION TYPE                      START - END             [ VSIZE] PRT/MAX SHRMOD  REGION DETAIL
      UNUSED SPACE AT START
--->  
      __TEXT                 0000000100b7c000-0000000100b84000 [   32K] r-x/r-x SM=COW  ...pp/MyGreatApp

引用未映射的内存触发崩溃在 0x0000000000000000,这是一个无效地址,即一个 NULL 指针。此无效地址位置是应用程序地址控件中有效内存区域的 4307009536 字节。

另外,例如:

Exception Type:  EXC_BAD_ACCESS (SIGBUS)
Exception Subtype: KERN_PROTECTION_FAILURE at 0x000000016c070a30
VM Region Info: 0x16c070a30 is in 0x16c070000-0x16c074000;  bytes after start: 2608  bytes before end: 13775
      REGION TYPE                      START - END             [ VSIZE] PRT/MAX SHRMOD  REGION DETAIL
      Stack                  000000016bfe8000-000000016c070000 [  544K] rw-/rwx SM=COW  thread 12
--->  STACK GUARD            000000016c070000-000000016c074000 [   16K] ---/rwx SM=NUL  ...for thread 11
      Stack                  000000016c074000-000000016c0fc000 [  544K] rw-/rwx SM=COW  thread 11

内存地址为 0x000000016c070a30,位于由箭头标识的堆栈保护的特殊内存区域中,该内存区域将一个线程的堆栈与另一个线程的堆栈缓冲。 PRT栏显示了内存区域当前的权限属性,r为可读,w为可写,x为可执行。由于没有权限,所以访问无效,且崩溃报告将此内存访问标识为违反内存保护属性。
堆栈保护只是受保护内存的一个示例,还有其他类型的受保护内存区域,具有不同的保护属性组合。
有关 VM Region Info 的更多信息可参阅:Interpreting vmmap’s Output


2.2.3. 内存访问类型

计数寄存器器包含导致内存访问异常指令的地址

  1. 无效内存读取:当代码取消引用无效指针时。计数寄存器与异常地址不同。如:
Exception Type:  SIGSEGV
Exception Codes: SEGV_MAPERR at 0x21474feae2c8
...
Thread 12 crashed with X86-64 Thread State:
   rip: 0x00007fff61f5739d    rbp: 0x00007000026c72c0    rsp: 0x00007000026c7248    rax: 0xe85e2965c85400b4 
   rbx: 0x00006000023ee2b0    rcx: 0x00007f9273022990    rdx: 0x00007000026c6d88    rdi: 0x00006000023ee2b0 
   rsi: 0x00007fff358aae0f     r8: 0x00000000000003ff     r9: 0x00006000023edbc0    r10: 0x000021474feae2b0 
   r11: 0x00007fff358aae0f    r12: 0x000060000237af10    r13: 0x00007fff61f57380    r14: 0x00006000023ee2b0 
   r15: 0x0000000000000006 rflags: 0x0000000000010202     cs: 0x000000000000002b     fs: 0x0000000000000000 
    gs: 0x0000000000000000 

计数寄存器是 0x00007fff61f5739d,与异常地址 0x21474feae2c8 不同。

  1. 无效指令读取:当函数通过错误的函数指针或通过对意外对象的函数调用跳转到另一个函数时。寄存器与异常地址相同。如:
Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000040
...
Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   ???                               0x0000000000000040 0 + 64
...
Thread 0 crashed with ARM Thread State (64-bit):
    x0: 0x0000000000000002   x1: 0x0000000000000040   x2: 0x0000000000000001   x3: 0x000000016dcfe080
    x4: 0x0000000000000010   x5: 0x000000016dcfdc8f   x6: 0x000000016dcfdd80   x7: 0x0000000000000000
    x8: 0x000000010210d3c8   x9: 0x0000000000000000  x10: 0x0000000000000014  x11: 0x0000000102835948
   x12: 0x0000000000000014  x13: 0x0000000000000000  x14: 0x0000000000000001  x15: 0x0000000000000000
   x16: 0x000000010210c0b8  x17: 0x00000001021063b0  x18: 0x0000000000000000  x19: 0x0000000102402b80
   x20: 0x0000000102402b80  x21: 0x0000000204f6b000  x22: 0x00000001f6e6f984  x23: 0x0000000000000001
   x24: 0x0000000000000001  x25: 0x00000001fc47b690  x26: 0x0000000102304040  x27: 0x0000000204eea000
   x28: 0x00000001f6e78fae   fp: 0x000000016dcfdec0   lr: 0x00000001021063c4
    sp: 0x000000016dcfdec0   pc: 0x0000000000000040 cpsr: 0x40000000
   esr: 0x82000006 (Instruction Abort) Translation fault

Binary Images:
0x102100000 - 0x102107fff MyCoolApp arm64  <87760ecf8573392ca5795f0db63a44e2> /var/containers/Bundle/Application/686CA3F1-6CC5-4F84-8126-EE22D03BC161/MyCoolApp.app/MyCoolApp

计数寄存器为 0x0000000000000040,与异常子类型中报告的地址一致。因为是一次错误的取指令,所以回溯中的0帧不包含正在运行的函数(是 ???,而不是符号名)。链接寄存器lr正常情况下包含调用后代码将返回的位置,可以跟踪到错误指令指针。
链接寄存器 0x00000001021063c4 是应用程序进程中加载二进制文件中的指令地址,二进制图像部分显示该地址位于二进制文件内。

x86_64 CPU 架构返回地址存储在堆栈上,而不是链接寄存器中,所以无法追踪函数指针来源。


3. EXC_CRASH(SIGABRT)

表示进程收到 SIGABRT 信号而终止,通常此信号是因为进程调用了 abort()函数。
如应用程序遇到了未捕获的 OC 或 C++ 的语言异常。

3.1. 语言异常

Apple 的系统框架在运行时遇到某些类型的编程错误时会引发语言异常,如: 访问数组的索引越界 或 未实现协议所需的方法。这类异常包含以下信息:

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note:  EXC_CORPSE_NOTIFY

语言异常导致的崩溃包含 Last Exception Backtrace

Last Exception Backtrace:
0   CoreFoundation                    0x19aae2a48 __exceptionPreprocess + 220
1   libobjc.A.dylib                   0x19a809fa4 objc_exception_throw + 55

根据 Last Exception Backtrace 的堆栈信息,可以定位到引发异常的代码。

Note: C++ 异常引发的崩溃,Apple 不提供代码回溯。
Note: 如果抛出异常的 API 是 doesNotRecoganizeSelector(_:),则崩溃可能是由于僵尸对象造成的。

如果没有 Last Exception Backtrace 表明语言异常触发了崩溃,请查看崩溃线程的回溯以确定进程中的代码是否调用了 abort()


3.2. 配置错误 - 缺少框架

如果程序因缺少必要框架而崩溃,报告会包含 EXC_CRASH (SIGABRT)Exception CodesTermination Description 描述 dyld (识别动态链接器)找不到特定的框架。如:

Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Description: DYLD, dependent dylib '@rpath/MyFramework.framework/MyFramework'
    not found for '/MyCoolApp.app/MyCoolApp', tried but didn't find: 
    '/usr/lib/swift/MyFramework.framework/MyFramework' 
    '<path>/MyCoolApp.app/Frameworks/MyFramework.framework/MyFramework' 
    '@rpath/MyFramework.framework/MyFramework' 
    '/System/Library/Frameworks/MyFramework.framework/MyFramework'

当 app 的 extension 花费太多时间在 initialize 时,系统会发送 SIGABRT 信号中断进程。报告的 Exception Subtype 字段会包含 LAUNCH_HANG 信息。因为 extensions 没有 main 函数,所以初始化所花费的时间都发生在 扩展 和 依赖库中的静态构造函数 和 load() 方法中,尽管异常信息不同于 watchdog 。


4. EXC_CRASH(SIGKILL)

操作系统终止了该进程,报告中的 Termination Reason 带有解释崩溃原因的代码。如:

Exception Type:  EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note:  EXC_CORPSE_NOTIFY
Termination Reason: Namespace RUNNINGBOARD, Code 0xdead10cc
  1. 0x8badf00d watchdog

(ate bad food) 操作系统使用 watchdog 来监控应用程序的响应能力,watchdog 会终止长时间无法响应的应用程序。被 watchdog 终止的崩溃报告,Termination Reason 中的 Code 为 0x8badf00d。 例如:

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

Termination Description 包含应用程序如何花费时间的信息,如:

Termination Description: SPRINGBOARD, 
    scene-create watchdog transgression: application<com.example.MyCoolApp>:667
    exhausted real (wall clock) time allowance of 19.97 seconds 
    | ProcessVisibility: Foreground 
    | ProcessState: Running 
    | WatchdogEvent: scene-create 
    | WatchdogVisibility: Foreground 
    | WatchdogCPUStatistics: ( 
    |  "Elapsed total CPU time (seconds): 15.290 (user 15.290, system 0.000), 28% CPU", 
    |  "Elapsed application CPU time (seconds): 0.367, 1% CPU" 
    | )

Termination Description 中出现:
scene-create:表示未在允许的事件内将UI的第一帧渲染到屏幕上
scene-update:表示没有足够快地更新其UI,因为主线程太忙。
Elapsed total CPU time:显示 CPU 在挂钟事件内为系统上的所有进程运行了多少时间。(此时间是跨 CPU 的总 CPU 利用率,可能超过100%。如:一个 CPU 利用率为 100%,第二个利用率为 20%,则总利用率为 120%)这个数字处于任意一个极端都是表明存在问题的,若过高,则应用程序正在其所有线程中执行大量工作(包括所有线程,而不仅是主线程);若过低,则应用程序大部分处于空闲状态,因为它正在等在系统资源,如:网络连接。

主线程的回溯并不一定包含问题根源。例如:有项任务需要4s,而允许的总挂钟时间为5s。当 watchdog 在 5s 后终止程序时,花费 4s 的代码不会出现在回溯中,因为它已经完成,但它几乎消耗掉了整个挂钟时间。崩溃报告记录了 watchdog 终止程序时正在执行的操作回溯,即使它并不是问题根源。

隐藏的同步网络代码:

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libsystem_kernel.dylib            0x00000001c22f8670 semaphore_wait_trap + 8
1   libdispatch.dylib                 0x00000001c2195890 _dispatch_sema4_wait$VARIANT$mp + 24
2   libdispatch.dylib                 0x00000001c2195ed4 _dispatch_semaphore_wait_slow + 140
3   CFNetwork                         0x00000001c57d9d34 CFURLConnectionSendSynchronousRequest + 388
4   CFNetwork                         0x00000001c5753988 +[NSURLConnection sendSynchronousRequest:returningResponse:error:] + 116  + 14728
5   Foundation                        0x00000001c287821c -[NSString initWithContentsOfURL:usedEncoding:error:] + 256
6   libswiftFoundation.dylib          0x00000001f7127284 NSString.__allocating_init+ 680580 (contentsOf:usedEncoding:) + 104
7   libswiftFoundation.dylib          0x00000001f712738c String.init+ 680844 (contentsOf:) + 96
8   MyCoolApp                         0x00000001009d31e0 ViewController.loadData() (in MyCoolApp) (ViewController.swift:21)

示例在第7帧中,调用init(contentsOf:)方法,会在返回前隐式触发同步网络请求(网络好的情况下会立马返回,但在网络差的情况下会耗时很长)。其他通过 URL 初始化的类,如:XMLParserNSData 也一样。
隐式同步网络其他常见示例:

  • SCNetworkReachability:默认是同步的,如 SCNetworkReachabilityGetFlags(_:_:)方法。可以使用 NWPathMonitor 代替。
  • BSD 提供的 DNS 方法:如:gethostbyname(_:)gethostbyaddr(_:_:_:) 在主线程调用都是不安全的。getnameinfo(_:_:_:_:_:_:_:)getaddrinfo(_:_:_:_:) 只有当你只使用 IP 地址,而不是 DNS名称 时才是安全的(即:你分别指定 AI_NUMERICHOSTNI_NUMERICOST)。可以使用 CFHost 里的 APIs 代替。

  1. 0xc00010ff 发热

(cool off) 系统由于发热事件终止了程序。可能是发生崩溃的特定设备或其运行环境的问题导致。有关使程序更高效运行,可参阅iOS Performance and Power Optimization with Instruments


  1. 0xdead10cc 死锁

(dead lock) 系统终止了程序,因为其在挂起期间保留了文件锁或 SQLite 数据库锁。使用 beginBackgroundTask(withName:expirationHandler:) 请求在主线程上额外的后台执行时间。在开始写入文件之前发出此请求,以便在应用程序挂起之前完成这些操作并放弃锁定。在程序扩展中使用 beginActivity(options:reason:) 来管理此工作。


  1. 0xbaadca11 错误访问

(bad call) 系统未能报告 CallKit 的调用,以响应 PushKit 的通知,而终止了程序


  1. 0xbad22222 频繁调用
    系统终止了 VoIP 程序,因为它恢复太频繁

  1. 0xbaddd15c 空间不足
    (bad disc) 系统终止程序以删除缓存来尝试回收磁盘空间。许多因素都会导致磁盘空间不足,建议最大限度地减少写入磁盘的内容并管理文件的整个生命周期。

  1. 0xc51bad01 占用CPU
    watchOS 终止了程序,因为它在执行后台任务时使用了太多CPU的时间。优化执行后台任务的代码以提供 CPU 效率,或减少程序在后台运行时执行的工作量以解决此崩溃问题。

  1. 0xc51bad02 超时执行
    watchOS 终止了程序,因为它未能在分配的时间内完成后台任务。减少程序在后台运行时执行的工作量以解决此崩溃问题。

  1. 0xc51bad03 系统繁忙
    watchOS 终止了程序,因为它未能在分配的时间内完成后台任务,但系统总体上足够繁忙,以至于程序可能没有获得太多的CPU时间来执行后台任务。尽管可以通过减少应用程序在后台任务中执行的工作量来避免该问题,但0xc51bad03并不表明该应用程序做了任何错误。更有可能的是,由于整体系统负载,应用程序无法完成其工作。

5. EXC_CRASH(SIGQUIT) 应另一个进程的请求而终止

EXC_CRASH (信号退出) 表示进程应另一个有权管理其生命周期的进程的请求而终止。SIGQUIT并不意味这进程崩溃了,但可能以可检测的方式出现了错误行为。

如果 iOS 和 iPadOS 键盘扩展加载时间过长,主应用程序会终止键盘扩展。尽管与 watchdog 的异常信息不同,可参考:2.4.1. 0x8badf00d watchdog


6. EXC_GUARD 受保护资源

进程入侵了受保护的资源,尽管受保护的系统资源有很多类型,但大多数受保护的资源崩溃都有来自受保护文件的描述,这些文件描述在字段中具有值。系统将文件描述标记为受保护,以使普通文件描述的 API 无法修改它们。
Exception Message字段包含具体的违规行为:

  • CLOSE:程序尝试对受保护的文件调用 close()
  • DUP:程序尝试通过 F_DUPFDF_DUPFD_CLOEXEC 命令,对受保护的文件调用 dup()dup2()fcntl()
  • NOCLOEXEC:程序尝试删除受保护文件的 FD_CLOEXEC 标志
  • SOCKET_IPC:程序尝试通过 socket 发送受保护的文件
  • FILEPORT:程序尝试获取受保护文件的 Mach 发送权
  • WRITE:程序尝试对受保护的文件描进行写入

7. EXC_RESOURCE 超出了资源消耗限制

来自系统的EXC_RESOURCE通知,表明该进程超出了资源消耗限制。
如果 Exception Note 字段包含 NON-FATAL CONDITION,即使系统生成崩溃报告,进程也不会终止。
Exception Message 字段描述了特定时间间隔内消耗的资源量。
Exception Subtype字段列出了特定资源:

  • CPUCPU_FATAL:进程中的线程在短时间内占用过多的 CPU
  • MEMORY:进程超出了系统设置的内存限制
  • IO:进程短时间内对磁盘的写入量过多
  • WAKEUPS:进程中的线程每秒唤醒次数过多,这会消耗电池寿命。线程通信 API,如 perform(_:on:with:waitUntilDone:)async(execute:)dispatch_async,当无意识的调用导致频率远高于预期时会导致这种崩溃。因为触发此异常的通信频繁发生,通常多个后台线程具有非常相似的回溯,指示线程通信的起源。了解如何更有效地管理并发工作负载,可参阅Modernizing Grand Central Dispatch Usage

参考:
Identifying the cause of common crashes
Analyzing a crash report
Addressing language exception crashes
Understanding the exception types in a crash report
Signal (IPC)
Investigating memory access crashes
Addressing watchdog terminations

你可能感兴趣的:(iOS开发,ios,cocoa,macos,crash)