项目相关-崩溃类型

本文罗列一下目前项目中遇到的崩溃的场景以及对崩溃的理解。

# 崩溃场景

# dispatch_group 非对称调用

Firebase 后台看到的异常信息

BUG IN CLIENT OF LIBDISPATCH: Unbalanced call to dispatch_group_leave()

原因是多调用了一次 dispatch_group_leave(),尝试复现得到如下的崩溃信息

Exception Type:  EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x00000001801796c4
Termination Reason: SIGNAL 5 Trace/BPT trap: 5
Terminating Process: exc handler [5338]

Swift 运行时对特定类型的不可恢复错误使用跟踪陷阱(trace trap),即 SIGTRAP 信号量。 上面这种崩溃就是 Swift 运行时遇到的程序错误,同时运行时会捕获错误并使用应用崩溃,参考 Determine whether the crash is a Swift runtime error (opens new window)

SIGTRAP 在 POSIX 的定义是「指示一个实现定义的硬件故障」。当执行断点指令时,实现常用此信号将控制转移至调试程序,但现在的场景明显不是这样。

我理解是 Swift 运行时候触发了陷阱,内核向进程发出了 SIGTRAP 信号,默认情况下进程对 SIGTRAP 的处理就是终止程序,除非我们预先注册了信号处理方法,我理解大概是这个样子,图来自这里 (opens new window)

Untitled

系统调用(system call),指运行在用户空间 (opens new window)程序 (opens new window)操作系统 (opens new window)内核 (opens new window)请求需要更高权限运行的服务。

关于 Exception Type: EXC_BREAKPOINT (SIGTRAP) 多说一句,异常信息中的 EXC_BREAKPOINT 是 iOS 底层 Mach Exception 的一种类型;SIGTRAP 是 Unix 中信号(signal)的一种类型。这两个异常信息是对应的,Mach 根据不同的场景产生了 EXE_ 前缀的异常信息。

在 iOS 和 macOS 中信号是通过 Mach Exception 经过内核转换,为什么要进行转换?因为 iOS 和 macOS 是实现了 POSIX 标准的(通过 Mach 上的 BSD 层),而信号(signal) 就是基于 POSIX 标准开发的通信机制。我自己理解就是为了兼容 POSIX 标准才对底层的 Mach Exception 做了一次映射,让它有对应的信号类型。

# 强制解包

声明变量的时候可以使用强制解包符作为类型后缀,这样可以绕过编译器自动初始化变量的安全检查,但是这意味着开发者自己担负起初始化变量的职责,否则在使用变量的时候就会异常。

var view:TestView!
view.backgroundColor = .clear 
- Unexpectedly found nil while implicitly unwrapping an Optional value

如果线上遇到类似问题,报错也和上面的 dispatch_group 非对称类似,是运行时捕获到的错误引起的应用崩溃。

实际开发时候遇到类似的错误场景会比上面绕一些,但是原理是类似的。所以还是尽量不要使用强制解包符去声明变量。

# 强制类型转换

Swift 是类型安全的语言,但是如果有的时候想要做强制的类型转换的话,语言是提供了强制类型转换机制,但是运行时发现强制类型转换失败的话,就会报异常。

var value:Any = 10
var name:String = value as! String
- Could not cast value of type 'Swift.Int' (0x1c1d85300) to 'Swift.String' (0x1c1d83150).

解决办法也就是用条件类型转换 as? 提供一种类型转换失败的处理逻辑,

同上面的异常一样,这种异常也是 Swift 运行时捕获的异常,对应默认的处理方式是终止程序。

# 数组越界

这个也是比较常见的错误,不过可以通过添加 safe 拓展解决,还算优雅

var arr = [Int]()
print(arr[0])
- Swift/ContiguousArrayBuffer.swift:600: Fatal error: Index out of range

它的崩溃类型也是 EXC_BREAKPOINT (SIGTRAP),即也是 Swift 运行时捕获的崩溃

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libswiftCore.dylib  0x18c986650 closure #1 in closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:) + 224
1   libswiftCore.dylib  0x18c98652c closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:) + 316
2   libswiftCore.dylib  0x18c985f98 _assertionFailure(_:_:file:line:flags:) + 168
3   libswiftCore.dylib  0x18c96bbbc Array._checkSubscript(_:wasNativeTypeChecked:) + 152
4   libswiftCore.dylib  0x18c96c318 Array.subscript.getter + 84

OC 中数组越界的崩溃类型并不是这个,而是 EXC_CRASH (SIGABRT)。参考 Addressing language exception crashes (opens new window)

