手作り組み込み Linux -- QEMU 編

組み込み Linux の勉強のために Mastering Embedded Linux Programming という本を読んでいます。今まで謎だった部分がはっきりして非常に楽しい。この本では Yocto などの便利ツールを使わず必要な一つ一つパーツを手作りして Linux 環境を組み立てます。とは言え面倒くさくならない程度の努力で作れます。読書メモとしてここでは、ARM 向けの最低限の Linux 環境を作成して QEMU で動かしてみます。後ほど BeagleBone Black でも試す予定です。

Mastering Embedded Linux Programming の内容を実際に試したい場合は結構落とし穴があるので以下サポートページの Errata を読んだ方が良いです。

https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition

ちなみに今回作成したカーネルとファイルシステムは合わせて 25M 程度です。かつての Linux はフロッピー一枚で動いたらしいので頑張ってみても面白いと思います。

## 組み込み Linux に必要な部品

組み込み Linux には以下の部品が必要です。

* ツールチェイン
    * 組み込み Linux は非力なので普通は開発マシンでターゲットマシンのバイナリを作成するクロスコンパイルを行います。そのための部品を Toolchain と呼びます。ここではツールチェインを構築するために crosstool-NG を使います。開発環境として私は Ubuntu 20.04 LTS を使いました。
* ブートローダー
    * ブートローダーとは、ターゲットマシンが起動してからカーネルを読み込む最初のソフトウェアです。QEMU はブートローダーを内蔵しているので今回は省略します。
* カーネル
    * カーネルとは、アプリとハードウェアの仲立ちをするソフトウェアです。ツールチェインを使って作成します。
* ルートファイルシステム
    * ルートファイルシステムとは、アプリやデータが置いてある場所です。今回は busybox を使って /bin/sh が動くだけの簡単なルートファイルシステムを作ります。

## 作業環境

Ubuntu 20 の場合以下のパッケージをインストールします。

    sudo apt-get install autoconf automake bison bzip2 cmake flex g++ gawk gcc gettext git gperf help2man libncurses5-dev libstdc++6 libtool libtool-bin make patch python3-dev rsync texinfo unzip wget xz-utilsw libssl-dev u-boot-tools

説明のため、開発マシンの作業スペースはこんな感じになってるとします。

* `~/src`: ソースコード置き場
    * `~/src/linux-stable/`: カーネルソース
* `~/rootfs`: ルートファイルシステム置き場
* `~/x-tools/`: ツールチェイン置き場
* `~/initramfs.cpio.gz`: 完成したルートファイルシステム

## crosstool-NG を使ったツールチェインの作成

ツールチェインの作成には crosstool-NG 自体のビルドと crosstool-NG を使ったターゲットの環境に合わせたツールチェイン作成の二つのステップがあります。まず crosstool-NG を作ります。`./configure --prefix=${PWD}` を指定して作業ディレクトリにそのまま crosstool-NG をインストールしてしまいます。

ここでは本の通りやりたかったので crosstool-ng-1.24.0 を使いましたが ERRATA によると依存パッケージの URL が変わってしまっているので最新版を使ったほうが良いそうです。

```
cd ~/src
git clone https://github.com/crosstool-ng/crosstool-ng.git
cd crosstool-ng
git checkout -b crosstool-ng-1.24.0 crosstool-ng-1.24.0
./bootstrap
./configure --prefix=${PWD}
make install
```

crosstool-NG は多くのターゲットに対応していますが、付属するサンプルを改造して希望のターゲットにカスタマイズします。付属サンプルは以下のコマンドで表示されます。

    bin/ct-ng list-samples

ここでずらずらと対応するツールチェインの名前が表示されます。これを prefix と呼びます。実際のツール名は (prefix)-gcc のように prefix とツール名をあわせたものです。このように GNU では prefix でツールチェインを見分けます。prefix は３つか４つの tuple で構成されており、例えば `arm-cortex_a8-linux-gnueabi` の場合以下の意味になります。

* CPU: `arm` CPU アーキテクチャ。追加でエンディアンがつく時がある。
    * `el` がつけば littel-endian
    * `eb` が付けば big-endian
