LeavaTailの日記

LeavaTailの日記

Linuxエンジニアを目指した技術者の備忘録

QEMUでARM用Linuxカーネルを起動する

はじめに

ARMアーキテクチャは、CPUアーキテクチャの一つである。
私たちの身近なPCはx86_64アーキテクチャであることが多いが、組み込み機器はARMアーキテクチャであることが多い。

また最近でも、AppleARMアーキテクチャを採用したMacを発表したことによって、ARMアーキテクチャはより一層注目されている。

一方で、アーキテクチャ毎に命令セットが異なるため、ARM用にビルドされたバイナリを別のアーキテクチャで実行することはできない。

そこで、プロセッサエミュレータでもあるQEMUを用いてARM環境を構築し、ARM用にビルドされたLinuxカーネルを動かす方法を解説する。
また本記事では、以下の動作をする環境を目指す。

f:id:LeavaTail:20201212144827p:plain
カーネル起動ワークフローとメモリマップイメージ図

変更履歴

  • 2020/12/12: 記事公開
  • 2021/2/25: initramfsの作成手順を修正

initramfs とは

initramfsでは、kernel起動直後にRAM上に展開されるファイルシステムとなっている。
カーネルドキュメントに詳細な説明が記載されているので、興味のある方は一読することを推奨する。

https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt

initramfsを利用する意義として、ルートファイルシステムをマウントする前に必要なドライバのインストールや処理 (ファイルシステムの復号や改ざんチェックなど)があげられる。

同じような仕掛けとしてinitrdがある。それらの大きな違いはフォーマット形式である。 initramfsはアーカイブ形式(cpio + gzip)、initrdはファイルシステム形式(gzip)となっている。

そして、initramfsはinitスクリプト内で下記のようなルートファイルシステムを変更する処理が必要となる。 一般的には、switch_rootコマンドで実現している。

f:id:LeavaTail:20201212030218p:plain
initramfsからルートファイルシステムへの切り替え

環境構成

本記事は、下記の環境とソフトウェアバージョンに基づいて説明する。

環境 パラメータ
ホスト環境 x86_64
ホストOS Ubuntu 20.04 (python3arm-linux-gnueabi-をインストール済み)
Buildroot buildroot-2020.02.8
QEMU QEMU emulator version 5.0.0
ターゲットボード vexpress-a9
linux 5.4.58
U-Boot 2020.07
Busybox 1_32_stable

ARM用のLinuxを構築する

Buildrootを用いてLinux(kernel, rootfs, qemu, device tree)を構築する。

  1. Buildrootをインターネットからダウンロード、ファイルを解凍する。

     leava@ubuntu-bionic:~$ wget https://buildroot.org/downloads/buildroot-2020.02.8.tar.gz
     leava@ubuntu-bionic:~$ tar zxvf  buildroot-2020.02.8.tar.gz && cd buildroot-2020.02.8
    
  2. arm専用のデフォルトコンフィグqemu_arm_vexpress_defconfig (ちなみに、vexpressVersatile Expressの略で汎用的なarmの評価ボードを意味する) を利用する。

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ make qemu_arm_vexpress_defconfig
    
  3. Buildrootのビルド対象にU-Bootを追加する。

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ make menuconfig
    
     Bootloaders  --->
         [*] U-Boot
         (vexpress_ca9x4) Board defconfig 
    
  4. Buildrootのビルド (rootユーザでビルドしてしまうと失敗してしまうので注意)

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ make
    
  5. カーネルが実行できるかどうかQEMUで確認してみる。

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ ./output/images/start-qemu.sh serial-only
     ...
     buildroot login:                                                  # rootでログイン可能
     # 
    

ARM用のinitramfsを構築する