# 多线程操作

对同一个数组进行多线程操作会导致异常

var array = [Int]()
for i in 0..<100 {
    DispatchQueue.global().async { array.append(i) }
    DispatchQueue.global().async { array.append(i) }
}

对应的崩溃类型和信息

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000beaddccdd610 -> 0x00003eaddccdd610 (possible pointer authentication failure)
Termination Reason: SIGNAL 11 Segmentation fault: 11
Thread 0::  Dispatch queue: com.apple.main-thread
0   libsystem_platform.dylib      	       0x104dbb570 _platform_memset + 128
1   libsystem_malloc.dylib        	       0x180199654 _nanov2_free + 456
2   Foundation                    	       0x180d11f44 -[NSConcreteHashTable dealloc] + 180
3   Foundation                    	       0x180d41dc0 NSKeyValueWillChangeWithPerThreadPendingNotifications + 520
4   QuartzCore                    	       0x1884cfb5c CA::Layer::begin_change(CA::Transaction*, unsigned int, objc_object*, objc_object*&) + 224
5   QuartzCore                    	       0x1884d72b8 CA::Layer::set_delegate(objc_object*, bool) + 140
6   UIKitCore                     	       0x10de7ae30 _UIViewSetLayer + 356
7   UIKitCore                     	       0x10de7b6dc UIViewCommonInitWithFrame + 1264

关于 KERN_INVALID_ADDRESS 的说明崩溃的线程通过访问数据或取指令来访问未映射的内存。确定导致问题的内存访问类型描述了如何区分差异。

这里的异常信息包含「possible pointer authentication failure」这样一条语句,关于指针验证代码(PAC)的说明,可以参考这篇 Preparing your app to work with pointer authentication (opens new window)。我自己理解是为了保证对指针读写的原子性。

Pointer authentication works by offering a special CPU instruction to add a cryptographic signature — or PAC — to unused high-order bits of a pointer before storing the pointer. Another instruction removes and authenticates the signature after reading the pointer back from memory. Any change to the stored value between the write and the read invalidates the signature. The CPU interprets authentication failure as memory corruption and sets a high-order bit in the pointer, making the pointer invalid and causing the app to crash.

# Objective-C 数组插入空值

有的时候从服务器解析回来的数据进行反序列化的时候,可能会失败导致 Model 为空,此时如果直接将 Model 插入到数组会引起应用闪退。

NSString *value = nil;
NSMutableArray *values = [NSMutableArray arrayWithCapacity:0];
[values addObject:value];

对应的崩溃类型如下

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Termination Reason: SIGNAL 6 Abort trap: 6
Last Exception Backtrace:
0   CoreFoundation                	       0x18046589c __exceptionPreprocess + 160
1   libobjc.A.dylib               	       0x18005c09c objc_exception_throw + 56
2   CoreFoundation                	       0x18034ac7c -[__NSArrayM insertObject:atIndex:] + 1252
3   OCVCDemo                      	       0x10432a830 -[ViewController viewDidLoad] + 1296 (ViewController.m:65)

OC 中的数组异常和 Swift 数组异常的崩溃类型不一样,应该是触发陷阱的方式不一样。

SIGABRT 对应的是调用 abort 函数时候产生的信号,标记进程异常终止。

# 内存异常

从 MRC 切换到 ARC 之后内存异常已经少很多了,从 Objective-C 切换到 Swift 之后,就感觉几乎没怎么遇见过内存异常,比如说访问野指针访问,以及访问未初始化变量之类这种异常,因为语言层面已经做的很完善了。循环引用倒时不时的会遇见,但是循环引用也不会引起崩溃啊,内存泄漏是需要通过别的方式去发现。

像是 OC 中之前遇到的未初始化崩溃的问题是

@property(nonatomic, strong) void(^block)(void); 
self.block(); //未初始化直接调用

对应崩溃信息如下

