RaspberryPi
QEMU

Raspberry Pi のセルフビルド環境を QEMU で作る

More than 1 year has passed since last update.


動機

ハードウェアエンコーダ対応の FFmpeg をビルドしようとすると、実機ビルドはやってられない。


流れ

典型的な QEMU in chroot です。


  1. ファイルシステムを準備して

  2. QEMU を入れて

  3. chroot


用意するもの


  • Debian か Ubuntu 環境、本稿では Debian Stretch amd64


chroot 用ファイルシステムの準備


イメージのダウンロード

https://www.raspberrypi.org/downloads/raspbian/ から Raspbian Stretch Lite の zip を入手する。これを unzip すると 2017-09-07-raspbian-stretch-lite.img が出てくる。

$ file 2017-09-07-raspbian-stretch-lite.img

2017-09-07-raspbian-stretch-lite.img: DOS/MBR boot sector;
partition 1 : ID=0xc, start-CHS (0x0,130,3), end-CHS (0x5,214,7), startsector 8192, 85622 sectors;
partition 2 : ID=0x83, start-CHS (0x5,220,24), end-CHS (0xe1,120,63), startsector 94208, 3528040 sectors


イメージから tarball を作成


新しいやり方

いきなりイメージを loopback デバイスにアタッチする。

$ sudo losetup -frP 2017-09-07-raspbian-stretch-lite.img 



  • -f: 空いている loop デバイス名を適当に探して使う


  • -r: readonly


  • -P: パーティションをスキャンして loopXpY を作る

fdisk してみるとパーティションに対応するデバイスファイルもできている。

$ sudo fdisk -l /dev/loop0

Disk /dev/loop0: 1.7 GiB, 1858076672 bytes, 3629056 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x37665771

Device Boot Start End Sectors Size Id Type
/dev/loop0p1 8192 93236 85045 41.5M c W95 FAT32 (LBA)
/dev/loop0p2 94208 3629055 3534848 1.7G 83 Linux

Linux パーティションをどこか適当にマウント

$ sudo mount -o ro /dev/loop0p2 /mnt

そしてそれを tarball に固める(イメージのサイズが 1.7GB しかないので、ここを直接運用すると容量が足りなくなるため)。このとき root で行う。

$ sudo tar cvjf ~/raspbian-stretch-lite.tar.bz2 -C /mnt .

tarball ができたら unmount して losetup -d しておく。


古いやり方

イメージ中のパーティションテーブルを調べて、Linux パーティションの先頭セクタを求める(FAT32 のほうは /boot なのでスルー)。

$ /sbin/fdisk -l -u 2017-09-07-raspbian-stretch-lite.img

Disk 2017-09-07-raspbian-stretch-lite.img: 1.7 GiB, 1854590976 bytes, 3622248 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x11eccc69

Device Boot Start End Sectors Size Id Type
2017-09-07-raspbian-stretch-lite.img1 8192 93813 85622 41.8M c W95 FAT32 (LBA)
2017-09-07-raspbian-stretch-lite.img2 94208 3622247 3528040 1.7G 83 Linux

この出力では Linux パーティションの先頭セクタは 94208 なので、セクタサイズ 512 をかけたオフセットでループバックデバイスを作成する。

$ sudo losetup -o `expr 512 \* 94208` -f 2017-09-07-raspbian-stretch-lite.img

ループバックデバイスができたらそれをどこか適当にマウント

$ sudo mount -o ro /dev/loop0 /mnt

そしてそれを tarball に固める(イメージのサイズが 1.7GB しかないので、ここを直接運用すると容量が足りなくなるため)。このとき root で行う。

$ sudo tar cvjf ~/raspbian-stretch-lite.tar.bz2 -C /mnt .

tarball ができたら unmount して losetup -d しておく。


Raspbian ファイルシステムの準備

Raspbian 用ファイルシステムを置く場所を用意し、そこに tarball を展開する。このとき root で行う。

$ mkdir -p ~/arm/raspbian

$ cd ~/arm/raspbian
$ sudo tar xjf ~/raspbian-stretch-lite.tar.bz2


ARM 実行環境の準備

QEMU のユーザモードエミュレータの static 版をインストールする。

$ sudo apt-get install qemu-user-static

このとき binfmt の設定も自動で入るはずなので、特に interpreter 行のパスを確認しておく。

$ sudo update-binfmts --display | grep arm

qemu-arm (enabled):
interpreter = /usr/bin/qemu-arm-static
qemu-armeb (enabled):
interpreter = /usr/bin/qemu-armeb-static


chroot 先の整備

tarball を展開したディレクトリを / としたときに interpreter 行のパスを再現するように、qemu-arm-static をコピーしておく。

$ sudo cp /usr/bin/qemu-arm-static arm/raspbian/usr/bin/

tarball を展開した中にある /sys, /proc, /dev などにホストのそれをマウントしておく。 mount --bind でホストから持ってきてもよい。このほかにもホスト側とやり取りする必要があればそのディレクトリもマウントしておく。

$ sudo mount -t sysfs sysfs arm/raspbian/sys

