1. Synopsis

RubyMotion のデバッガは LLVM のデバッガ LLDB をベースとしています。

LLDB は伝統的に C 言語ベースで書かれたプログラムをデバッグするために用いられてきました。RubyMotion は LLDB に Ruby のサポートをもたらし、RubyMotion アプリのプロセスへの接続と内部の調査ができるようにします。

Note 現時点では、LLDB サポートは実験的なもので、またとても低レベルなものです。我々の目標は、より高いレベル、より友好的なデバッガを LLDB 上に構築し、Ruby 開発者のみなさんにより良いエクスペリエンスを提供することです。

このドキュメントは LLDB で RubyMotion アプリをデバッグするために必要な主要な機能について説明するもので、LLDB の完全なマニュアルではありません。完全なガイドを必要とされる場合には LLDB 公式ドキュメント を参照することをお勧めします。

Note このガイドでは、すべてのデバッガコマンドを非省略語で記載します。すべてではありませんが、ほとんどのコマンドは省略語もあり LLDB 公式ドキュメント で確認できます。

2. Debugging symbols

RubyMotion コンパイラは Ruby 言語向けに DWARF と呼ばれるデバッグ用のデータフォーマットを実装しています。これはデバッガやプロファイラといった外部プログラムが、RubyMotion アプリケーションについてソースレベルの情報を取得するためのものです。

メタデータは .dSYM バンドルファイルへ保存されます。このファイルはみなさんのプロジェクトの build ディレクトリの .app バンドルと同じ階層にあります。

developmentrelease モードの双方でデバッギング用のシンボルを保持します。しかし、release モードではコンパイル時の最適化が有効となるため、development モードの方がより良いデバッギング体験をもたらすでしょう。たとえば、release モードではローカル変数は CPU レジスタに収まるように最適化され、デバッガでローカル変数へアクセスできない場合があります。

3. Starting the debugger

デバッガを起動するために、debug オプションを用います。適切な rake タスクと一緒に、debug オプションに任意の値を設定します。

rake の simulator タスクで作業する場合、インタラクティブシェル (REPL) は置き換えられ、デバッガはアプリに直接アタッチするようになります。

$ rake simulator debug=1

rake の device タスクを実行する場合、アプリケーションがデバイスに deploy された後に、ビルドシステムはデバイス上で iOS デバッギングサーバを起動します。そしてシェル上でデバッガがリモートのサーバと接続します。

$ rake device debug=1

3.1. Entering commands before starting

デフォルトでは、デバッガが起動したあとにアプリケーションが実行されます。アプリケーションが実行される前に GDB コマンドを実行したいケースがあるかもしれません。

no_continue オプションに任意の値を設定することで、それができるようになります。オプションが設定された場合、デバッガはアプリケーションに対して continue を行わないようになります。これにより、デバッギングの設定などを変更することができます(たとえば、ブレークポイントの設定など)。アプリケーションの実行を再開する準備ができたら、continue コマンドを実行しましょう。

$ rake debug=1 no_continue=1
[...]
(lldb) breakpoint set --file hello_view.rb --line 10
Breakpoint 3: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.
(lldb) c
Process 87523 resuming
[...]

3.2. Saving commands

アプリケーション実行時に自動的に実行したいコマンドをファイルに保存しておくこともできます。

デバッガはプロジェクトのルートディレクトリ内の debugger_cmds というファイルを参照します。もしこのファイルが存在すれば、ファイルの内容は改行文字で区切られたデバッグコマンドのリストとして解釈されます。

app/hello_view.rb:10 のようにフルパスではなく、hello_view.rb:10 のようにファイル名だけを使います。

$ echo "breakpoint set --file hello_view.rb --line 10" > debugger_cmds
$ rake debug=1
[...]

4. Managing breakpoints

ブレークポイントを設定するには、breakpoint コマンドを用いてソースコードの場所を指定します。デバッガは指定された場所で実行を停止します。停止する場所は --line オプションを用いて指定します。

例として、次のコマンドは hello_view.rb ファイルの 10 行目にブレークポイントを設定します。

(lldb) breakpoint set --file hello_view.rb --line 10

現在のデバッグ環境で設定されているブレークポイントを確認するには breakpoint list コマンドを用いることができます。