BusyBoxを用いて簡易initramfsを構築する。

  1. Buildrootをインターネットからダウンロード、stable versionにチェックアウトする。(投稿時は1_32_stableが最新版)

     leava@ubuntu-bionic:~$ git clone git://busybox.net/busybox.git
     leava@ubuntu-bionic:~$ cd busybox
     leava@ubuntu-bionic:~/busybox$ git checkout remotes/origin/1_32_stable
    
  2. arm専用のデフォルトコンフィグを利用する。

     leava@ubuntu-bionic:~/busybox$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- defconfig
    
  3. initramfs用に最小限の設定を変更する。(initramfsの場合、スタティックリンクであるほうが好ましい)

     leava@ubuntu-bionic:~/busybox$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig
    
     Settings  --->
         [*] Build static binary (no shared libs)
    
  4. BusyBoxのビルド (make installを実行してもホスト環境に何かしらパッケージを変更するわけではなく、_installディレクトリにディレクトリツリーが構築される)

     leava@ubuntu-bionic:~/busybox$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
     leava@ubuntu-bionic:~/busybox$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- install
    
  5. initramfs用のディレクトリに移動する

     leava@ubuntu-bionic:~/busybox$ cd _install
    
  6. initramfsとして必要なディレクトリを作成する

     leava@ubuntu-bionic:~/busybox/_install$ mkdir dev
     leava@ubuntu-bionic:~/busybox/_install$ mkdir proc
     leava@ubuntu-bionic:~/busybox/_install$ mkdir sys
     leava@ubuntu-bionic:~/busybox/_install$ mkdir -p mnt/newroot
    
  7. initramfsからルートファイルシステムをマウントするようにinitスクリプトを修正する。

     leava@ubuntu-bionic:~/busybox/_install$ cat <<EOF > init
     #!/bin/busybox sh
     echo "Mounting Proc and Sysfs"
     # Mount the /proc and /sys filesystems. 
     mount -t devtmpfs devtempfs /dev
     mount -t proc none /proc
     mount -t sysfs none /sys
     # Mount the root filesystem
     mount -t ext4 /dev/mmcblk0 /mnt/newroot
     # Switch mount point        
     mount -n -o move /sys /mnt/newroot/sys
     mount -n -o move /proc /mnt/newroot/proc
     mount -n -o move /dev /mnt/newroot/dev
     # Execute new mount rootfilesystem
     exec switch_root -c /dev/console /mnt/newroot /sbin/init
     EOF
    
  8. 作成したディレクトリツリーを用いて、initramfsを構築する

     leava@ubuntu-bionic:~/busybox/_install$ find . | cpio -o --format=newc > ../rootfs.img;
    
  9. initramfsを使用するようにQEMUの起動スクリプトを修正する

     leava@ubuntu-bionic:~/busybox/_install$ cd ~/buildroot-2020.02.8
     leava@ubuntu-bionic:~/buildroot-2020.02.8$ sed -e '$d' ./output/images/start-qemu.sh > ./output/images/start-qemu2.sh
     leava@ubuntu-bionic:~/buildroot-2020.02.8$ echo "exec   qemu-system-arm -M vexpress-a9 -smp 1 -m 256 -kernel \${IMAGE_DIR}/zImage -dtb \${IMAGE_DIR}/vexpress-v2p-ca9.dtb -initrd ../rootfs.img -drive file=\${IMAGE_DIR}/rootfs.ext2,if=sd,format=raw -append \"console=ttyAMA0,115200 rootwait\"  -net nic,model=lan9118 -net user  \${EXTRA_ARGS}" >> output/images/start-qemu2.sh
    
  10. initramfsからルートファイルシステムを正常にマウントできるかどうか、QEMUで確認してみる。

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ ./output/images/start-qemu2.sh serial-only
     ...
     Mounting Proc and Sysfs                      # initramfsのinitスクリプトが実行されている
     ...
     buildroot login:                                                  # rootでログイン可能
     # 
    

U-Bootからカーネルをロードする

  1. initramfsを使用するようにQEMUの起動スクリプトを修正する

     leava@ubuntu-bionic:~/buildroot-2020.02.8$ sed -e '$d' ./output/images/start-qemu.sh > ./output/images/start-qemu3.sh
     leava@ubuntu-bionic:~/buildroot-2020.02.8$ echo "exec   qemu-system-arm -M vexpress-a9 -smp 1 -m 256 -kernel \${IMAGE_DIR}/../build/uboot-2020.07/u-boot -device loader,file=\${IMAGE_DIR}/zImage,addr=0x62000000 -device loader,file=\${IMAGE_DIR}/vexpress-v2p-ca9.dtb,addr=0x63000000 -device loader,file=urootfs.img,addr=0x63008000 -drive file=\${IMAGE_DIR}/rootfs.ext2,if=sd,format=raw   -net nic,model=lan9118 -net user  \${EXTRA_ARGS}" >> output/images/start-qemu3.sh
    
  2. U-Bootを起動させる。
    このとき、カーネル0x62000000、device treeを0x63000000、initramfsを0x630080000にロードする。 (これは参考文献より、今回のボードvexpress-a9は0x6000000からSDRAMが実装されているためである)

      leava@ubuntu-bionic:~/buildroot-2020.02.8$ ./output/images/start-qemu3.sh serial-only
    
      U-Boot 2020.07 (Dec 12 2020 - 04:16:22 +0000)
    
      DRAM:  256 MiB
      WARNING: Caches not enabled
      Flash: 128 MiB
      MMC:   MMC: 0
      *** Warning - bad CRC, using default environment
    
      In:    serial
      Out:   serial
      Err:   serial
      Net:   smc911x-0
      Hit any key to stop autoboot:  0 
      => 
    
  3. device treeが展開されているアドレスなどを指定してカーネル起動させる。

     => bootz 0x62000000 0x63008000 0x63000000
     Kernel image @ 0x62000000 [ 0x000000 - 0x46a478 ]                                                        
     ## Loading init Ramdisk from Legacy Image at 63008000 ...                                                
        Image Name:                                                                                           
        Image Type:   ARM Linux RAMDisk Image (gzip compressed)                                               
        Data Size:    1123310 Bytes = 1.1 MiB                                                                 
        Load Address: 00000000                                                                                
        Entry Point:  00000000                                                                                
        Verifying Checksum ... OK                                                                             
     ## Flattened Device Tree blob at 63000000                                                                
        Booting using the fdt blob at 0x63000000                                                              
        Loading Ramdisk to 6fd65000, end 6fe773ee ... OK                                                      
        Loading Device Tree to 6fd5e000, end 6fd6473e ... OK                                                  
    
     Starting kernel ...
     ...
     buildroot login:                                                  # rootでログイン可能
     # 
    

