GDB dashboard で Raspberry Pi Pico の L チカをデバッグする (後編)
前回に引き続き、GDB と GDB dashboard を使って Pico の L チカプログラムをデバッグしていきます。最後におまけで、GDB に標準添付されている「TUI モード」も紹介します。今回は Pico に搭載されている RP2040 マイコンの内部にも言及するので、必要に応じて下記 URL のデータシートを参照してください。
[1] | RP2040 Datasheet https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf |
よく使う GDB コマンド
前回は GDB の基本的な使い方をやりました。ここではもう少し進んだ使い方を見ていきます。よく使いそうなコマンドを下の表にまとめました。
コマンド名 | 省略形 | 説明 | 備考 |
---|---|---|---|
load | ― | ELF ファイルをフラッシュメモリにロードする | ― |
continue | c | 実行開始または停止しているプログラムを再開する | ― |
Ctrl-c | ― | 強制停止 | ― |
break | b | ブレークポイントを設定 | (*1) |
enable | en | ブレークポイント、ウォッチポイントを有効化 | (*1) |
disable | dis | ブレークポイント、ウォッチポイントを無効化 | (*1) |
delete | d | ブレークポイント、ウォッチポイントを削除 | (*1) |
watch | wa | ウォッチポイントを設定 | (*2) |
rwatch | rw | ウォッチポイントを設定 | (*2) |
awatch | aw | ウォッチポイントを設定 | (*2) |
info breakpoints | i b | ブレークポイント,ウォッチポイントの一覧を表示 | ― |
backtrace | bt | ブレークした位置から関数の呼び出し元をたどる | ― |
dump memory | dump mem | メモリの中身をファイルにダンプする | (*3) |
next | n | 1 行実行 (ステップオーバー) | ― |
step | s | 1 行実行 (ステップイン) | ― |
finish | fin | 関数の末尾まで実行 | ― |
quit | q | GDB 終了 | ― |
(*1) ブレークポイントについて
ソースファイル名、関数名、行番号等を組み合わせてブレークポイントを置く位置を指定できます。if
文を使ってブレークが成立する条件を指定することもできます。次の例では上から順にdelay_ms
関数の先頭、main.c にあるdelay_ms
関数の先頭、main.c の 10 行目、main.c の 10 行目 (cnt
の値が 50 以外だった場合のみブレーク) にブレークを張ります。
(gdb) b delay_ms
(gdb) b main.c:delay_ms
(gdb) b main.c:10
(gdb) b main.c:10 if cnt != 50
info breakpoints
コマンドでブレークポイントの一覧を表示します。ここに表示されるブレークポイントの番号を指定して、そのブレークポイントを有効化・無効化・削除することもできます。
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x100001f0 in delay_ms at ...
2 breakpoint keep y 0x100001f0 in delay_ms at ...
3 breakpoint keep y 0x100001fe in delay_ms at ...
4 breakpoint keep y 0x100001fe in delay_ms at ...
stop only if cnt != 50
(gdb) dis 1
(gdb) d 2 3
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep n 0x100001f0 in delay_ms at ...
4 breakpoint keep y 0x100001fe in delay_ms at ...
stop only if cnt != 50
(*2) ウォッチポイントについて
指定した変数や番地を監視して、その場所への読み書きが発生した場合にブレークします。watch
は書き込み、rwatch
は読み出し、awatch
は読み書きどちらか、というように使い分けます。メモリ番地を指定するときは先頭に「*」をつけます。次の例では上から順に 0xD000001C 番地 (GPIO_OUT_XOR レジスタ) への書き込み、0xE000E010 番地 (SYST_CSR レジスタ) からの読み出し、0x40028000 番地 (PLL の CS レジスタ) への読み書きが発生した場合にブレークします。
(gdb) wa *0xd000001c
(gdb) rw *0xe000e010
(gdb) aw *0x40028000
例えば 1 番目の 0xD000001C 番地の例では、サンプルプログラムのmain
関数内で LED の表示反転を行うたびにout_w
関数を呼んでこのレジスタに書き込みを行います。よってout_w
関数の末尾 (stdlib.h 18 行目) で何度でもブレークします。その際、print
コマンドで引数adr
の中身を表示すると 0xD000001C になっているので、確かに SYST_CSR レジスタへの書き込みでブレークしたことがわかります。
(gdb) print/x adr
$1 = 0xd000001c
ウォッチポイントの一覧表示・有効化・無効化・削除は、ブレークポイントとまったく同じコマンドで実行することができます。
(*3) メモリダンプについて
メモリをダンプしてバイナリデータとしてファイルに保存します。次の例では 0x40008000 ~ 0x400080C7 番地の 200 バイト分を mem.bin に保存します。
(gdb) dump mem mem.bin 0x40008000 0x400080C8
実際にデバッグしてみる
それでは実際にデバッグしてみましょう。この L チカプログラムは Cortex-M0+ の SysTick タイマを使って 500 ミリ秒ごとに LED の出力を反転させています。具体的には、SYST_RVR というレジスタに 1249999 という値1) をセットしておくと、10 ミリ秒ごとに SYST_CSR レジスタの COUNTFLAG ビットが立つので、それを 50 回カウントしてから LED の出力を反転させる、ということをやっています。
- RP2040 のシステムクロック周波数は 125 MHz なので、10 ミリ秒に相当するクロックパルス数は (125e6 / 1e3) × 10 = 1250000 パルスとなります。ここから 1 を引いた値を SYST_RVR レジスタにセットします。
そこでデバッグの一例として、SYST_RVR レジスタに正しく 1249999 がセットされていることを確認してみましょう。何かのまちがいで「1197492064」のようなとんでもない値がセットされていると、点滅しているんだかいないんだかわからないくらいスローな L チカになってしまいます (別のマイコンボードでだけど実際にやらかした)。まず、すでに説明した手順で GDB を起動し、必要に応じてプログラムをロードします。
% cd ~/pico/repo/interface_trykernel/build_part2/sect_3/
% pgdb build/blink.elf
(gdb) load
プログラムがスタートして最初にやってくるのが reset_hdr.c にあるReset_Handler()
です。ここにブレークを張ってプログラムをスタート (continue
) してみます。
(gdb) b Reset_Handler
(gdb) c
ブレークポイントが正しく設置されていれば、図 1 のように reset_hdr.c の 192 行目、Reset_Handler()
の先頭でブレークします。このように、ブレークするたびに GDB dashboard がプログラムの内部状態を表示してくれます。何かの拍子に表示されなかったとしても dashboard
コマンドを引数なしで実行すれば表示されます。
ここで、SYST_RVR レジスタの中身を GDB dashboard で確認できるように設定します。Reset_Handler()
の先頭でブレークしている今の状態からプログラムを再開すると、init_systim
関数が呼ばれ、その中で SYST_RVR レジスタにクロックカウンタのリロード値をセットしています。その直前の位置にブレークポイントを置きましょう。reset_hdr.c の 181 行目です。
(gdb) b reset_hdr.c:181
次に、SYST_RVR レジスタの中身を表示させるようにします。それには dashboard memory watch
コマンドを使います。このレジスタの番地とサイズを RP2040 のデータシートで確認すると、それぞれ 0xE000E014 番地、4 バイトであることがわかります ([1] 2.4.8 節)。そこで、
(gdb) dashboard memory watch 0xe000e014 4
としてもいいですし、あるいは SysTick 関係のレジスタをまとめて
(gdb) dashboard memory watch 0xe000e010 16
としてもおもしろいです。ここは後者でいきましょう。continue
コマンドでプログラムを再開すると、reset_hdr.c の 181 行目、今まさに SYST_RVR に値を書き込もうとする直前でブレークします (図 2)。
next
コマンドで 1 行進めて、SYST_RVR レジスタに値を書き込みます (図 3)。Memory モジュールの表示を見ると、0xE000E014 番地からの 4 バイトが「cf 12 13 00」になっています。バイトオーダーがリトルエンディアンであることに注意して、数値としては 0x001312CF、10 進数になおすと 1249999 であり、無事に意図したとおりの値が書き込まれていることがわかりました。
問題なさそうなので、実際に 1 秒周期で点滅してもらいましょう。ここのブレークポイントを削除するか無効にして、プログラムを再開します。
(gdb) dis 2
(gdb) c
プログラムを止めるときは Ctrl-c キーです。
GDB コマンドをスクリプトに書く
ところで、毎回上のようにブレークポイントを設置したりメモリの番地を指示したりするのは面倒くさいです。GDB ではそのようなコマンドをスクリプトに書いておいて一括して実行したり、いくつかのコマンドをまとめてひとつの新しいコマンドとして定義したりすることができます。
スクリプトを読み込む方法はいくつかあるのですが、内容からしてプロジェクト固有の記述になるので、前回紹介した-x
オプションをつけて GDB を起動する方法が良いと思います。というわけで前回作った .gdbinit.pico に次のような感じで追記します。
target remote localhost:3333
monitor reset init
break reset_hdr.c:181
dashboard memory watch 0xE000E010 0x10
dashboard memory watch 0xD0000000 0x30
define start
break Reset_Handler
continue
end
1,2 行目: | 初期設定 (前回から変更なし) |
4 行目: | reset_hdr.c の 181 行目にブレークポイントを設定する。 |
5 行目: | メモリの 0xE000E010 番地から 16 バイト を Memory モジュールに表示する。 |
6 行目: | 必要に応じて他の番地も追加する。 |
8~11 行目: | 新しいコマンドを定義してstart と名づける。GDB を起動してstart コマンドを実行すると、Reset_Handler() の先頭にブレークを張ったうえでプログラムを開始する (その結果Reset_Handler() の先頭でブレークして待機状態になる)。 |
前回と同様、GDB の起動時に-x
オプションでこのファイルを読み込ませます。GDB が起動してプロンプトが表示される頃には、ブレークポイントと Memory モジュールの表示設定は済んでいる状態です。必要に応じてload
を行ったのち、新しく定義したstart
コマンドを実行すると、Reset_Handler()
の先頭でブレークして待機します。これであとはデバッグを開始するばかり、という状態です。
% cd ~/pico/repo/interface_trykernel/build_part2/sect_3/
% arm-none-eabi-gdb -x .gdbinit.pico build/blink.elf
(gdb) load
(gdb) start
(gdb)
おまけ: GDB の TUI モード
「わざわざ GDB dashboard をダウンロードしてきて設定やらなにやらは面倒くさい」というときは、GDB dashboard ほど便利ではないにしろ、GDB が標準で具備する TUI モードという選択肢もあります (図 4) 。layout
コマンドで表示を切り替えなければならない点が若干不便ですが、ブレークするたびにレジスタや変数の内容を勝手に表示してくれます。レスポンスは GDB dashboard よりも速い気がします (個人の感想です)。
TUI モードで起動するにはarm-none-eabi-gdb
コマンドに-tui
オプションをつけます。
% cd ~/pico/repo/interface_trykernel/build_part2/sect_3/
% arm-none-eabi-gdb -tui build/blink.elf
TUI モードでは、上で紹介したコマンドに加えて、次のコマンドが有用です。
コマンド名 | 省略形 | 説明 | 備考 |
---|---|---|---|
layout next | la n | 表示切り替え (ソース/アセンブリ/レジスタ) | ― |
display | disp | ブレークするたびに変数・メモリ等の値を表示 | (*4) |
(*4) display
コマンドについて
指定した変数やメモリの値をブレークするたびに表示してくれます。メモリ番地を指定するときは先頭に「*」をつけます。また、オプションに/d
をつけると 10 進数、/x
をつけると 16 進数で表示します。
(gdb) disp cnt
(gdb) disp /x *0xe000e014
(gdb) next
1: cnt = 50
2: /x *0xe000e014 = 0x1312cf
外観の設定は ~/.gdbinit に書きます。例えば図 4 でソースコードを取り囲んでいる太い枠線の色は、次のようにして変えることができます。
set style tui-active-border foreground color
color
の部分にはnone
、black
、red
、green
、yellow
、blue
、magenta
、cyan
、white
のいずれかが入ります。これらが実際にどのような色になるかは、使用する端末エミュレータのカラースキームに依存します。その他のカスタマイズ可能な項目についてはhelp set style
やhelp set style tui-active-border
などのコマンドで調べてください。ちなみに図 4 は次のようにスタイルを何も適用していない素の状態です。
set style enabled off