Arm64用にビルドしたLinuxをBusyBoxを使って起動する on QEMU

はじめに

Armと戯れてえ~となったのでQEMUで環境を用意することにした. せっかくなので,Arm向けにLinuxをビルドして,BusyBoxで簡易Linuxディストリビューション 的なものを作成してQEMUで起動をしてみる. QEMUで何かしらやろうとすると毎回オプションに踊らされてる気もするので, これを機に一つ一つ調べながらやってみる.

実験環境

$ uname -mrsv
Linux 5.13.0-51-generic #58~20.04.1-Ubuntu SMP Tue Jun 14 11:29:12 UTC 2022 x86_64
$ qemu-system-aarch64 --version
QEMU emulator version 4.2.1 (Debian 1:4.2-3ubuntu6.23)
$ aarch64-linux-gnu-gcc --version
aarch64-linux-gnu-gcc (GNU Toolchain for the A-profile Architecture 10.2-2020.11 (arm-10.16)) 10.2.1 20201103

ディレクトリ構成

作業は qemu_arm というディレクトリで行う.

qemu_arm
├── busybox
├── hello_world
└── linux

Arm64向けLiuxの準備

まず,LinuxカーネルをArm64向けにビルドするために必要なツールをインストールする. ついでにQEMUもインストールしてなければしておく.

$ sudo apt install gcc-aarch64-linux-gnu qemu-system-arm qemu-user

次に,Linuxカーネルのソースコードを手元にクローンする.

$ git clone git@github.com:torvalds/linux.git

LinuxカーネルをArm64向けにビルドする.

$ cd linux
$ make ARCH=arm64 defconfig
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image dtbs -j`nproc`

ビルドが終わったら,

$ ls ./arch/arm64/boot
dts  Image  install.sh  Makefile

Image が作成されていることを確認.

QEMU起動! -カーネルパニック編-

さて,無事Linuxカーネルのビルドも終わったので早速起動してみる.

まず,QEMUで起動させるために,エミュレートするmachineを決める.

$ qemu-system-aarch64 -M ?
...
virt                 QEMU 4.2 ARM Virtual Machine (alias of virt-4.2)
...

-M ? のオプションで,qemuでエミュレート可能なmachine一覧が表示される. 今回はQEMU Virtual Machineである virt を指定する.

さて,machineが決まったので次はエミュレートするcpuを設定する. 同様に, -M オプションでmachineを指定したあと -cpu ? オプションで qemuでエミュレート可能なmachine一覧を表示できる.

$ qemu-system-aarch64 -M virt -cpu ?
...
  cortex-a15
  cortex-a53
  cortex-a57
  cortex-a7
  cortex-a72
...

virt の仕様については,実は https://qemu.readthedocs.io/en/latest/system/arm/virt.html にも書かれている. 今回は64bitのものを使用したいので, cortex-a72 を選択する.

あとは, -kernel オプションで,先程ビルドした Image ファイルを教えてあげれば,カーネルが起動するはずである.

-nographic オプションは,QEMUがGUIを立ち上げないために使用している.これはあってもなくても起動に影響はない.

では,早速上記のオプションを指定してQEMUを起動する.

$ qemu-system-aarch64 -M virt -cpu cortex-a72 -kernel ./arch/arm64/boot/Image -nographic

出力結果

...
 [    0.468264] Kernel Offset: disabled
 [    0.468369] CPU features: 0x800,00007810,00001086
 [    0.468617] Memory Limit: none
 [    0.468965] ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) ]---

あれ,カーネルがパニックを起こして終了してしまった.

これは,出力されたログから,ルートファイルシステムがないためカーネルがパニックを起こして終了していることがわかる. このことから,カーネルが起動後,マウントするためのファイルシステムを用意してあげれば,無事に起動できそうである.

忘れがちなのでメモしておくと, C-a x でQEMUを終了できる.

QEMU起動! -Hello world編-

さて,早速ファイルシステムを用意したいところだが,そもそもカーネルの起動後にどのようにしてシェルが起動するかを把握しなければ, このファイルシステムというのも何を用意すればよいかわからない.

このため,用意すべきファイルを知るためには,ブート後の流れを理解する必要がある. (今回はカーネル起動後の流れが理解できればよいため,電源が投入され,UEFIがブートローダを読み込み,カーネルをメモリに配置するまでの流れは割愛する)

init実行までの流れ

とりあえず,準備すべきファイルであるinitrdがわからないため,

$ man initrd

を読む.

DESCRIPTION
The special file /dev/initrd is a read-only block device. This device is a RAM disk that is initialized (e.g., loaded) by the boot loader before the kernel is started. The kernel then can use /dev/initrd’s contents for a two-phase system boot-up.

