QEMUで動くLinuxをGDBで観察する

「必要なコンパイルオプションを設定し挑戦したけどうまく行かなかった」そんな失敗を供養をまとめていたら渡してるパラメータのポカミスに気づきました。 やり直したらうまくいきました。というわけで成功した方法のメモです。

QEMU+GDBで起動やシステムコールの動きを実際に確認したかった。

やったこと

作業としては5ステップ。

  • カーネルビルド環境を作る
  • カーネルをビルドする
  • デバッグ環境を準備する
  • QEMUとGDBを繋げる
  • GDBでアクセスして動きを確認する

環境

1
2
$ uname -a
Linux ubuntu-bionic 4.18.0-13-generic #14~18.04.1-Ubuntu SMP Thu Dec 6 14:09:52 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

カーネルビルド環境の準備

必要なパッケージをインストールします。

1
2
3
$ apt-get install \
  build-essential bc bison flex libssl-dev \
  libelf-dev libssl-dev libncurses5-dev

Linuxのソースコードを取得します。

1
2
$ wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.9.12.tar.xz
$ tar xvf ./linux-5.9.12.tar.xz

カーネルをビルドする

デバッグに必要なオプションは DEBUG_INFO=y, DEBUG_KERNEL=y, RELOCATABLE=n らしいので設定しました。 また CONFIG_DEBUG_INFO_DWARF4=y を設定してみました。これによるとデバッグ情報が大きくなるけど最適化コードでも変数の解決成功率が高まるらしいです。

色々と必要な設定をするのが大変なので make defconfig.config を作ってからmake menuconfig で設定しました。

1
2
3
$ cd linux-5.9.12
$ make defconfig
$ make menuconfig

メニューで設定できることが多くて迷子になるけど、 make menuconfig のダイアログで/を打つとオプションを検索できます。

1
$ make -j 2

デバッグ環境を準備する

はじめにツールを準備します。

1
2
$ sudo apt-get install \
  qemu gdb busybox-static

QEMUでは初期RAMディスクを -initrd フラグで指定します。初期RAMディスクを指定しないと下のようにpanicが起こります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[    3.284738] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
[    3.286360] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 5.9.12 #7
[    3.286608] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
[    3.290602] Call Trace:
[    3.292184]  dump_stack+0x57/0x6a
[    3.292386]  panic+0xf3/0x2b4
[    3.295570]  mount_block_root+0x17d/0x21d
[    3.297225]  ? rdinit_setup+0x26/0x26
[    3.297515]  mount_root+0xec/0x10a
[    3.297737]  ? initrd_load+0x37/0x3a
[    3.298702]  prepare_namespace+0x130/0x15f
[    3.299168]  kernel_init_freeable+0x1dc/0x1eb
[    3.300168]  ? rest_init+0xb0/0xb0
[    3.300391]  kernel_init+0x5/0x110
[    3.300480]  ret_from_fork+0x22/0x30
[    3.301873] Kernel Offset: disabled
[    3.302444] ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) ]---

下のように作っておきましょう。 mknod(1) で2つのデバイスファイルを定義していてこれは必要なようです。 パラメータの意味は $KERNEL_SOURCE/Documentation/admin-guide/devices.txt に書かれています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 初期RAMの内容を準備する
$ mkdir mk-initrd
$ cd mk-initrd
$ mkdir bin dev proc sbin sys
$ cp /bin/busybox ./bin/
$ cd /bin
$ ln -s busybox sh
$ ln -s busybox mount
$ cd ../sbin
$ cat <<EOF > init
mount -t proc none /proc
mount -t sysfs none /sys
exec /bin/sh
EOF
$ cd ../dev
$ sudo mknod console c 5 1
$ sudo mknod null c 1 3
# イメージを作る
$ cd ..
$ find .| cpio -o -H newc | gzip > ~/initfs.img

QEMUとGDBを繋げる

2つのターミナルを使います。1つはQEMUでLinuxを起動します。

1
2
3
4
5
6
7
# カーネル起動 on QEMU
$ qemu-system-x86_64 \
  -kernel ./arch/x86/boot/bzImage \
  -initrd ~/initramfs.img \
  --append "console=ttyS0 nokaslr rdinit=/sbin/init" \
  -nographic \
  -gdb tcp::12345 -S

もう一方でGDBを起動してQEMUで開けているポートに接続します。

1
2
3
4
5
6
7
8
$ gdb
target remote localhost:12345
symbol-file linux-5.9.12/vmlinux
source linux-5.9.12
b start_kernel
b __x64_sys_clone
layout split
continue

こんな感じで進めていると下のようになります。 画面はVMの中で ls コマンドを打って sh が呼んだ clone(2) の中でのブレークポイント。

ブレークポイント @ __x64_sys_clone() ブレークポイント @ __x64_sys_clone()

QEMUは Ctrl-A,x でQEMU自体を止めないと何もできなくなるので注意が必要だった。

おわりに

ここまでするのに凡ミスで意図した結果にならなくて苦労もしましたが、安心して実験できるようになれて嬉しいです。 反省文を書くの大事です。仕事でも質問をまとめてると解決したりしますね。 またカーネル内部であっても start_kernel()sys_clone() 含めて知りたい関数の動きを実際にGDBで追えるようになったのは勉強しやすくなると思っています。 一方で、モジュールやシステム全体をQEMUで準備するのはまだ難しいと感じています。今後は時間をみてkprobeなど観測する技術を身につけて行きたいです。

Linuxの話にもどりますが、clone(2) のカーネル側のハンドラを sys_clone() だと思っていたのですが、今回の環境では __x64_sys_clone でした。 マクロやリンカなどがアーキテクチャ毎に実装を変えているというのは自然に思います。 ですが、 kprobe で見えていた sys_clone() とはなんだったのだろうかという疑問が湧きました。 これもまた宿題のようです。

comments powered by Disqus