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
オプションにより,カーネルパラメータを設定することができる.ここで,先程説明した root
と rdinit
をそれぞれ指定する.
また, 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〉