まず, initrd は,ReadOnlyのブロックデバイスである. ブロックデバイスとは,簡単にいうとCD-ROMとかSSDみたいにデータをある程度の塊でやりとりするデバイスである.(wikipedia)

In the first boot-up phase, the kernel starts up and mounts an initial root filesystem from the contents of /dev/initrd (e.g., RAM disk initialized by the boot loader). In the second phase, additional drivers or other modules are loaded from the initial root device’s contents. After loading the additional modules, a new root filesystem (i.e., the normal root filesystem) is mounted from a different device.

そして,このデバイスは一回目のブートアップ時にカーネルにより初期ルートファイルシステムとしてマウントされ, 二段階目では,初期ルートファイルシステムに含まれているものからドライバやモジュールがロードされる.


Boot-up operation
When booting up with initrd, the system boots as follows:

ここからは,initrdを使用したブートアップの詳細が書かている.

1.The boot loader loads the kernel program and /dev/initrd’s contents into memory.

1.カーネルプログラムと initrd の内容をメモリにロードする.

2.On kernel startup, the kernel uncompresses and copies the contents of the device /dev/initrd onto device /dev/ram0 and then frees the memory used by /dev/initrd.

2.カーネルは initrd の内容を展開し, /dev/ram0 というブロックデバイスにコピーする.そして, /dev/initrd が利用していたメモリを解放する.

3.The kernel then read-write mounts the device /dev/ram0 as the initial root filesystem.

3.カーネルは, /dev/ram0 を初期ルートファイルシステムとして読み書き可能なデバイスとしてマウントする.

4.If the indicated normal root filesystem is also the initial root filesystem (e.g., /dev/ram0) then the kernel skips to the last step for the usual boot sequence.

4. /dev/ram0 が通常時ルートファイルシステムとして指定されていれば,ブートシーケンスの最後のための最後のステップ(7番目)をスキップする.

5.If the executable file /linuxrc is present in the initial root filesystem, /linuxrc is executed with UID 0. (The file /linuxrc must have executable permission. The file /linuxrc can be any valid executable, including a shell script.)

5. /linuxrc が初期ルートファイルシステムに存在すれば,これをルート権限で実行する.ちなみに, /linuxrc は通常時ルートファイルシステムのマウントに必要なデバイスドライバ等をロードする役割が主である.

6.If /linuxrc is not executed or when /linuxrc terminates, the normal root filesystem is mounted. (If /linuxrc exits with any filesystems mounted on the initial root filesystem, then the behavior of the kernel is UNSPECIFIED. See the NOTES section for the current kernel behavior.)

6. /linuxrc が存在しない,もしくは実行が終了した場合,通常時ルートファイルシステムをマウントする.

7.If the normal root filesystem has a directory /initrd, the device /dev/ram0 is moved from / to /initrd. Otherwise, if the directory /initrd does not exist, the device /dev/ram0 is un‐ mounted. (When moved from / to /initrd, /dev/ram0 is not unmounted and therefore processes can remain running from /dev/ram0. If directory /initrd does not exist on the normal root filesystem and any processes remain running from /dev/ram0 when /linuxrc exits, the behavior of the kernel is UNSPECIFIED. See the NOTES section for the current kernel behavior.)

7.通常時ルートファイルシステムに /initrd ディレクトリがあれば, /dev/ram0/initrd に移動する. もし, /initrd が存在しなければ, /dev/ram0 はアンマウントされる.

8.The usual boot sequence (e.g., invocation of /sbin/init) is performed on the normal root filesystem.

8.通常のブートシーケンス( /sbin/init の実行)が通常時ルートファイルシステム上で実行される.


総括すると, initrd は,カーネル起動後に最初にマウントされるブロックデバイスであり, もし通常時ルートファイルシステムとして /dev/ram0 が指定されていた場合は,カーネル起動後に initrd に存在する /sbin/init が実行される.

また,ここでカーネルパラメータについて説明する. カーネルパラメータはブートローダがカーネルを起動する際にカーネルに渡すパラメータであり, 通常時ルートファイルシステムの指定や, /sbin/init の代わりに実行するプログラムの指定などが可能である.

以下に今回使用するカーネルパラメータについてまとめた.

カーネルパラメータ 内容
root 通常時ルートファイルシステムの指定
rdinit カーネル起動後に最初に起動するプログラムの指定 何も指定しなければ /sbin/init が実行される
console カーネルに接続するコンソールを指定

