Swift数组越界分析

通常我们去获取奔溃日志都是通过NSSetUncaughtExceptionHandler这个API得到NSException对象的信息。对于NSArray对象的数据越界是有用的,但是对于Array的数据越界是不会触发这个方法的。

NSArray和Array越界处理机制

NSArray

测试源码

1
2
3
let arr = [1, 3, 4]
let array = NSArray(array: arr)
print(array[4])

NSArray越界奔溃线程调用栈

可以看出NSArray通过调用[NSArrayI objectAtIndexedSubscript:]方法获取到具体元素,在该方法调用完成后(pop rbp),触发了_CFThrowFormattedException方法,一个参数是NSExceptionName对象(NSRangeException), 另一个参数是异常原因(“*** %s: index %lu beyond bounds for empty array”),还有一个参数是调用方法(“-[NSArrayI objectAtIndexedSubscript:]”)。这三个参数会初始化一个NSException对象,当然当前线程的调用栈信息会赋值给这个NSException对象。这时就可以通过NSSetUncaughtExceptionHandler这个API的回调获取到这个NSException对象信息。

最后调用系统内核库的pthread kill方法,然后再执行act __set_ _astbsd()方法,最后exit()退出应用。

Array

测试源码

1
2
let arr = [1, 3, 4]
print(arr[4])

Array越界奔溃线程调用栈

可以看到Array越界的处理方式和NSArray完全不一样,最后是通过一个断言方法(Swift._fatalErrorMessage)让程序终止. “/BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang_Fall2018/swiftlang_Fall2018-1000.11.37.1/src/swift/stdlib/public/core/ContiguousArrayBuffer.swift”这个路径是Swift Array部分源码的路径,由于Swift开源的,可以到GitHub上clone下来看看源码



可以看到Array源码获取元素前会先通过断言先判断数组是否会越界。但是我怎么知道这个断言方法被调用呢,或者这个断言触发了什么方法或指令让程序退出呢?
通过念茜姐姐的这个文章知道了有Mach异常还有Unix信号异常。既然断言不走NSSetUncaughtExceptionHandler,那么很有可能会通过给线程发送异常信号量来处理的。但是由于在debug环境下,Xcode会停在数组越界这行代码,不会再让你的程序往下走了,此时你的程序并没有跑完,可以将编译后的二进制文件在终端上运行(如下图)

得到运行结果如下:

得到一个错误的信息(“illegal hardware instruction”), 意思是非法指令,这就知道了,Swift的断言方法触发了非法指令。

非法指令对应的信号量

这时可以写这么一段代码来检测下了(当然还是在终端上运行编译后的二进制文件)

1
2
3
4
5
6
7
signal(SIGILL) { (info) in
print(info, "我在这里退出了")
exit(0)
}

let arr = [1, 3, 4]
print(arr[4])

运行结果如下:

bingo!

结论

  1. NSArray和Array越界的处理机制不同
  2. NSArray越界可以通过NSSetUncaughtExceptionHandler获取到程序奔溃的线程调用栈信息。
  3. Array越界通过断言来处理异常,而断言执行了非法指令,导致程序退出。
  4. 收集Array越界线程调用栈信息,可以通过signal(SIGILL, callBackHandle)方法