作成ファイル置き場

#!/bin/busybox sh
 echo "Mounting Proc and Sysfs"
 # Mount the /proc and /sys filesystems. 
 mount -t devtmpfs devtempfs /dev
 mount -t proc none /proc
 mount -t sysfs none /sys
 # Mount the root filesystem
 mount -t ext4 /dev/mmcblk0 /mnt/newroot
 # Switch mount point        
 mount -n -o move /sys /mnt/newroot/sys
 mount -n -o move /proc /mnt/newroot/proc
 mount -n -o move /dev /mnt/newroot/dev
 # Execute new mount rootfilesystem
 exec switch_root -c /dev/console /mnt/newroot /sbin/init
#!/bin/sh
IMAGE_DIR="${0%/*}/"

if [ "${1}" = "serial-only" ]; then
    EXTRA_ARGS='-nographic'
else
    EXTRA_ARGS='-serial stdio'
fi

export PATH="/srv/src/buildroot-2020.08/output/host/bin:${PATH}"
exec   qemu-system-arm -M vexpress-a9 -smp 1 -m 256 -kernel ${IMAGE_DIR}/zImage -dtb ${IMAGE_DIR}/vexpress-v2p-ca9.dtb -initrd ../rootfs.img -drive file=${IMAGE_DIR}/rootfs.ext2,if=sd,format=raw -append "console=ttyAMA0,115200 rootwait"  -net nic,model=lan9118 -net user  ${EXTRA_ARGS}
#!/bin/sh
IMAGE_DIR="${0%/*}/"

if [ "${1}" = "serial-only" ]; then
    EXTRA_ARGS='-nographic'
else
    EXTRA_ARGS='-serial stdio'
fi

export PATH="/srv/src/buildroot-2020.08/output/host/bin:${PATH}"
exec   qemu-system-arm -M vexpress-a9 -smp 1 -m 256 -kernel ${IMAGE_DIR}/../build/uboot-2020.07/u-boot -device loader,file=${IMAGE_DIR}/zImage,addr=0x62000000 -device loader,file=${IMAGE_DIR}/vexpress-v2p-ca9.dtb,addr=0x63000000 -device loader,file=urootfs.img,addr=0x63008000 -drive file=${IMAGE_DIR}/rootfs.ext2,if=sd,format=raw   -net nic,model=lan9118 -net user  ${EXTRA_ARGS}

おわりに

本記事では、QEMUでARM用Linuxカーネルを起動させる手順を説明した。
Buildrootで構築した場合、自動でセットアップしてくれるため非常に使いやすく便利である。

また、今回はその恩恵にあやかっていないが、initramfsを使用した起動方法を紹介した。 initramfsを利用しないと実現できない要件もあるので、役割や作り方を一度おさらいしておくとよい。

FAQ

  • initramfs がうまく起動しない。
    • 横着して find _install | cpio -o --format=newc > ../rootfs.img;などとしていませんか?initramfs用のディレクトリ直下で実行しましょう。
  • BusyBoxswitch_rootコマンドで、ルートファイルシステムの変更に失敗する。
    • /sbin/switch_rootでコマンドを実行するとPIDが変わってしまい起動に失敗します。execコマンドを使用しましょう。
  • u-bootで、bootzコマンドを実行後にStarting kernel ...で止まってしまう。
    • メモリマップを確認して、「ロードしたイメージと衝突していないか」と「ロードしたイメージ同士が衝突していないか」を確認しましょう。

参考