このカーネルパラメータにより,通常時ルートファイルシステムを /dev/ram0 を指定し,最初に起動するプログラムとして,実行したいプログラム名を示せば, initrd に配置した任意のプログラムをカーネル起動後に実行することが可能である.( console については後述)

hello_worldの作成

さて,カーネルブート後の流れも理解できたので,ブート後に起動するプログラム(上記の /sbin/init に相当)を用意する. 今回は,わかりやすいようにとりあえず Hello world を出力するプログラムを作成してみる.

また,作業は hello_world というディレクトリを作成し,この中で行う.

$ mkdir hello_world
$ cd hello_world

以下に作成したプログラムを載せている. Hello world を出力後,カーネルパニックを起こさないように,無限ループをさせている.

// hello_world.c
#include <stdio.h>

int main(int argc, char* argv[])
{
  printf("Hello world\n");
  while(1);
}

プログラムが用意できたので,Arm64向けにクロスコンパイルする. このとき,実行ファイル単体で実行できるようにライブラリを静的リンクしておく. ( -static )

$ aarch64-linux-gnu-gcc -static -o hello_world hello_world.c

早速実行して動作確認してみる.

$ qemu-aarch64 hello_world
Hello world

qemu-aarch64 コマンドにより,Arm64向けの実行ファイルをエミュレートすることができる. 実行後, Hello world が表示され,その後無限ループすることを確認できる.

これで,カーネルブート後に起動するプログラムは完成した. あとはこのプログラムをルートファイルシステムに組み込んでいく.

初期ルートファイルシステムの作成

/sbin/init に相当するプログラムである hello_world を作成したため,これを含む initrd を作成していく. initrd は,cpio形式のアーカイブファイルであり,基本的にはgzipで圧縮されている.(圧縮形式は複数サポートされているらしい) どのように圧縮されているかはカーネルが判断しており,圧縮しなくても問題なく認識される.このため,今回は圧縮せずに進める.

cpioは,引数に受け取ったファイルをアーカイブに書き出す.

$ find . | cpio -o > archive.cpio

