# コンパイラ実験に便利なgdb (cgdb)の使い方 課題3,4のようにある程度コードのサイズが大きくなると,目視でコードを追ったり,`printf()`を挿入するいわゆる「printfデバッグ」ではあまりにもデバッグの効率が悪い。そこで**デバッガ**というツールを用いてデバッグするのが普通である。 デバッガを用いると一時停止しながら少しずつ実行したり,途中で変数/式の現在値を表示したり,変数の値を変更したりできる。関数の呼び出しをトレースしたり,変数の値が変化したときに実行を一時停止するよう設定したりもできる。 この資料では代表的なデバッガである`gdb`(GNU debugger)とそれを使いやすくするインターフェース`cgdb`を使ってデバッグする基本を解説している。 :::danger :expressionless: 旧式のWSL1ではgdb (cgdb)を用いたデバッグはできない。WSL2を用いること。 >https://github.com/microsoft/WSL/issues/8356 ::: ## cgdbの起動 ``` cgdb ./picoc ``` のように起動する(入力ファイルは後で`run`コマンドで与える)。 ## 上下のペイン(枠)の間の移動 起動直後は下のペイン(gdb)にフォーカスしている。 エスケープキー(`ESC`)を押すと上のペイン(ソースコードを表示)にフォーカスする。vimのように`j`,`k`キー(あるいは`↓`,`↑`キー)で上下にスクロールする。 `i`キーを押すと下のペイン(gdb)にフォーカスする。入力したコマンド行の履歴は`ctrl-p`, `ctrl-n`(あるいは`↓`,`↑`キー)で履歴をたどれる。またタブキーで入力補完が効く。 ## 基本のステップ実行 ``` (gdb) break main (または b main) ``` でmain関数の最初で一時停止するように設定した後、 ``` (gdb) run <example/fib.pc (または r <example/fib.pc) ``` で`example/fib.pc`を標準入力にセットして実行を開始。 >ここで >Enable debuginfod for this session? (y or [n]) >のようにきかれたときは単にエンターキーを押してデフォルトのnを選べばよい。 ``` (gdb) next (または n) ``` で一行ずつ止めながら実行できる。関数呼び出しの行で`next`を実行しても関数の中には入らない。関数の中に入るには、代わりに ``` (gdb) step (または s) ``` を使う。うっかり間違って関数の中に入った(例えば`printf()`の中に踏み込んだ)ときは ``` (gdb) finish (または fin) ``` で一気にその関数から抜けられる。 `main()`関数の最初から再度実行しなおしたいときには単に ``` (dgb) run (または r) ``` とすればよい。cgdbを終了するには ``` (gdb) quit (または q) ``` とする。 ## 式の値の表示 ``` print 式 (または p 式) ``` で式の値を表示できる。以下は変数`token`の値を表示する例: ``` (gdb) p token $5 = 59 ``` 59は`;`の文字コードである。文字としてわかりやすく表示したいときは ``` (gdb) p/c token $6 = 59 ';' ``` のように書式`/c`を指定する。 文字列を表示するには ``` (gdb) p/s name $8 = ";", '\000' <repeats 30 times> ``` のように書式`/s`を指定する。文字列を表示するときには、メモリの内容を表示する`x`コマンドに`/s`を付けて ``` (gdb) x/s name 0x55555555b0a0 <name>: ";" ``` のほうが見やすいかもしれない。あるいは`printf`コマンドを使うとC言語と同様の書式が使える。 ``` (gdb) printf "%s\n", name ; ``` 表示できる変数の値は、グローバル変数と、(当然ながら)現在実行中の関数ローカル変数だけである(後述のバックトレースで、呼び出し元の関数にさかのぼってみることも可能)。現在有効なローカル変数の一覧は ``` (gdb) info locals (または i lo) ``` で分かる。 `print`コマンドには代入式も指定できるので、変数の値を変更することもできる。以下の例では変数`token`の値を`(`に変更している。 ``` (gdb) p token = '(' ``` ## ブレークポイント(一時停止行)の設定 ``` (gdb) break expr (またはb expr) ``` で関数`expr()`の最初の行にブレークポイントをセットできる。 ``` (gdb) continue (または c) ``` で実行を再開すると、ブレークポイントの設定されている行までいっきに実行されて一時停止する。 `expr()`の最初にセットしたブレークポイントを解除するには ``` (gdb) clear expr ``` とする。引数なしで ``` (gdb) clear ``` とするとすべてのブレークポイントが解除される。現在設定されているブレークポイントの一覧を確認するには ``` (gdb) info break (または i b) ``` とする。 ``` (gdb) break 152 ``` のように行番号を指定してpicoc.c 152行目にブレークポイントをセットすることもできる(ブレークポイントがセットできない行を指定したときはその直後の行にセットされる)。 行番号を指定してブレークポイントをセットしたいときは,cgdbの上側のペインを用いるほうが簡単。cgdbの上のペインにフォーカスし、152行目にカーソルがある状態でスペースキーを押すと152行目にブレークポイントがセットされる。逆に,ブレークポイントがセットされた行でスペースキーをおすと、ブレークポイントが解除される。 停止と同時に自動的に設定解除される一時的なブレークポイントを設定するには`break`のかわりに`tbreak`を用いる。 ``` (gdb) tbreak 152 (またはtb 152) ``` `break/tbreak`コマンドには停止条件を`if 条件`で指定したりもできる。以下の例では`token`の値が'('の時に限り`expr()`関数の最初の行で停止するようになる。 ``` (gdb) break expr if token == '(' ``` ## 字句の変化を監視する(ウォッチポイントの利用) 変数の値が変化したタイミングで一時停止するウォッチポイントという機能もある。例えば、 ``` (gdb) watch token (gdb) display/c token (gdb) display/s name ``` と設定した上で ``` (gdb) continue (または c) ``` で実行を再開すると次の字句を読みこむ毎に一時停止し、字句の文字列を表示(下記の例)する。 `continue`を実行する毎に次の字句を読み込んだとこまで進み一時停止する。 例: ``` (gdb) continue Continuing. Hardware watchpoint 1: token Old value = 129 New value = 59 expect (expected=129) at picoc.c:40 40 return; 1: /c token = 59 ';' 2: x/s name 0xaaaaaaac0050 <name>: ";" ``` ウォッチポイントは`clear`コマンドでは削除できない. ``` (gdb) info watch (またはi wat) ``` で表示される番号を指定して`delete`コマンドで ``` (gdb) delete 1 ``` のようにして解除する。 ## 関数呼び出しの履歴(コールスタック)を表示・移動する ``` (gdb) backtrace (または bt) ``` で関数呼び出しの履歴(コールスタック)を表示できる。下の例では ``` main() -> program() -> block() -> statement() -> assign_st() ``` の順に呼び出されて、今`assign_st()`関数の中を実行中であることが分かる。 例: ``` (gdb) backtrace #0 assign_st () at picoc.c:144 #1 0x00005555555582e5 in statement () at picoc.c:218 #2 0x0000555555558289 in block () at picoc.c:203 #3 0x00005555555583ec in program () at picoc.c:249 #4 0x000055555555843b in main () at picoc.c:260 (gdb) ``` セグメント違反等で異常終了したときにはオーバーランしてしまいライブラリの関数の中に入ってから止まる場合がある。このようなときには、最後に呼び出された自作の関数まで戻るために`up`コマンドを用いる。上の例で ``` (gdb) up ``` を実行すると、`assign_st()`のひとつ前の`statement()`までさかのぼる。`up`の逆は`down`である。