QEMUのgdbserver機能でNetBSD kernelをデバッグする

![Peek 2018-04-15 17-18.gif](https://qiita-image-store.s3.amazonaws.com/0/167409/97d09f12-052c-be87-c6aa-cca529be4695.gif)

QEMUにはgdbserver[^1]が内蔵されており、ホストのgdbからtcp越しにゲストOSのカーネルをデバッグすることができます。この記事は上のGIF動画のようにNetBSD kernelのデバッグを行うHowtoです。

（VMWareでも同じことができるらしいです。WindowsならVirtualBox+[VBoxGDB](http://sysprogs.com/VBoxGDB/tutorial/)でもできるらしいです。が、私は日常的にNetBSD仮想マシンを使っている訳では無いので、気軽に環境構築・撤去ができるQEMUが好きです。libvertでもできるらしいのでそのうちやってみようと思ってます。）

<!--
TODO Linux FreeBSD OpenBSD minix
-->

<!-------------------------------------------------->
## 注意事項

- ホストOSはLinux (Ubuntu 18.04), macOS (Mojave 10.14.3) で検証済みです。Windowsや他のOSでの動作報告お待ちしております！
- ゲストOSは NetBSD - 8.0-amd64 で検証済みです。
  - i386の場合、`qemu-system-x86_64` の代わりに `qemu-system-i386` を使用して下さい。
- 仮想マシン上のNetBSDで走らないコードはデバッグできません。デバイスドライバなどのマシン依存・アーキテクチャ依存のコードをデバッグする際はkgdbを使って下さい。別記事 [NetBSDのkgdbをQEMUでお手軽に試す](https://qiita.com/wataash/items/80c415dc43b6463290d2) にHowtoを書きました。

<!-------------------------------------------------->

## クロスgdbのビルド

- [gdb](https://ftp.gnu.org/gnu/gdb/) 最新版を `configure --enable-targets=all` でビルド（`--target=x86_64--netbsd` としても良いがあまりサイズは変わらない）
- `build.sh -T <TOOLDIR> -VMKCROSSGDB=yes tools` で `${TOOLDIR}/tools/bin/x86_64--netbsd-gdb` をビルド

のどちらかでビルドして下さい。2019/04/06現在、gdb最新版のバージョンは8.2.1、[NetBSD付属のgdbは8.0.1](https://github.com/IIJ-NetBSD/netbsd-src/blob/master/external/gpl3/gdb/dist/gdb/version.in)です。[NetBSD付属ののgdbは古くなってくることがある](https://github.com/IIJ-NetBSD/netbsd-src/commits/master/external/gpl3/gdb/dist/gdb/version.in)ので、その場合はGNUから最新版をダウンロードしてビルドすることをおすすめします。筆者の経験的に最新版の方がトラブル（ビルドできない、バグってる、mi APIが古い）が少ないです。

<!-- gdb build記事 -->

<!-------------------------------------------------->
## 仮想イメージの準備

新規仮想イメージにNetBSD 8.0をインストールします。

```sh
curl http://ftp.jaist.ac.jp/pub/NetBSD/iso/8.0/NetBSD-8.0-amd64.iso -o NetBSD-8.0-amd64.iso
qemu-img create -f qcow2 netbsd.qcow2 100G
qemu-system-x86_64  -m 256  -hda netbsd.qcow2  -cdrom ./NetBSD-8.0-amd64.iso  -display curses  -netdev user,id=n1 -device e1000,netdev=n1  -monitor telnet::5555,server,nowait  -rtc base=utc
```

`qemu-system-x86_64` のオプションについては [カーネルデバッグで使うQEMUオプションチートシート](https://qiita.com/wataash/items/174b454d4478898a556b) を参照して下さい。強制終了するにはホストから `telnet localhost 5555` して `q` です。

NetBSDをいつも通りインストールします。いつも通りでない方は @furandon_pig さんの [NetBSD-7.0をインストールしてみる - NetBSDのインストール](https://qiita.com/furandon_pig/items/8b7d13a119067098d561#netbsd%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB) を参考にして下さい。インストール後は一旦 `poweroff` でQEMUを終了します。

再インストールする等で、ブートデバイスがhdaになってしまう場合は `-boot d` （[qemu(1) 2.3.1 Standard options](https://qemu.weilnetz.de/doc/qemu-doc.html#Standard-options)）で明示的にCDROMブートして下さい。

<!--
release, live-imageはビルドがコケる確率が高い

### `build.sh live-image` でインストール済イメージを作成する

`build.sh -O <OBJDIR> release live-image` を実行すると `${OBJDIR}/distrib/i386/liveimage/emuimage/NetBSD-8.99.4-i386-live-wd0root.img`[^2]（及び `NetBSD-8.99.4-i386-live-wd0root.img.gz`）が生成されますが、これをそのまま仮想イメージとして利用できます。

`NetBSD-8.99.4-i386-live-wd0root.img` のデフォルトサイズは2GiBで、これは `build.sh` の `-V` 変数 [IMAGEMB](https://github.com/IIJ-NetBSD/netbsd-src/blob/master/distrib/common/bootimage/Makefile.bootimage)・[LIVEIMAGEMB](https://github.com/IIJ-NetBSD/netbsd-src/blob/master/distrib/common/bootimage/Makefile.liveimage) で変更できます (例：`./build.sh -VLIVEIMAGEMB=4096`)。

以降、コマンドに登場する `netbsd.qcow2` は `NetBSD-8.99.4-i386-live-wd0root.img` に置き換えて下さい。

[^2]: `-O <OBJDIR>` を指定しない場合 `netbsd-src/distrib/i386/liveimage/emuimage/obj/NetBSD-8.99.4-i386-live-wd0root.img[.gz]`
-->

## デバッグするkernelのビルドとインストール

`build.sh -O <OBJDIR> kernel=GENERIC` で

- `${OBJDIR}/sys/arch/amd64/compile/GENERIC/netbsd`
- `${OBJDIR}/sys/arch/amd64/compile/GENERIC/netbsd.gdb`

をビルドします[^2]。ビルド中、gccに `-g` フラグがついていることを確認します。

```
#   compile  GENERIC/kern_syscall.o
/home/wsh/src/netbsd/src/../tools/bin/x86_64--netbsd-gcc -mcmodel=kernel ... -fno-delete-null-pointer-checks -g -O2 -fno-omit-frame-pointer ... /home/wsh/src/netbsd/src/sys/kern/kern_syscall.c -o kern_syscall.o
```

amd64の場合は[デフォルトで `-g` がついています](https://github.com/IIJ-NetBSD/netbsd-src/blob/6a402eeb4f85febc59e1804c0c4041adfdb9ac48/sys/arch/amd64/conf/GENERIC#L114)が、他のアーキテクチャでは [`-VMKKDEBUG=yes` で明示的に `-g` をつける](https://github.com/IIJ-NetBSD/netbsd-src/blob/0e3a6a1d3e7ce1ee4511ce894deec8eecb17c037/sys/conf/Makefile.kern.inc#L20)必要があるかもしれません。

カーネルをビルドしたら仮想マシンを起動してインストールします。

```sh
qemu-system-x86_64  -m 512  -hda netbsd.qcow2  -display curses  -netdev user,id=n1,hostfwd=tcp::10022-:22 -device e1000,netdev=n1  -monitor telnet::5555,server,nowait  -rtc base=utc
```

仮想マシンが起動したら、rootでログインして以下を実行します。

```sh
telnet -4 qiita.com 80  # ネットワーク疎通性のテスト
passwd  # root のパスワードが設定されていない場合。sshログインのために必要
cp /netbsd /netbsd.orig
vi /etc/rc.conf  # dhcpcd=YES, dhcpcd_flags="-qM wm0", sshd=YES を確認
vi /etc/ssh/sshd_config  # PermitRootLogin yes
service sshd reload
```

[Linuxホストのuser mode networkではTCPとUDPしか使えない](https://wiki.archlinux.org/index.php/QEMU#User-mode_networking)ため、pingではなくtelnetでテストしています。

ホストから `scp -P10022 ${OBJDIR}/sys/arch/amd64/compile/GENERIC/netbsd root@localhost:/netbsd` を実行して、ゲストにデバッグ対象のカーネルをインストールします。
接続できなければまず `ssh -p10022 root@localhost` でログインできるか確認して下さい。

仮想マシンを `reboot` して正常に起動できることを確認後、`poweroff` で一旦シャットダウンします。

正常に起動しない場合は、起動直後のブートオプションで `5. Drop to boot prompt` を選択後 `boot netbsd.orig` でオリジナルのカーネルを起動して…頑張って下さい。

## gdbでステップ実行する

以下のコマンドでゲストを起動：

```sh
qemu-system-x86_64  -m 512  -hda netbsd.qcow2  -display curses  -netdev user,id=n1,hostfwd=tcp::10022-:22 -device e1000,netdev=n1  -monitor telnet::5555,server,nowait  -S -s  -rtc base=utc
```

`-s` は `-gdb tcp::1234` 等価で、ポート1234でgdbプロトコルをlistenします。このポートが使われていたら他のポートを使用して下さい。

ホストからgdbserverにつないでみます。

```sh
$ ${TOOLDIR}/bin/x86_64--netbsd-gdb -q ${OBJDIR}/sys/arch/amd64/compile/GENERIC/netbsd.gdb
Reading symbols from ../obj.amd64/sys/arch/amd64/compile/GENERIC/netbsd.gdb...done.
Reading in symbols for /home/wsh/src/netbsd/src/sys/kern/init_main.c...done.

(gdb) b main
Breakpoint 1 at 0xffffffff80d262cf: file /home/wsh/src/netbsd/src/sys/kern/init_main.c, line 275.

(gdb) target remote :1234
Remote debugging using :1234
0x000000000000fff0 in ?? ()

(gdb) l
245	static void start_init(void *);
246	static void configure(void);
247	static void configure2(void);
248	static void configure3(void);
249	void main(void);
250	
251	/*
252	 * System startup; initialize the world, create process 0, mount root
253	 * filesystem, and fork to create init and pagedaemon.  Most of the
254	 * hard work is done in the lower-level initialization routines including

(gdb) c
Continuing.
^CReading in symbols for /home/wsh/src/netbsd/src/sys/external/bsd/acpica/dist/dispatcher/dsmthdat.c...done.

Program received signal SIGINT, Interrupt.
AcpiDsMethodDataInit (
    Reading in symbols for /home/wsh/src/netbsd/src/sys/external/bsd/acpica/dist/dispatcher/dswstate.c...done.
WalkState=0xffffe4000fc65810)
    at /home/wsh/src/netbsd/src/sys/external/bsd/acpica/dist/dispatcher/dsmthdat.c:111
111	    for (i = 0; i < ACPI_METHOD_NUM_ARGS; i++)

(gdb) l
106	    ACPI_FUNCTION_TRACE (DsMethodDataInit);
107	
108	
109	    /* Init the method arguments */
110	
111	    for (i = 0; i < ACPI_METHOD_NUM_ARGS; i++)
112	    {
113	        ACPI_MOVE_32_TO_32 (&WalkState->Arguments[i].Name,
114	            NAMEOF_ARG_NTE);
115	

(gdb) bt
Reading in symbols for /home/wsh/src/netbsd/src/sys/external/bsd/acpica/dist/parser/psxface.c...done.
...
Reading in symbols for /home/wsh/src/netbsd/src/sys/arch/amd64/amd64/autoconf.c...done.
#0  AcpiDsMethodDataInit (WalkState=0xffffe4000fc65810)
    at /home/wsh/src/netbsd/src/sys/external/bsd/acpica/dist/dispatcher/dsmthdat.c:111
#1  0xffffffff8125a131 in AcpiDsCreateWalkState (OwnerId=11 '\v', Origin=0x0, MethodDesc=0x0, 
    Thread=0x0) at /home/wsh/src/netbsd/src/sys/external/bsd/acpica/dist/dispatcher/dswstate.c:629
#2  0xffffffff81278335 in AcpiPsExecuteMethod (Info=0xffffe40002686638)
    at /home/wsh/src/netbsd/src/sys/external/bsd/acpica/dist/parser/psxface.c:185
#3  0xffffffff8126e5c8 in AcpiNsEvaluate (Info=0xffffe40002686638)
    at /home/wsh/src/netbsd/src/sys/external/bsd/acpica/dist/namespace/nseval.c:251
...
#21 0xffffffff80d26ae7 in configure () at /home/wsh/src/netbsd/src/sys/kern/init_main.c:754
#22 0xffffffff80d265dc in main () at /home/wsh/src/netbsd/src/sys/kern/init_main.c:517

(gdb) 
```

ctrl-Cで任意の場所で停止できます。適当に止めたらACPIのアタッチで止まりました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/167409/c6cccb57-cb64-0014-d613-dbf503ba8d86.png)

`s, n, fin` で進めていくとちょっとずつconfigureされていきます。これは楽しいですｗ

![image.png](https://qiita-image-store.s3.amazonaws.com/0/167409/1b613295-d91b-1d88-cfbc-40788721023d.png)

生の `gdb` でデバッグしてもあまりうれしくないのでテキストエディタやIDEから使いましょう。冒頭のGIF動画はCLionから使っています。別記事で解説予定です。

<!-- TODO -->

## 最適化を止める

カーネルはデフォルトで `-O2` でビルドされるのでデバッグしづらいです。

[カーネルコンフィグの `makeoptions COPTS=-O2`](https://github.com/IIJ-NetBSD/netbsd-src/blob/6a402eeb4f85febc59e1804c0c4041adfdb9ac48/sys/arch/amd64/conf/GENERIC#L107) を `-O0` にしましょう。ビルド中、`-O0` でコンパイルされていることを確認します。

```
#   compile  GENERIC/kern_syscall.o
/home/wsh/src/netbsd/src/../tools/bin/x86_64--netbsd-gcc ... -g -O0 ...
```

<!-- -VCOPTS=-O0 は意味なし -->

## トラブルシューティング

### gdbでソースコードが表示できない

`netbsd.gdb` にはソースコードのパスがハードコードされています。そのため、ビルド後にソースの場所を変えたりすると、以下のようにgdbがソースを発見できません。

```sh
^C
Program received signal SIGINT, Interrupt.
0xffffffff8021ebfe in ?? ()
(gdb) l
245	/home/wsh/src/netbsd/src/sys/kern/init_main.c: No such file or directory.
(gdb)
```

この場合は `set substitute-path` でパスのマッピングをしてやると良いです。

```sh
(gdb) set substitute-path /home/wsh/src/netbsd/src/ /usr/opt/src/netbsd/
(gdb) l
245	static void start_init(void *);
246	static void configure(void);
247	static void configure2(void);
248	static void configure3(void);
249	void main(void);
250	
251	/*
252	 * System startup; initialize the world, create process 0, mount root
253	 * filesystem, and fork to create init and pagedaemon.  Most of the
254	 * hard work is done in the lower-level initialization routines including
(gdb) info source
Current source file is /home/wsh/src/netbsd/src/sys/kern/init_main.c
...
```

### 特定のファイルだけソースコードが表示されない

そのソースコードのみ `-g` でコンパイルされてないと考えられます。gdbの `info sources` に当該ファイルが含まれているか確認して下さい。

### `'g' packet reply is too long`

[gdbserverとgdbの間の通信](https://sourceware.org/gdb/onlinedocs/gdb/Packets.html)が上手くできていません。例えば、i386のカーネルを `qemu-system-i386` ではなく `qemu-system-x86_64` で起動してデバッグしようとすると、以下のようなエラーが出力されます：

```sh
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
warning: Selected architecture i386 is not compatible with reported target architecture i386:x86-64
Remote 'g' packet reply is too long: 2ad034740000000080faffef000000001501000000000000010000000000000000000000000000002300000000000000f41f44f000000000cc1f44f00 ...
```

他には

- クロスgdbでなく普通のgdbを使っている
- クロスgdbを使っているがアーキテクチャを誤って解釈している

など考えられます。後者の場合は `show architecture` `set architecture` で何とかなるかもしれません。`set debug remote 1` が助けになるかもしれません。

<!--
TODO
ステップ実行のコードがめちゃくちゃ
-O0でビルドしてますか？
カーネル再ビルド後、再インストールしてますか？
ちょっと変えてリビルドすると、一部のアドレスがちょっとずれるだけなので案外気づかない
-->

## ライセンス

<a rel="license" href="http://creativecommons.org/licenses/by/4.0/"><img alt="クリエイティブ・コモンズ・ライセンス" style="border-width:0" src="https://i.creativecommons.org/l/by/4.0/88x31.png" /></a><br />この 作品 は <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">クリエイティブ・コモンズ 表示 4.0 国際 ライセンス</a>の下に提供されています。

関連するソフトウェアの公式ドキュメントのメンテナの方へ：この記事の内容を公式ドキュメントに記載して頂ける場合、この記事にコメントして頂くか、twitter [@wata_ash](https://twitter.com/wata_ash) にご連絡下さい。

[^1]: "gdbserver" というとGNU gdbについてくる[gdbserver](https://en.wikipedia.org/wiki/Gdbserver)のことを指すので "gdb stub" という一般名称にしようかと思いましたが、[QEMUがgdbserverという名詞を使っている](https://qemu.weilnetz.de/doc/qemu-doc.html#index-gdbserver)ので "gdbserver" にしておきました。
[^2]: `-O <OBJDIR>` を指定しない場合 `${OBJDIR}/sys/arch/amd64/compile/GENERIC/netbsd`、`${OBJDIR}/sys/arch/amd64/compile/GENERIC/netbsd.gdb`