とすることで,カレントディクレトリをツリーごと archive.cpio というファイルにアーカイブすることが可能である.(参考:wikipedia

-o はcpioのコピーアウトモード, -format=newc は,new ASCII(SVR4)フォーマットを指定している. よって,以下のコマンドにより,SVR4フォーマットで, hello_world のみを含む rootfs を作成している.

$ echo hello_world | cpio -o --format=newc > rootfs

さて,これで必要なファイルは全て揃ったので実行してみる.

QEMUでは, -append オプションにより,カーネルパラメータを設定することができる.ここで,先程説明した rootrdinit をそれぞれ指定する. また, console により接続するコンソールを指定できる.実は,1回目のQEMU起動の際,

[    0.090217] Serial: AMBA PL011 UART driver
[    0.111516] 9000000.pl011: ttyAMA0 at MMIO 0x9000000 (irq = 13, base_baud = 0) is a PL011 rev1
[    0.112436] printk: console [ttyAMA0] enabled
[    0.116406] printk: console [ttyAMA0] printing thread started

のようなメッセージが流れており, virt の仕様にも書かれているUARTでのシリアル接続は ttyAMA0 に接続することで可能となる. 実はカーネルが自動検出しているみたいでこのオプションは指定せずとも問題ないものの,わかりやすいように明示的に指定しておく.

$ cd ../
$ qemu-system-aarch64 -M virt -cpu cortex-a72\
 -kernel ./linux/arch/arm64/boot/Image\
 -initrd ./hello_world/rootfs\
 -append "console=ttyAMA0 root=/dev/ram0 rdinit=/hello_world"\
 -nographic

出力結果

...
 [    0.482414] Run /hello_world as init process
 Hello world

無事, hello_world が実行され,カーネル起動後に Hello world が表示された.

これで,Linuxカーネルが起動後に最初のプロセスを実行するまでの流れを理解できたため, 最後にBusyBoxを用いてシェルの起動まで行う.

QEMU起動!-BusyBox編-

BusyBoxのビルド

それでは,早速BusyBoxをビルドしていく. その前にBusyBoxとは,UNIXコマンドが単一の実行ファイルに詰め込まれているプログラムで,十徳ナイフとか言われている. つまりこの章ではこのプログラムを利用することで,楽して汎用ツールをインストールしたルートファイルシステムを構築してやろうということである.

では,はじめにソースコードをダウンロードする.

$ git clone git://git.busybox.net/busybox

次に,BusyBoxをArm64用にクロスコンパイルしていく. このとき,実行ファイルを静的リンクするため, menuconfig で,「Build static binary (no shared libs)」を有効にする必要がある.

$ cd busybox/
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig
# Setting->Build static binary (no shared libs)で有効化する
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- install

さて,コンパイルが完了すると, _install というディレクトリに簡単なファイルシステムが構築されている. このため, _install に移動し, rootfs を作成する.

$ cd _install
$ find . | cpio -o --format=newc > ../rootfs

必要なツールをインストールした rootfs も作成できたため,早速QEMUを起動する.

$ cd ../../
$ qemu-system-aarch64 -M virt -cpu cortex-a72\
 -kernel ./linux/arch/arm64/boot/Image\
 -initrd ./busybox/rootfs\
 -append "console=ttyAMA0 root=/dev/ram0 rdinit=/sbin/init"\
 -nographic

出力結果

...
 [    0.541625] Run /sbin/init as init process
 can't run '/etc/init.d/rcS': No such file or directory

 can't open /dev/tty2: No such file or directory
 can't open /dev/tty3: No such file or directory
 can't open /dev/tty4: No such file or directory
...

無事, /sbin/init を起動するところまでは進んだが,どうやら /etc/init.d/rcS というファイルが存在しないため,処理が止まってしまっている. また, /dev ディレクトリも作成しておらず, udev も実行していないため,デバイスが読み込めず,エラーメッセージが延々と表示される.

とりあえず,必要となるディレクトリを作成していく.

$ cd busybox/_install
$ mkdir proc
$ mkdir sys
$ mkdir dev
$ mkdir -p etc/init.d

また,dev/nullデバイスも作成しておく.(詳細:https://www.kernel.org/doc/html/latest/admin-guide/devices.html

$ sudo mknod dev/null c 1 3

また, udev に相当するコマンドである mdev について知るため, busybox/docs/mdev.txt を読んでみる.


Basic Use


Mdev has two primary uses: initial population and dynamic updates. Both require sysfs support in the kernel and have it mounted at /sys. For dynamic updates, you also need to have hotplugging enabled in your kernel.

Here’s a typical code snippet from the init script:
[0] mount -t proc proc /proc
[1] mount -t sysfs sysfs /sys
[2] echo /sbin/mdev > /proc/sys/kernel/hotplug
[3] mdev -s

…ということなので, etc/init.d/rcS に,必要なディレクトリをマウントする処理を記述していく. なお,[2]については今回は必要ないので記述しない.ちなみに mdev-s は実行の意味.

#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
/sbin/mdev -s

最後に,作成した etc/init.d/rcS に実行権限を与え, rootfs を作成する.

$ chmod +x etc/init.d/rcS
$ find . | cpio -o --format=newc > ../rootfs

これで, /sbin/init を起動するために必要な準備が終わったので,もう一度カーネルを起動してみる.

$ cd ../../
$ qemu-system-aarch64 -M virt -cpu cortex-a72\
 -kernel ./linux/arch/arm64/boot/Image\
 -initrd ./busybox/rootfs\
 -append "console=ttyAMA0 root=/dev/ram0 rdinit=/sbin/init"\
 -nographic

出力結果

...
 [    0.528404] Run /sbin/init as init process

 Please press Enter to activate this console.
 / # uname -a
 Linux (none) 5.19.0-rc3-00048-gde5c208d533a #1 SMP PREEMPT Thu Jun 23 15:44:20 JST 2022 aarch64 GNU/Linux
 / # ls
 bin      etc      proc     sbin     usr
 dev      linuxrc  root     sys
 / #

無事カーネル起動後 /sbin/init が起動し,シェルからコマンドを実行することができた.

おわりに

身近であるものの,あまり使ったことなかったBusyBoxを使って簡単なLinuxディストリビューションを作成した. 身近なディストリビューションは複雑で,様々なことをしてくれているものの,具体的に何をしているのか意識することはあまりない. そこで,こういう簡単なものを作ることで,それらのディストリビューションが何をしてくれているかよく理解でき,ありがたみを知ることができた. せっかくArm環境を作成できたのでArmで遊んでいくぞ.

参考

LinuxカーネルをARM向けにビルドしてQEMUで起動する 〈https://blog.hirokikana.com/dev/runnning-linux-kernel-for-arm-on-qemu/
QEMUでARM64用Linuxカーネルを起動する 〈https://leavatail.hatenablog.com/entry/2020/01/26/232646
initramfsについて 〈https://qiita.com/akachochin/items/d38b538fcabf9ff80531
新装改訂版 Linuxのブートプロセスをみる 〈https://amzn.to/3HWVFIG

mc4nf
mc4nf

軽率にFollow me!:)