すでに多くの方が似たような取り組みを行っていてブログ記事 1 2 3 として丁寧にまとめられているように、 やはりこういった環境を手元にさっと作れることの意味は大きいと思う。 ここではざっくりとした仕組みを記録しておく。 成果物をスクリプトとしてまとめGithubにあげている。
特徴
- CentOS6、CentOS7、Ubuntu20.04などメジャーなディストリビューション向けカーネルのビルドに対応しているので、実務よりの応用ができる
- Busyboxを使ってユーザランドをメモリ上に展開するので、起動のたびにピュアでミニマルな環境を作れる
- SSHログインや外部ネットワーク疎通が可能なので、他システムとの連携が絡む動作を検証しやすい
- GDBを使ったデバッグによってカーネル内部のデータを参照できる
- 現時点ではx86/64のみに対応している
カーネルのビルド
カーネルのビルドは端的に言えば、ビルド設定を.config ファイルに記述し、makeコマンドを叩くことに対応する。.config はテキストファイルなので適用なエディタでも編集できるが、専用のコマンド(make oldconfig、make defconfig、make menuconfig など)が用意されているので、それを使うことが多い。カーネルはアップストリームのものと各ディストリビューションが手を加えたものがあるがここでは以下の全てのカーネルをビルドできるよう環境を整えた。
- upstream (kernel v2.6.39)
- centos6 (kernel v2.6.32-754.35.1.el6)
- centos7 (kernel v3.10.0-1160.13.1.el7)
- ubuntu20.04 (kernel v5.4.0-65.73)
最近のGCCで古いカーネルをコンパイルするのは難儀なので、カーネルバージョンごとにビルド専用Dockerイメージを用意した。例えばカーネルv2.6.39はCentOS6のビルド環境を使ってビルドすることにした。
FROM ghcr.io/buddying-inc/centos68:latest
RUN sed -i "s|#baseurl=|baseurl=|g" /etc/yum.repos.d/CentOS-Base.repo \
&& sed -i "s|mirrorlist=|#mirrorlist=|g" /etc/yum.repos.d/CentOS-Base.repo \
&& sed -i "s|http://mirror\.centos\.org/centos/\$releasever|https://vault\.centos\.org/6.10|g" /etc/yum.repos.d/CentOS-Base.repo
RUN yum install -y gcc perl glibc-static kernel kernel-devel \
autoconf zlib-devel zlib-static openssl-static openssl-devel
上のDockerfileをもとに buildenv-v2.6.39 という名前のDockerイメージを作る。これがビルド専用環境に対応する。 あとはLinuxカーネルソースツリーをDockerコンテナにアタッチして、make bzImageコマンドを叩けば良い。
sudo docker run --rm -v linux-v2.6.39:/tmp \
buildenv-v2.6.39 \
/bin/bash -c "cd tmp; make bzImage"
ユーザランドのビルド
ユーザランドはRAMディスクで完結するようBusyboxをベースに作成した。 Busyboxは基本的なCLIツールを同梱したシングルバイナリで、シンボリックリンクによってそれぞれのコマンドの振る舞いができる。
ls -la /bin/ls /bin/vi /bin/cat /bin/busybox
# -rwxr-xr-x 1 0 0 2332560 Jul 6 05:31 /bin/busybox
# lrwxrwxrwx 1 0 0 7 Jul 6 05:31 /bin/cat -> busybox
# lrwxrwxrwx 1 0 0 7 Jul 6 05:31 /bin/ls -> busybox
# lrwxrwxrwx 1 0 0 7 Jul 6 05:31 /bin/vi -> busybox
また、DHCPクライアントやSSHサーバの設定を行なったので、QEMU上の仮想マシンから外部ネットワークへの疎通やSSHによるログインができる。ビルド手順は比較的長いので、https://github.com/bobuhiro11/understanding-the-linux-kernel/tree/main/busybox を参照。
QEMU上での実行
カーネルイメージ bzImage やRAMディスク initrd を、QEMU上で起動する。
カーネルアドレス領域のランダム化はnokaslr
をブートパラメータに追記することで無効化しておく。
仮想マシンの22番ポートはSSH用に使っている。これをポートフォワーディングによってホスト側の10022番ポートに対応させておく。
# 仮想マシンの起動
qemu-system-x86_64 -kernel linux-v2.6.39/arch/x86/boot/bzImage \
-m size=512 -initrd busybox/initrd --nographic \
--append "root=/dev/ram rw console=ttyS0 rdinit=/sbin/init init=/sbin/init nokaslr" \
-nic user,model=virtio-net-pci,hostfwd=tcp::10022-:22
# SSHログイン
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@localhost -p 10022
もしデバッガーGDBを使いたい場合には、QEMUの起動オプションに-gdb tcp::10000 -S
を追加した状態で起動し、
以下のようにGDBからアタッチすれば良い。
gdb --directory=./linux-v2.6.39 ./linux-v2.6.39/vmlinux \
-ex 'target remote localhost:10000'
おしまい。 新しめのカーネルから、書籍「詳解Linuxカーネル 第3版」で説明されている比較的古いカーネルまで網羅してカーネルビルドできる環境が整ったと思う。 少しでも参考になれば幸いです。