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

## 動機

ハードウェアエンコーダ対応の 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` が出てくる。

```shell-session
$ 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 を作成

ダウンロードしたディスクイメージは、実機で実行することで焼いた先のデバイス（microSD とか SSD とか）のサイズいっぱいまで拡大されるようになっているが、本稿の手順ではその拡大が行われない。

ディスクイメージファイルのまま直接運用すると容量が足りなくなるのは必至なので、イメージの中身だけを tar に固めておき、ホスト側のファイルシステムに直接展開する。

#### 新しいやり方

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

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

* `-f`: 空いている loop デバイス名を適当に探して使う
* `-r`: readonly
* `-P`: パーティションをスキャンして `loopXpY` を作る

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

```shell-session
$ 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 パーティションをどこかに適当にマウントする。

```shell-session
$ sudo mount -o ro /dev/loop0p2 /mnt
```

そしてそれを tarball に固める。このとき root で行う。

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

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


#### 古いやり方

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

```shell-session
$ /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 をかけたオフセットでループバックデバイスを作成する。

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

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

```shell-session
$ sudo mount -o ro /dev/loop0 /mnt
```

そしてそれを tarball に固める。このとき root で行う。

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

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

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

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

```shell-session
$ mkdir -p ~/arm/raspbian
$ cd ~/arm/raspbian
$ sudo tar xpjf ~/raspbian-stretch-lite.tar.bz2
```

## ARM 実行環境の準備

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

```shell-session
$ sudo apt-get install qemu-user-static
```

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

```shell-session
$ 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` をコピーしておく。

```shell-session
$ sudo cp /usr/bin/qemu-arm-static arm/raspbian/usr/bin/
```

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

```shell-session
$ 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 を展開したディレクトリに入る。

```shell-session
$ 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)
```

最初のコマンドは `sudo chroot arm/raspbian su - pi` としてもよい。

この中で 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`）。