DockerでユーザモードQEMUによるARMエミュレーション環境を構築する

手元にx64マシンしかない状況でARM環境を用意しようとした場合、以下のような選択肢が考えられる。

  • 実機を用意する(Raspberry Pi、Android端末など)
    • お金がかかる、使うのに手間がかかる
  • QEMUのシステムエミュレーションを使う
    • 再現性が高い一方、重い
  • QEMUのユーザモードエミュレーションを使う(参考
    • 再現性はシステムエミュレーションに比べ劣るが軽い、実行時やgdbデバッグ時のライブラリパス指定がやや煩雑
  • QEMUのユーザモードエミュレーションにbinfmtとchrootを組み合わせて使う(参考1参考2
    • 軽い上にライブラリパスの指定が不要だが、chroot環境下に各種プログラムを用意するのに手間がかかる
  • QEMUのユーザモードエミュレーションにbinfmtとコンテナ仮想化を組み合わせて使う(参考
    • 軽い上にライブラリパスの指定が不要、さらに各種パッケージインストール済みのイメージとして扱える

ここでは最後の選択肢において、コンテナ仮想化にDockerを利用した場合のARM環境構築について記す。 Docker自体のインストール、利用方法については「Dockerを使ってみる」を参照。 ホストOSにはUbuntu 14.04.1 LTS amd64x86_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

オプション指定を簡略化し実行できていることが確認できる。

関連リンク