はじめに
Xcodeでアプリが停止したとき、デバッグコンソールに (lldb) と表示されることがあります。しかし、これが何を意味しているのか、何を入力すればよいのか分からず戸惑ったことはないでしょうか。
この記事では、(lldb) の正体から、デバッガとは何か、LLDBとLLVMの関係、スタックトレースの読み方、そして最低限知っておくと便利なLLDBコマンドまでを整理します。
デバッガとは何か
デバッガは、実行中のプログラムを観察し、制御するためのツールです。
代表的な役割は次の3つです。
- 実行を止める
- 止まった時点の状態を見る
- 必要に応じて実行を再開したり、別の場所まで進めたりする
Xcodeでブレークポイントを置いて処理を止めたり、変数の値を確認したり、ステップ実行したりできるのは、まさにデバッガの働きです。コードを書き直して print を追加しなくても、その場でプログラムの状態を確認できるのが大きな違いです。
LLDBとは何か
LLDBは、LLVMプロジェクトのデバッガです。
LLVMプロジェクト自体は、モジュール化され再利用可能なコンパイラおよびツールチェーン技術の集合として説明されています。名前に "LLVM" と入っていますが、現在のLLVMは単なる1つのコンパイラではなく、コンパイルや解析やデバッグを支える基盤の集まりです。
LLDBはその中で、プログラムをデバッグする役割を持ちます。
Xcodeのデバッグ機能の裏側では、このLLDBが動いています。つまり、Xcodeのデバッグコンソールに表示される (lldb) は、LLDBがコマンド入力を待っている状態です。
(lldb) は何を意味しているのか
(lldb) はプログラミング言語ではありません。
これは LLDBのコマンドプロンプト です。
たとえばターミナルでシェルがコマンド入力待ちになるのと同じように、LLDBもコマンドを受け取る待機状態になります。ここで入力するのは、Swiftのアプリ本体のコードではなく、デバッグのためのコマンドです。
ただし少しややこしいのが、LLDBでは「デバッガ用コマンド」と「式の評価」の両方を扱えることです。
たとえば thread backtrace や frame variable はLLDBコマンドですが、expression は現在のフレームで式を評価するためのコマンドです。po も独自言語ではなく、expression -O -- のエイリアスとして使われます。
LLVMとは何か
LLVMは、コンパイラ基盤およびツールチェーン基盤です。
大事なのは、「LLVM = 1つのコンパイラ」と思わないことです。LLVMは複数のサブプロジェクトを含む土台であり、その中にデバッガのLLDBも含まれています。
LLVMは、もともと静的コンパイルと動的コンパイルの両方を支えられる、SSAベースの現代的なコンパイル基盤として始まりました。現在では、コンパイラフロントエンド、最適化、コード生成、デバッグ情報の扱いなど、広い範囲を支えるエコシステムになっています。
LLVMとLLDBの関係
関係を一言で言うと、LLVMがコンパイル基盤、LLDBがデバッグ基盤です。
プログラムは、コンパイルされて実行ファイルになります。
そのとき、デバッグに必要な情報も一緒に生成されます。LLDBはその情報を使って、「今このアドレスはどのソースコードの何行か」「この変数の型は何か」「今どの関数の中にいるのか」といったことを人間が読める形で見せてくれます。
デバッグ情報とDWARF
機械語だけを見ても、人間にはかなり厳しいです。
メモリアドレスや命令列だけ並んでいても、「これは user.name なのか」「どの関数のどの行なのか」は分かりません。
そこで使われるのがデバッグ情報です。LLVMの資料では、デバッグ情報の主要な利用先として DwarfDebug と CodeViewDebug が挙げられており、このうち DwarfDebug はGDBやLLDBなどのDWARFベースのデバッガで使えるDWARF情報を生成します。つまり、LLDBがソースコードレベルで変数や関数や行番号を扱えるのは、DWARFのようなデバッグ情報があるからです。
スタックトレースとは何か
クラッシュ時や停止時に、次のような表示を見ることがあります。
thread #11, name = 'com.apple.uikit.eventfetch-thread'
* frame #0: 0x00000002426d6cd4 libsystem_kernel.dylib`mach_msg2_trap + 8
frame #1: 0x00000002426da2f8 libsystem_kernel.dylib`mach_msg2_internal + 76
frame #2: 0x00000002426da214 libsystem_kernel.dylib`mach_msg_overwrite + 428
...
これはスタックトレース、あるいはバックトレースです。
現在のスレッドが、どの関数呼び出しを辿って今の場所に来たのかを並べたものです。Appleのクラッシュレポートの説明でも、各スレッドの完全なバックトレースが含まれ、その時点で実行されていたコードを示すものとして扱われています。
thread と frame の意味
thread とは
thread #11 の thread は、実行の単位であるスレッドです。
アプリは複数のスレッドを持つことがあり、UI用のスレッド、バックグラウンド処理用のスレッド、イベント取得用のスレッドなどが存在します。LLDBでは thread list で現在のスレッド一覧を確認できます。
frame とは
frame #0 や frame #1 の frame は、関数呼び出し1つ分の実行文脈です。
ある関数が別の関数を呼び、その先でさらに別の関数を呼ぶと、呼び出しの階層ができます。その階層を1段ずつ並べたものが frame です。LLDBでは frame select で、どのフレームを基準に変数を見るかを切り替えられます。
イメージとしては、次のような感じです。
main()
↓
loadData()
↓
parseResponse()
↓
updateUI()
この呼び出しの連なりを、現在位置から見える形で並べたものがスタックトレースです。
frame #0 が意味するもの
一般に frame #0 は、現在そのスレッドが今まさに止まっている場所です。
クラッシュした瞬間なら、まずは frame #0 が最初の確認候補になります。ただし、常にそこが「自分の書いたコード」とは限りません。システムライブラリの中で止まっていることもあります。
その場合は、フレームを下に見ていって、自分のアプリのコードが出てくる場所を探します。frame select や up / down を使うと、フレームを切り替えながら状態を確認できます。
これは何語なのか
スタックトレースに出てくる表示は、1つのプログラミング言語ではありません。
実行状態の表示形式です。
その中に現れる関数名は、C由来のもの、Objective-C由来のもの、Swiftのもの、システムライブラリのものが混在します。
たとえば、次のような違いがあります。
-
mach_msg2_trapのような名前はC系の関数名に見えることが多い -
-[NSRunLoop runUntilDate:]のような表記はObjective-Cのメソッド表記 - Swiftの関数が表示される場合は、Swift由来のシンボルが見える
つまり、これは「何語か」と聞かれたら、正確には 言語ではなく、各言語で書かれた関数呼び出しをLLDBが並べて表示したもの です。
LLDBのコマンド体系
LLDBのコマンドは、基本的に command subcommand options という形で構成されます。
たとえば、現在のスレッドのバックトレースを表示する正式なコマンドは thread backtrace です。よく使われる bt は、そのエイリアスです。
同じように、po も独立した魔法の呪文ではなく、expression -O -- のエイリアスとして扱われます。短いコマンドは便利ですが、正式名称を知っておくと「何をしているコマンドなのか」が分かりやすくなります。
最低限よく使うコマンド
thread backtrace / bt
現在のスレッドのスタックトレースを表示します。
(lldb) thread backtrace
(lldb) bt
bt all を使うと、全スレッドのバックトレースを表示できます。
thread list
全スレッドを一覧表示します。
(lldb) thread list
今どのスレッドで止まっているのか、他にどんなスレッドがあるのかを確認するときに使います。
frame select
現在注目するフレームを切り替えます。
(lldb) frame select 0
(lldb) frame select 1
up や down もフレーム移動に使えます。
クラッシュ時にシステム側のフレームで止まっていて、自分のコードが少し下にあるときに便利です。
frame variable
現在のフレームの引数やローカル変数を表示します。
(lldb) frame variable
(lldb) frame variable user
変数名を指定しなければ、そのフレームの引数とローカル変数がまとめて表示されます。個別の変数や、そのサブ要素も辿れます。フレームを切り替えてから frame variable を使うと、そのフレーム基準の状態が見られます。
expression / expr
現在のフレームで式を評価します。
(lldb) expression 1 + 1
(lldb) expr user.name
これは単なる表示ではなく、式の評価です。
LLDBでは expression は raw command として扱われ、複雑な式を渡しやすくなっています。必要に応じて -- を使ってオプションの終わりを明示します。
po
オブジェクトの説明を表示します。
(lldb) po user
po は expression -O -- のエイリアスです。
「オブジェクトを人間向けに見やすく出したい」ときによく使われます。何となく便利だから使われがちですが、正体を知っておくと理解がぐっと楽になります。
breakpoint set
ブレークポイントをコマンドで設定します。
(lldb) breakpoint set --file ViewController.swift --line 42
(lldb) breakpoint set --name loadData
行番号でも、関数名でも設定できます。複数の --name を並べることもできます。XcodeのUIで置くブレークポイントと同じ考え方ですが、コマンドで扱えると、デバッグコンソールだけで作業を続けられます。
process continue
停止中のプログラムを再開します。
(lldb) process continue
LLDBでは continue 系のエイリアスも用意されています。
また、実行中でも一部のコマンドは使えます。たとえば breakpoint set などは、明示的に中断しなくても設定できます。
ステップ実行
(lldb) thread step-in
(lldb) thread step-over
(lldb) thread step-out
それぞれ「中に入る」「次へ進む」「現在の関数から抜ける」に対応します。
step、next などのエイリアスもあります。さらにLLDBでは、ステップ実行を内部的に操作のスタックとして管理しており、途中でブレークポイントに引っかかっても、continue によって元のステップ操作に戻れるよう設計されています。
help と apropos がかなり強い
LLDBには help と apropos があります。
(lldb) help
(lldb) help breakpoint set
(lldb) apropos thread
help はコマンド一覧や詳細を確認するためのものです。
apropos はキーワード検索のように使えます。「コマンド名はうろ覚えだけど、たしか thread っぽいやつだった」というときに地味に助かります。
raw command とは何か
LLDBには raw command という考え方があります。
通常のコマンドはオプションや引数を細かく解釈しますが、raw command はオプションの処理後、残りの文字列をほぼそのままコマンドに渡します。expression がその代表例です。式は記号が多く、普通のコマンド引数のように厳密に分割されると扱いづらいので、この仕組みが使われています。
alias を知っておくと混乱しにくい
LLDBには多くのエイリアスがあります。
短いコマンドは便利ですが、正式コマンドとの対応を知っておくと混乱しにくくなります。
代表例は次の通りです。
| 短縮コマンド | 正式コマンド |
|---|---|
bt |
thread backtrace |
po |
expression -O -- |
f 12 |
frame select 12 |
また、LLDBは shortest unique string match を使うため、ある程度まで短く書いても解釈できることがあります。さらに ~/.lldbinit に自分のエイリアスを定義することもできます。
クラッシュしたとき、まず何を見るか
実務寄りに言うと、クラッシュ直後に最初に見る候補はかなり絞れます。
まずは現在のスレッドのバックトレースを見ます。
(lldb) bt
そのうえで、今注目しているフレームが自分のコードなのか、システムの中なのかを見ます。
もしシステムライブラリのフレームで止まっているなら、up や frame select で自分のコードが見える位置まで移動します。
その後、現在のフレームの変数を見ます。
(lldb) frame variable
(lldb) po self
この順番で見るだけでも、かなり多くのクラッシュで状況把握がしやすくなります。
「何を書けばいいのか」に対する実践的な答え
(lldb) を見たとき、最初から難しいことをする必要はありません。
まずは次の3つで十分です。
(lldb) bt
(lldb) frame variable
(lldb) po self
-
btで今どこで止まっているかを見る -
frame variableでそのフレームの変数を見る -
po selfなどで重要なオブジェクトを確認する
この3つだけでも、「黒い画面で何が起きているのか分からない」状態からはかなり抜け出せます。
デバッガは怖いものではなく、止まったプログラムの状態を静かに見せてくれる観察装置です。まあ、最初は威圧感がありますが、やっていること自体は意外とまっとうです。
まとめ
(lldb) は、Xcodeのデバッグコンソールに現れるLLDBのコマンドプロンプトです。
そこで見ているのは、独自のプログラミング言語ではなく、デバッグ用のコマンドと、実行中プログラムの状態です。
LLDBはLLVMプロジェクトのデバッガであり、LLVMが生成・保持するデバッグ情報を利用して、関数・変数・行番号・フレームといった情報を人間が読める形で扱います。
スタックトレースは、現在のスレッドがどの関数呼び出しを辿って今の位置にいるのかを示したものです。thread はスレッド、frame は関数呼び出し1つ分の文脈です。
最初の一歩としては、次の3つを覚えておくと実用的です。
(lldb) bt
(lldb) frame variable
(lldb) po self
これだけでも、クラッシュ時の見え方がだいぶ変わります。
参考文献
- LLVM Project, "The LLVM Compiler Infrastructure Project." https://llvm.org/
- LLDB Documentation, "Tutorial." https://lldb.llvm.org/use/tutorial.html
- LLDB Documentation, "GDB to LLDB command map." https://lldb.llvm.org/use/map.html
- LLVM Documentation, "Source Level Debugging with LLVM." https://llvm.org/docs/SourceLevelDebugging.html
- Apple Developer Documentation, "Diagnosing issues using crash reports and device logs." https://developer.apple.com/documentation/xcode/diagnosing-issues-using-crash-reports-and-device-logs