BLOG.tass.io

応用LLDB

2015-01-18

前回の入門LLDBに引き続き、objc↑↓からLLDBデバッガの話題の続きをざっくりまとめてみました。

ブレークポイントなしに停止させる方法

LLDBはプログラムが停止中にコマンドが実行できます。
そのためにはブレークポイントを設定しておく以外にも、XCodeなら任意の時点でPauseボタン(||)を押すことでSIGSTOPを発生させ、プログラムを一時停止させることもできます。

Pauseボタンで停止した場合、XCode上ではスレッドの任意の地点で停止します。
ソースウィンドウには、SIGSTOPを受け取ったmach_msg_trapのコード(アセンブリ)が表示されることになるでしょう。 また、デバッグウィンドウではlldbのコマンドが受付できるようになります。(下図の通り)

ここで po [[[UIApplication sharedApplication] keyWindow] recursiveDescription] コマンドを入力すると、 現在のウィンドウの階層構造が出力されます。 どこにブレークポイントを貼るかに悩むこと無く、現在表示されているビューの階層構造を確認することができるので便利です。

(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
<UIWindow: 0x7fc9196262f0; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x7fc91961d6e0>; layer = <UIWindowLayer: 0x7fc91961f850>>
| <UILayoutContainerView: 0x7fc91972d510; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x7fc919427510>; layer = <CALayer: 0x7fc919743510>>
|    | <UINavigationTransitionView: 0x7fc91941f5e0; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x7fc91941da40>>
|    |    | <UIViewControllerWrapperView: 0x7fc919599360; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fc919597ac0>>
|    |    |    | <UITableView: 0x7fc919825000; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x7fc91958b7b0>; layer = <CALayer: 0x7fc919434af0>; contentOffset: {0, -64}; contentSize: {375, 0}>
(略)

なお、Chiselをインストールしていれば、上記のように長いコマンドを入力することなく、 pviewsコマンドで同じことが実現できます。

(lldb) pviews
<UIWindow: 0x7fc9196262f0; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x7fc91961d6e0>; layer = <UIWindowLayer: 0x7fc91961f850>>
| <UILayoutContainerView: 0x7fc91972d510; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x7fc919427510>; layer = <CALayer: 0x7fc919743510>>
|    | <UINavigationTransitionView: 0x7fc91941f5e0; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x7fc91941da40>>
|    |    | <UIViewControllerWrapperView: 0x7fc919599360; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fc919597ac0>>
|    |    |    | <UITableView: 0x7fc919825000; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x7fc91958b7b0>; layer = <CALayer: 0x7fc919434af0>; contentOffset: {0, -64}; contentSize: {375, 0}>
(略)

LLDBからViewの背景色を変える

LLDBからUIを更新してみます。
先ほど出力した中に、<UITableView: 0x7fc919825000; (略)がありましたね。 UITableViewのアドレスが 0x7fc919825000 だったので、デバッガで扱うことができる変数に設定してメソッドを呼び出してみます。

まずは$で始まる変数を定義して、キャストして値を設定します。

(lldb) e UIView *$myView = (UIView *)0x7fc919825000

次に、setBackgroundColorメソッドを呼び出してみます。この時、戻り値を(void)で明示しておく必要があります。

(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]

最後に、UIの更新を反映させるために、CATransactionクラスのflushメソッドをコールします。 これでCore Animationのrepaintが実行され、UIが反映されます。

(lldb) e (void)[CATransaction flush]

コードを一切修正することなく、デバッガからUIが更新できました☆
試しにちょっと変えてみるくらいなら、デバッガでも十分やれちゃいますね。

なお、Chiselをインストールしていれば、e (void)[CATransaction flush]の代わりに caflush コマンドを使うことができます。より詳しいヘルプが読みたいときには、help caflushコマンドを使ってみてください。

LLDBから画面遷移させる

コーディングすることなく、LLDB上でViewControllerを生成して、rootViewControllerにPUSHすることで、画面遷移を実行させることもできます。

(lldb) e UINavigationController *$nvc = (UINavigationController *)[[[UIApplication sharedApplication] keyWindow] rootViewController]
(lldb) e UIViewController *$vc = [UIViewController new]
(lldb) e (void)[[$vc view] setBackgroundColor:[UIColor redColor]]
(lldb) e (void)[$vc setTitle:@"Test"]
(lldb) e (void)[$nvc pushViewController:$vc animated:YES]
(lldb) e (void)[CATransaction flush]

ボタンのターゲットを探す

とあるUIButtonを押下した時に呼び出されるメソッドが何か、デバッガ上で探しだしてみます。

実際にアプリを起動して、Pauseボタンで強制ブレークをかけて停止させます。
そしてまずは po [[[UIApplication sharedApplication] keyWindow] recursiveDescription] (Chiselがあればpviews)でViewの一覧を出力させます。

(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
<UIWindow: 0x7fdbf8c65bb0; frame = (0 0; 375 667); gestureRecognizers =...
...
|    |    |    | <UITableView: 0x7fdbfc086800; frame = (0 0; 375 667); cli...
...
|    |    |    |    | <UIButton: 0x7fdbf8c5d060; frame = (10 10; 100 50); opa...
...

ここで、お目当ての階層にある UIButtonを見つけます。 アドレスが 0x7fdbf8c5d060 と判明したので、デバッガから参照できるよう変数 $button に入れておきます。

(lldb) e UIButton *$button = (UIButton *)0x7fdbf8c5d060

その上で、po [$button allTargets]でこのbuttonのターゲットを全て出力します。

(lldb) po [$button allTargets]
{(
    <TDSItemListViewController: 0x7fdbfb142310>
)}

すると、TDSItemListViewControllerクラスのインスタンス 0x7fdbfb142310 が1つ見つかります。
これをactionsForTargetで該当メソッドまで見つけ出してみます。

(lldb) po [$button actionsForTarget:(id)0x7fdbfb142310 forControlEvent:0]
<__NSArrayM 0x7fdbf8e21f20>(
    hoge:
)

これで、このUIButtonが押された時にTDSItemListViewControllerクラスのhogeというメソッドが コールされることがデバッガから判明しました。 あとは必要に応じて、このメソッドにブレークポイントを貼ればOKです。

ちなみに参考まで、上記のUIBUttonは下記のコードで設定していたものでした。

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.navigationItem.leftBarButtonItem = self.editButtonItem;

    UIButton *button = [UIButton new];
    [button setTitle:@"ボタン" forState:UIControlStateNormal];
    [button setBackgroundColor:[UIColor redColor]];
    button.frame = CGRectMake(10, 10, 100, 50);
    [button addTarget:self
    action:@selector(hoge:) forControlEvents:UIControlEventTouchUpInside];
    [self.tableView addSubview:button];
}
-(void)hoge:(UIButton*)button{
    NSLog(@"hoge pushed!!!");
}

ちなみに、storyboard上でActionを設定している場合は、常に_sendAction:withEvent:メソッドが見つかりますので、 本当にコールされるメソッドが知りたい場合はstoryboardから見つけて下さい。

(lldb) po [$button actionsForTarget:(id)0x********* forControlEvent:0]
<__NSArrayM 0x7ff13ac3cb60>(
_sendAction:withEvent:
)

インスタンス変数の変化を監視する

UIViewのインスタンスは、全てがメンバ変数_layerを必ず持っています。 この_layerは必要に応じて上書きされてしまうので、それをデバッガで検知したいと思うのですが、 XCodeのGUI上ではシンボリックブレークポイントを貼ることができません。

そこで、LLDBのCLIを使って、_layerに変化があった時にブレイクするよう設定してみます。 試しに、先ほどのUIButtonの例でやってみます。

まずは、UIButtonクラスの中にある_layerメンバが、先頭から何バイトの位置に配置されているかを取得します。

(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([UIButton class], "_layer"))
(ptrdiff_t) $0 = 8

これで、UIButtonインスタンスの先頭から8バイトの位置に_layerメンバが配置されていることがわかりました。
あとは、アドレスを指定して監視用のブレイクポイントを設定します。
さきほどのUIButtonは e UIButton *$button = (UIButton *)0x7fdbf8c5d060で設定していましたので、下記のwatchpointコマンドで変化の監視を設定できます。

(lldb) watchpoint set expression -- (int *)0x7fdbf8c5d060 + 8
Watchpoint created: Watchpoint 1: addr = 0x7fdbf8c5d080 size = 8 state = enabled type = w
new value: 0x00007fea3bc85c00

オーバーライドしていないメソッドのブレークポイント

とあるUIViewControllerで、viewDidLoadメソッドはオーバーライドしているけれど、 viewDidAppear:メソッドはオーバーライドしていないケースがあったとします。 この時、viewDidLoadにブレークポイントを貼るのは簡単ですが、オーバーライドしていないので そもそもコードが存在しないviewDidAppear:がコールされるタイミングが分からず、ブレークポイントが貼れません。 しかし、スーパークラスのviewDidAppear:がコールされたタイミングは知りたいというケースはあると思います。

例1)TDSItemListViewControllerクラスでオーバーライドしているviewDidLoadにはブレークポイントを設定できる

