BLOG

Linux カーネルを QEMU 上で起動する

Linux カーネルを開発する際、コードを書き換える度に、ビルド、インストール、再起動、テスト、というのはなかなか面倒です。この手間を出来るだけ省くために、私は [QEMU](https://www.qemu.org/) を利用しています。この記事では私がカーネル開発で使用する環境を構築する方法について紹介します。 ## 環境 普段は EC2 上で開発を行っています。特にディストリビューションにこだわりは無いのでなんとなく Amazon Linux 2 を選んでいます。 OS: Amazon Linux 2 ## 手順 ホームディレクトリ (~/) 配下で作業を行なっているので、必要に応じて書き換えてください。 ``` $ WORKDIR=~ ``` ### 1. カーネルのダウンロード git でカーネルのソースコードをダウンロードします。各サブシステムの最新のカーネルは [git.kernel.org](https://git.kernel.org/) からダウンロードできます。私はネットワークサブシステムの開発を行うために、 [netdev の net-next](https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/) を選びました。特に決まったものがなければ [linux-next](https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/) を選ぶと良いでしょう。 ``` $ cd ${WORKDIR} $ sudo yum install -y git $ git clone git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git ``` ### 2. カーネルのビルド QEMU で x86_64 をエミュレートするので、 x86_64 向けのデフォルトの設定を `.config` に書き込んでビルドしています。 `make menuconfig` で設定を書き換えることも可能ですが、その場合は `ncurses-devel` をインストールする必要があります。 ``` $ sudo yum install -y gcc flex bison elfutils-libelf-devel openssl-devel $ cd net-next $ make x86_64_defconfig $ make -j $(nproc) ``` ### 3. Buildroot のダウンロード QEMU でカーネルを起動する際、ルートファイルシステムが必要になります。基本的なコマンド群を含むルートファイルシステムを簡単に作成できるので、 [Buildroot](https://buildroot.org/) を使用します。 ``` $ cd ${WORKDIR} $ git clone git://git.buildroot.net/buildroot ``` ### 4. Buildroot のビルド ビルドするコマンドは以下の通りです。 `make menuconfig` でアーキテクチャや必要なコマンドを選択します。ビルドにはそこそこ時間がかかるので洗濯機を回して、 YouTube で期間限定で公開されている好きなアーティストの[ライブ映像](https://www.youtube.com/watch?v=YU-RGLuC-Ho)を流しながら、たまったお皿洗いでもすると良いかもしれません。 ``` $ sudo yum install -y ncurses-devel gcc-c++ patch perl-Data-Dumper perl-ExtUtils-MakeMaker perl-Thread-Queue $ cd buildroot $ make menuconfig $ make -j $(nproc) ``` QEMU で x86_64 をエミュレートするので、アーキテクチャは x86_64 を選択します。読み書き出来ればファイルシステムはなんでもいいと思いますが、昔から Ubuntu をよく使っていたので ext4 を選びました。 Btrfs の開発をしたい方は btrfs を選びましょう。 - Target options - Target Architecture - x86_64 を選択 - Filesystem images - ext2/3/4 root filesystem にチェックを入れる - ext2/3/4 variant - ext4 を選択 複数セッションでコマンドを実行したい場面は良くあると思うので、 openssh は選択した方が良いでしょう。その他には curl, iptables, nginx, tcpdump, python3 を選択しました。 - Toolchain - C library - glibc を選択 - Install glibc utilities にチェックを入れる - Target packages - Interpreter languages and scripting - python3 にチェックを入れる - core python3 modules - ssl にチェックを入れる - Libraries - Crypto - openssl support - ssl library - openssl を選択 - openssl - openssl binary にチェックを入れる - openssl additional engines にチェックを入れる - Networking - libcurl にチェックを入れる - curl binary にチェックを入れる - SSL/TLS library to use - OpenSSL を選択 - Networking applications - iptables にチェックを入れる - nginx にチェックを入れる - ngx_http_ssl_module にチェックを入れる - openssh にチェックを入れる - tcpdump にチェックを入れる ### 5. DHCP の設定 外部向けの通信をするためには IP アドレスの設定が必要になります。 QEMU でカーネルを起動する時に `-netdev` オプションで特に何も指定しない場合、 QEMU のビルトイン DHCP サーバーを利用できます。([参考](https://wiki.archlinux.org/index.php/QEMU#User-mode_networking)) Buildroot で作成したルートファイルシステム (`${WORKDIR}/buildroot/output/images/rootfs.ext4`) 内の、 `/etc/network/interfaces` に DHCP の設定があります。 ``` # interface file auto-generated by buildroot auto lo iface lo inet loopback ``` デフォルトではループバックインターフェイス (lo) のみ設定されており、このままでは外部宛の通信ができないので、 eth0 に IP アドレスが割り当てられるように以下の 2 行を書き加える必要があります。 ``` auto eth0 iface eth0 inet dhcp ``` 以下のコマンドで Buildroot で作成したルートファイルシステムをマウントして設定を追記します。 ``` $ sudo mkdir /mnt/buildroot $ sudo mount -o loop ${WORKDIR}/buildroot/output/images/rootfs.ext4 /mnt/buildroot $ echo -e '\nauto eth0\niface eth0 inet dhcp' | sudo tee -a /mnt/buildroot/etc/network/interfaces ``` ### 6. SSH の設定 Buildroot ではデフォルトユーザーが root で、パスワードも設定されていません。手元の OS であればちゃんと別のユーザーで公開鍵認証の設定をするところですが、 QEMU 上のテスト環境なのでパスワードなしで root にログインできるようにします。そのために `/etc/ssh/sshd_config` で以下の設定に書き換えます。 ``` PermitRootLogin yes PermitEmptyPasswords yes ``` 既にマウントしているファイルシステム内の `/etc/ssh/sshd_config` を書き換えます。(かっこつけて sed コマンドを使用していますが、初めは emacs で書き換えてました。) ``` $ sudo sed -i -e 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' \ -e 's/#PermitEmptyPasswords no/PermitEmptyPasswords yes/' \ /mnt/buildroot/etc/ssh/sshd_config $ sudo umount /mnt/buildroot ``` ### 7. QEMU でカーネルを起動 QEMU をインストールして、ちょっと長いコマンドでカーネルを起動します。カーネルのツリーや、ファイルシステムに応じて少しコマンドが変わるので、適宜変更してください。このコマンドではホストの 10022 番ポートを QEMU で起動したカーネルの 22 番ポートへフォワードするようになっています。 ``` $ sudo yum install -y qemu $ qemu-system-x86_64 -boot c -m 2049M \ -kernel ${WORKDIR}/net-next/arch/x86/boot/bzImage \ -hda ${WORKDIR}/buildroot/output/images/rootfs.ext4 \ -append "root=/dev/sda rw console=ttyS0,115200 acpi=off nokaslr" \ -serial stdio -display none \ -nic user,hostfwd=tcp::10022-:22 ``` ユーザー名は root でパスワードなしでログインできます。 ``` Welcome to Buildroot buildroot login: root # uname -r 5.8.0-rc1 ``` 別のターミナルを開いて SSH でもログインできます。 ``` $ ssh root@localhost -p 10022 ... # curl -kI https://kuniyu.jp HTTP/1.1 200 OK ... ``` 終了するには QEMU を起動したターミナルで Ctrl+C を押してください。 ``` # (Ctrl+C を押す) qemu-system-x86_64: terminating on signal 2 ``` ### 8. Alias の設定 QEMU のコマンドは当然覚えられませんし、毎回コピペして実行するのも面倒です。なのでエイリアスを登録して短いコマンドで実行出来るようにしています。私は `net-next` コマンドでカーネルを起動できるようにしています。 ``` $ echo "alias net-next='qemu-system-x86_64 -boot c -m 2049M -kernel ${WORKDIR}/net-next/arch/x86/boot/bzImage -hda ${WORKDIR}/buildroot/output/images/rootfs.ext4 -append \"root=/dev/sda rw console=ttyS0,115200 acpi=off nokaslr\" -serial stdio -display none -nic user,hostfwd=tcp::10022-:22'" >> ~/.bashrc $ source ~/.bashrc ``` ## おわりに カーネル開発を始めた半年ちょっと前、コードを書き換えてはビルドして、、、と行なっていたのですが、 GRUB の設定を間違えたり、間違ったコードを書いて EC2 がお釈迦になることが多々ありました。自分と同じようにカーネルに興味を持った方が同じ悲しみを味合わないよう、少しでも参考になれば幸いです。