DockerでユーザモードQEMUによるARMエミュレーション環境を構築する
手元にx64マシンしかない状況でARM環境を用意しようとした場合、以下のような選択肢が考えられる。
- 実機を用意する(Raspberry Pi、Android端末など)
- お金がかかる、使うのに手間がかかる
- QEMUのシステムエミュレーションを使う
- 再現性が高い一方、重い
- QEMUのユーザモードエミュレーションを使う(参考)
- QEMUのユーザモードエミュレーションにbinfmtとchrootを組み合わせて使う(参考1、参考2)
- 軽い上にライブラリパスの指定が不要だが、chroot環境下に各種プログラムを用意するのに手間がかかる
- QEMUのユーザモードエミュレーションにbinfmtとコンテナ仮想化を組み合わせて使う(参考)
- 軽い上にライブラリパスの指定が不要、さらに各種パッケージインストール済みのイメージとして扱える
ここでは最後の選択肢において、コンテナ仮想化にDockerを利用した場合のARM環境構築について記す。 Docker自体のインストール、利用方法については「Dockerを使ってみる」を参照。 ホストOSにはUbuntu 14.04.1 LTS amd64(x86_64)版を使用している。
qemu-user-staticのインストール
まず、ホストOSにユーザモードエミュレーションを行うQEMUをインストールする。
$ sudo apt-get install qemu-user-static
Dockerイメージのダウンロード
次のリポジトリから、binfmt設定済みのDockerイメージが利用できる。
いきなりDockerfileからイメージを作成しても問題ないが、ここでは後々のことも考え一式ダウンロードしておく。
$ sudo docker pull mazzolino/armhf-ubuntu
コンパイラ等インストール済みイメージの作成
上でダウンロードしたDockerイメージをベースに、必要なパッケージのインストール手順をDockerfileとして記述する。 ただし現時点において14.04というタグからではbash等のコマンドをうまく実行できなかったため、ここでは代わりにlatestを使用する。
$ vi Dockerfile $ cat Dockerfile FROM mazzolino/armhf-ubuntu:latest RUN apt-get update && apt-get -y upgrade RUN apt-get -y install build-essential gdb python git $ sudo docker build -t 'user/armhf-ubuntu:latest' . $ sudo docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE user/armhf-ubuntu latest de8aa0360ee1 11 minutes ago 568.9 MB
コンテナの作成
作成したパッケージインストール済みDockerイメージをもとに、ubuntu-arm
という名前のコンテナを作成し起動する。
$ sudo docker run --name ubuntu-arm -i -t user/armhf-ubuntu:latest /bin/bash root@c7b94bb2fc1e:/# uname -a Linux c7b94bb2fc1e 2.6.32 #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 armv7l armv7l armv7l GNU/Linux root@c7b94bb2fc1e:/# lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.2 LTS Release: 14.04 Codename: trusty root@c7b94bb2fc1e:/# file /bin/bash /bin/bash: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=f8059e177cfff17568e8957e78cb3b997ef23f93, stripped root@c7b94bb2fc1e:/# exit user@192.168.56.2:~/tmp/docker/ubuntu-arm $ sudo docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c7b94bb2fc1e user/armhf-ubuntu:latest /bin/bash 23 seconds ago Exited (0) 3 seconds ago ubuntu-arm
コマンドの実行結果から、ARMエミュレーション環境として実行できていることがわかる。
プログラムのコンパイル、実行
あらためてコンテナを起動し、簡単なプログラムをコンパイル、実行してみる。
$ sudo docker start -a -i ubuntu-arm root@c7b94bb2fc1e:/# vi hello.c root@c7b94bb2fc1e:/# cat hello.c #include <stdio.h> int main() { puts("Hello, world!"); return 0; } root@c7b94bb2fc1e:/# gcc hello.c root@c7b94bb2fc1e:/# ./a.out Hello, world! root@c7b94bb2fc1e:/# file a.out a.out: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=d44febb8922097957fa5a3f8e7d84e58efc9af6e, not stripped
ARMバイナリとしてコンパイル、実行できていることが確認できる。
readelfによるELF情報表示
readelfコマンドを使って、ELFオブジェクトの各種情報を表示してみる。
root@c7b94bb2fc1e:/# readelf -a a.out ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: ARM Version: 0x1 Entry point address: 0x8315 Start of program headers: 52 (bytes into file) Start of section headers: 4500 (bytes into file) Flags: 0x5000402, has entry point, Version5 EABI, hard-float ABI Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 9 Size of section headers: 40 (bytes) Number of section headers: 30 Section header string table index: 27 (snip)
objdumpによるディスアセンブル
objdumpコマンドを使って、実行ファイルをディスアセンブルしてみる。
root@c7b94bb2fc1e:/# objdump -d a.out a.out: file format elf32-littlearm Disassembly of section .init: 000082c4 <_init>: 82c4: e92d4008 push {r3, lr} 82c8: eb00001d bl 8344 <call_weak_fn> 82cc: e8bd8008 pop {r3, pc} (snip)
strace相当のシステムコールトレース
QEMUのユーザモードエミュレーションでは、ptraceシステムコールやnetlinkが使えない(参考)。 すなわち、ptraceシステムコールを使用するstraceやltraceを使うことができない。
ただし、straceについてはqemu-arm-staticコマンドにstrace相当のオプションが用意されているのでこれを代替として利用することができる。
root@c7b94bb2fc1e:/# qemu-arm-static -strace a.out 23 brk(NULL) = 0x00012000 23 uname(0xf6fff930) = 0 23 access("/etc/ld.so.nohwcap",F_OK) = -1 errno=2 (No such file or directory) 23 mmap2(NULL,8192,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0xf67dc000 23 access("/etc/ld.so.preload",R_OK) = -1 errno=2 (No such file or directory) 23 open("/etc/ld.so.cache",O_RDONLY|O_CLOEXEC) = 3 (snip)
代替として十分な結果が得られていることが確認できる。
gdbによるデバッグ
strace同様、gdbもptraceシステムコールを使うため直接利用することができない。 これについても、qemu-arm-staticにGDBのリモートデバッグ用オプションが用意されており、代替として利用することができる。
まず、TCPの1234番ポートを使いリモートデバッグの準備をする。
root@c7b94bb2fc1e:/# qemu-arm-static -g 1234 a.out & [1] 24 root@c7b94bb2fc1e:/# netstat -antp Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:1234 0.0.0.0:* LISTEN -
次に、gdb上でtarget remote :1234
を実行することでLISTEN状態のポートに接続する。
ここではgdbの-ex
オプションを使い、gdbの起動に合わせてリモートデバッグを開始する。
root@c7b94bb2fc1e:/# gdb -q -ex "target remote :1234" a.out Reading symbols from a.out...(no debugging symbols found)...done. Remote debugging using :1234 Reading symbols from /lib/ld-linux-armhf.so.3...Reading symbols from /usr/lib/debug//lib/arm-linux-gnueabihf/ld-2.19.so...done. done. Loaded symbols for /lib/ld-linux-armhf.so.3 0xf67debc0 in _start () from /lib/ld-linux-armhf.so.3 (gdb) disas Dump of assembler code for function _start: => 0xf67debc0 <+0>: ldr.w r10, [pc, #104] ; 0xf67dec2c <_dl_start_user+94> 0xf67debc4 <+4>: ldr.w r4, [pc, #104] ; 0xf67dec30 <_dl_start_user+98> 0xf67debc8 <+8>: mov r0, sp 0xf67debca <+10>: bl 0xf67e1880 <_dl_start> End of assembler dump. (gdb) b main Breakpoint 1 at 0x83f8 (gdb) c Continuing. Breakpoint 1, 0x000083f8 in main () (gdb) disas Dump of assembler code for function main: 0x000083f0 <+0>: push {r7, lr} 0x000083f2 <+2>: add r7, sp, #0 0x000083f4 <+4>: movw r0, #33880 ; 0x8458 => 0x000083f8 <+8>: movt r0, #0 0x000083fc <+12>: blx 0x82e4 <puts> 0x00008400 <+16>: movs r3, #0 0x00008402 <+18>: mov r0, r3 0x00008404 <+20>: pop {r7, pc} End of assembler dump. (gdb) c Continuing. Hello, world! [Inferior 1 (Remote target) exited normally] (gdb) quit [1]+ Done qemu-arm-static -g 1234 a.out
接続に成功し、ディスアセンブル結果としてARM命令が表示されていることが確認できる。
シェル関数を定義してみる
straceおよびgdbを手軽に使えるようにするため、.bashrc
に次のようなシェル関数を書いておくと便利である。
if [[ -n "$PS1" ]]; then qemu-strace() { qemu-arm-static -strace "$@" } qemu-gdb() { qemu-arm-static -g 1234 "${!#}" <&0 & gdb -ex "target remote :1234" "$@" </dev/tty } fi
.bashrc
の読み込みのため一旦コンテナを立ち上げ直し、実際に使ってみると次のようになる。
root@c7b94bb2fc1e:/# exit $ sudo docker start -a -i ubuntu-arm root@c7b94bb2fc1e:/# qemu-strace ./a.out 9 brk(NULL) = 0x00012000 9 uname(0xf6fff930) = 0 9 access("/etc/ld.so.nohwcap",F_OK) = -1 errno=2 (No such file or directory) 9 mmap2(NULL,8192,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0xf67dc000 9 access("/etc/ld.so.preload",R_OK) = -1 errno=2 (No such file or directory) 9 open("/etc/ld.so.cache",O_RDONLY|O_CLOEXEC) = 3 (snip) root@c7b94bb2fc1e:/# qemu-gdb -q ./a.out [1] 11 Reading symbols from ./a.out...(no debugging symbols found)...done. Remote debugging using :1234 Reading symbols from /lib/ld-linux-armhf.so.3...Reading symbols from /usr/lib/debug//lib/arm-linux-gnueabihf/ld-2.19.so...done. done. Loaded symbols for /lib/ld-linux-armhf.so.3 0xf67debc0 in _start () from /lib/ld-linux-armhf.so.3 (gdb) c Continuing. Hello, world! [Inferior 1 (Remote target) exited normally] (gdb) quit [1]+ Done qemu-arm-static -g 1234 "${!#}" 0<&0
オプション指定を簡略化し実行できていることが確認できる。