
Debuggingをより楽しく – LLDB
-
2023年7月4日
こんにちは、クライアント開発者Pです。
十数年間Xcode IDEを使用して開発してきましたが、他のプラットフォームのプロジェクトに参加して開発環境が変わったため、しばらくはXcodeを触る機会がなくなりました。その記念(?)でXcode IDEに関連する、しておけば役に立つ情報を共有したいと思います。 : )
LLDB(Low-Level Debugger)とは?
- LLVMプロジェクトの一部として開発されたデバッグツールであり、C、C++、Objective-C、Swift、およびアセンブリ言語で書かれたプログラムのデバッグが可能です。LLDBはさまざまなオペレーティングシステムでサポートされており、デバッグ用のコマンドラインインタフェースを提供します。
- マルチプラットフォームをサポートし、macOS、Linux、FreeBSD、NetBSD、Windowsなど、さまざまなオペレーティングシステムで使用されています。また、開発者が迅速にデバッグを実行できるように、デバッグ用のコマンドラインインタフェースを提供しています。
- 強力なデバッグ機能を提供するオープンソースのデバッガであり、さまざまなプラットフォームで使用することができます。LLDBはgdbに似たインタフェースを提供しながら、より高速で正確で、モジュラーなアーキテクチャを持っています。これらの利点により、LLDBは短時間でプログラムのバグを見つけて解決するのに役立ちます。
- 現在、Xcodeのデフォルトのデバッガとして組み込まれています。
LLDB の使い方
LLDBは、ターミナルのlldbコマンドを使用して実行中のプロセスにアタッチして使用することもできますが、説明の便宜と実際の業務に活用するために、Xcode上での使用方法を基本として説明します。
事前準備
Xcodeで新しいプロジェクトを作成します。この例では、Storyboard/Swiftプロジェクトを作成しました。
さまざまなコマンドのテストのため、一時的に以下のコードを追加しましょう。
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
var myView: UIView
myView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
myView.backgroundColor = UIColor.blue
self.view.addSubview(myView)
let numbers = [1, 2, 3, 4, 5]
print("\\(numbers)")
}
}
Xcode上で LLDBコマンドを使用するには、プロジェクトのプロセスをブレークポイントで停止させるか、一時停止ボタンを押す必要があります。
LLDBコマンドの入力は、Xcodeのデバッグエリア内のコンソールウィンドウで行います。
次の図のように、デバッグエリアを有効にし、プロジェクトのViewControllerの(void)viewDidLoad関数にブレークポイントを設定して実行します。
コンソールウィンドウに(lldb)と表示されたら、準備が完了です。
コマンド
LLDBの基本的なコマンドやオプションなどは、以下のコマンドで確認できます。
Help
// 提供されているコマンドのリストを確認する
(lldb) help
// 特定のコマンドを確認する
(lldb) help po // "po"コマンドを確認する
// 特定のコマンドのオプションを確認する
(lldb) help breakpoint delete // "breakpoint"の"delete"オプションを確認する
Apropos
特定の単語やトピックに関連するデバッガコマンドの一覧を確認する場合に使用します。例えば、”dump”と関連するコマンドを探したい場合は、以下のように入力します。
(lldb) apropos dump
The following commands may relate to 'dump':
dump -- Dump information on Objective-C classes
known to the current process.
info -- Dump information on a tagged pointer.
renderscript allocation dump -- Displays the contents of a particular
allocation
renderscript context dump -- Dumps renderscript context information.
renderscript module dump -- Dumps renderscript specific information for
all modules.
...
Expression
LLDBのExpressionは、デバッグ中に特定のコードを実行し、結果を確認できるコマンドです。Expressionを使用すると、変数の値を変更したり、新しい変数を作成したり、関数やクラスのメンバ関数を呼び出したりすることができます。これにより、デバッグプロセスで発生した問題をより迅速に分析できます。
Expressionの基本
Expressionは通常、次のように入力できます。
(lldb) expression numbers
([Int]) $R0 = 5 values {
[0] = 1
[1] = 2
[2] = 3
[3] = 4
[4] = 5
}
// 以下のコマンドも同じ結果を返します。
(lldb) expr numbers
(lldb) e numbers
オブジェクト(UIView)の場合、オブジェクトのプロパティを出力してくれます。
(lldb) e myView
(UIView) $R4 = 0x0000000136d04600 {
baseUIResponder@0 = {
baseNSObject@0 = {
isa = UIView
}
}
_constraintsExceptingSubviewAutoresizingConstraints = 0x0000000000000000
_cachedTraitCollection = some {
some = 0x0000600003462200 {
baseNSObject@0 = {
isa = UITraitCollection
}
_clientDefinedTraits = 0x0000000000000000
_environmentWrapper = 0x0000000000000000
_tintColor = some {
some = 0x0000600001d21840 {
baseUIDynamicColor@0 = {
baseUIColor@0 ={...}
}
_cuiColorName = 7
_cachedColor = 0x0000000000000000
_cachedThemeKey = 0
}
}
}
}
...
format を指定する場合、以下のように出力することができます。
(lldb) expression -f d -- numbers
([Int]) $R0 = 5 values {
[0] = 11
[1] = 21
[2] = 35
[3] = 44
[4] = 51
}
(lldb) expression -f b -- numbers
([Int]) $R1 = 5 values {
[0] = 0b0000000000000000000000000000000000000000000000000000000000001011
[1] = 0b0000000000000000000000000000000000000000000000000000000000010101
[2] = 0b0000000000000000000000000000000000000000000000000000000000100011
[3] = 0b0000000000000000000000000000000000000000000000000000000000101100
[4] = 0b0000000000000000000000000000000000000000000000000000000000110011
}
(lldb) expression -f h -- numbers
([Int]) $R2 = 5 values {
[0] = 0x000000000000000b
[1] = 0x0000000000000015
[2] = 0x0000000000000023
[3] = 0x000000000000002c
[4] = 0x0000000000000033
}
オブジェクトのdebugDescriptionを出力する場合、-Oオプションを使用します。
(lldb) expression -O -- myView
<UIView: 0x150e04610; frame = (0 0; 100 100); backgroundColor = UIExtendedSRGBColorSpace 0 0 1 1; layer = <CALayer: 0x6000038f0300>>
一般的には短縮形であるpoを使用します。
(lldb) po myView
<UIView: 0x150e04610; frame = (0 0; 100 100); backgroundColor = UIExtendedSRGBColorSpace 0 0 1 1; layer = <CALayer: 0x6000038f0300>>
Single and multi-line expressions
expressionは基本的には1行で完結するコマンドである必要があります。
ただし、複数行のコマンドを入力したい場合は、multi-line入力モードに切り替えてから入力する必要があります。
単に空のexpressionを入力してEnterキーを押すと、multi-line入力モードに切り替わります。
multi-line入力モードから再びシングルライン入力モードに戻りたい場合は、再度Enterキーを押せばよいです。
(lldb) expression
Enter expressions, then terminate with an empty line to evaluate:
1 let $a = 5
2 let $b = 10
3 let $c = 5 + 10
4
(lldb) po $c
15
po
正確に説明すると、-O(po)はNSObjectのdebugDescriptionを実行する表現です。そのため、サンプルプロジェクトで以下のようにdebugDescriptionをオーバーライドすると、
override var debugDescription: String {
var desc = "私のDescは" + "\\(super.debugDescription) です。"
return desc
}
次のような結果が出力されます。
(lldb) po self
私のDescは です。
Expression variables
Expressionコマンドの後にオブジェクトや変数名を指定すると、LLDBが使用する内部変数にその値が保存され、参照や変更が可能になります。以下に簡単な例を示します。例題プロジェクトを実行し、ブレークポイントでLLDBを有効にします。
LLDBコンソールで次のように入力します。
(lldb) expression myView
(UIView) $R4 = 0x0000000130505600 {
baseUIResponder@0 = {
baseNSObject@0 = {
isa = UIView
}
}
....
出力結果を見ると、$Rで始まる文が表示されます(例では$R4)。これがLLDBが保存した変数です。この変数の番号はexpressionが実行されるたびに自動的に増加し、上書きされずに保存されます。
この例では、$R4にmyViewが保存されています。そして、この$R4を使用してmyViewオブジェクトに直接アクセスすることができます。
では、この変数を制御してみましょう。以下のようにmyViewのプロパティを変更します。
(lldb) expression $R4.backgroundColor = UIColor.red
() $R5 = {}
(lldb) expression $R4.frame.origin = CGPoint(x:100, y:100)
() $R6 = {}
その後、continueコマンドを使用して中断されたプロセスを再開します。
(lldb) continue
Process 9006 resuming
[11, 21, 35, 44, 51]
青色のmyViewが赤色に変わり、位置が変更されていることが確認できます。
User defined variables
LLDBでは、直接変数を宣言して使用する方法もあります。
letまたはvarキーワードを使用して変数を宣言します。キーワードの意味通り、変更できない場合はletを使用し、変更可能な場合はvarを使用します。
(lldb) expression var $number = 10
(lldb) po $number
1234
(lldb) expression let $text = "abcd"
(lldb) po $text
"abcd"
letで宣言した変数の値を変更しようとするとエラーが発生します。
(lldb) expression
Enter expressions, then terminate with an empty line to evaluate:
1 let $variable = "This is an immutable variable."
2 $variable = "Can I change this?"
3
error: expression failed to parse:
<EXPR>:4:1: note: change 'let' to 'var' to make it mutable
let $variable = "This is an immutable variable."
^~~
var
error: <EXPR>:9:1: error: cannot assign to value: '$variable' is a 'let' constant
$variable = "Can I change this?"
^~~~~~~~~
<EXPR>:8:1: note: change 'let' to 'var' to make it mutable
let $variable = "This is an immutable variable."
^~~
var
この「User defined variable」は、NSObject型のオブジェクトも扱うことができ、現在のプロセスに直接関連付けることもできます。実行時に新しいViewを作成して追加する例を見てみましょう。
(lldb) expression
Enter expressions, then terminate with an empty line to evaluate:
1 var $newView = UIView(frame: CGRect(x: 100, y: 200, width: 100, height: 100))
2 $newView.backgroundColor = UIColor.magenta
3 self.view.addSubview($newView)
4
(Void) $R1 = {}
(lldb) continue
Process 39432 resuming
結果画面で見るように、マゼンタ色の新しいViewが追加されています。
LLDB の応用
実際のiOSプロジェクトでLLDBを活用するためのコマンドリストを紹介します。
iOS Framework
LLDB上でUIKitなどのフレームワークをインポートすると、直接フレームワークを使用してUIApplicationを制御することができます。ここでは、バッジとUserDefaultsを使用する方法を紹介します。
Badge
バッジに関連する開発では、バッジの数が正しく更新されているかを確認する必要がある場合があります。サーバーやローカルプッシュを使用せずに、LLDB上でバッジの数を変更しながらUIを確認することができます。
まず、アプリアイコンにバッジを表示するために、例のプロジェクトに以下のコードを入力します。
override func viewDidLoad() {
UNUserNotificationCenter.current().requestAuthorization(options: .badge) { (granted, error) in
if error != nil {
// success!
}
...
}
このコードを入力した後、アプリをXcode上で一度起動すると、プッシュの許可に関するアラートが表示されます。バッジが表示されるように許可してください。
一時停止ボタンを押してXcode上で実行中のアプリを一時停止させます。
その後、実行中のシミュレータでホームボタンを押してホーム画面に移動させます。
表示される画面のように、現在のアプリアイコンのバッジには何も表示されていません。
この状態でLLDBコンソールに以下のように入力します。
(lldb) expression -l swift -- import UIKit
(lldb) expression -- UIApplication.sharedApplication.applicationIconBadgeNumber = 33
入力と同時に、シミュレータのバッジ情報が更新される様子を確認できます。
ブレークポイントを設定したりテストコードを作成せずに、リアルタイムでアプリのデータを変更し、確認することができるため、デバッグが非常に便利です。
UserDefault
同様に、FoundationフレームワークをインポートしてUserDefaultsを制御してみましょう。
以下のコードをサンプルプロジェクトに追加します。
override func viewDidLoad() {
...
super.viewDidLoad()
UserDefaults.standard.set("I love LLDB", forKey: "lldb")
let button = UIButton(type: .system, primaryAction: UIAction(title: "UserDefault", handler: { _ in
// value
let value = UserDefaults.standard.string(forKey: "lldb")
// alert
let alertController = UIAlertController(title: "Saved Value", message: value, preferredStyle: .alert)
let confirmAction = UIAlertAction(title: "Confirm", style: .default) { (_) in }
alertController.addAction(confirmAction)
self.present(alertController, animated: true, completion: nil)
}))
button.frame = CGRect(x: 100, y: 200, width: 200, height: 50)
self.view.addSubview(button)
...
}
コードを実行した後、ボタンを押すとUserDefaultsに保存された “I love LLDB” が表示されることを確認できます。
次に、一時停止ボタンを押し、LLDBコンソールに以下のように入力して、UserDefaultsに保存されているvalueを変えます。
(lldb) expression -l swift -- import Foundation
(lldb) expression -l swift -- UserDefaults.standard.set("LLDB is the best", forKey: "lldb")
その後、再びプロセスを再開するために再生ボタンを押し、ViewControllerのボタンをもう一度押すと、LLDBで入力した値に変更されていることが確認できます。
Chisel
次に、LLDBを利用して作成されたパッケージを紹介します。
Chiselは、LLDBをベースにした拡張パッケージであり、iOSアプリのデバッグにカスタムコマンドを提供します。Chiselを使用すると、LLDBで実行中のアプリのビュー構造を表示したり、特定のUIViewオブジェクトの表示/非表示を制御したり、UIViewオブジェクトを可視化して別途表示したりするなど、便利な機能を利用することができます。
Chisel Install
Chiselをインストールするには、brewを使用します。
brew update
brew install chisel
その後、Chiselをデフォルト設定で使用するために、ユーザーのホームディレクトリに.lldbinitファイルを作成し、次の内容を入力します。
touch ~/.lldbinit
# M1 Macの場合
echo "command script import /usr/local/opt/chisel/libexec/fbchisellldb.py" >> ~/.lldbinit
# x86 Macの場合
echo "command script import /opt/homebrew/opt/chisel/libexec/fbchisellldb.py" >> ~/.lldbinit
Xcodeが実行中であればXcodeを終了し、再度起動します。
サンプルプロジェクトを開いてプロセスを一時停止または再開し、デバッグエリアの一時停止ボタンをクリックしてLLDBコンソールを準備します。
テストのために、次のように追加のUIViewを作成してみましょう。
var myView2: UIView
myView2 = UIView(frame: CGRect(x: 0, y: 150, width: 100, height: 100))
myView2.backgroundColor = UIColor.yellow
self.view.addSubview(myView2)
var myView3: UIView
myView3 = UIView(frame: CGRect(x: 0, y: 180, width: 100, height: 100))
myView3.backgroundColor = UIColor.black
self.view.addSubview(myView3)
Chiselの代表的なコマンド
Chiselには多くのコマンドがあり、その中で実際のプロジェクトで役立つ可能性のあるいくつかの機能を紹介します。
なお、Chiselで提供されているすべてのコマンドのリストは、LLDBのヘルプの中の「Current user-defined commands:」に記載されています。
(lldb) help
Debugger commands:
... 省略 ...
Current user-defined commands:
alamborder -- Put a border around views with an ambiguous layout
alamunborder -- Removes the border around views with an ambiguous layout
bdisable -- Disable a set of breakpoints for a regular expression
benable -- Enable a set of breakpoints for a regular expression
binside -- Set a breakpoint for a relative address within the
framework/library that's currently running. This does the
work of finding the offset for the framework/library and
sliding your address accordingly.
... 省略 ...
pviews
pviewsコマンドは、UIViewまたはNSViewオブジェクトの再帰的な説明を表示する機能を提供します。現在のウィンドウにアタッチされているすべてのサブビューを一度に確認するのに非常に便利です。
また、このコマンドで表示される各ビューのアドレスを他のコマンドの引数として使用することもできます。
(lldb) pviews
<UIWindow: 0x1366067e0; frame = (0 0; 393 852); gestureRecognizers = <NSArray: 0x6000024310e0>; layer = <UIWindowLayer: 0x600002431860>>
| <UITransitionView: 0x121804080; frame = (0 0; 393 852); autoresize = W+H; layer = <CALayer: 0x600002a32000>>
| | <UIDropShadowView: 0x121804700; frame = (0 0; 393 852); autoresize = W+H; layer = <CALayer: 0x600002a33040>>
| | | <UIView: 0x136506aa0; frame = (0 0; 393 852); autoresize = W+H; backgroundColor = <UIDynamicSystemColor: 0x600003f7de80; name = systemBackgroundColor>; layer = <CALayer: 0x600002a36ae0>>
| | | | <UIView: 0x126506740; frame = (0 0; 100 100); backgroundColor = UIExtendedSRGBColorSpace 0 0 1 1; layer = <CALayer: 0x600002a00240>>
| | | | <UIView: 0x126507ff0; frame = (0 150; 100 100); backgroundColor = UIExtendedSRGBColorSpace 1 1 0 1; layer = <CALayer: 0x600002a02e00>>
| | | | <UIView: 0x126509330; frame = (0 180; 100 100); backgroundColor = UIExtendedGrayColorSpace 0 1; layer = <CALayer: 0x600002a02e60>>
pvc
その名前から分かる通り、ViewControllerを再帰的に表示します。
(lldb) pvc
<LLDB.ViewController 0x136606100>, state: appeared, view: <UIView: 0x136506aa0>
fv, fvc
正規表現を引数に指定し、その引数にマッチするクラスのView / ViewControllerを見つけます。また、見つかったViewをクリップボードに保存します。
(lldb) fv UIView
0x136506aa0 UIView
0x126506740 UIView
0x126507ff0 UIView
0x126509330 UIView
show / hide
特定のViewやLayerを表示または非表示にします。UIの特定のパターンを確認するのに役立ちます。
上記の例では、黒いUIViewが黄色いUIViewに重なっています。一時的に黒いUIViewを非表示にしてみましょう。
pviewsコマンドを使用して非表示にしたいUIViewのアドレスを見つけてコピーします。この例では、 “0x129304940″です。これをhideコマンドの引数として実行すると、
(lldb) hide 0x126509330
以下のように黒いUIViewが消えたことが確認できます。
flicker
指定したビューまたはレイヤーを一瞬点滅させます。特定のビューを制御したい場合に便利です。
(lldb) flicker 0x126509330
visualize
指定したアドレスのUIImage、CGImageRef、UIView、CALayerをMacのPreview.appを使用して表示します。オブジェクトがどのようなUIを持っているかを確認する場合に有用です。
(lldb) visualize 0x126509330
実行結果から、上記のアドレスが黒いUIViewであることがわかります。
その他にも、アニメーションの速度を調整する(slowanim)、NSURLRequestをcurlコマンドに変換する(pcurl)、NSDataを文字列として表示する(pdata)など、さまざまな機能が含まれています。
最後に
LLDBを使用すると、単純にブレークポイントを使用するだけのデバッグよりも柔軟で拡張性のあるデバッグが可能です。例では触れていませんが、フレームワークレベルの非公開領域の情報を確認したり、クラッシュログのダンプをシンボリックリンクして表示したり、OSのシェルコマンドと組み合わせて複雑な処理をしたりすることが可能です。
ユーザーはLLDBを介してソースコードとアセンブリコードの両方をデバッグすることができ、さまざまなデータ型やメモリ構造を調査することもできます。また、LLDBはスレッド、スタック、変数、オブジェクトの状態を監視および変更する機能も提供しています。
このように、LLDBを使用することで、プログラムのバグを素早く発見・修正できるようになり、より速く安定したソフトウェア開発が可能となるため、まだLLDBを試したことがない場合は、今回試してみることをお勧めします。