Lを探す日常

I lov esoteric programming

QEMU on Vagrant on Mac の環境構築メモ

やったこと

TSGソースコードリーディング分科会で Linux カーネルmmap を読むことになり、デバッグのために QEMU を動かした。
分科会前に直接 Mac 上で QEMU を動かそうとしたり色々試したがうまくいかず、分科会中に @jp3bgy プロや id:kaki_no_tane プロに見てもらってやっと構築できた。

開始前の環境

参考

表記

環境が3つ (Mac, Vagrant, QEMU) あってどこでどの操作をすればいいかという常識がなく困ったので、この記事では全て明示するようにした。
コマンドが羅列されているブロックに# in fugaと書いてあれば、fugaのシェルでコマンドを叩けばよい。

環境構築

VagrantUbuntu 18.04 を動かす

最初は Vagrant 関連のどこかのサイトで見かけた hashicorp/precise64 を使用していたが、Ubuntu のバージョンが 12.04 だということにだいぶ後で気付いた。
おそらくそのせいで apt-get 周りの何かがうまくいっていなかった。
気を取り直して 18.04 を入れる。

# in mac
vagrant init bento/ubuntu-18.04
vagrant up
vagrant ssh

諸々の環境変数を設定する。gist では単に export コマンドを打っていたが、結局複数タブで vagrant ssh する可能性が高いので .bashrc に書いた方がよい。

# ~/.bashrc in vagrant
# (前略)
export OPT=~/opt
export BUILDS=~/builds
export BUSYBOX=$OPT/busybox
export BUSYBOX_BUILD=$BUILDS/busybox
export INITRAMFS_BUILD=$BUILDS/initramfs
export LINUX=$OPT/linux
export LINUX_BUILD=$BUILDS/linux
export INITRAMFS_BUILD=$BUILDS/initramfs
# in vagrant
. ~/.bashrc
mkdir -p 【いろいろ】

カーネル

まず MacRelease v4.19 · torvalds/linux · GitHub から tar.gz を落とし、Vagrantfile のあるディレクトリで解凍した (単なる git clone はもちろん、--depth 1 を付けても圧縮はされない(らしい)ので、tar.gz より遅い) (ダウンロード中に以下の §§ Busybox も進められる)。
これを所定の位置に移す (vagrant 内で全部やった方が良かったと思う)。

# in vagrant
cp -r /vagrant/linux-4.19 $LINUX
cd $LINUX
# pwd: ~/opt/linux
make O=$LINUX_BUILD allnoconfig
sudo apt-get install bison # 怒られたので入れる
make O=$LINUX_BUILD allnoconfig # retry
sudo apt-get install flex # 怒られたので入れる
make O=$LINUX_BUILD allnoconfig # retry → 成功

この allnoconfig のところは、kakinotane プロはブログで localmodconfig を使っていた。それを真似して実行したが、30秒くらいエンターキーを押し(デフォルトを選択し)続けてもまだ終わらないプロンプトに絶望して interrupt してしまった。kakinotane プロは根気よく押し続けて通したらしい。

# in vagrant
cd $LINUX_BUILD
# pwd: ~/builds/linux
make menuconfig
sudo apt-get install ncurses-dev # 怒られたので入れる
make menuconfig # retry

gist の 64-bit kernel ---> yes から始まるブロックの通りに設定する。設定項目の場所が見つからなければ / キーで検索する。
条件によって現れない設定項目があるので、上から順番に設定していくべき。
この gist の項目に加えて、次の設定もする。

Kernel hacking ---> Compile-time checks and compiler options ---> Compile the kernel with debug info
Kernel hacking ---> Compile-time checks and compiler options ---> Compile the kernel with debug info ---> Provide GDB scripts for kernel debugging

*** End of configuration. と言われたらおそらく成功、Your configuration changes were NOT saved. と言われたら失敗。

# in vagrant
# pwd: ~/builds/linux
time make -j4
# (長い出力)
# real    13m53.952s

待っている間にBusyboxをやる。

Busybox

実際はこの辺りでディレクトリ階層を適当に扱っていて、後の混乱に繋がってしまった (ブログに書いているのは修正後のもの)。

# in vagrant
cd $BUSYBOX
# pwd: ~/opt/busybox
git clone git://busybox.net/busybox.git --depth 1 . # この `.` が大切
make O=$BUSYBOX_BUILD defconfig
sudo apt install gcc # 怒られたので入れる
make O=$BUSYBOX_BUILD defconfig
# (長い出力)
cd $BUSYBOX_BUILD
# pwd: ~/builds/busybox
make menuconfig

gist の static binary の行の通りに設定する。
*** End of configuration. と言われたらおそらく成功、Your configuration changes were NOT saved. と言われたら失敗。

