デバッグする

どのような仕組みなのか?

小さなプログラムをデバッグする前に、少し寄り道をして何が起こるのか簡単に理解しましょう。 前章でボード上の2つ目のチップの役割とどうやって私たちのコンピュータとやり取りするか、を説明しましたが果たしてどうやって使うのでしょう?

Embed.tomldefault.gb.enabled = trueのオプションを追加すると、フラッシュへの書き込み後、cargo-embedは「GDBスタブ」と呼ばれるものを立ち上げます。 GDBスタブはGDBが接続できるサーバーで、「ブレイクポイントをX番地に設定する」といったコマンドをサーバーに送信します。 その後、サーバーはこのコマンドをどう扱うか決めます。 cargo-embedの場合、GDBスタブはコマンドをUSB経由でボード上のデバッグプローブに転送し、デバッグプローブが実際にMCUとやり取りします。

デバッグしてみよう!

cargo-embedが今使っているシェルをブロックしているので、新しいシェルを立ち上げてプロジェクトディレクトリに移動し直します。 まず最初に、プロジェクトディレクトリに居る状態で、次のようにgdbでバイナリを読み込まなければなりません。

# For micro:bit v2
$ gdb target/thumbv7em-none-eabihf/debug/led-roulette

# For micro:bit v1
$ gdb target/thumbv6m-none-eabi/debug/led-roulette

注意 どのGDBをインストールしたかによって、GDB起動のコマンドが違います。 どのGDBをインストールしたか忘れた場合は、第3章を確認してください。

注意 もしcargo-embedがたくさん警告を出力しても気にしないでください。 cargo-embedはGDBプロトコルを全て実装しているわけではないので、GDBから送信したコマンドが認識できないことがあります。 cargo-embedが異常終了しない限り、問題ありません。

次にGDBスタブに接続しなければなりません。 GDBスタブはデフォルトではlocalhost:1337で動いており、これに接続するには次のコマンドを実行します。

(gdb) target remote :1337
Remote debugging using :1337
0x00000116 in nrf52833_pac::{{impl}}::fmt (self=0xd472e165, f=0x3c195ff7) at /home/nix/.cargo/registry/src/github.com-1ecc6299db9ec823/nrf52833-pac-0.9.0/src/lib.rs:157
157     #[derive(Copy, Clone, Debug)]

続いて、プログラムのmain関数に行きたいです。 そのためにまずブレイクポイントをmain関数に設定して、ブレイクポイントに到達するまでプログラムの実行を続けます。

(gdb) break main
Breakpoint 1 at 0x104: file src/05-led-roulette/src/main.rs, line 9.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline () at src/05-led-roulette/src/main.rs:9
9       #[entry]

ブレイクポイントはプログラムのフローを止めるために使えます。 continueコマンドはブレイクポイントに到達するまで、プログラムを自由に実行します。 この場合、main関数にブレイクポイントがあるので、そこに到達するまでプログラムを実行します。

GDBが「Breakpoint 1」と出力していることに注意してください。 今使っているプロセッサでは限られた数のブレイクポイントしか使えないことを覚えておいてください。 先程のようなメッセージに注目を払うのは良いアイデアです。 もしブレイクポイントを使い果たしてしまったら、info breakで現在設定しているブレイクポイントの一覧が見れます。 そしてdelete <ブレイクポイント番号>でお望みのブレイクポイントを削除します。

より良いデバッグを体験するために、GDBのテキストユーザーインターフェース(TUI)を使ってみましょう。 TUIモードに切り替えるために、GDBシェルに次のコマンドを入力します。

(gdb) layout src

