Swift 中的崩溃回溯
新的 Swift 5.9 版本包含许多有用的调试代码的新功能,包括用于实时检查崩溃的进程外交互式崩溃处理程序、用于即时调试的触发调试器的能力,以及并发感知的回溯,以便更容易理解使用结构化并发的程序中的控制流。
进程外崩溃处理
在 Swift 5.9 之前,当你的程序崩溃时,你只会收到来自父进程(通常是 shell)的消息,告诉你子进程崩溃了
$ ./crash
I'm going to crash now
zsh: segmentation fault ./crash
在 Apple 平台或 Windows 上,你可以查看操作系统内置崩溃报告器捕获的崩溃日志,但在 Linux 上,通常你只能依靠这些。
现在,取代上面晦涩的消息,结果看起来像这样
$ ./crash
I'm going to crash now
💣 Program crashed: Bad pointer dereference at 0x0000000000000004
Thread 0 crashed:
0 reallyCrashMe() + 404 in crash at /Users/alastair/Source/crashDotSwift/crash.swift:4:15
2│ print("I'm going to crash now")
3│ let ptr = UnsafeMutablePointer<Int>(bitPattern: 4)!
4│ ptr.pointee = 42
│ ▲
5│ }
6│
1 crashMe() + 12 in crash at /Users/alastair/Source/crashDotSwift/crash.swift:8:3
6│
7│ func crashMe() {
8│ reallyCrashMe()
│ ▲
9│ }
10│
2 main + 12 in crash at /Users/alastair/Source/crashDotSwift/crash.swift:11:1
9│ }
10│
11│ crashMe()
│ ▲
12│
Press space to interact, D to debug, or any other key to quit (30s)
或者,如果你在管道中(未连接到终端)运行相同的程序,你将看到如下报告
*** Program crashed: Bad pointer dereference at 0x0000000000000004 ***
Thread 0 crashed:
0 0x00000001045a3df0 reallyCrashMe() + 404 in crash at /Users/alastair/Source/crashDotSwift/crash.swift:4:15
1 [ra] 0x00000001045a3ea4 crashMe() + 12 in crash at /Users/alastair/Source/crashDotSwift/crash.swift:8:3
2 [ra] 0x00000001045a3c50 main + 12 in crash at /Users/alastair/Source/crashDotSwift/crash.swift:11:1
3 [ra] [system] 0x000000018705d058 start + 2224 in dyld
Registers:
x0 0x0000000000000001 1
x1 0x0000000000000000 0
x2 0x0000000000000000 0
x3 0x000060000016c1c0 c0 c1 28 fd 79 96 00 00 fb 07 00 00 00 00 00 00 ÀÁ(ýy···û·······
...
x26 0x0000000000000000 0
x27 0x0000000000000000 0
x28 0x0000000000000000 0
fp 0x000000016b85f310 20 f3 85 6b 01 00 00 00 a4 3e 5a 04 01 00 00 00 ó·k····¤>Z·····
lr 0x72268001045a3d44 8225402511295135044
sp 0x000000016b85f280 a0 f2 85 6b 01 00 00 00 00 00 00 00 00 00 00 00 ò·k············
pc 0x00000001045a3df0 28 01 00 f9 fd 7b 49 a9 ff 83 02 91 c0 03 5f d6 (··ùý{I©ÿ···À·_Ö
Images (42 omitted):
0x00000001045a0000–0x00000001045a4000 6776aba03ad432b68bc57220ac4e6ef8 crash /Users/alastair/Source/crashDotSwift/crash
0x0000000187057000–0x00000001870ea874 ee3f4181cec538c2b8a84d310be33491 dyld /usr/lib/dyld
这个新功能极大地改善了 Linux 上的崩溃时调试体验,在 Linux 上它是默认启用的。它在 macOS 上也很有用,但必须手动启用。目前 Windows 上不支持。
交互式回溯
你可能想知道上面终端内回溯最后一行的消息,它说
Press space to interact, D to debug, or any other key to quit (30s)
通常在终端开发程序时,你可能会发现程序崩溃了,但你无法重现问题。如果没有合适的崩溃日志,这可能会非常令人沮丧——你知道你的程序有错误,但你不知道是什么错误,也不知道如何重现它。
此功能背后的想法是让程序暂停(默认为 30 秒,但可以配置),并为你提供连接调试器或对崩溃进程执行一些额外检查的机会。
如果在出现此提示时敲击空格键,你将看到一个简单的命令提示符,允许你更改回溯器设置、生成新的回溯、列出已加载的镜像、显示寄存器和内存内容,并获取进程中所有线程的列表。在提示符下键入 help 将显示可用命令列表
>>> help
Available commands:
backtrace Display a backtrace.
bt Synonym for backtrace.
debug Attach the debugger.
exit Exit interaction, allowing program to crash normally.
help Display help.
images List images loaded by the program.
mem Synonym for memory.
memory Inspect memory.
process Show information about the process.
quit Synonym for exit.
reg Synonym for registers.
registers Display the registers.
set Set or show options.
thread Show or set the current thread.
threads Synonym for process.
如果你使用 debug 命令或在提示符下按 D,回溯器将帮助你将调试器附加到你的程序。这里究竟会发生什么取决于平台。
如果你按任何其他键,或者如果 30 秒计时器结束,程序将允许正常崩溃。
默认情况下,如果你的程序的标准输入和输出都连接到终端,则会触发交互式功能。在许多情况下,这意味着如果你在 CI 系统中或作为自动化脚本的一部分运行,你将自动获得正确的行为,因为这些通常在程序输出重定向到管道或文件的情况下运行。
在你不希望启用此功能的情况下,你可以通过将环境变量 SWIFT_BACKTRACE 设置为 interactive=no 来显式禁用它。你还可以使用 color=no 禁用彩色输出,以及组合多个选项,例如 interactive=no,color=no。
结构化并发支持
回溯器是并发感知的,并将正确地回溯异步帧。例如,给定程序
func level(n: Int) async {
if n < 5 {
await level(n: n + 1)
} else {
let ptr = UnsafeMutablePointer<Int>(bitPattern: 4)!
ptr.pointee = 42
}
}
@main
struct CrashAsync {
static func main() async {
await level(n: 1)
}
}
回溯将如下所示
$ ./crashAsync
💣 Program crashed: Bad pointer dereference at 0x0000000000000004
Thread 1 crashed:
0 level(n:) + 308 in crashAsync at /Users/alastair/Source/crashDotSwift/crashAsync.swift:6:17
4│ } else {
5│ let ptr = UnsafeMutablePointer<Int>(bitPattern: 4)!
6│ ptr.pointee = 42
│ ▲
7│ }
8│ }
1 level(n:) in crashAsync at /Users/alastair/Source/crashDotSwift/crashAsync.swift:3
1│ func level(n: Int) async {
2│ if n < 5 {
3│ await level(n: n + 1)
│ ▲
4│ } else {
5│ let ptr = UnsafeMutablePointer<Int>(bitPattern: 4)!
2 level(n:) in crashAsync at /Users/alastair/Source/crashDotSwift/crashAsync.swift:3
3 level(n:) in crashAsync at /Users/alastair/Source/crashDotSwift/crashAsync.swift:3
4 level(n:) in crashAsync at /Users/alastair/Source/crashDotSwift/crashAsync.swift:3
5 static CrashAsync.main() in crashAsync at /Users/alastair/Source/crashDotSwift/crashAsync.swift:13
11│ struct CrashAsync {
12│ static func main() async {
13│ await level(n: 1)
│ ▲
14│ }
15│ }
Press space to interact, D to debug, or any other key to quit (30s)
在 Apple 平台上,此功能没有特殊要求,但对于其他平台,回溯器需要能够查找符号以确定给定帧是否为异步。如果必要的符号不可用,回溯将遵循正常的程序堆栈,而不是异步激活链。这通常会导致它显示来自并发运行时的帧,这在调试大多数类型的问题时不太可能有帮助。
提高可读性
新的回溯器还具有许多提高可读性的选项。
你可以配置回溯器将生成的最大帧数(默认为 64),但由于你可能还想查看堆栈顶部的帧,因此回溯器还具有一个设置,用于捕获那里的帧数(默认为 16)。如果你的程序由于过度递归而崩溃,这将特别方便,因为你通常会同时看到递归和崩溃的原因,而不会被成千上万的帧淹没。
默认情况下,回溯器还会跳过系统帧和 Swift thunk。这些通常与编译器或运行时工程师无关,并且通常会导致大多数开发人员的输出更加混乱。
此外,回溯器将自动解构 Swift 和 C++ 的 mangled 名称。
总结
Swift 5.9 中新的崩溃时调试选项可帮助你在程序行为异常时进行调试。回溯器具有许多有用的功能,包括
- 进程外崩溃处理
- 智能的程序源代码内联显示(如果可用)
- 暂停和检查崩溃程序,甚至触发调试器进行即时调试的选项
- 对 Swift 并发性的支持
- 除了 Swift 之外,还支持 C++ 名称解构
- 彩色输出以提高可读性
- 扩展的配置选项(请参阅文档)。
新功能在 Linux 上默认启用,并且可以为 macOS 启用(请参阅文档)。目前尚不支持 Windows。