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

デバッグしてみましょう

すでにデバッグセッションに入っているので、プログラムをデバッグしていきましょう。

load コマンドの後、プログラムはその entry point で停止しています。これは GDB の出力にある Start address 0x8000XXX の部分が示しています。エントリポイントとは、プロセッサ / CPU が 最初に実行するプログラムの部分です。

こちらで用意したスタータープロジェクトには、main 関数の に実行される追加コードが含まれています。 今はその「pre-main」部分には関心がないので、main 関数の先頭まで一気に進みましょう。 そのためにブレークポイントを使います。(gdb) プロンプトで break main を実行してください。

NOTE これらの GDB コマンドについては、通常はコピー可能なコードブロックを示しません。 どれも短く、自分で入力したほうが速いからです。さらに、その多くは短縮できます。 たとえば breakbsteps のように省略できます。詳しくは GDB Quick Reference を参照するか、そのほかのコマンドは Google で調べてください。加えて、タブ補完も使えます。 最初の数文字を入力してから Tab を 1 回押すと補完され、Tab を 2 回押すと 利用可能なコマンドをすべて表示できます。

最後に、xxxx の部分にコマンド名を入れた help xxxx を使うと、短縮名やそのほかの情報を確認できます:

(gdb) help s
step, s
Step program until it reaches a different source line.
Usage: step [N]
Argument N means step N times (or till program stops for another reason).
(gdb) break main
Breakpoint 1 at 0x80001f0: file src/05-led-roulette/src/main.rs, line 7.
Note: automatically using hardware breakpoints for read-only addresses.

次に continue コマンドを実行します:

(gdb) continue
Continuing.

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

ブレークポイントは、プログラムの通常の流れを止めるために使えます。continue コマンドを使うと、 プログラムはブレークポイントに到達する まで 自由に実行されます。この場合は #[entry] に到達するまでです。ここは main 関数へのトランポリンであり、break main によって ブレークポイントが設定される場所です。

Note GDB の出力に “Breakpoint 1” と表示されていることに注目してください。私たちの プロセッサではこの種のブレークポイントを 6 個しか使えないので、こうしたメッセージに 注意を払うのは良い考えです。

OK。#[entry] で停止しているので、disassemble /m を使うと entry のコードが見えます。 これは main へのトランポリンです。つまり、スタックをセットアップしてから、 ARM の branch and link 命令 bl を使って main 関数をサブルーチン呼び出ししている ということです。

(gdb) disassemble /m
Dump of assembler code for function main:
7       #[entry]
   0x080001ec <+0>:     push    {r7, lr}
   0x080001ee <+2>:     mov     r7, sp
=> 0x080001f0 <+4>:     bl      0x80001f6 <_ZN12led_roulette18__cortex_m_rt_main17he61ef18c060014a5E>
   0x080001f4 <+8>:     udf     #254    ; 0xfe

End of assembler dump.

次に、step という GDB コマンドを実行する必要があります。これは関数やプロシージャの中に入りながら、 プログラムを文ごとに進めるコマンドです。したがって、この最初の step コマンドの後には main の中に入り、最初の実行可能な rust 文である 10 行目の位置にいますが、 まだ 実行はされていません:

(gdb) step
led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:10
10          let x = 42;

次に 2 回目の step を実行すると、10 行目が実行され、11 _y = x; の行で止まります。 ここでも 11 行目は まだ実行されていません

NOTE 2 回目の (gdb) プロンプトでは Enter を押せば、 直前のコマンド step を再実行できましたが、わかりやすさのために このチュートリアルでは基本的にコマンドを毎回入力し直します。

(gdb) step
11          _y = x;

このように、このモードでは step コマンドを実行するたびに、GDB は現在の文を 行番号付きで表示します。後で見る TUI モードでは、コマンド領域にこの文は表示されません。

今は _y = x 文の「上」にいます。この文はまだ実行されていません。つまり、x は 初期化されていますが、_y はまだです。print コマンド(短縮形は p)を使って、 これらのスタック / ローカル変数を確認してみましょう。

(gdb) print x
$1 = 42
(gdb) p &x
$2 = (*mut i32) 0x20009fe0
(gdb) p _y
$3 = 536870912
(gdb) p &_y
$4 = (*mut i32) 0x20009fe4

予想どおり、x には値 42 が入っています。一方で _y には 536870912 (?) という値が 入っています。これは _y がまだ初期化されておらず、ゴミ値が入っているためです。

print &x コマンドは変数 x のアドレスを表示します。ここで興味深いのは、GDB の出力に 参照の型が *mut i32、つまり i32 値への mutable pointer として表示されている点です。 もう 1 つ興味深いのは、x_y のアドレスが互いに非常に近いことです。両者のアドレスは わずか 4 バイトしか離れていません。

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

(gdb) info locals
x = 42
_y = 536870912

OK。さらに step を 1 回実行すると、loop {} 文の位置に来ます。

(gdb) step
14          loop {}

そして、この時点で _y は初期化されているはずです。