注意 Windowsユーザーのみなさんごめんなさい。GNU ARM Embeddedツールチェインで配布されているGDBではTUIモードがサポートされていません :-(

GDBセッション

GDBのブレイクコマンドは関数名だけでなく、特定の行番号でも効果を発揮します。 もし13行目でブレイクしたければ、単に次のようにします。

(gdb) break 13
Breakpoint 2 at 0x110: file src/05-led-roulette/src/main.rs, line 13.
(gdb) continue
Continuing.

Breakpoint 2, led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:13
(gdb)

次のコマンドでTUIモードをいつでも終了できます。

(gdb) tui disable

現在_y = xの文の「上」にいます。 この文はまだ実行されていません。 そのため、xは初期化されていますが、_yは初期化されていません。 printコマンドを使って、これらのスタック/ローカル変数を調べてみましょう。

(gdb) print x
$1 = 42
(gdb) print &x
$2 = (*mut i32) 0x20003fe8
(gdb)

期待通り、xは値42を格納しています。 print &xコマンドは変数xのアドレスを表示します。 ここでちょっとおもしろいところは、GDBがi32*という型を表示している点です。 これはi32のポインタ型です。

プログラムの実行を1行ずつ続けたい場合は、nextコマンドを使います。 それでは、loop {}文まで進んでみましょう。

(gdb) next
16          loop {}

すると、_yが初期化されています。

(gdb) print _y
$5 = 42

1つずつローカル変数を表示する代わりに、info localsコマンドを使うことができます。

(gdb) info locals
x = 42
_y = 42
(gdb)

loop {}文でnextを再び実行すると、そこから操作できなくなります。 これはloop {}文を抜けることがないためです。 代わりに、layout asmコマンドでディスアンセンブル画面に切り替えて、stepiコマンドを使って1命令ずつ実行を進めます。 layout srcコマンドで、Rustソースコード画面にいつでも戻ってくることができます。

注意: 間違ってnextcontinueコマンドを使ってしまいGDBが操作できなくなった場合は、Ctrl+Cで再びGDBを操作できます。

(gdb) layout asm

GDB session

TUIモードを使わない場合、disassemble /mコマンドで現在実行している行付近のプログラムをディスアセンブルできます。

(gdb) disassemble /m
Dump of assembler code for function _ZN12led_roulette18__cortex_m_rt_main17h3e25e3afbec4e196E:
10      fn main() -> ! {
   0x0000010a <+0>:     sub     sp, #8
   0x0000010c <+2>:     movs    r0, #42 ; 0x2a

11          let _y;
12          let x = 42;
   0x0000010e <+4>:     str     r0, [sp, #0]

13          _y = x;
   0x00000110 <+6>:     str     r0, [sp, #4]

14
15          // infinite loop; just so we don't leave this stack frame
16          loop {}
=> 0x00000112 <+8>:     b.n     0x114 <_ZN12led_roulette18__cortex_m_rt_main17h3e25e3afbec4e196E+10>
   0x00000114 <+10>:    b.n     0x114 <_ZN12led_roulette18__cortex_m_rt_main17h3e25e3afbec4e196E+10>

End of assembler dump.

左側に太矢印の=>があるところを見てください。 これはプロセッサが次に実行する命令です。

TUIモードじゃない場合、stepiコマンドを実行するたびに、GDBはプロセッサが次に実行する命令の文と行番号を表示します。

(gdb) stepi
16          loop {}
(gdb) stepi
16          loop {}

より楽しい内容に行く前に、最後のトリックを紹介します。 次のコマンドをGDBに入力してください。

(gdb) monitor reset
(gdb) c
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline () at src/05-led-roulette/src/main.rs:9
9       #[entry]
(gdb)

mainの最初に戻っています!

monitor resetはマイクロコントローラをリセットし、プログラムのエントリーポイントで停止します。 続くcontinueコマンドでプログラムをmainに到達するまで実行し、ブレイクポイントのあるmainで止まります。

このコンボは、プログラムの調査したいところを間違ってスキップしてしまったときに便利です。 簡単にプログラムを最初の状態に戻すことが出来ます。

補足: resetコマンドはRAMをクリアしません。 RAMはresetコマンド実行前と同じ値を保持しています。 プログラムが初期化されていない変数の値に依存していない限り、問題にはなりません。 ただし、そのような状態は未定義動作(Undefined Behavior: UB)と定義付けられています。

デバッグセッションを完了しました。 quitコマンドでデバッグセッションを終了できます。

(gdb) quit
A debugging session is active.

        Inferior 1 [Remote target] will be detached.

Quit anyway? (y or n) y
Detaching from program: $PWD/target/thumbv7em-none-eabihf/debug/led-roulette, Remote target
Ending remote debugging.
[Inferior 1 (Remote target) detached]

ノート デフォルトのGDB CLIがお気に召さない場合は、gdb-dashboardをチェックしてみて下さい。 これはPythonを使って、デフォルトのGDB CLIをレジスタやソースコード表示画面、アセンブリ表示画面などをダッシュボード化してくれます。

GDBでできることについてさらに知りたい場合、GDBの使い方を参照して下さい。

さて、お次は? お約束した高レベルのAPIです。