(lldb) breakpoint list
Current breakpoints:
1: name = 'rb_exc_raise', locations = 1, resolved = 1
  1.1: where = Hello`rb_exc_raise, address = 0x00049d70, resolved, hit count = 0

2: name = 'malloc_error_break', locations = 1, resolved = 1
  2.1: where = libsystem_malloc.dylib`malloc_error_break, address = 0x030b47f9, resolved, hit count = 0

3: file = 'hello_view.rb', line = 10, locations = 1, resolved = 1
  3.1: where = Hello`rb_scope__drawRect:__ + 1034 at hello_view.rb:10, address = 0x0000964a, resolved, hit count = 1

先ほど指定したブレークポイント hello_view.rb:10 と、それが有効であることを確認できます。breakpoint enablebreakpoint disable コマンドは、指定された番号のブレークポイントを有効・無効にすることができます。

私たちが追加したブレークポイントはリストの 3 番で、これを無効にするには次のように実行します。

(lldb) breakpoint disable 3
1 breakpoints disabled.

5. Getting the backtrace

ブレークポイントにヒットし実行が停止すると、そのメソッドがどこから呼び出されたのかバックトレースを実行し確認することができます。

thread backtrace コマンドを用いることで、それを行うことができます。

(lldb) thread backtrace
* thread #1: tid = 0x36e013, 0x0000964a Hello`rb_scope__drawRect:__(self=0x08fa34d0, rect=0x08f8d8a0) + 1034 at hello_view.rb:10, queue = 'com.apple.main-thread, stop reason = breakpoint 3.1
    frame #0: 0x0000964a Hello`rb_scope__drawRect:__(self=0x08fa34d0, rect=0x08f8d8a0) + 1034 at hello_view.rb:10
    frame #1: 0x00009d77 Hello`__unnamed_9 + 231
    frame #2: 0x0065be43 UIKit`-[UIView(CALayerDelegate) drawLayer:inContext:] + 504
    frame #3: 0x024bb800 QuartzCore`+[CATransaction flush] + 52
    frame #4: 0x005eed70 UIKit`_UIApplicationHandleEvent + 683
    frame #5: 0x03add6e1 GraphicsServices`PurpleEventCallback + 46
    frame #6: 0x01da8ffb CoreFoundation`CFRunLoopRunInMode + 123
    frame #7: 0x005ec8be UIKit`-[UIApplication _run] + 840
    frame #8: 0x005eeabb UIKit`UIApplicationMain + 1225
    frame #9: 0x000024cc Hello`main(argc=1, argv=0xbfffec0c) + 156 at main.mm:15

みなさんのコードのバックトレース・フレームは rb_scope__ というプレフィックスと、file:line の情報で確認することができます。

5.1. Frames

バックトレースの一番最初のフレームは、ブレークポイントが設定されている drawRect: というメソッドです。ブレークポイントより下の他のフレームはネイティブの iOS から呼び出されたものです。drawRect:UIView クラスから正しく呼び出されていることが確認できます。

