Raspberry Pi で U-boot/vmlinux/initramfs を作ってブート



「RaspberryPi の U-boot をクロスコンパイルする」
https://qiita.com/nanbuwks/items/4f2d676d2c5696c33c04
では、RaspberryPi 3 用の  U-boot を PC 上の Ubuntu 20.04 でビルドしました。
今回は、更に進めて initramfs まで進めます。

# Linux の起動

PC で Linux を起動する場合、一般的に以下のようになっています。

- BIOS や UEFI やブートコード
- GRUB や U-boot 
- vmlinuz + initramfs
- ルートファイルシステムをマウント
- ルートファイルシステム上の /sbin/init や systemd を実行

BIOSやUEEFIやGRUBはPCで用いられ、組み込みではブートコードやU-bootを使うことが多いです。

## 組み込み Linux での起動

以下のような流れが多く採用されています。

- ブートコード
- U-boot 
- vmlinuz + initramfs
- ルートファイルシステムをマウント
- ルートファイルシステム上の /sbin/init や systemd を実行

# Raspberry Pi での起動

 RaspberryPi  は組み込みの範疇となりますが、組み込みで標準的に使われる u-boot は使われておらず独特な起動方法となります。

Raspberry Pi / 2 / 3 の場合
- ROM から GPU の DPS コアのブートストラップを起動
- GPU は SDカードの第1パーティションから bootcode.bin を起動
- GPU は bootcode.bin に沿って start.elf を起動、start.elf は HDMI 出力に GPU のテストパターン表示(虹色パターン)
- GPU は start.elf に沿って config.txt を読み込み、Liux カーネル(RaspberryPi3の場合 SD カードの第1パーティション中の Kernel7.img)をメモリにロード
- CPU起動
- Linux カーネルは initramfs を読み込み、ROOTパーティションをマウント、init プロセスを起動


cf., https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#boot-sequence

Raspberry Pi 4 の場合
- ROM から GPU の DPS コアのブートストラップを起動
- GPU は EEPROM から bootloader を起動、起動メニューを HDMI 出力に表示
- GPU は bootloader に沿って SD カードの 第1パーティーションから start4.elf を起動、start4.elf は HDMI 出力に GPU のテストパターン表示(虹色パターン)
- GPU はstart7.elf に沿って EEPROM から config , SDカードの第1パーティションから config.txt を読み込み、Liux カーネル(SD カードの第1パーティション中の Kernel7l.img または Kernel8.img )をメモリにロード
- CPU起動
- Linux カーネルは initramfs を読み込み、ROOTパーティションをマウント、init プロセスを起動

cf., https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#raspberry-pi-4-boot-flow

RaspberryPi はオプションで様々な起動方法がありますが、SDカードから起動するスタンダードな起動フローが上記のものとなります。

# 今回の目的

Raspberry Pi の起動フローは上記のように独特ですが、今回は上記の方法をアレンジして U-boot を間に挟み、U-boot 以降は組み込みLinuxのスタンダードな流れで起動するようにします。

- ROM から GPU の DPS コアのブートストラップを起動
- GPU は SDカードの第1パーティションから bootcode.bin を起動
- GPU は bootcode.bin に沿って start.elf を起動
- GPU は start.elf に沿って config.txt を読み込み、u-boot (SD カードの第1パーティション中の「 u-boot.bin 」)をメモリにロード
- CPU起動
- CPU は u-boot に沿って Linux カーネル、デバイスファイル、initramfs を読み込み (SD カードの第1パーティション中に格納)、メモリにロード
- Linux カーネル起動
- init プロセスを起動


すべてを自前で作りたいのですが、最初の方に使う

- ROM 中の GPU のブートストラップ (GPUコード)
- SDカード中の bootcode.bin (GPUコード)
- SDカード中の start.elf (GPUコード)

はオープンソースではなく、 Raspberry Pi 専用に事前に用意されているものを使います。
cf.,https://elinux.org/RPi_Software#GPU_bootloaders
ちなみに、Raspberry Pi 3 ではここで動作するのは ARM のコードではなく Broadcom BCM2837 ( Raspberry Pi 3 の場合)／Broadcom BCM2837B0 ( Raspberry Pi 3 + の場合 ) SOC に統合されている Broadcom VideoCore IV GPU 用のコードになります。

