Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

デバッグしてみよう

いったいこれはどう動いているのでしょう?

この小さなプログラムをデバッグする前に、ここで実際に何が 起きているのかを手短に理解しておきましょう。前の章では、ボード上の 2 つ目のチップの役割と、 それがどのようにコンピューターと通信するのかについてはすでに説明しましたが、では実際にそれをどう使えばよいのでしょうか?

Embed.toml にある小さなオプション default.gdb.enabled = true によって、cargo-embed は書き込み後に いわゆる「GDB stub」を開きます。これは GDB が接続できるサーバーで、そこに 「アドレス X にブレークポイントを設定する」といったコマンドを送れます。このサーバーは、そのコマンドをどう処理するかを 自分で判断できます。cargo-embed の GDB stub の場合は、このコマンドを USB 経由でボード上のデバッグプローブへ転送し、その後はそのプローブが実際に MCU と通信する役割を担ってくれます。

デバッグしてみましょう!

cargo-embed は現在のシェルを占有しているので、新しいシェルを開いて プロジェクトディレクトリに戻れば大丈夫です。そこに移動したら、まずは次のようにして gdb でバイナリを開く必要があります。

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

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

NOTE: インストールした GDB によって、起動に使うコマンドが異なります。 どれだったか忘れた場合は、chapter 3 を確認してください。

NOTE: target/thumbv7em-none-eabihf/debug/led-roulette: No such file or directory というエラーが出る場合は、ファイルパスの先頭に ../../ を付けてみてください。たとえば次のようになります。

$ gdb ../../target/thumbv7em-none-eabihf/debug/led-roulette

これは、各サンプルプロジェクトが書籍全体を含む workspace の中にあり、workspace では target ディレクトリが 1 つだけ共有されるためです。詳しくは Workspaces chapter in Rust Book を参照してください。

NOTE: ここで cargo-embed が大量の警告を出しても心配しないでください。現時点では GDB プロトコルを完全には実装していないため、GDB から送られてくるすべてのコマンドを認識できないことがあります。 クラッシュしない限りは問題ありません。

次に、GDB stub に接続する必要があります。これはデフォルトで 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 関数まで進むことです。 そのためには、まずそこにブレークポイントを設定し、その後 ブレークポイントに到達するまでプログラムの実行を続けます。

(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 <breakpoint-num> で不要なものを削除できます。

より快適にデバッグするために、ここでは GDB のテキストユーザーインターフェイス(TUI)を使います。 このモードに入るには、GDB シェルで次のコマンドを入力します。

(gdb) layout src

NOTE: Windows ユーザーのみなさんには申し訳ありません。GNU ARM Embedded Toolchain に 同梱されている GDB は、この TUI モードをサポートしていません :-(

GDB セッション

GDB の break コマンドは、関数名に対してだけでなく特定の行番号でも使えます。 たとえば 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 を実行すると、その文をプログラムが 決して通過しないため、そこで止まってしまいます。そこで代わりに layout asm コマンドで逆アセンブル表示に切り替え、stepi を使って 1 命令ずつ進めます。あとで 언제でも layout src コマンドを再度実行すれば、Rust のソースコード表示に戻せます。

NOTE: もし誤って nextcontinue コマンドを使って GDB が止まってしまった場合は、Ctrl+C を押せば抜け出せます。

(gdb) layout asm

GDB セッション

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          // 無限ループ。これはこのスタックフレームを抜けないようにするためです
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 {}

もう少し面白いものに進む前に、最後にもう 1 つ便利なテクニックを紹介します。次のコマンドを 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 関数に到達するまで、プログラムを自由に実行できます。

この組み合わせは、誤って詳しく調べたかったプログラムの一部を 通り過ぎてしまったときに便利です。プログラムの状態を簡単に いちばん最初の時点まで巻き戻せます。

細かい注意点: この reset コマンドは RAM を消去したり変更したりしません。そのメモリには 前回の実行時の値がそのまま残ります。ただし、プログラムの挙動が 未初期化 変数の値に依存していない限り、これは問題にならないはずです。もっとも、それは 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]

NOTE: デフォルトの GDB CLI が好みに合わない場合は、gdb-dashboard を試してみてください。これは Python を使って、 デフォルトの GDB CLI を、レジスタ、ソースビュー、アセンブリビュー などを表示するダッシュボードに変えます。

GDB でできることをもっと知りたい場合は、GDB の使い方 のセクションを参照してください。

次は何かって? 約束していた高レベル API です。