(gdb) print _y
$5 = 42

loop {} 文の上で再び step を使うと、プログラムはその文を決して通過しないため、 そこで固まってしまいます。

NOTE 間違って step やほかのコマンドを使って GDB が固まってしまった場合は、 Ctrl+C を押せば復帰できます。

前に紹介したように、disassemble /m コマンドを使うと、現在いる行の周辺のプログラムを 逆アセンブルできます。さらに set print asm-demangle on を設定して、 名前をデマングル表示するようにしておくとよいでしょう。これはデバッグセッションごとに 1 回だけ行えば十分です。後で、これやほかのコマンドは初期化ファイルに入れることになり、 デバッグセッションの開始が簡単になります。

(gdb) set print asm-demangle on
(gdb) disassemble /m
Dump of assembler code for function _ZN12led_roulette18__cortex_m_rt_main17h51e7c3daad2af251E:
8       fn main() -> ! {
   0x080001f6 <+0>:     sub     sp, #8
   0x080001f8 <+2>:     movs    r0, #42 ; 0x2a

9           let _y;
10          let x = 42;
   0x080001fa <+4>:     str     r0, [sp, #0]

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

12
13          // 無限ループ。このスタックフレームを抜けないようにするため
14          loop {}
=> 0x080001fe <+8>:     b.n     0x8000200 <led_roulette::__cortex_m_rt_main+10>
   0x08000200 <+10>:    b.n     0x8000200 <led_roulette::__cortex_m_rt_main+10>

End of assembler dump.

左側の太い矢印 => が見えますか? これは、次にプロセッサが実行する命令を示しています。

また、先ほど触れたように step コマンドを実行すると、GDB は自分自身への分岐命令を 実行してその先に進めなくなるため、固まってしまいます。そのため、制御を取り戻すには Ctrl+C を使う必要があります。代わりに stepisi)という GDB コマンドを使う方法もあります。 これは asm 命令を 1 つだけ進めるコマンドで、GDB は次にプロセッサが実行する文のアドレス 行番号を表示し、しかも固まりません。

(gdb) stepi
0x08000194      14          loop {}

(gdb) si
0x08000194      14          loop {}

もう少し面白い内容に進む前に、最後の小技を 1 つ紹介します。以下のコマンドを GDB に入力してください:

(gdb) monitor reset halt
Unable to match requested speed 1000 kHz, using 950 kHz
Unable to match requested speed 1000 kHz, using 950 kHz
adapter speed: 950 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000194 msp: 0x2000a000

(gdb) continue
Continuing.

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

(gdb) disassemble /m
Dump of assembler code for function main:
7       #[entry]
   0x080001ec <+0>:     push    {r7, lr}
   0x080001ee <+2>:     mov     r7, sp
=> 0x080001f0 <+4>:     bl      0x80001f6 <led_roulette::__cortex_m_rt_main>
   0x080001f4 <+8>:     udf     #254    ; 0xfe

End of assembler dump.

これで #[entry] の先頭に戻ってきました!

monitor reset halt はマイクロコントローラーをリセットし、プログラムのまさに先頭で停止させます。 その後 continue コマンドを使うと、ブレークポイントに到達するまでプログラムを自由に実行できます。この場合は #[entry] にあるブレークポイントです。

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

細かい注意点: この 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.

より快適にデバッグしたい場合は、GDB の Text User Interface(TUI)を使えます。このモードに入るには、GDB シェルで次のいずれかのコマンドを入力してください。

(gdb) layout src
(gdb) layout asm
(gdb) layout split

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

以下は、layout split を使うための準備の例です。以下のコマンドを実行します。 ご覧のとおり、--target パラメータを渡すのはやめています。

$ cargo run
(gdb) target remote :3333
(gdb) load
(gdb) set print asm-demangle on
(gdb) set style sources off
(gdb) break main
(gdb) continue

以下は、上記のコマンドを -ex パラメータとしてまとめたコマンドラインで、入力の手間を少し省けます。 まもなく、初期コマンド群をもっと簡単に実行する方法を提供します。

cargo run -- -q -ex 'target remote :3333' -ex 'load' -ex 'set print asm-demangle on' -ex 'set style sources off' -ex 'b main' -ex 'c' target/thumbv7em-none-eabihf/debug/led-roulette

そして以下がその結果です。

GDB セッション layout split

次に、上側のソースウィンドウを下へスクロールしてファイル全体が見えるようにし、layout split を実行してから step を実行します。

GDB セッション layout split

その後、info localsstep を数回実行します。

(gdb) info locals
(gdb) step
(gdb) info locals
(gdb) step
(gdb) info locals

GDB セッション layout split

どの時点でも、次のコマンドで TUI モードを終了できます。

(gdb) tui disable

GDB セッション layout split

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

ただし、OpenOCD は閉じないでください! これから先も何度も使います。 そのまま動かし続けておくほうがよいです。GDB でできることについてもっと知りたい場合は、GDB の使い方 のセクションを参照してください。

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