JOS から学ぶ GDB デバッグ手法
JOS というのは MIT 6.828 の授業で使用される OS 教材です。 MIT 6.828 といえば Sixth Edition Unix (V6) を x86 に移植した xv6 という OS が有名だと思いますが、JOS はそれを元に授業の課題で使用できるように修正を加えたものという感じです。
授業では OS の動作確認を QEMU 上で、デバッグを GDB で行うことにしているのですが、そのために必要な設定やコマンドに自分があまり馴染みがなかったのでまとめてみることにしました。JOS に限らず今後自分で OS を作ってみるときにも役に立ちそうな内容だと思います。
GDB 起動時にコマンドを実行したい
gdb -x <file>
で指定したファイルの内容をコマンドとして実行します。
あるいは単一コマンドで良ければ gdb -ex <command>
です。
例えば JOS ではプロジェクト直下にある gdb -x .gdbinit
を指定して実行しています。
その内容を以下に記載します。
set $lastcs = -1
define hook-stop
# There doesn't seem to be a good way to detect if we're in 16- or
# 32-bit mode, but we always run with CS == 8 in 32-bit mode.
if $cs == 8 || $cs == 27
if $lastcs != 8 && $lastcs != 27
set architecture i386
end
x/i $pc
else
if $lastcs == -1 || $lastcs == 8 || $lastcs == 27
set architecture i8086
end
# Translate the segment:offset into a physical address
printf "[%4x:%4x] ", $cs, $eip
x/i $cs*16+$eip
end
set $lastcs = $cs
end
echo + target remote localhost:26000\n
target remote localhost:26000
# If this fails, it's probably because your GDB doesn't support ELF.
# Look at the tools page at
# http://pdos.csail.mit.edu/6.828/2009/tools.html
# for instructions on building GDB with ELF support.
echo + symbol-file obj/kern/kernel\n
symbol-file obj/kern/kernel
個人的に見慣れないコマンドがいくつかあるのでついでに確認します。 ただし symbol-file, target については別の項で見ていきます。
set
VAR = EXP
で変数 VAR に EXP で計算された値を代入することができます。
ここでいう変数には 3 種類あります。
- レジスタ変数 (e.g.
$esp
) - GDB 上で定義できる変数 (e.g.
$x
)- prefix として
$
が必要。なので esp という変数をこのタイプの変数として定義することはできないはず (レジスタ変数として既に使われているので)
- prefix として
- プログラム上の変数 (e.g.
x
)
定義した変数を確認したい場合 print (p) コマンドを使用します。
例えば set を使うと以下のようにプログラムの途中で変数を書き換えることができます。
#include <stdio.h>
int main(void) {
int x = 0;
x++;
printf("%d\n", x);
return 0;
}
# printf に breakpoint を設定
(gdb) b gdb1.c:7
(gdb) set $x = 10
(gdb) r
Breakpoint 1, main () at dbg1.c:7
7 printf("%d\n", x);
(gdb) p x
$1 = 1
# x を書き換える
(gdb) set x = x + $x
# printf の実行、書き換えた値が表示される
(gdb) n
11
define
新しいコマンドを定義します。
(gdb) define echo-hello
Type commands for definition of "echo-hello".
End with a line saying just "end".
>echo hello\n
>end
(gdb) echo-hello
hello
更に GDB には hooks というあるコマンドの前後で別のコマンドを呼び出すための仕組みがあります。
これによるとどうやら hook-<command>
で command の前に実行する、hookpost-<command>
で command のあとに実行するコマンドが定義できるようです。
ここでは hook-stop というものを定義しているのでプログラムの実行がデバッガによって止められる度に実行されるコマンドを定義しています (stop は実際にはコマンドではないが hooks 上ではコマンドのようにして扱うことができる)。
echo
そのままです。渡した文字列を echo します。
別の object file のシンボル情報を参照したい
通常デバッグ時には指定した実行ファイル内のシンボルテーブルを使用するのですが、それを変更したい場合 symbol-file
command を使用します。
簡単な例で確認します。かなり作為的ですが。
# デバッグ情報付きでコンパイル
$ cc -o hello -g hello.c
# デバッグ情報なし & シンボルテーブルなし
$ cc -o hello-s hello.c
$ strip hello-s
$ gdb ./hello-s
GNU gdb (GDB) 8.3.1
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./hello-s...
(No debugging symbols found in ./hello-s)
(gdb) b main
Function "main" not defined.
Make breakpoint pending on future shared library load? (y or [n]) n
(gdb) symbol-file hello
Reading symbols from hello...
(gdb) b main
Breakpoint 1 at 0x113d: file hello.c, line 4.
(gdb) r
Starting program: /tmp/gdb-sample/hello-s
Breakpoint 1, main () at hello.c:4
4 printf("hello\n");
この例だと用途が見えてきませんが、JOS ではユーザプログラムのデバッグをしたいときに symbol-file を利用しています。通常時はカーネルのシンボルテーブルを使用するのですが、ユーザプログラムは異なる仮想アドレス空間で実行されるので symbol-file でシンボルテーブルを切り替えて対応しているようです (ref. 6.828 lab tools guide)。
QEMU 上で動かすプログラムのデバッグがしたい
JOS を参考に 簡単な例 を作成したのでこちらを見て頂くのが早いです。 各コマンド、オプションについては下記に記載します。
QEMU 起動時のコマンド例: qemu-system-i386 -gdb tcp::26000 -S
-gdb <dev>
- 指定したデバイスで gdb 接続を待ち受ける
- TCP 接続ならば
tcp:<host>:<port>
のようなフォーマットになる
-S
- QEMU startup 直後に停止する
- BIOS 環境でいえば BIOS プログラムの最初の命令を実行する直前 (CS = 0xf000, IP = 0xfff0) で停止する
- QEMU startup 直後に停止する
GDB 側でのコマンド:
target remote tcp::<port>
- 接続先の設定
make gdb
実行後、QEMU 起動直後の命令で停止するので、b start
のように breakpoint を貼ったりといつものようにデバッグができます。
参考
- Using the GNU Debugger
- 久々に GDB を使うとき等に一読すると良さそう