(lldb) b -[TDSItemListViewController viewDidLoad]
Breakpoint 3: where = ToDoSample`-[TDSItemListViewController viewDidLoad] + 23 at TDSItemListViewController.m:20, address = 0x0000000106381677

例2)TDSItemListViewControllerでオーバーライドしていないviewDidAppear:にはブレークポイントを設定できない

(lldb) b -[TDSItemListViewController viewDidAppear:]
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.

スーパークラスであるUIViewControllerクラスの実装はAppleに隠蔽されているので、 そのままではブレークポイントを貼ることができません。 とはいえ、メソッドの実行コードもレジスタまたはスタック上に配置されるので、そのアドレスを見つけ出せば ブレイクポイントを設定することもできます。ただ、x86やx86-64、ARMv7、armv64とそれぞれのアーキテクチャ毎に メソッドを見つけ出すのは非情に面倒です…。

なので、ここは大人しくChiselをインストールして、bmessageコマンドを使えるようにしましょう。

(lldb) bmessage -[TDSItemListViewController viewDidAppear:]
Setting a breakpoint at -[UITableViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 0x0000000106385540
Breakpoint 4: where = UIKit`-[UITableViewController viewDidAppear:], address = 0x00000001072e6a32

Chiselのbmessageコマンドを使えば、自動的にメソッドのアドレスを探しだしてブレークポイントを設定してくれます。