その後に使用する以下のものはオープンソースのものを使い、自前で作成します。

- SDカード中にある config.txt。( start.elf に u-boot.bin を読み込ませる指示をするためのテキストファイル)
- SDカード中の u-boot.bin。(ARMコード)
- SDカード中の Image。これは Linux カーネルイメージ。(ARMコード)
- SDカード中の initramfs イメージ。(RAMに展開されるファイルシステムを圧縮したもの)

これらを作成、実行は RaspberryPi の実機で行います。
作成にあたっては、RaspberryPi でも可能ですが時間がかなりかかりますので今回は PC 上にクロス環境を構築して作成します。クロス環境は Ubuntu Linux および Windows で確認しています。Macの場合は手元に環境が無いのでチャレンジしていませんが、基本的に同じようにして作成できるはず? 

理解できるよう、作業していきます。

# 元ネタ


「Boot a Raspberry Pi 4 using u-boot and Initramfs」
https://hechao.li/2021/12/20/Boot-Raspberry-Pi-4-Using-uboot-and-Initramfs/
を元にします。

元ネタは Raspberry Pi 4 ですが、本稿では Raspberry Pi 3 をターゲットとします。

# 環境

- Ubuntu 22.04 LTS ja デスクトップ および WSL2
- Raspberry Pi 3
- マイクロ SD カード

# WSL2

の人は以下のようにしてインストール、Ubuntu が使えるようにしておきます。
https://qiita.com/nanbuwks/items/e3db5fe9c03c76a62227


# SDイメージを用意する

元ネタの2. Preparation は実際のSDカードですが、今回はイメージファイルを作成して作業します。
「Ubuntu / WSL2 でディスクイメージファイルを作成」
https://qiita.com/nanbuwks/items/11c296a26a17c4ed3129
のように用意してください。

# ツールチェイン

あらかじめ、以下のようにインストールしておきます。


元ネタの「3. Toolchain」を参考に、以前記した以下の記事のように作業をします。


「RaspberryPi 用のクロスツールチェインを Ubuntu 上にビルド」
https://qiita.com/nanbuwks/items/e19887e6a66262d325ca

のとおりですが、以下の2点のみ変更します。

ひとつは git checkout において、

```
$ git checkout crosstool-ng-1.25.0 -b 1.25.0
```

とします。

もうひとつは
```
$ ./ct-ng menuconfig
```
で Operating System を確認したら、 5.16.9 になっていました。

後ほど Linux カーネルをダウンロードしますが、テスト時は rpi-5.15.y を使うことにしたのでその場合問題が起こります。なのでカーネルソースよりも古いバージョンにします。今回は以下のようにかなり古いバージョンに設定しました。

```

   ┌────────────────────── Version of linux ───────────────────────┐
   │  Use the arrow keys to navigate this window or press the      │  
   │  hotkey of the item you wish to select followed by the <SPACE │  
   │  BAR>. Press <?> for additional information about this        │  
   │ ┌───────────────────────────^(-)────────────────────────────┐ │  
   │ │                       ( ) 5.1.21                          │ │  
   │ │                       ( ) 5.0.19                          │ │  
   │ │                       (X) 4.20.17                         │ │  
   │ │                       ( ) 4.19.229                        │ │  
   │ │                       ( ) 4.18.20                         │ │  
   │ │                       ( ) 4.17.19                         │ │  
   │ └───────────────────────────v(+)────────────────────────────┘ │  
   ├───────────────────────────────────────────────────────────────┤  
   │                    <Select>      < Help >                     │  
   └───────────────────────────────────────────────────────────────┘  
                                                          
                                                                                              
```

あとは解説通り進めて、「ツールチェインをビルド」の

```

$ ./ct-ng build
```
まで終わらせて、パスを通しておきます。

# U-boot

以下の記事の通りU-bootを作ります。