# in vagrant
# pwd: ~/builds/busybox
time make -j8 # j4 の方がいいのか? わからん
# (ignoring return value の警告とかがいっぱい表示された)
# real    7m6.257s
make install
# (長い出力)
cd $INITRAMFS_BUILD
# pwd: ~/builds/initramfs
mkdir -p bin sbin etc proc sys usr/bin usr/sbin
cp -a $BUSYBOX_BUILD/_install/* .

init ファイルに、gist の Add a $INITRAMFS_BUILD/init script の部分のブロックを書き込む。
この AA はただの遊び心だと思うが、後で qemu を実行するときにこれが見えて安心した🙏

# in vagrant
# pwd: ~/builds/initramfs
chmod +x init
find . -print0 | cpio --null -ov --format=newc | gzip -9 > $BUILDS/initramfs.cpio.gz

QEMU実行

カーネルBusyboxの両方が終われば環境構築はだいたい終了。QEMU を動かしてみる。

# in vagrant
cd $INITRAMFS_BUILD
# pwd: ~/builds/initramfs
qemu-system-x86_64 -kernel $LINUX_BUILD/arch/x86_64/boot/bzImage \
  -initrd $BUILDS/initramfs.cpio.gz -nographic \
  -append "console=ttyS0" -s # -s を忘れない
sudo apt-get install qemu # 怒られたので入れる
sudo apt-get install qemu-system-x86 # 怒られたので入れる

qemu-system-x86 のインストール時に Not Found エラーが出た。いろいろ調べたが、結局 sudo apt update で解決した。

# in vagrant
# pwd: ~/builds/initramfs
sudo apt-get install qemu-system-x86 # retry → 成功
qemu-system-x86_64 -kernel $LINUX_BUILD/arch/x86_64/boot/bzImage \
  -initrd $BUILDS/initramfs.cpio.gz -nographic \
  -append "console=ttyS0" -s # retry → 成功

上の AA が表示され、QEMU が起動する。シェルがちょっとおかしいかと最初思ったが、何回かエンターキーを押していると正常なプロンプト / # が現れた。
QEMU を終了したいときは、(別のタブの) vagrant 内で kill `pgrep qemu` するしかない。

mmapデバッグ

gdb

これで QEMU は動くようになった。次にデバッグの準備をする。$LINUX_BUILD にある vmlinux というファイルが QEMU と何らかの方法で繋がっているらしい。

# in vagrant (QEMUとは別タブ)
cd $LINUX_BUILD
# pwd: ~/builds/linux
sudo apt-get install gdb
echo "add-auto-load-safe-path ~/builds/linux/vmlinux-gdb.py" > ~/.gdbinit
gdb vmlinux

QEMU が動いている状態で別タブで gdb を起動する。

# in vagrant
gdb
(gdb) target remote :1234

こうすると、QEMU の動作が中断される。gdbc を打つと再び動き出す。

# gdb in vagrant
(gdb) c
Ctrl-C # 止まる
(gdb) b vm_mmap_pgoff
(gdb) c # 動く
# in qemu
ls # 途中で breakpoint にかかって止まる
# gdb in vagrant
Breakpoint 1, vm_mmap_pgoff
(gdb) tui

こうして vm_mmap_pgoff の中身を追うことができる。

C プログラムを動かしたい

上のコードでは ls コマンドが直接 syscall mmap を呼んでいる訳ではなく、backtraceによると __x64_sys_execve (syscall execve) から来ていた。
今度はそうではなく、実際に syscall mmap が呼ばれ、__x64_sys_mmap が実行されるところを見たいので、C プログラムで直接 mmap を呼び出す。

#include <sys/mman.h>

int main(void) {
  mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, 0, 0);
  return 0;
}

しかし、QEMU ではこれをコンパイルできない。

# in qemu
gcc hoge.c #=> /bin/sh: gcc: not found

そのため、vagrantコンパイルして、その a.out をqemuに持っていく。

# in vagrant
cd $INITRAMFS_BUILD
# pwd: ~/builds/initramfs
gcc hoge.c
find . -print0 | cpio --null -ov --format=newc | gzip -9 > $BUILDS/initramfs.cpio.gz # さっきと同じコマンド

$INITRAMFS_BUILD 内にあるファイル(から作られた initramfs.cpio.gz の中身)は、全て QEMU のルートディレクトリに配置される。もう一度 QEMU を立ち上げると a.out が見える。
(実際はさっきディレクトリ階層を間違えていたせいで、ここで a.out が見えず詰まっていた。)

# in vagrant
# pwd: ~/builds/initramfs
qemu-system-x86_64 -kernel $LINUX_BUILD/arch/x86_64/boot/bzImage \
  -initrd $BUILDS/initramfs.cpio.gz -nographic \
  -append "console=ttyS0" -s # さっきと同じコマンド
# in qemu
ls -hla # a.out が実行権限を持っていることを確認
./a.out #=> /bin/sh: ./a.out: not found

しかし、この a.out は QEMU 上に libc がないため動作しない。そのため、libc を静的にリンクする必要がある。

# in vagrant
# pwd: ~/builds/initramfs
gcc hoge.c -static
find . -print0 | cpio --null -ov --format=newc | gzip -9 > $BUILDS/initramfs.cpio.gz # さっきと同じコマンド
qemu-system-x86_64 -kernel $LINUX_BUILD/arch/x86_64/boot/bzImage \
  -initrd $BUILDS/initramfs.cpio.gz -nographic \
  -append "console=ttyS0" -s # さっきと同じコマンド

こうして qemu を再起動すると、a.out を動かせるようになった (何もしないプログラムなので出力はない)。
これで __x64_sys_mmap (か __x64_sys_mmap_pgoff ) に gdbブレークポイントを設定すると、a.out 実行時にそのブレークポイントにかかってデバッグできる。
(追記 2018/12/10: 個人的に gdb の引数の vmlinux を忘れがちだから注意する)

まとめ

環境構築つらかった。ここに到るまで自力で動かそうと数時間費やして失敗したが、分科会でプロが近くにいると前提知識や根本的な知識を色々と得られて心強かった。