Takahiro Octopress Blog

-1から始める情弱プログラミング

iOSアプリ開発に便利なLLDBのp/po/vコマンドを覚えよう!

はじめに

筆者はこれまでLLDBコマンドでは po を利用することが多かったのですが、
WWDC2019のビデオであるLLDB: Beyond “po”を視聴して改めて v コマンドの使いやすさを勉強しました。

今回はその v コマンドについて実例を交えながら見ていきたいと思います。 ( ppo コマンドでなぜか変数の中身が見れない…と思っていた方、必見です。 )

p/po/vコマンドについて

まず、各種コマンドについて簡単に見ていきます。

poコマンドについて

poprint object の略だそうです。
poexpression コマンドのエイリアス(ショートカット機能)であり、簡略化して書くことができます。

具体的には、以下の通りです。

1
2
3
(lldb) expression --object-description -- engineer

(lldb) po engineer

また、 po は式を評価した結果のオブジェクトを返すため、非常に見やすい形式で出力されます。
早速例を見てみましょう。

下記のような構造体が定義されているとします。

1
2
3
4
5
6
7
struct iOSEngineer {
  var name: string
  var age: Int
  var specialty: [String]
  var swiftLevel: Int
  var obcLevel: Int
}

これを初期化した直後にブレークポイントで止めているとしましょう。

1
let engineer = iOSEngineer(name: "Ichiro", age: 33, specialty: ["gps", "bluetooth", "animation"], swiftLevel: 3, obcLevel: 2)

ブレークポイントで止めた状態で po コマンドを打ち出すと、

1
2
3
4
5
6
7
8
9
10
(lldb) po engineer
 iOSEngineer
  - name : "Ichiro"
  - age : 33
   specialty : 3 elements
    - 0 : "gps"
    - 1 : "bluetooth"
    - 2 : "animation"
  - swiftLevel : 3
  - obcLevel : 2

という出力結果が得られます。

pコマンドについて

pprint の略だそうで、po と同じくコンパイルして式を評価します。
po と異なるのは、出力結果の形式です。
以下はその例です。

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) p engineer
(Sample.iOSEngineer) $R4 = {
  name = "Ichiro"
  age = 33
  specialty = 3 values {
    [0] = "gps"
    [1] = "bluetooth"
    [2] = "animation"
  }
  swiftLevel = 3
  obcLevel = 2
}

po と違って $R4 のような名前がつけられていることがわかります。
この $R4 のような名前を引数に指定して pop コマンドを実行することができます。

1
2
3
4
5
(lldb) p $R4.name
(String) $R10 = "Ichiro"

(lldb) po $R4.name
"Ichiro"

このように以前の結果から更に値を取得するような場面で使い勝手が非常に良いです。

もちろん pexpression コマンドのエイリアスになっています。

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) expression -- engineer
(MeasurementSample.iOSEngineer) $R16 = {
  name = "Ichiro"
  age = 33
  specialty = 3 values {
    [0] = "gps"
    [1] = "bluetooth"
    [2] = "animation"
  }
  swiftLevel = 3
  obcLevel = 2
}

vコマンドについて

vvariable の略のようです。
出力形式自体は p コマンドと同じになっています。

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) v engineer
(MeasurementSample.iOSEngineer) engineer = {
  name = "Ichiro"
  age = 33
  specialty = 3 values {
    [0] = "gps"
    [1] = "bluetooth"
    [2] = "animation"
  }
  swiftLevel = 3
  obcLevel = 2
}

pop と同じくエイリアスなのですが、 expression ではなく frame variable コマンドのエイリアスです。

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) frame variable engineer
(MeasurementSample.iOSEngineer) engineer = {
  name = "Ichiro"
  age = 33
  specialty = 3 values {
    [0] = "gps"
    [1] = "bluetooth"
    [2] = "animation"
  }
  swiftLevel = 3
  obcLevel = 2
}

一見、 p コマンドを使えば良いのでは?と思われそうですが、 v コマンドは以下の特徴があります。

  • コンパイルしないため、結果の出力が速い
  • メモリから変数を読み込み、動的解決を複数回繰り返すことで、サブフィールドの読み込みが可能

上記のどちらにも言えることですが、代わりに式の評価ができないので使い分けが必要です。

vコマンドの便利機能

もう少し具体的に v コマンドの良さを見ていきましょう。

次のように定義された構造体があったとします。

1
2
3
4
5
6
7
8
9
10
11
12
13
protocol Engineer {
    var name: String { get set }
    var age: Int { get set }
    var specialty: [String] { get set }
}

struct iOSEngineer: Engineer {
    var name: String
    var age: Int
    var specialty: [String]
    var swiftLevel: Int
    var obcLevel:Int
}

上記の通り、 iOSEngineer はプロトコル Engineer に準拠しています。
加えて独自に swiftLevelobcLevel という変数を持ちます。