「RaspberryPi の U-boot をクロスコンパイルする」
https://qiita.com/nanbuwks/items/4f2d676d2c5696c33c04

資料の通り、以下の4つを用意します。

- bootcode.bin
- start.elf
- u-boot.bin
- config.txt

このうち、u-boot.bin は今作成したものですが、  bootcode.bin と start.elf はRaspberryPi 専用のできあいのものを使うことになります。config.txt は start.elf で利用するためのもので start.elf が u-boot.bin を読み込みできるように設定します。
この4つを SD カードイメージの boot パーティションに配置し、umount します。

SD カードにイメージを書き込み、Raspberry Pi 3 に装填、起動できたら U-boot 作成は OK です。

# Linux カーネルの作成

通常は含まれている bcや cpio コマンドですが、最低限の環境だと bc が無かったので、あらかじめ入れておきます。

```
$ sudo apt install bc cpio
```
元ネタの 「5. Kernel」に沿って、以下のように作業します。

```
$ git clone --depth=1  https://github.com/raspberrypi/linux.git
$ cd linux

```

ここで取得したブランチは rpi-5.15.y でした。
Raspberry Pi 3 のデフォルト設定をそのまま使用します。

```
$ make ARCH=arm64 CROSS_COMPILE=aarch64-rpi3-linux-gnu- bcm2711_defconfig
$ make -j$(nproc) ARCH=arm64 CROSS_COMPILE=aarch64-rpi3-linux-gnu-

```

10分ぐらいかかりました。
ビルドできたら Image ファイルと .dtb と名前のついたデバイスツリーファイルを SDカードイメージにコピーします。

```
$ cp arch/arm64/boot/Image ../boot
$ cp arch/arm64/boot/dts/broadcom/bcm2710-rpi-3-b.dtb ../boot
$ cd ..
```


ちなみに、bcm271* でこれだけ種類があるのでそれっぽいのを選択しました。
```
bcm2710-rpi-2-b.dtb       bcm2710-rpi-3-b.dts       bcm2710-rpi-zero-2.dtb    bcm2711-rpi-400.dts
bcm2710-rpi-2-b.dts       bcm2710-rpi-cm3.dtb       bcm2710-rpi-zero-2.dts    bcm2711-rpi-cm4.dtb
bcm2710-rpi-3-b-plus.dtb  bcm2710-rpi-cm3.dts       bcm2711-rpi-4-b.dtb       bcm2711-rpi-cm4.dts
bcm2710-rpi-3-b-plus.dts  bcm2710-rpi-zero-2-w.dtb  bcm2711-rpi-4-b.dts       bcm2711-rpi-cm4s.dtb
bcm2710-rpi-3-b.dtb       bcm2710-rpi-zero-2-w.dts  bcm2711-rpi-400.dtb       bcm2711-rpi-cm4s.dts
```
# u-boot から Linux カーネルを呼び出せるようにする

以下の内容の boot_cmd.txt を作ります。

```
fatload mmc 0:1 ${kernel_addr_r} Image
setenv bootargs "console=serial0,115200 console=tty1 root=/dev/mmcblk0p1 rw rootwait init=/hello"
booti ${kernel_addr_r} - ${fdt_addr}

```


起動コマンドの意味:

- `fatload mmc 0:1 ${kernel_addr_r} Image`
この行は、カーネルイメージファイルを 0番目のmmc デバイスのパーティション1 から読み込み、メモリに配置します。
- `setenv bootargs "console=serial0,115200 console=tty1 root=/dev/mmcblk0p1 rw rootwait 
 init=/hello"`
カーネルの起動オプションを設定します。ルートファイルシステムとして SD カードの第一パーティションを使うようにしてます。また、init プロセスとしてルートファイルシステム中の /hello を指定していますがまだ /hello は配置していないため実際には動作しません。
- `booti ${kernel_addr_r} -  ${fdt_addr}`
カーネルにデバイスツリーバイナリのアドレスを指定して起動します。 - とあるのは、もし initramfs を使う場合はRAMディスクファイルシステムのメモリ上のアドレスをここに指定します。

コマンドファイルを変換して u-boot で使用する uImage フォーマットに変換します。

