動機
ハードウェアエンコーダ対応の FFmpeg をビルドしようとすると、実機ビルドはやってられない。
流れ
典型的な QEMU in chroot です。
- ファイルシステムを準備して
- QEMU を入れて
- 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 を作成
ダウンロードしたディスクイメージは、実機で実行することで焼いた先のデバイス(microSD とか SSD とか)のサイズいっぱいまで拡大されるようになっているが、本稿の手順ではその拡大が行われない。
ディスクイメージファイルのまま直接運用すると容量が足りなくなるのは必至なので、イメージの中身だけを tar に固めておき、ホスト側のファイルシステムに直接展開する。
新しいやり方
いきなりイメージを 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 に固める。このとき 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 に固める。このとき 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 xpjf ~/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
でホストから持ってきてもよい。このほかにもホスト側とやり取りする必要があればそのディレクトリもマウントしておく(シンボリックリンクでは chroot をまたげないのでダメ)。
$ 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)
最初のコマンドは 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
)。