LLDBとPython拡張

LLDBはPythonでの拡張をサポートしています。前述のChiselも、Pythonのスクリプトです。
LLDBでは、scriptコマンドにつづいてPythonのコードをREPLで記述することができます。

例)

(lldb) script import os
(lldb) script os.system("open http://www.objc.io/")

さらに、独自のコマンドを定義することもできます。
例えば、ホームディレクトリに~/myCommands.pyというファイルを作成して、 下記のスクリプトを記述していたとします。

#!/usr/bin/python
import lldb
import commands
import optparse
import shlex

def caflushCommand(debugger, command, result, internal_dict):
    debugger.HandleCommand("e (void)[CATransaction flush]")

def lsCommand(debugger, command, result, internal_dict):
    print >>result, (commands.getoutput('/bin/ls %s' % command))

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand('command script add -f myCommands.lsCommand ls')
    debugger.HandleCommand('command script add -f myCommands.caflushCommand cflush')
    print 'custom module loaded'

その後、LLDBでcommand script import <ファイル名>を指定することでロードすることができます。

(lldb) command script import ~/myCommands.py

独自コマンドファイルを読み込んだ後は、LLDB上でそのコマンドが使用できるようになります。
上記の例では「ls」「cflush」という2つのコマンドを追加しましたので、下記のように使うことができるようになりました。

(lldb) cflush
(lldb) ls
Applications
Incompatible Software
Library
Network
System
Users
Volumes
bin
cores
dev
etc
home
installer.failurerequests
net
opt
private
sbin
tmp
usr
var

また、ホームディレクトリに~/.lldbinitというファイル名でPythonコードを記載しておけば、 LLDBの起動時に自動的にロードされます。Chiselもこの方法でロードされているので、参考にしてください。


Michael Kuroneko

Written by Michael Kuroneko. Follow me on twitter, github.