LLDB: Beyond “po” 前半(日本語まとめ)
本記事は、WWDC2019のセッションである ‘LLDB: Beyond “po”’ を日本語にまとめたものです。
はじめに
LLDBはXcodeのデフォルトデバッガで、変数の閲覧を可能にします。左下のパネルで利用している変数とその型が確認できます。デバッグ中、右下のコンソールパネルからLLDBに直接コマンドを叩くこともできます。
poコマンド
LLLDBでもっとも使われるコマンドはおそらくpo
でしょう。po
は一つの変数を出力するコマンドです。
CustomDebugStringConvertible
を継承させることによって、アウトプットの形式をカスタマイズすることができます。debugDescription
プロパティを実装すると、アウトプットの一番上のdescriptionが書き換わります。それより下の階層を書き換えたい場合には、CustomReflectable
を継承します。
さて、poができることは変数の出力だけではありません。Stringを大文字にしたり、配列を辞書順にソートしたり、様々な操作をすることもできます。
実際は、poとは以下のコマンドのエイリアスです。
expression --object-description -- 変数
LECCとLLDBはcommand alias
を使うことで、エイリアスを簡単に保存することができます。初めの引数に作りたいコマンド名を渡し、次にエイリアスにしたいコマンドを記述します。一度宣言すれば他のコマンドと同じようにLLDBで利用することができます。
command alias my_po expression --object-description
poコマンドの中身
poコマンドが値を受け渡す際どのように実行されるか見てみましょう。LLDBは式自体を解析および評価することはありません。代わりに以下のように、与えられた式からコンパイルできるソースコードを生成することから始めます。
(lldb) po view
↓
func __lldb_expr() {
__lldb_res = view
}
それからこれをコンパイルし、デバックプロセス上で実行します。実行が完了したら、LLDBは結果の値を取得しなければいけません。結果のdescriptionを取得するために、LLDBはまた以下のような別のソースコードを生成します。
func __lldb_expr2 -> String() {
return __lldb_res.description
}
このソースコードが再びコンパイルおよび実行されます。この実行結果が、poコマンドの結果になります。
poコマンドは、LLDBで変数を出力する3つの方法のうちの最初の1つです。他のものを見ていきましょう。
pコマンド
2つ目はp
コマンドです。オブジェクトのdescriptionがない出力と言ってもいいでしょう。早速出力してみましょう。
poの時と少し形式が異なりますが、同じ情報を含んでいます。また、$R0という名前で結果が与えられています。これはLLDBの特別な慣習です。それぞれの式の結果は、$R1や$R2のようにインクリメントされた名前で与えられます。そしてこれらはLLDB内で後に続く式に利用することができます。$R0はプロジェクト内の他の変数と同じように扱うことができるのです。例えば、このようにフィールドの一部を出力することができます。
(lldb) p $R0.destinations
pコマンドは、poと同じくexpressionコマンドのエイリアスです。ただ、--object-description
は付いていません。
pコマンドの中身
先ほどと同様、pコマンドがどのように実行されているか見てみましょう。pコマンドはオブジェクトのdescriptionを取得する必要がないので、そんなにすることはありません。頭の方はpoの時と同じです。しかし結果を取得した後は、LLDBは “Dynamic type resolution”(動的型の解決)という名前のステップを実行します。詳しく説明しましょう。
Dynamic type resolution
これを解説するために、サンプルコードを少し修正します。Trip構造体をActivityというプロトコルに適合させます。Swiftにおいて、ソースコードでの静的表現と実行時の動的型は、同じである必要はありません。なので例えば、Trip構造体がActivityプロトコルとして宣言されるかもしれません。このサンプルコードでは、cruiseの静的型はActivity型です。しかし実行時には、この変数はTrip型のインスタンスを持つでしょう。つまりcruiseの動的型はTrip型になります。
LLDBでcruiseを出力したら、Trip型のオブジェクトが返ってくるでしょう。なぜなら、LLDBは与えられたプログラムの位置で与えられた変数に対して、最も正確な型を表示するように結果のメタデータを再生成するようになっているからです。これが、私たちがDynamic type resolutionと呼んでいる理由です。
pコマンドにおいて、Dynamic type resolutionは式の結果に対してのみ実行されます。cruiseのフィールドの一つを取得したいと仮定します。
(lldb) p cruise.name
error: <EXPR>:3:1: error: value of type 'Activity' has no member 'name'
cruise.name
^~~~~~ ~~~~
LLDBがpコマンドを通してこの式を評価しようとした時、cruiseはActivity型で、nameというメンバ変数は持っていないと言われてしまいます。 LLDBはpコマンドを実行しているコードをコンパイルしますが、そこで見ることができる唯一の型はソースコード内の静的型になるので、このようなことが起こります。ソースコード内でcruise.name
にアクセスしようとする時にも同じエラーが起こります。
この式をエラーなしで評価したい場合は、初めにはっきりと動的型へキャストし、それからフィールドにアクセスします。デバッガーとソースコード両方に対して言えます。
pコマンドはこれで終わりではありません。結果に対するdynamic type resolutionのあと、LLDBは結果のオブジェクトをFormatterに渡します。人が読める形でオブジェクトのdescriptionを出力する責務があるからです。詳しく見てみましょう。
Formatter
フォーマッタがどのように動いているか説明するために、まずそれらのインプットとアウトプットを見せます。これは、フォーマッタがない場合の出力です。
(lldb) expression --raw -- cruise.name
(Swift.String) $R0 = {
_guts = {
_object = {
_countAndFlagsBits = {
_value = -3458764513820540908
}
_object = <extracting data from value failed>
}
}
}
StringやIntegerのようなシンプルな標準ライブラリの型でさえ、複雑な表現になっています。これは、スピードとサイズのために高度に最適化されているからです。この文字列をフォーマッタに通すと、みなさんが想像するような文字列の並びになります。
(lldb) p cruise.name
(String) $R1 = "Mediterranean Cruise"
LLDBはよく使われる型を知っていて、それらのためのフォーマッタを提供しています。カスタマイズフォーマッタを書くこともできます。
vコマンド
変数を出力する3つ目の方法、v
コマンドを説明しましょう。vコマンドの出力はpコマンドと同じで、先ほど説明したフォーマッタを利用しています。
vコマンドも、他2つのコマンドと同じくエイリアスで、Xcode 10.2から導入されたframe variable
コマンドと同値です。他2つの仕組みと異なり、vコマンドはコードのコンパイルおよび実行を全くしないので、実行がとても速いです。
セッション中には特に触れていませんが、vコマンドはbreakpointと同スコープ内にある変数しか出力できないようです。
vコマンドはコンパイルを行わないため、独自のシンタックスで実行されます。このシンタックスは、デバッグしている言語と同じである必要はありません。例えば、vコマンドはアクセスするのにドットと添字演算子を用いましたが、これは計算を必要とするプロパティは評価できません。ここでわかる通り、vコマンドは他2つとかなり異なる動きをします。詳しく見ていきましょう。
vコマンドの中身
まず初めに、vコマンドはプログラム状態を調べてメモリ内の変数を探します。そしてメモリから変数の値を読み取り、その後dynamic type resolutionを実行します。ユーザがサブフィールドの取得を依頼した場合、それぞれのフィールドに対してメモリ内の探索とdynamic type resolutionの実行を繰り返します。それが終わったら、結果がデータフォーマッタに渡されます。vコマンドがdynamic type resolutionを複数回実行する可能性があるということは、覚えておくべき重要な詳細で、pコマンドとvコマンドの大きな違いです。
フォーマッタは実は、dynamic type resolutionをたった一回しか実行しません。詳しく説明します。cruiseのメンバ変数へのアクセスに失敗したサンプルコードに戻りましょう。それぞれのステップにおけるdynamic type resolutionの実行によって、vコマンドはcruiseがTrip型のオブジェクトであることを理解でき、メモリ上でそのフィールドにアクセスすることができます。pコマンドでは明確なキャストをしなければできないので、vコマンドがpコマンドよりパワフルであることがわかります。
まとめ
LLDBで変数を出力する3つの方法についての説明は以上です。po, p, vの違いをまとめてみましょう。
poは変数を出力する際にobject descriptionを利用しますが、pとvはdata formatterを用います。結果がどのように計算されるかも覚えておきたいですね。poとpは式をコンパイルし、すべての言語にアクセスすることができます。一方vは式を解釈する独自のシンタックス持っており、dynamic type resolutionを解釈中のそれぞれのステップで実行します。
以上がLLDB: Beyond “po”セッションの前半部分です。後半には、pコマンドとvコマンドが利用するdata formatterのカスタマイズ方法が続きます。開発中非常に頻繁に利用するpoコマンドですが、pやvの特徴も理解して使い分けていきたいですね。
参考文献