```
$ ../../u-boot/tools/mkimage -A arm64 -O linux -T script -C none -d boot_cmd.txt boot.scr
Image Name:   
Created:      Sun Sep 25 14:08:37 2022
Image Type:   AArch64 Linux Script (uncompressed)
Data Size:    182 Bytes = 0.18 KiB = 0.00 MiB
Load Address: 00000000
Entry Point:  00000000
Contents:
   Image 0: 174 Bytes = 0.17 KiB = 0.00 MiB

```

できた boot.scr を boot パーティションにコピーします。

SDカードをRaspberryPi に取り付け起動すると、Linuxカーネルが呼び出されます。今回は /hello が無いので 途中でカーネルパニックで止まります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/58eb329d-0f0a-aa21-e4d9-e13fb4e2050a.png)

# init プログラムの作成

先程は /hello をinitプロセスとして実行するようにしていましたが、ここを処理するテストプログラムを作成します。
実行がなされるかどうかだけのテストとして、以下のプログラムとしました。
```
#include <stdio.h>
int main(void) {
  printf("Hello Init!\n");
  return 0;
}
```

 static ビルドしておきます。

```
$ ~/x-tools/aarch64-rpi3-linux-gnu/bin/aarch64-rpi3-linux-gnu-gcc -static -o hello hello.c
```

できた hello を boot パーティションにコピーします。
起動すると Kernel panic で止まりますが、途中で hello プログラムが実行されていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/2cbeeb6b-4eeb-3543-3430-15a6dcfbbdc9.png)

なお、ここで  FATAL: kernel too old と出ることがありました。
これは、 crosstool-ng の Operating system の設定と、カーネルソースのバージョンがミスマッチしているために起こります。カーネルソースのバージョンまたは crostool-ng の設定を修正してください。


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

先程は、SD カードの第1パーティションをルートファイルシステムとして指定しました。しかしながらSDカード内部はディレクトリも何も作っていませんでした。
「Filesystem Hierarchy Standard」
https://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.pdf
のような、Linux ぽいファイルシステムを作ります。


元ネタの「6. Root filesystem」に沿って作業していきます。

まずは、ローカルで作業用フォルダを作ります。
```
$ mkdir rootfs
```
移動し、必要なディレクトリを作成します。


```
$ cd rootfs
$ mkdir {bin,dev,etc,home,lib64,proc,sbin,sys,tmp,usr,var}
$ mkdir usr/{bin,lib,sbin}
$ mkdir var/log
$ sudo chown -R root:root *
$ cd ..
```

更に、usr に 先程の hello を配置します。


```
$ sudo cp hello rootfs/usr
```

この rootfs を SDカードに書き込みましょう。

```
$ sudo cp -a rootfs/* <SDカード第2パーティション>
```

 boot_cmd.txt を以下のように変更します。

```
fatload mmc 0:1 ${kernel_addr_r} Image
setenv bootargs "console=serial0,115200 console=tty1 root=/dev/mmcblk0p2 rw rootwait init=/usr/hello"
booti ${kernel_addr_r} - ${fdt_addr}

```

第二パーティションの /usr/hello を起動するようにします。

boot.scr に反映させます。

```
$ ../../u-boot/tools/mkimage -A arm64 -O linux -T script -C none -d boot_cmd.txt boot.scr
```

これを boot パーティションに書き込み、RaspberryPi に装着して起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/e2816f62-f68a-85f6-a6c1-c055a113af81.png)

第2パーティションの /usr/hello が起動していることがわかります。

# initramfs をつくる

ルートファイルシステムを、カーネルが認識するファイルシステムに配置すればそれを使ってLinuxが起動できます。
ここまで作成した rootfs フォルダを、RAM ディスクにする作業を行います。

 もとねたの「7. Boot the Board」に沿って作業していきます。

initramfs は cpio の newc フォーマットを使うらしいです。


> The cpio file format used by initramfs is the "newc" (aka "cpio -H newc") format, 
cf., https://www.kernel.org/doc/Documentation/early-userspace/README