Exception Type:  EXC_BAD_ACCESS (SIGABRT)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000010
Exception Codes: 0x0000000000000001, 0x0000000000000010
Termination Reason: SIGNAL 6 Abort trap: 6
//
Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libsystem_kernel.dylib        	       0x1028609ec __pthread_kill + 8
1   libsystem_pthread.dylib       	       0x10298f1d0 pthread_kill + 256
2   libsystem_c.dylib             	       0x1801375ec abort + 104
3   libclang_rt.asan_iossim_dynamic.dylib	       0x10396bc58 __sanitizer::Abort() + 64
4   libclang_rt.asan_iossim_dynamic.dylib	       0x10396b3c4 __sanitizer::Die() + 208
5   libclang_rt.asan_iossim_dynamic.dylib	       0x103951118 __asan::ScopedInErrorReport::~ScopedInErrorReport() + 1120
6   libclang_rt.asan_iossim_dynamic.dylib	       0x10394d9ac __asan::ReportDeadlySignal(__sanitizer::SignalContext const&) + 340
7   libclang_rt.asan_iossim_dynamic.dylib	       0x10394cf8c __asan::AsanOnDeadlySignal(int, void*, void*) + 96
8   libsystem_platform.dylib      	       0x1028d7c60 _sigtramp + 52
9   OCVCDemo                      	       0x10278e63c -[ViewController viewDidLoad] + 864 (ViewController.m:55)

# 权限申请异常

崩溃的原因是 App 尝试访问用户的隐私数据,但并没有在 info.plist 添加使用说明配置。

崩溃信息如下

Exception Type: EXC_CRASH (SIGKILL) 
Exception Codes: 0x0000000000000000, 0x0000000000000000 
Exception Note: EXC_CORPSE_NOTIFY 
Termination Reason: TCC, This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist 
Triggered by Thread: 10

崩溃的场景是我们的 App 加载 H5 的时候,H5 里面有很多图片,如果不做额外设置的话,用户可以长按 H5 中的图片唤起 WebView 默认的图片操作弹窗,这时候点击弹窗就会报上面的崩溃。

解决方案是让前端同事禁掉图片的交互。

# 看门狗超时

崩溃的原因是因为看门狗超时

崩溃信息如下,对应苹果开发者网站的问题地址 (opens new window)

Exception Type:  EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note:  EXC_CORPSE_NOTIFY
Triggered by Thread:  0
Kernel Triage:
VM - Compressor failed a blocking pager_get

崩溃的场景是,在 App 被强杀时,神策 SDK 会尝试上报数据,同时会卡线程等待上报数据返回结果,在弱⽹环境下可能会触发 watchdog 机制导致崩溃,iOS 15 上苹果缩短了后台时间,因此更容易触发 watchdog。

崩溃对应对应具体代码如下:

- (void)applicationWillTerminateNotification:(NSNotification *)notification { 
	SALogDebug(@"applicationWillTerminateNotification"); 
	dispatch_sync(self.serialQueue, ^{...}); 
}

解决⽅案是升级 SDK,升级后的 SDK也会尝试上报数据,但不再卡线程等待返回结果。

一时间想不到更多的崩溃了,感觉还是需要在日常使用中不断积累和记录相关的崩溃的日志。

# 陷阱和中断

我感觉「深入解析 MAC OS X & IOS 操作系统」书里对陷阱和中断的描述比较到位,摘录如下

ARM 上的陷阱处理程序 系统调⽤是利⽤ SVC 指令通过模拟的中断完成的。SVC 是 “SuperVisor Call” 的简称,过去称为 SWI 即Software lnterrupt(软件中断), 其实过去的这个名称更为准确。当这条指令执⾏的时候,CPU ⾃动将控制权转交给机器的陷阱向量,在陷阱向量中有一个预定义的内核指令正在等待,遍常是分⽀跳转到某个具体处理程序的指令。 内核要负责设置妤 CPU ⽀持的所有模式的陷阱处理程序。

其实在这段描述里,软件中断、系统调用和陷阱是一个意思。可能从操作系统层面来说这三个术语的概念并不相同,但是在 ARM 的实现上,是使用同一个方式去实现的。

所以像上面的 Swift 运行时捕获的错误就是通过「系统调用」触发进入内核态,然后在内核态执行对应预定义的陷阱向量,所以我们可以说 Mach 异常都是由陷阱(trap)引起的。

其实 iOS 中还有一个地方用到陷阱的概念,就是 Runloop,这里直接引用YY的描述 (opens new window)

为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作。 RunLoop 的核心就是一个 mach_msg() (见上面代码的第7步),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。

这部分的理论知识也可以参考「深入理解计算机系统-第八章-异常控制流」学习。

以上如果有理解的不对的地方,欢迎指出来,有疑问也可以一起讨论一下

参考地址:

  1. Unix 高级编程第十章-信号
  2. mach/machine/exception.h (opens new window) #mach exception 定义
  3. signal.h (opens new window) #signal 定义
  4. Swift-Addressing crashes from Swift runtime errors (opens new window)
  5. Trap_(computing) (opens new window)
  6. 《深入理解osx ios 操作系统》读书笔记 (opens new window)
  7. 浅谈 iOS 中的 Crash 捕获与防护 (opens new window)