frame コマンドを使用すると、バックトレースの特定フレームに切り替えることができます。デフォルトでは、トップフレーム (#0) ですが、状況を確認するためにフレーム #2 へ移動してみましょう。次のコマンドを実行することでフレームを切り替えることができます。

(lldb) frame select 2
frame #2: 0x0065be43 UIKit`-[UIView(CALayerDelegate) drawLayer:inContext:] + 504
UIKit`-[UIView(CALayerDelegate) drawLayer:inContext:] + 504:
-> 0x65be43:  calll  0x6156b5                  ; UIGraphicsPopContext
   0x65be48:  addl   $108, %esp
   0x65be4b:  popl   %esi
   0x65be4c:  popl   %edi

言うまでもありませんが、バックトレースで示される Ruby で定義された特定のメソッドへ移動することが重要です。そうでなければ、前述の例のようにアセンブリコードだけが表示されます。

5.2. Threads

thread backtrace コマンドは現在実行しているスレッドのバックトレースのみを表示します。マルチスレッドプログラムを扱う際に、たとえば競合状態をデバッグするときに、実行中のすべてのスレッドのバックトレースを表示することもできます。

次のコマンドを実行すると、Terminal に実行中のすべてのスレッドのバックトレースが表示されます。

(lldb) thread backtrace all
* thread #1: tid = 0x36e013, 0x0000964a Hello`rb_scope__drawRect:__(self=0x08fa34d0, rect=0x08f8d8a0) + 1034 at hello_view.rb:10, queue = 'com.apple.main-thread, stop reason = breakpoint 3.1
    frame #0: 0x0000964a Hello`rb_scope__drawRect:__(self=0x08fa34d0, rect=0x08f8d8a0) + 1034 at hello_view.rb:10
    frame #1: 0x00009d77 Hello`__unnamed_9 + 231
    frame #2: 0x0065be43 UIKit`-[UIView(CALayerDelegate) drawLayer:inContext:] + 504
    frame #3: 0x024bb800 QuartzCore`+[CATransaction flush] + 52
    frame #4: 0x005eed70 UIKit`_UIApplicationHandleEvent + 683
    frame #5: 0x03add6e1 GraphicsServices`PurpleEventCallback + 46
    frame #6: 0x01da8ffb CoreFoundation`CFRunLoopRunInMode + 123
    frame #7: 0x005ec8be UIKit`-[UIApplication _run] + 840
    frame #8: 0x005eeabb UIKit`UIApplicationMain + 1225
    frame #9: 0x000024cc Hello`main(argc=1, argv=0xbfffec0c) + 156 at main.mm:15

  thread #2: tid = 0x36e04e, 0x031bf992 libsystem_kernel.dylib`kevent64 + 10, queue = 'com.apple.libdispatch-manager
    frame #0: 0x031bf992 libsystem_kernel.dylib`kevent64 + 10
    frame #1: 0x02de018e libdispatch.dylib`_dispatch_mgr_invoke + 238
    frame #2: 0x02ddfeca libdispatch.dylib`_dispatch_mgr_thread + 60

  thread #3: tid = 0x36e04f, 0x031bf046 libsystem_kernel.dylib`__workq_kernreturn + 10
    frame #0: 0x031bf046 libsystem_kernel.dylib`__workq_kernreturn + 10
    frame #1: 0x03182dcf libsystem_pthread.dylib`_pthread_wqthread + 372

  thread #4: tid = 0x36e050, 0x031bdd2e libsystem_kernel.dylib`accept$UNIX2003 + 10
    frame #0: 0x031bdd2e libsystem_kernel.dylib`accept$UNIX2003 + 10
    frame #1: 0x001397b6 Hello`-[RMREPL start] + 134
    frame #2: 0x015567a7 Foundation`-[NSThread main] + 76
    frame #3: 0x01556706 Foundation`__NSThread__main__ + 1275
    frame #4: 0x031815fb libsystem_pthread.dylib`_pthread_body + 144
    frame #5: 0x03181485 libsystem_pthread.dylib`_pthread_start + 130

フレームの切り替えと同じように、thread select コマンドを用いるとデバッガはスレッドを切り替えます。他の実行中スレッドで特定の Ruby メソッドを調査したいという場合に役立つでしょう。次のコマンドを実行すると、デバッガのプロンプトはスレッド #4 に切り替わります。

(lldb) thread select 4
* thread #4: tid = 0x36e050, 0x031bdd2e libsystem_kernel.dylib`accept$UNIX2003 + 10
    frame #0: 0x031bdd2e libsystem_kernel.dylib`accept$UNIX2003 + 10
libsystem_kernel.dylib`accept$UNIX2003 + 10:
-> 0x31bdd2e:  jae    0x31bdd3e                 ; accept$UNIX2003 + 26
   0x31bdd30:  calll  0x31bdd35                 ; accept$UNIX2003 + 17
   0x31bdd35:  popl   %edx
   0x31bdd36:  movl   29423(%edx), %edx

6. Inspecting objects

バックトレースを確認したあと、オブジェクトについて調査したいことがあるかもしれません。デバッガは特別なコマンドを用いることでそれらを表示します。

6.1. Local variables

drawRect:(rect) メソッドで設定されているブレークポイントにヒットして停止しています。ブレークポイントの内容から、selfrect という二つの引数を受け付けるメソッドの内部にいることが分かります。rect は間違いなく CGRect 引数です。self は何でしょうか?

RubyMotion では、self 引数は Ruby で公開されている self オブジェクトへのポインターで、メソッドのレシーバを表しています。デバッガでは、self はメソッドの第一引数として表示されます。

print-ruby-object コマンドを使用することで、selfrect の両方の値を調査することができます。RubyMotion で定義されているこのコマンドは、指定されたオブジェクトに inspect メッセージを送信しその値を返します。このコマンドは利用しやすいように pro というショートカットで呼び出すこともできます。

frame #0: 0x0000964a Hello`rb_scope__drawRect:__(self=0x08fa34d0, rect=0x08f8d8a0) + 1034 at hello_view.rb:10
   7          end
   8          text = "ZOMG!"
   9        else
-> 10         bgcolor = UIColor.blackColor
   11         text = @touches ? "Touched #{@touches} times!" : "Hello RubyMotion!"
   12       end
   13
(lldb) print-ruby-object self
#<HelloView:0x8fa34d0>
(lldb) pro rect
#<CGRect origin=#<CGPoint x=0.0 y=0.0> size=#<CGSize width=320.0 height=568.0>>

ローカル変数のリストは frame variable コマンドを用いて表示できます。リストでは各ローカル変数のアドレスも表示されます。

(lldb) frame variable
(void *) self = 0x08fa34d0
(void *) rect = 0x08f8d8a0
(void *) bgcolor = 0x08f931c0
(void *) red = 0x00000004
(void *) green = 0x00000004
(void *) blue = 0x00000004
(void *) text = 0x09c55230
(void *) font = 0x00000004

これらのローカル変数は pro コマンドを用いて Terminal 上で個別に調査することもできます。

(lldb) pro bgcolor
#<UICachedDeviceWhiteColor:0x8f931c0>
(lldb) pro text
"Hello RubyMotion!"
(lldb) pro font
nil

6.2. Instance variables

print-ruby-ivar コマンドまたはショートカットの pri を使用することで、オブジェクトのインスタンス変数を表示することができます。

コマンドに二つの引数を与えた場合には、一つ目はインスタンス変数を取得する対象のオブジェクトとなります。二つ目はインスタンス変数を表す文字列でなければなりません。インスタンス変数名には @ を必ず含めてください。

(lldb) pri self "@touches"
2

引数を一つだけでコマンドを実行すると、コマンドは self から指定されたインスタンス変数を取得します。

(lldb) pri "@touches"
2

7. Control flow

next コマンドはソースコードレベルで次の行まで実行を継続します。たいていの場合、Ruby ソースコードの次の行までとなります。 表示されている現在の行は、デバッガがまだ実行していないことを意味します。変数やその値を確認する際には、このことを注意してください。

* thread #1: tid = 0x3702a0, 0x0000964a Hello`rb_scope__drawRect:__(self=0x08ed1600, rect=0x08d91620) + 1034 at hello_view.rb:10, queue = 'com.apple.main-thread, stop reason = breakpoint 3.1
    frame #0: 0x0000964a Hello`rb_scope__drawRect:__(self=0x08ed1600, rect=0x08d91620) + 1034 at hello_view.rb:10
   7          end
   8          text = "ZOMG!"
   9        else
-> 10         bgcolor = UIColor.blackColor
   11         text = @touches ? "Touched #{@touches} times!" : "Hello RubyMotion!"
   12       end
   13
(lldb) next
Process 87162 stopped
* thread #1: tid = 0x3702a0, 0x000096c9 Hello`rb_scope__drawRect:__(self=0x08ed1600, rect=0x08d91620) + 1161 at hello_view.rb:11, queue = 'com.apple.main-thread, stop reason = step over
    frame #0: 0x000096c9 Hello`rb_scope__drawRect:__(self=0x08ed1600, rect=0x08d91620) + 1161 at hello_view.rb:11
   8          text = "ZOMG!"
   9        else
   10         bgcolor = UIColor.blackColor
-> 11         text = @touches ? "Touched #{@touches} times!" : "Hello RubyMotion!"
   12       end
   13
   14       bgcolor.set

continue コマンドで、ブレークポイントに到達するまで実行を再開します。

(lldb) continue
Process 87162 resuming

プログラム実行中に、control+c (^C) をタイプすることでいつでも実行を停止しデバッガのプロンプトに戻ることができます。

^C
Process 87162 stopped
[...]
(lldb)

デバッガを終了する場合には、quit コマンドを実行し終了確認をします。アプリケーションを終了しシェルプロンプトへ戻ります。

(lldb) quit
Quitting LLDB will detach from one or more processes. Do you really want to proceed: [Y/n] y
$