すでに多くの方が似たような取り組みを行っていてブログ記事 1 2 3 として丁寧にまとめられているように、 やはりこういった環境を手元にさっと作れることの意味は大きいと思う。 ここではざっくりとした仕組みを記録しておく。 成果物をスクリプトとしてまとめGithubにあげている。

bobuhiro11/understanding-the-linux-kernel - 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版」で説明されている比較的古いカーネルまで網羅してカーネルビルドできる環境が整ったと思う。 少しでも参考になれば幸いです。