* Vendor: `cortex_a8` ツールチェインの作者。
* Kernel: `linux` カーネル。
* Operating System: `gnueabi` ユーザースペース部品 `gnu` か `musl`
    * `gnueabi`: glibc + Extended Application Binary Interface 浮動小数点引数を整数レジスタで渡す。
    * `gnueabifh`: glibc + Extended Application Binary Interface Hard-Float 浮動小数点引数を浮動小数点レジスタで渡す。
    * `musleabi`: musl libc https://musl.libc.org/ + eabi
    * `musleabihf`: musl libc https://musl.libc.org/ + eabihf

ここでは QEMU でも動く ARM926EJ-S 向けのクロスコンパイル環境 `arm-unknown-linux-gnueabi` を作ります。`show-` で内容を確認してから `menuconfig` で設定を読み込んで CUI で編集します。

```
bin/ct-ng show-arm-unknown-linux-gnueabi
bin/ct-ng arm-unknown-linux-gnueabi
bin/ct-ng menuconfig
```

変更箇所は一点だけです。もともと read only なのを書き込めるようにします。結果は `.config` に保存されます。

* In Paths and misc options > Render the toolchain read-only (CT_PREFIX_DIR_RO): 無効
    * デフォルトで Read only なのを書き込み可能にする。

`.config` の差分は以下だけなので CUI が面倒であれば直接ファイルを編集しても良いです。

```
# CT_PREFIX_DIR_RO is not set
```

次に、ビルドに必要な各コマンドのソースコードをダウンロードします。ソースコードは `~/src` にキャッシュされます。

    bin/ct-ng source

ここで crosstool-ng-1.24.0 では古い URL を使っているためエラーが出ます。最新版では大丈夫らしいですが、もし URL が更に変更された場合のため回避策を紹介します。

