Swift Digest
Blog | Swift.org Blog

Swift のクラッシュ時バックトレース

On-Crash Backtraces in Swift

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

この記事の要点

背景: クラッシュ時に得られる情報が乏しかった

Swift 5.9 より前は、プログラムがクラッシュしても、親プロセス(多くの場合シェル)が「子プロセスがクラッシュした」と知らせるだけでした。

$ ./crash
I'm going to crash now
zsh: segmentation fault  ./crash

Apple プラットフォームや Windows では OS 組み込みのクラッシュレポータが残したログを見られますが、Linux ではこのメッセージだけが頼りになりがちで、何が起きたのかをつかむのが困難でした。

何ができるのか

読みやすいバックトレースの出力

Swift 5.9 では、上のような不透明なメッセージの代わりに、クラッシュ原因・クラッシュしたスレッド・各フレームの関数名とソース位置、さらに該当行付近のソースコードまでを表示します。

*** Program crashed: Bad pointer dereference at 0x0000000000000004 ***

Thread 0 crashed:

0               0x00000001045a3df0 reallyCrashMe() + 404 in crash at /.../crash.swift:4:15
1 [ra]          0x00000001045a3ea4 crashMe() + 12 in crash at /.../crash.swift:8:3
2 [ra]          0x00000001045a3c50 main + 12 in crash at /.../crash.swift:11:1
3 [ra] [system] 0x000000018705d058 start + 2224 in dyld

レジスタの内容や読み込まれているイメージ(ライブラリ)の一覧も併せて出力されます。この機能は 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.
memory     Inspect memory.
registers  Display the registers.
set        Set or show options.
thread     Show or set the current thread.

debug コマンドまたは D キーでデバッガをアタッチでき、何が起きるかはプラットフォームに依存します。それ以外のキーを押すか 30 秒のタイマーが切れると、プログラムは通常どおりクラッシュします。

この対話モードは、標準入力・標準出力がともにターミナルに接続されているときだけ起動します。CI システムやスクリプトでは出力がパイプやファイルにリダイレクトされることが多いため、多くの場合は自動的に「対話モードを起動しない」挙動になります。明示的に無効化したい場合は、環境変数 SWIFT_BACKTRACEinteractive=no を設定します。色付き出力は color=no で無効化でき、interactive=no,color=no のように複数のオプションを組み合わせられます。

構造化された並行処理への対応

バックトレーサは並行処理を理解しており、async フレームを正しくさかのぼります。たとえば次のプログラムでは、

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)
  }
}

バックトレースは level(n:) の再帰呼び出しと、それを呼び出した CrashAsync.main() までを、async の活性化チェーン(activation chain)に沿って正しく表示します。通常のスタックを単純にたどると並行処理ランタイムの内部フレームが現れて分かりづらくなりますが、この機能はそれを避けてくれます。

Apple プラットフォームでは特別な準備は不要です。それ以外のプラットフォームでは、各フレームが async かどうかを判定するためにバックトレーサがシンボルを参照できる必要があります。必要なシンボルが無い場合は、async チェーンではなく通常のプログラムスタックをたどるため、並行処理ランタイムのフレームが表示されることがあります。

読みやすさのための工夫

使い方・注意点

関連リンク