$ sudo mount -t proc proc arm/raspbian/proc
$ sudo mount -t devtmpfs udev arm/raspbian/dev
$ sudo mount -t devpts devpts arm/raspbian/dev/pts


chroot

そうして chroot で tarball を展開したディレクトリに入る。

$ sudo chroot arm/raspbian /bin/bash

root@len:/# su - pi

pi@len:~ $ uname -a
Linux len 4.9.0-3-amd64 #1 SMP Debian 4.9.30-2+deb9u1 (2017-06-18) armv7l GNU/Linux

pi@len:~ $ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description: Raspbian GNU/Linux 9.1 (stretch)
Release: 9.1
Codename: stretch

pi@len:~ $ gcc -v
Using built-in specs.
COLLECT_GCC=/usr/bin/gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/arm-linux-gnueabihf/6/lto-wrapper
Target: arm-linux-gnueabihf
Configured with: ../src/configure -v --with-pkgversion='Raspbian 6.3.0-18+rpi1' --with-bugurl=file:///usr/share/doc/gcc-6/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-6 --program-prefix=arm-linux-gnueabihf- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-libitm --disable-libquadmath --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-6-armhf/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-6-armhf --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-6-armhf --with-arch-directory=arm --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-sjlj-exceptions --with-arch=armv6 --with-fpu=vfp --with-float=hard --enable-checking=release --build=arm-linux-gnueabihf --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf
Thread model: posix
gcc version 6.3.0 20170516 (Raspbian 6.3.0-18+rpi1)

この中で apt-get することで環境を整備していく。

これで Pi 実機よりは圧倒的に速いビルド環境ができたので、あとは x264 でも FFmpeg でもビルドし放題。


種明かし

以下は昔に社内 Wiki に書いたものの転載。


binfmt-support

実行可能ファイルのヘッダを見て、必要なランタイムやインタプリタ等を準備する。

スクリプトの #! 行を見てインタプリタ経由で実行させたり、ELF バイナリのヘッダを見てダイナミックリンカ (ld-linux.so) 経由で実行させたりしているのはこの仕組み。このパッケージはこの仕組みを設定ファイルで拡張可能にする。


qemu-arm-static

ARM 用の Linux バイナリを、x86 で実行できるようにする。

ARM 機械語の部分を x86 機械語に翻訳して実行し、システムコールはホストのものを呼び出すようになっている。通常の QEMU と違ってハードウェアをエミュレートしなくて良い分、高速に動作する。

スタティックリンク版を使う理由は後述。

ELF バイナリのヘッダにはアーキテクチャを示す項目があるので、「ARM アーキテクチャな ELF ヘッダを見つけたら qemu-arm-static を使って実行する」ように binfmt に設定すれば、ARMバイナリを x86 でも透過的に実行できるようになるはずである。

しかしこれは失敗してしまう。このまま実行しようとしても ARM 版のダイナミックリンカが見つからないというエラーになってしまう。

Linux のダイナミックリンカは /lib/ld-linux.so という名前なのだが、ARM も x86 も同じ名前で参照しようとするところ、実際にこのパスにあるのは x86 版なわけなのでエラーになる。このファイルは x86 バイナリの実行に必須なため置き換えることもできない。同様の問題が libc (libc.so) にもあてはまる。


chroot

ルートディレクトリを指定のディレクトリに変更した上で、指定のコマンドを実行する。このコマンドの子孫となるプロセスは、新しいルートディレクトリを引き継ぐ。新しいルートディレクトリ以下には、コマンドの実行に必要なライブラリやディレクトリ構造が揃っていないといけない。

chroot 先のディレクトリに ARM バイナリ一式(ランタイムも)と qemu-arm-static を置くことで、以下の流れが成立する。

chroot がルートディレクトリを変更する(元のディレクトリにはもう戻れない)

  ↓
(たとえば)chroot 先の /bin/bash を実行する
  ↓
ARM バイナリなので binfmt が /usr/bin/qemu-arm-static 経由で実行しようとする
  ↓
chroot 先の /usr/bin/qemu-arm-static を実行(これは x86 バイナリ)
  ↓
ARM 版のダイナミックリンカやランタイムを chroot 先から探すようになる
  ↓
bash 起動。この bash から起動したコマンドはすべて chroot の下で動作する。

binfmt は元々のルートディレクトリのつもりで qemu-arm-static を実行しようとするが、この場合すでに chroot してしまっているので、chroot 先のディレクトリにも、binfmt の設定に書かれているパスのとおりに qemu-arm-static を配置しておく必要がある。

ARM バイナリしか入っていない chroot 先のディレクトリで x86 バイナリのqemu-arm を実行するため、qemu-arm がダイナミックリンクされていると、x86

ランタイムライブラリを chroot 先で見つけられずに実行は失敗してしまう。スタティックリンク版を使うのはこのためである。

chroot 先のディレクトリに /dev/proc, /sys がないとコマンドの実行に支障をきたすことがあるので、これらはホストのものがそのまま見えるようにしておく必要がある。シンボリックリンクでは chroot を越えることができないので、bind mount するか直接マウントする(mount -t udev devfs /path/to/arm/dev)。