Linux上でARMおよびRISC-Vコードを直接実行:QEMU User-Modeでパフォーマンスを「ハック」する

Virtualization tutorial - IT technology blog
Virtualization tutorial - IT technology blog

課題:なぜ実機ボードを待つために時間を費やす必要があるのか?

組み込みシステム(Embedded Systems)に携わっているなら、古いRaspberry Piでコードをビルドするためだけに午前中を丸々潰してしまった経験があるはずです。安価なARMやRISC-Vチップは、最新のコンパイラを動かすにはパワー不足なことが多いです。通常、X86マシンでクロスコンパイル(cross-compile)し、scpでファイルをボードに転送してテストしますが、この繰り返し作業は非常に時間がかかり、ストレスの原因になります。

私のホームラボでは、IoTファームウェアのテストのためにProxmox上で約12台のVMを管理しています。ARMの仮想マシンを丸ごとエミュレートする(System Emulation)のは、リソースを不必要に消費し、動作も重くなりがちです。より効率的な方法として、私はQEMU User-Modebinfmt_miscを組み合わせて使用しています。これにより、別のアーキテクチャのバイナリを、あたかもX86 Linux上のネイティブアプリケーションのように実行できるようになります。

この手法では、エミュレートされたOSを起動する必要が完全になくなります。仮想マシンのメモリ管理や複雑なネットワーク設定を心配する必要もありません。

コアコンセプト:QEMU User-Modeとbinfmt_misc

QEMU User-Modeの仕組みは?

ハードウェア全体をエミュレートするSystem Emulationとは異なり、User-ModeはCPU命令とシステムコール(syscalls)の変換だけに集中します。ARMプログラムがopen()を呼び出すと、QEMUがそれをキャッチし、X86 Linuxカーネルの対応する命令に変換します。ハードウェアエミュレーションを省くことで、実行パフォーマンスが大幅に向上します。

binfmt_misc:カーネルの「通訳者」

Linuxカーネルにはbinfmt_miscという興味深い機能があります。これは実行ファイルの先頭にある「マジックバイト(magic bytes)」に基づいてファイル形式を識別します。ARMのELFファイルを実行すると、カーネルは即座にそのアーキテクチャを認識します。QEMUをハンドラとして登録しておけば、カーネルが自動的にQEMUを呼び出してファイルを実行してくれます。長いエミュレーションコマンドを打つ代わりに、./my_arm_appと入力するだけで済むのです。

実践:マルチアーキテクチャ環境の構築

現在の組み込み開発者の標準的な環境であるUbuntu/Debianでの手順を説明します。

ステップ1:ツールのインストール

QEMUのstatic版パッケージをインストールする必要があります。static版は動的ライブラリに依存しないため、chroot環境でも安定して動作します。

sudo apt update
sudo apt install -y qemu-user-static binfmt-support qemu-user

カーネルの準備ができているか確認するには、以下のディレクトリを確認してください。

ls /proc/sys/fs/binfmt_misc/

もしqemu-armqemu-riscv64といったファイルがあれば、システムがこれらのアーキテクチャを認識する準備が整っていることを意味します。

ステップ2:X86上でARMプログラムをテスト実行する

簡単なC言語のコードを書き、ARM64 (AArch64) 向けにコンパイルして直接実行してみましょう。まず、クロスコンパイラをインストールします。

sudo apt install -y gcc-aarch64-linux-gnu

シンプルな内容のhello.cファイルを作成します:

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("こんにちは、itfromzero.comの皆さん!\n");
    printf("現在のアーキテクチャ: ");
    fflush(stdout);
    system("uname -m");
    return 0;
}

すべてのライブラリを1つのファイルにまとめるために、-staticオプションを付けてコンパイルします:

aarch64-linux-gnu-gcc -static hello.c -o hello-arm64

このファイルを通常のLinuxアプリケーションと同じように実行してみましょう:

./hello-arm64

X86マシン上にいるにもかかわらず、結果はaarch64と返ってきます。不思議ですよね?

ステップ3:複雑なアプリにchrootを使用する

多くの.soライブラリを必要とするアプリケーションの場合、スタティックビルドは現実的ではありません。解決策は、ターゲットアーキテクチャの完全なルートファイルシステム(rootfs)chrootすることです。

# QEMUのバイナリをボードのrootfsにコピーする
sudo cp /usr/bin/qemu-arm-static /path/to/raspberry-rootfs/usr/bin/

# ARM環境にchrootする
sudo chroot /path/to/raspberry-rootfs /bin/bash

これで、apt installを使ったり、自由にコードをデバッグしたりできます。ホストマシンのCPUパワーを利用して、組み込みボード向けの重い処理を実行できるのです。

実践テクニック:DockerでCI/CDを高速化する

私はよく、X86サーバー上でARM用のDockerイメージをビルドするためにこのトリックを使います。binfmt_miscのおかげで、DockerはDockerfile内の別のアーキテクチャ向けのRUN命令を実行できます。有効にするには、以下のコマンドを一度だけ実行するだけです:

docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

その後は、X86イメージと同じくらい簡単にARM64イメージをビルドできます。これにより、CI/CDプロセスが非常にスムーズになります。電力消費が激しく管理も難しい、実機ボードによる「ビルドファーム」を維持する必要はもうありません。

重要な注意点

  1. パフォーマンス: User-Modeは依然としてエミュレーションであるため、ネイティブコードより2〜5倍程度遅くなります。しかし、それでも組み込みボードの非力なチップ上で直接実行するよりは遥かに高速です。
  2. システムコールの制限: GPIOや特定のレジスタなど、ハードウェアに深く関わる一部の命令はQEMU経由では動作しない場合があります。
  3. マルチスレッド: 非常に重いマルチスレッドアプリケーションでは、稀にレースコンディションが発生することがあります。しかし、ほとんどの一般的なアプリケーションにおいて、QEMUは非常に安定して動作します。

結論

QEMU User-Modeとbinfmt_miscは、シンプルながらも非常に強力なツールセットです。これは組み込みソフトウェアの開発手法を変え、ビルド待ちの時間を大幅に削減してくれます。もしRISC-Vを試してみたいけれど、ボードを購入する予算がまだないという方にとって、これが開始するための最短ルートです。

Share: