🙄

Xcodeデバッグ入門 LLDBのお役立ちコマンド

2024/05/05に公開

はじめに

最近は音楽がけたゝましくなる中で、自転車をこぐエクササイズにハマっている Nao-RandD です 🚴🏻‍♂️

今回はiOS開発で使うことのあるLLDBを紹介しようと思います。

LLDBは強力なデバッグツールではありつつ毎日必ず必要になることはなく(少なくとも自分は)、いざ必要そうなバグ調査などの場面ではパッと使えないということがあります。

最低限このくらいは使えると役に立てられるかなというものを紹介してみようと思います。

LLDBとは?

まずは、LLDBとは何かを紹介しておきましょう。

LLDB(Low Level Debugger)は、プログラムの実行をコントロールし、デバッグするための強力なツールになります。

プログラムの実行中に変数の値を調べたり、特定のコード行でプログラムの実行を停止したり(ブレークポイント設定)、ステップ実行などの操作が可能です。

これにより、バグの原因を特定したり、プログラムの挙動を理解するのに役立ちます。

https://developer.apple.com/library/archive/documentation/IDEs/Conceptual/gdb_to_lldb_transition_guide/document/lldb-basics.html

https://developer.apple.com/library/archive/documentation/General/Conceptual/lldb-guide/chapters/Introduction.html

使えそうなコマンド

ここからは、実際に使えそうなコマンドを紹介して行こうと思います。

まずは使い方から

iOSエンジニアの方には釈迦に説法な説明かもしれませんが、LLDBを利用する状態を確認していきます。

LLDBはXcodeに組み込まれており、ブレークポイントを仕掛けてランタイムで停止させた箇所から、現在のコンテキスト(停止した行、実行中の関数、ローカル変数の値など)が表示されます。


Xcodeでブレークポイントをセットして停止させた状態

Hogeというクラスのhogeメソッドで停止させています。

コンストラクタにInt型の1を渡してnumプロパティを初期化した状態ですが、停止させたときにその値も確認することができています。

停止させると右下に(lldb)と書かれた緑色のバーが見えると思います。

そこにLLDBのコマンドを入力して、実行することで様々なデバッグが可能になります。

前提となるコード

今回は複雑なコードは使用せず、とてもとてもシンプルなコードにブレークポイントを仕掛けて操作していきます。

numプロパティを持つHogeクラスがあり、初期化時にnumの値を入れて初期化します。
そこからhogeメソッドを呼び出してnumの値が出力されるというものです。

class Hoge {
    var num: Int

    init(num: Int) {
        self.num = num
    }

    func hoge() {
        print("------ 値は:\(num) -------")
    }
}

// 呼び出し
Hoge(num: 1).hoge()

po コマンド

まずは、基本とも言えるpoコマンドです。

poコマンドはPrint Objectの略で、デバッグ中に変数やオブジェクトの内容を詳細に表示するために使われます。

利用頻度として最も多いコマンドかと思います。

先ほどの停止させた状態からコマンド実行して確認してみましょう。

(lldb) po num
1
以下のコードを利用しています
class Hoge {
    var num: Int

    init(num: Int) {
        self.num = num
    }

    func hoge() {
        print("------ 値は:\(num) -------") // <<- ここにブレークポイント
    }
}

// 呼び出し
Hoge(num: 1).hoge()

Hogeインスタンスのnumプロパティの値が"1"であることが確認できますね。

po [プロパティ名]では対象の description プロパティが適切に設定されている場合、その内容が表示されます。
(numはInt型なので、数字がStringの数字として出力されています。)

p コマンド

poに似たコマンドとしてpコマンドがあります。

変数の生の値や、プログラムの状態を具体的な数値やデータ構造として確認したい場合に適しています。データ型を確認したり、計算結果を直接確認する場合などに使われます。

わかりづらいですね、、😇
実際に実行してpoコマンドと比較しながら、確認してみましょう。

(lldb) p num
(Int) 1

# poコマンドの場合
(lldb) po num
1
以下のコードを利用しています
class Hoge {
    var num: Int

    init(num: Int) {
        self.num = num
    }

    func hoge() {
        print("------ 値は:\(num) -------") // <<- ここにブレークポイント
    }
}

// 呼び出し
Hoge(num: 1).hoge()

実行した結果が異なっていますね。

pコマンドでは型情報などが一緒に出力されています。

プリミティブな値(基本的なデータ型, Int・Boolなど)をチェックするだけであればpコマンドでいいかなと思います。

v コマンド(frame variable)

vコマンドは現在のスコープの変数を表示することができます。

frame variableのエイリアスなので、そちらで実行しても同じ結果が得られます。

現在のスタックフレーム内のローカル変数、関数の引数、静的変数などを表示することができます。このコマンドはデバッグ中にプログラムの特定の部分で使用されているデータの概要を得るのに非常に便利です。

(lldb) v
(Hoge.Hoge) self = 0x0000600000278bc0 (num = 1)
以下のコードを利用しています
public class Hoge {
    public var num: Int

    public init(num: Int) {
        self.num = num
    }

    public func hoge() {
        print("------ 値は:\(num) -------") // <<- ここにブレークポイント
    }
}

// 呼び出し
Hoge(num: 1).hoge()

vコマンドを実行すると停止させた状態のスタックにある変数の一覧が出てきます。

サンプルコードでは停止させた状態で変数の数が多くないため特に便利さを感じませんが、実際のプロジェクトコードであればどういった変数が存在しているかを確認するのに力を発揮すると思います。

bt コマンド

bt コマンドでスタックトレースを取得することができます。

停止させた状態までにどういった流れで呼び出されたかを確認する場面で役に立ちます。

以下のコードを利用しています
class Hoge {
    var num: Int

    init(num: Int) {
        self.num = num
    }

    func hoge() {
        print("------ 値は:\(num) -------") // <<- ここにブレークポイント
    }
}

// 呼び出し
struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
            Button(action: {
                Hoge(num: 1).hoge() // <<- View側で見るとここで止まる
            }, label: {
                Text("Hogeを呼び出す")
            })
        }
        .padding()
    }
}
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000102b39e90 BlogProject`Hoge.hoge(self=(num = 1)) at Hoge.swift:11:15
    frame #1: 0x0000000102b383b4 BlogProject`closure #1 in closure #1 in ContentView.body.getter at ContentView.swift:20:30
    frame #2: 0x00000001cc8ed8b0 SwiftUI`___lldb_unnamed_symbol255351 + 160
    frame #3: 0x00000001cc02168c
・・・ 以下に沢山続く...

ただ、XcodeのDebug navigatorでも確認できるので、そちらを利用される方が多いかもしれません。


同じようにThreadごとのスタックトレースが確認できる

今回のケースだと、SwiftUIのContentViewでButtonを押したActionでhogeメソッドを呼び出しているので、その経路が確認できますね。

少しだけ踏み込んで、値を書き換えてみよう

ここまでだとブレークポイントで停止させて、中身を除くだけでそこまで特別役に立つように見えないかもしれません。

なので、ここでは先ほど少し出ていた変数のメモリアドレスを取得して、値を書き換えることをやってみようかと思います。

これができるようになると、UIKitで特定のUILabelの情報をランタイムのままリビルドすることなく、書き換えたりすることができるようになります。

コードはここまで利用してきたものを使用します。

Hogeクラスにはnumというコンストラクタ引数で初期化され、書き換え可能なプロパティが定義されています。

hogeメソッドを呼び出すと、"------ 値は:(num) -------" というnumプロパティの値が出力されます。

class Hoge {
    var num: Int

    init(num: Int) {
        self.num = num
    }

    func hoge() {
        print("------ 値は:\(num) -------") // <<- ここにブレークポイント
    }
}

// 呼び出し
Hoge(num: 1).hoge()

// ブレークポイントをセットしない場合には以下のようにprintされる
// ------ 値は:1 -------

ここまでと同じようにブレークポイントをセットして停止させます。

そして、以下を実行します。

(lldb) p self
(Hoge.Hoge) 0x0000600000278bc0 (num = 1)

hogeメソッドが呼ばれる時、self = Hogeクラスのインスタンスは、num = 1というプロパティの状態になります。

そして、インスタンスは0x0000600000278bc0というメモリアドレスに配置されていることがわかります。

メモリアドレスがわかればそのインスタンスをLLDBで取得することができます。
以下のように、po var $<変数名> = unsafeBitCast(<メモリアドレス>, to: <型>)というコマンドを実行します。

(lldb) po var $a = unsafeBitCast(0x0000600000278bc0, to: Hoge.self)

これによって、LLDB上でaという変数にHogeインスタンスを取得した状態になります。
($マークを忘れないこと)

実行後は$aでランタイムのインスタンスに操作が可能です。

numを試しに書き換えてみましょう。

(lldb) po $hoge.num = 10
0 elements

これで、Hogeインスタンスのnumプロパティは 1→10に変更されています。
("0 elements" と出力されているのは、実行後に戻り値がない処理のためです。)

試しにその後に処理を再開すると、値が変更されています。🎉

(lldb) po $hoge.num = 10
0 elements
# 以下処理を再開した後
------ 値は:10 -------

最後に

LLDBで基本的なデバッグ方法を今回は紹介しました。

まだまだ他にも様々なコマンドがあるので、以下のビデオやhelpコマンドを入れてみて、役に立ちそうなものがないかみてみても良いかも知れませんね🤗

https://developer.apple.com/videos/play/wwdc2019/429

https://developer.apple.com/videos/play/wwdc2021/10209

Discussion