debug
NetBSD
gdb
QEMU
gdbserver

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

Peek 2018-04-15 17-18.gif

QEMUにはgdbserverが内蔵されており、ホストのgdbからtcp越しにゲストOSのカーネルをデバッグすることができます。

この記事は上のGIF動画のようにNetBSD kernelのデバッグを行うHowtoです。

そのうちVMWare・libvertを用いた方法も解説します。WindowsではVBoxGDBを使えばVirtualBoxでもデバッグできるらしいです。

注意事項

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

クロスgdbのビルド

  • (オススメ)gdb 最新版を configure --enable-targets=all でビルド(--target=x86_64--netbsd としても良いがあまりサイズは変わらない)
  • (gdbのソースをダウンロードするのが面倒ならこちらで)build.sh -VMKCROSSGDB=yes tools${TOOLDIR}/tools/bin/x86_64--netbsd-gdb をビルド

のどちらかでビルドして下さい。gdb最新版がオススメですが、ソースをダウンロードするのが面倒であれば前者でOKです。

仮想イメージの準備

  • NetBSD-8.0をインストールする
  • build.sh live-image で作成する

の2つを紹介します。前者はお手軽、後者はビルドの時間や容量は食いますが最新版のユーザーランドを用意できます。

NetBSD-8.0をインストールする

wget http://ftp.jaist.ac.jp/pub/NetBSD/iso/8.0/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  -netdev user,id=n1 -device e1000,netdev=n1  -rtc base=utc

qemu-system-x86_64 のオプションについては カーネルデバッグで使うQEMUオプションチートシート を参照して下さい。

後はいつも通りインストールします。いつも通りでない方は @furandon_pig さんの NetBSD-7.0をインストールしてみる - NetBSDのインストール を参考にして下さい。

再インストールする等で、ブートデバイスがhdaになってしまう場合は -boot d で明示的にCDROMブートして下さい。

build.sh live-image で作成する

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

NetBSD-8.99.4-i386-live-wd0root.img のデフォルトサイズは2GiBで、これは build.sh-V 変数 IMAGEMBLIVEIMAGEMB で変更できます (例:./build.sh -VLIVEIMAGEMB=4096)。

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

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

build.sh kernel=GENERIC

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

をビルドします。ビルド中、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 がついていますが、他のアーキテクチャでは -VMKKDEBUG=yes で明示的に -g をつける必要があるかもしれません。

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

qemu-system-x86_64  -m 256  -hda netbsd.qcow2  -netdev user,id=n1,hostfwd=tcp::10022-:22 -device e1000,netdev=n1  -rtc base=utc

Linuxホストのuser mode networkではTCPとUDPしか使えないので、ネットワークのテストにpingは使えません。telnet -4 qiita.com 80 でテストして下さい。

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

passwd  # root のパスワードが設定されていない場合。sshログインのために必要
mv /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

ホストから 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でステップ実行する

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

qemu-system-x86_64  -m 256 -hda netbsd.qcow2  -netdev user,id=n1,hostfwd=tcp::10022-:22 -device e1000,netdev=n1  -S -s  -rtc base=utc

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

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

$ ${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

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

image.png

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

最適化を止める

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

カーネルコンフィグの makeoptions COPTS=-O2-O0 にしましょう。ビルド中、-O0 でコンパイルされていることを確認します。

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

トラブルシューティング

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

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

^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 でパスのマッピングをしてやると良いです。

(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の間の通信が上手くできていません。例えば、i386のカーネルを qemu-system-i386 ではなく qemu-system-x86_64 で起動してデバッグしようとすると、以下のようなエラーが出力されます:

(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 が助けになるかもしれません。