デバッグする
どのような仕組みなのか?
小さなプログラムをデバッグする前に、少し寄り道をして何が起こるのか簡単に理解しましょう。 前章でボード上の2つ目のチップの役割とどうやって私たちのコンピュータとやり取りするか、を説明しましたが果たしてどうやって使うのでしょう?
Embed.toml
にdefault.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のブレイクコマンドは関数名だけでなく、特定の行番号でも効果を発揮します。 もし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ソースコード画面にいつでも戻ってくることができます。
注意: 間違って
next
やcontinue
コマンドを使ってしまいGDBが操作できなくなった場合は、Ctrl+C
で再びGDBを操作できます。
(gdb) layout asm
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です。