`build.log` に出たエラーをよく読み、足りない物を探して ~/src に手動でダウンロードします。成功するまで `bin/ct-ng source` を再試行すれば大丈夫です。(参照: https://github.com/crosstool-ng/crosstool-ng/issues/1625)

```
wget -P ~/src https://libisl.sourceforge.io/isl-0.20.tar.bz2
wget -P ~/src https://github.com/libexpat/libexpat/releases/download/R_2_2_6/expat-2.2.6.tar.bz2
```

ツールチェインのビルドには 30分以上かかるのでゆっくり待ちます。

    in/ct-ng build

ツールチェインは `~/x-tools/arm-unknown-linux-gnueabi` にできます。もし別のツールチェインを作るなら、その前に作業ディレクトリを掃除してください。

    bin/ct-ng distclean

### おまけ: 作ったツールチェインで Hello world

動作確認として、作ったツールチェインで簡単なプログラムをビルドします。PATH を設定すれば使えるようになります。

```
PATH=~/x-tools/arm-unknown-linux-gnueabi/bin:$PATH
cat <<EOT > helloworld.c
#include <stdio.h>
int main() {
  printf("Hello, World!\n");
  return 0;
}
EOT
arm-unknown-linux-gnueabi-gcc helloworld.c -o helloworld
```

gcc の設定情報の確認は次のように確認できます。

    arm-unknown-linux-gnueabi-gcc -v

ライブラリの位置の起点になる sysroot のは次のように確認できます。

    arm-unknown-linux-gnueabi-gcc -print-sysroot

## QEMU 用に Linux カーネルを作成

QEMU ではブートローダーが不要なので次にカーネルをビルドします。ソースコードと開発環境の準備を行います。https://www.kernel.org/ を見て longterm のバージョンを選びます。ここでは linux-5.4.x を選びました。

```shell
git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git
cd linux-stable
git checkout linux-5.4.y
PATH=~/x-tools/arm-unknown-linux-gnueabi/bin:$PATH
```

前に作業したことある人は mrproper を使ってソースコードの掃除を行います。。参考までに以下の掃除用 make target があります。


* clean: ほとんどの一時ファイルを消す
* mproper: さらに .config も消す。
* distclean: エディタのバックアップファイルやパッチファイルや全部のカスを消す。

```
make ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- mrproper
```

QEMU では ARM Versatile PB という CPU をエミュレートするそうなので、対応する設定を読み込みます。

    make ARCH=arm versatile_defconfig

必要に応じてカスタマイズします。CONFIG_LOCALVERSION を変更すると後で uname などで確認できて便利です。

```shell
$ vi .config

# 以下変更
CONFIG_LOCALVERSION="-propella-1.0"
# 変更ここまで。手動で .config を書き換えた場合 make oldconfig で確認した方が良いらしい。

$ make ARCH=arm oldconfig
```

カーネルのフォーマットには色々ありますが、QEMU 用には zImage を使います。結果は arch/arm/boot/zImage にできます。

```
$ make -j4 ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- zImage
$ make ARCH=arm kernelrelease
5.4.241-propella-1.0
```

デバイスツリーを作ります。結果は ./arch/arm/boot/dts/ にできます。

    make ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- dtbs

カーネルモジュールを作ります。結果はあちこちにできます。今回は使わないので作らなくても良いです。

    make -j4 ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- modules

いよいよ qemu でカーネルを動かしてみましょう。

```
$ QEMU_AUDIO_DRV=none qemu-system-arm -m 256M -nographic -M versatilepb -kernel arch/arm/boot/zImage -append "console=ttyAMA0,115200" -dtb arch/arm/boot/dts/versatile-pb.dtb
...
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) ]---
```

Kernel panic になると成功です! Ctrl-A と x で終了します。ルートファイルシステムが無いので panic が期待通りというわけです。

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

### 大枠のディレクトリ構造を作る

これからルートファイルシステムを作成するために、必要なディレクトリを作ります。

```shell
mkdir ~/rootfs
cd ~/rootfs
mkdir bin dev etc home lib proc sbin sys tmp
mkdir -p usr/bin usr/lib usr/sbin
mkdir -p var/log
```

### Busybox のビルドとインストール

Linux には沢山のコマンドが必要ですが、Busybox は一つのコマンドで Linux の実行に必要なほとんどのコマンドの役目を果たす凄いコマンドです。これだけでルートファイルシステムが出来上がってしまいます。

```shell
git clone git://busybox.net/busybox.git
cd busybox
git checkout -b 1_31_1 1_31_1
make distclean
make defconfig
```

カーネルと同じように busybox も `make menuconfig` でカスタマイズ可能ですが、ここでは修正なしで使います。ただデフォルトでインストール位置が `./_install` になっているので、上で作った `~/rootfs` にシンボリックリンクを貼ります。ビルドとインストールはすぐ終わります。

```shell
ln -s ~/rootfs _install
make ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- install
```

これで `ls -l ~/rootfs` すると沢山のコマンドが busybox へのシンボリックリンクとしてインストールされている事が分かります。

```shell
$ ls -l ~/rootfs/bin/ | head
total 1056
lrwxrwxrwx 1 tyamamiya tyamamiya       7 Apr 28 13:20 arch -> busybox
lrwxrwxrwx 1 tyamamiya tyamamiya       7 Apr 28 13:20 ash -> busybox
lrwxrwxrwx 1 tyamamiya tyamamiya       7 Apr 28 13:20 base64 -> busybox
-rwxr-xr-x 1 tyamamiya tyamamiya 1080412 Apr 28 13:20 busybox
lrwxrwxrwx 1 tyamamiya tyamamiya       7 Apr 28 13:20 cat -> busybox
lrwxrwxrwx 1 tyamamiya tyamamiya       7 Apr 28 13:20 chattr -> busybox
lrwxrwxrwx 1 tyamamiya tyamamiya       7 Apr 28 13:20 chgrp -> busybox
lrwxrwxrwx 1 tyamamiya tyamamiya       7 Apr 28 13:20 chmod -> busybox
lrwxrwxrwx 1 tyamamiya tyamamiya       7 Apr 28 13:20 chown -> busybox
```

busybox を動かすには、コマンド本体だけでなく共有ライブラリも rootfs に置く必要があります。必要な共有ライブラリは `arm-unknown-linux-gnueabi-readelf` コマンドで調べられます。

```shell
$ arm-unknown-linux-gnueabi-readelf -a busybox | grep "program interpreter"
      [Requesting program interpreter: /lib/ld-linux.so.3]
$ arm-unknown-linux-gnueabi-readelf -a busybox | grep "Shared library"
 0x00000001 (NEEDED)                     Shared library: [libm.so.6]
 0x00000001 (NEEDED)                     Shared library: [libresolv.so.2]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
```

ということで、最低 `ld-linux.so.3`, `libm.so.6`, `libresolv.so.2`, `libc.so.6` の四つが必要です。これらのファイルはツールチェインの `~/x-tools/arm-unknown-linux-gnueabi/arm-unknown-linux-gnueabi/sysroot` の中にあります。シンボリックリンクで他のファイルを参照しているので注意深くコピーします。

```shell
$ SYSROOT=$(arm-unknown-linux-gnueabi-gcc -print-sysroot)
$ cp -a $SYSROOT/lib/libm[-.]* $SYSROOT/lib/ld[-.]* $SYSROOT/lib/libc[-.]* $SYSROOT/lib/libresolv[-.]* ~/rootfs/lib
$ ls -l ~/rootfs/lib
total 20664
-rwxr-xr-x 1 tyamamiya tyamamiya  1352768 Apr 26 14:41 ld-2.29.so
lrwxrwxrwx 1 tyamamiya tyamamiya       10 Apr 26 14:41 ld-linux.so.3 -> ld-2.29.so
-rwxr-xr-x 1 tyamamiya tyamamiya 16676696 Apr 26 14:41 libc-2.29.so
lrwxrwxrwx 1 tyamamiya tyamamiya       12 Apr 26 14:41 libc.so.6 -> libc-2.29.so
-rwxr-xr-x 1 tyamamiya tyamamiya  2680096 Apr 26 14:41 libm-2.29.so
lrwxrwxrwx 1 tyamamiya tyamamiya       12 Apr 26 14:41 libm.so.6 -> libm-2.29.so
-rwxr-xr-x 1 tyamamiya tyamamiya   440660 Apr 26 14:41 libresolv-2.29.so
lrwxrwxrwx 1 tyamamiya tyamamiya       17 Apr 26 14:41 libresolv.so.2 -> libresolv-2.29.so
```

### デバイスノードの作成

最後に `/bin/sh` を動かすのに必要な二つのデバイスを `mknod` で作成します。これだけは root ユーザで作成する必要があります。

```
sudo mknod -m 666 ~/rootfs/dev/null c 1 3
sudo mknod -m 600 ~/rootfs/dev/console c 5 1
```

mknod の引数の意味は

    mknod <name> <type> <major> <minor>

となっています。type major minor がどんなデバイスに割り当てられているかは https://www.kernel.org/doc/Documentation/admin-guide/devices.txt を御覧ください。

### RAM filesystem の作成

ファイルシステムが完成したら、ブートローダーから読み込める initramfs という形にします。`cpio` というプログラムを使ってアーカイブを作成します。ここで、`--owner` オプションを使うと所有者が root ユーザになります。`gzip` で圧縮して完成です。

```
cd ~/rootfs
find . | cpio -H newc -ov --owner root:root > ../initramfs.cpio
cd ..
gzip initramfs.cpio
```

## QEMU で手作り Linux を実行する。

いよいよ手作り Linux を実行しましょう! `-append` 引数に `rdinit=/bin/sh` を指定するとカーネルが最初に実行するプログラムを指定できます。

```shell
QEMU_AUDIO_DRV=none qemu-system-arm \
    -m 256M \
    -nographic \
    -M versatilepb \
    -kernel ~/src/linux-stable/arch/arm/boot/zImage \
    -append "console=ttyAMA0,115200 rdinit=/bin/sh" \
    -dtb ~/src/linux-stable/arch/arm/boot/dts/versatile-pb.dtb \
    -initrd ~/initramfs.cpio.gz
```

`# ` と表示されれば成功です。普通に linux のコマンドを実行できます。ただ、`ps` コマンドがうまく動きません。

以下のコマンドで `/proc` と `/sys` をマウントすると `ps` が動くようになります。

```shell
# mount -t proc proc /proc
# mount -t sysfs sysfs /sys
# ps
PID   USER     TIME  COMMAND
    1 0         0:01 /bin/sh
    2 0         0:00 [kthreadd]
    3 0         0:00 [kworker/0:0-eve]
    4 0         0:00 [kworker/0:0H]
...
```

ここで `proc` と `sysfs` は特殊なファイルシステムで、データの実態がありません。このファイルシステムの内容はデバイスの状態を元にカーネルが動的に生成します。なので `~/rootfs` の中には空のディレクトリだけ作っておきました。

一通り飽きて exit するとまた panic するので Ctrl-A x で終了します。

ということで、文章にすると長いですが案外簡単に組み込み Linux を作成する事ができました。