この構造体を下記のように初期化している場面があったとします。

1
let engineer: Engineer = iOSEngineer(name: "Ichiro", age: 33, specialty: ["gps", "bluetooth", "animation"], swiftLevel: 3, obcLevel: 2)

これを ppo コマンドで iOSEngineer 独自のフィールドである swiftLevel を出力させようとすると、

1
2
3
4
(lldb) p engineer.swiftLevel
error: <EXPR>:3:1: error: value of type 'Engineer' has no member 'swiftLevel'
engineer.swiftLevel
^~~~~~~~ ~~~~~~~~~~

このようなエラーが表示されます。
ppo コマンドは1回しか動的解決をしないため、
Engineer 型として格納された engineer には swiftLevel はメンバ変数として持ち合わせていないと見なされてしまうのです。

もちろん、ppo コマンドは式の評価が可能なので

1
2
(lldb) p (engineer as! iOSEngineer).swiftLevel
(Int) $R20 = 3

のようにキャストすることで中身を見ることはできます。
ですが、少々手間ではありますよね…
(深い階層のサブフィールドを見ようとすればするほど…)

これが v コマンドでは、

1
2
(lldb) v engineer.swiftLevel
(Int) engineer.swiftLevel = 3

とキャストせずとも結果を得ることができます。

filter機能

もう一つ、LLDBコマンドを利用する上で役立つのが filter 機能です。
折角なので使い方を見ていきましょう。

filter機能の紹介

フィールドの多い変数や、その変数と同じ型の要素を数十個持つ配列などを出力させる場合、とてもではないですが特定のフィールドを見つけることは困難です。

そんな時には filter 機能を利用して、特定の型であれば、特定のフィールドのみを表示するといったことができます。
指定の方法は簡単で、

1
type filter add <Xcodeのプロジェクト名>.<型の名前> --child <フィールド名>

のようにするだけです。
筆者の例で言えば、

1
type filter add Sample.iOSEngineer --child name

のような感じです。

これにより、

1
2
(lldb) v engineer
(MeasurementSample.iOSEngineer) engineer = (name = "Ichiro")

のように今必要のないフィールドを省略して、結果を得ることができます。
※便宜上、 v コマンドを使いましたが p コマンドでも同じ結果を得ることができます。

因みに、機能利用後は忘れずに filter を解除しましょう。

1
type filter delete <Xcodeプロジェクト名>.<型の名前>

で解除できます。

filter機能の限界

因みに、 filter 機能も万能ではなく、場合によっては複数回 filter をセットすることで結果を得なければいけないこともあります。
例えば、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protocol Engineer {
    var name: String { get set }
    var age: Int { get set }
    var specialty: [String] { get set }
}

struct iOSEngineer: Engineer {
    var name: String
    var age: Int
    var specialty: [String]
    var swiftLevel: Int
    var obcLevel:Int
}

struct WebEngineer: Engineer {
    var name: String
    var age: Int
    var specialty: [String]
    var htmlLevel: Int
    var cssLevel: Int
    var jsLevel: Int
}

のように新たに Engineer プロトコルに準拠した WebEngineer を定義し、以下のような配列を作成します。

1
2
3
4
5
6
let engineer1 = WebEngineer(name: "Taro", age: 30, specialty: ["react"], htmlLevel: 2, cssLevel: 1, jsLevel: 1)
let engineer2 = WebEngineer(name: "Jiro", age: 35, specialty: ["vue"], htmlLevel: 3, cssLevel: 2, jsLevel: 4)
let engineer3 = iOSEngineer(name: "Saburo", age: 25, specialty: ["gps", "bluetooth"], swiftLevel: 3, obcLevel: 2)
let engineer4 = WebEngineer(name: "Shiro", age: 28, specialty: ["react", "vue", "angular"], htmlLevel: 4, cssLevel: 4, jsLevel: 5)

let engineers = [engineer1, engineer2, engineer3, engineer4]

engineers をそのまま v コマンドで出力すると少し見えにくそうなので filter 機能が利用したいのですが、
Engineer に準拠しているからと言って

1
(lldb) type filter add Sample.Engineer --child name

としても思ったようには動かず、全フィールドが出力されてしまいます。

そこで、

1
2
(lldb) type filter add Sample.iOSEngineer --child name
(lldb) type filter add Sample.WebEngineer --child name

としてやれば、

1
2
3
4
5
6
(lldb) v engineers
([Engineer]) engineers = 4 values {
  [0] = (name = "Taro")
  [1] = (name = "Jiro")
  [2] = (name = "Saburo")
  [3] = (name = "Shiro")

このように期待した結果を得ることができます。

まとめ

さて如何でしたでしょうか。
p , po , v コマンドは各々用途がありそうですが、特に v コマンドの利便性を感じてもらえたら幸いです。
筆者も積極的に v コマンドを使っていきたいと思います。

といったところで本日はここまで。

参考URL:

Comments