```
$ find . | cpio -H newc -ov --owner root:root -F ../initramfs.cpio
$ cd ..
$ gzip initramfs.cpio
$ ls -alh initramfs.cpio.gz 
-rw-rw-r-- 1 nanbuwks nanbuwks 3.8M  9月 25 21:38 initramfs.cpio.gz
```

initramfs のイメージファイルは、 U-Boot から読みこまれてメモリ上に展開することになるのですが、U-boot で扱うために mkimage でヘッダを追加して uImage 形式にします。

```
$ ../../u-boot/tools/mkimage -A arm64 -O linux -T ramdisk -d initramfs.cpio.gz uRamdisk
Image Name:  
Created:      Sun Sep 25 21:39:46 2022
Image Type:   AArch64 Linux RAMDisk Image (gzip compressed)
Data Size:    3905858 Bytes = 3814.31 KiB = 3.72 MiB
Load Address: 00000000
Entry Point:  00000000
```

できた uRamdisk ファイルを boot パーティションにコピーします。

## U-boot を構成する

今回設定した initramfs を使用するように U-boot を設定します。
元ネタの以下のやりかたを採ることにします。
>  For simplicity, I’ll use the Busybox shell as the init program. 

以下の内容の boot_cmd.txt を作ります。

```
fatload mmc 0:1 ${kernel_addr_r} Image
fatload mmc 0:1 ${ramdisk_addr_r} uRamdisk
setenv bootargs "console=serial0,115200 console=tty1 rdinit=/usr/hello"
booti ${kernel_addr_r} ${ramdisk_addr_r} ${fdt_addr}
```

`fatload mmc 0:1 ${fdt_addr_r} bcm2710-rpi-3-b.dtb` も試したのですがこれが入ると起動しませんでした。

```
$ ../../u-boot/tools/mkimage -A arm64 -O linux -T script -C none -d boot_cmd.txt boot.scr
Image Name:   
Created:      Sun Sep 25 10:23:06 2022
Image Type:   AArch64 Linux Script (uncompressed)
Data Size:    262 Bytes = 0.26 KiB = 0.00 MiB
Load Address: 00000000
Entry Point:  00000000
Contents:
   Image 0: 254 Bytes = 0.25 KiB = 0.00 MiB
```

できた boot.scr を boot パーティションにコピーします。

今まで作成したファイルのうち、現時点で boot パーティションに必要なものは以下のファイルとなります。

- Image
- bcm2711-rpi-3-b.dtb
- boot.scr
- bootcode.bin
- config.txt
- start.elf
- uRamdisk
- u-boot.bin

これを使って起動してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/43e0ca5d-8e2c-fbf7-e26c-12fd9d4281d3.png)

RAMディスクが読み込まれているのがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/9d2bf103-ded3-8bb3-8229-d2ccf8a019de.png)

同様に、hello も動作しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/e108680d-7e9d-43c5-6674-a2194f318a38.png)

## busybox の作成

ここまでで、 U-boot 、 Linux カーネル、 initramfs が動作していることは確認できました。hello の後にKernel Panic が起きてますが exit せずに無限ループし、適切な処理をすることで希望の処理を続けることができます。

まだ成功していませんが、init プロセスとして busybox を動かすことを試してみます。

```
$ wget https://busybox.net/downloads/busybox-1.35.0.tar.bz2
$ tar xf busybox-1.35.0.tar.bz2
$ cd busybox-1.35.0/
$ CROSS_COMPILE=${HOME}/x-tools/aarch64-rpi3-linux-gnu/bin/aarch64-rpi3-linux-gnu-
$ make CROSS_COMPILE="$CROSS_COMPILE" defconfig
```

ビルド、インストールします
```
$ make CROSS_COMPILE="$CROSS_COMPILE"
$ sudo make CROSS_COMPILE="$CROSS_COMPILE" install
```
_install に展開されるので rootfs にコピーします。
```
$ sudo cp -a _install/* ../rootfs

```

たくさんのコマンドが展開されてますが
```
$ ls ../rootfs/bin
arch     chown     dnsdomainname  fsync     ipcalc    lsattr      mpstat         printenv   run-parts     stty    vi
ash      conspy    dumpkmap       getopt    kbd_mode  lzop        mt             ps         scriptreplay  su      watch
base32   cp        echo           grep      kill      makemime    mv             pwd        sed           sync    zcat
base64   cpio      ed             gunzip    link      mkdir       netstat        reformime  setarch       tar
busybox  cttyhack  egrep          gzip      linux32   mknod       nice           resume     setpriv       touch
cat      date      false          hostname  linux64   mktemp      pidof          rev        setserial     true
chattr   dd        fatattr        hush      ln        more        ping           rm         sh            umount
chgrp    df        fdflush        ionice    login     mount       ping6          rmdir      sleep         uname
chmod    dmesg     fgrep          iostat    ls        mountpoint  pipe_progress  rpm        stat          usleep
```
実体はこの1コマンドです。
```
$ ls -alh ../rootfs/bin/busybox
-rwxr-xr-x 1 root root 1010K  9月 24 19:58 ../rootfs/bin/busybox
```
多くできていたコマンドは、すべて busybox にリンクされているのがわかります。
```
$ ls -alh ../rootfs/bin/
合計 1020K
drwxrwxr-x  2 root     root      4.0K  9月 24 19:58 .
drwxrwxr-x 14 nanbuwks nanbuwks  4.0K  9月 24 19:58 ..
lrwxrwxrwx  1 root     root         7  9月 24 19:58 arch -> busybox
lrwxrwxrwx  1 root     root         7  9月 24 19:58 ash -> busybox
lrwxrwxrwx  1 root     root         7  9月 24 19:58 base32 -> busybox
lrwxrwxrwx  1 root     root         7  9月 24 19:58 base64 -> busybox
-rwxr-xr-x  1 root     root     1010K  9月 24 19:58 busybox
lrwxrwxrwx  1 root     root         7  9月 24 19:58 cat -> busybox
lrwxrwxrwx  1 root     root         7  9月 24 19:58 chattr -> busybox
.
.
.
lrwxrwxrwx  1 root     root         7  9月 24 19:58 umount -> busybox
lrwxrwxrwx  1 root     root         7  9月 24 19:58 uname -> busybox
lrwxrwxrwx  1 root     root         7  9月 24 19:58 usleep -> busybox
lrwxrwxrwx  1 root     root         7  9月 24 19:58 vi -> busybox
lrwxrwxrwx  1 root     root         7  9月 24 19:58 watch -> busybox
lrwxrwxrwx  1 root     root         7  9月 24 19:58 zcat -> busybox
```

# ライブラリのインストール

Busybox で使用する共有ライブラリを調べます。
```
$ readelf -a ../rootfs/bin/busybox | grep -E "(program interpreter)|(Shared library)"
      [Requesting program interpreter: /lib/ld-linux-aarch64.so.1]

```

必要な共有ライブラリを sysroot にコピーします。
```
$ export SYSROOT=$(aarch64-rpi3-linux-gnu-gcc -print-sysroot)
$ echo $SYSROOT
/home/nanbuwks/x-tools/aarch64-rpi3-linux-gnu/aarch64-rpi3-linux-gnu/sysroot
$ sudo cp -L ${SYSROOT}/lib64/ld-linux-aarch64.so.1 ../rootfs/lib64/
```

## デバイスノードを作成します。

Busybox で使うデバイスノードを作成します。

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

 boot_cmd.txt を以下のように変更します。
```
fatload mmc 0:1 ${kernel_addr_r} Image
setenv bootargs "console=serial0,115200 console=tty1 root=/dev/mmcblk0p2 rw rootwait init=/bin/sh"
booti ${kernel_addr_r} - ${fdt_addr}

```
boot.scr に反映します。

```
$ ../../u-boot/tools/mkimage -A arm64 -O linux -T script -C none -d boot_cmd.txt boot.scr
```

boot.scr を boot パーティションにコピーし、RaspberryPi に装着し起動します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/6f1899e1-0b76-5a50-c810-c9615a03e766.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/435d3577-b708-8f63-d186-0c67d877a6c3.png)

No working init found となってしまいました。調査中です!