はじめに
組み込みLinux開発では「手元のx86_64マシンでARM向けバイナリを作る」という作業が日常的に発生します。これがクロスコンパイルです。本記事では第1〜3回で使ったサンプルプロジェクトをそのまま使い、各ビルドシステムでクロスコンパイルをどう設定するかを比較します。
本シリーズは当初、第3回(CMake入門)で完結とする予定でした。しかし「ビルドシステムの基本を理解した上で、実際に手を動かして具体的な成果物を作ってみたい」という目的のもと、当初の計画には無かった第4回を追加することにしました。
本記事はシリーズの第4回です。
- 第1回:Makefileをちゃんと理解する
- 第2回:
./configureの正体——autoconf/automake入門 - 第3回:CMake入門——Autotoolsとの比較で理解する
- 第4回:クロスコンパイル入門——ARM Linux向けビルドを理解する(本記事)
クロスコンパイルとは
通常のビルドは「ビルドするマシン」と「実行するマシン」が同じです。クロスコンパイルはこれらが異なります。
【通常のビルド】
x86_64 PC → ビルド → x86_64向けバイナリ → x86_64 PCで実行
【クロスコンパイル】
x86_64 PC → ビルド → ARM向けバイナリ → Raspberry Pi等で実行
組み込み機器はCPUパワーが限られるため、高性能な開発機でビルドして機器に転送するのが一般的です。
ターゲットトリプルとは
クロスコンパイルでは「何向けのバイナリを作るか」をターゲットトリプルという文字列で表します。
arm - linux - gnueabihf
↑ ↑ ↑
CPU OS ABI(Application Binary Interface)
| フィールド | 今回の値 | 意味 |
|---|---|---|
| CPU | arm |
ARMアーキテクチャ |
| OS | linux |
Linux上で動作 |
| ABI | gnueabihf |
GNU libc、ハードウェア浮動小数点 |
本記事では arm-linux-gnueabihf をターゲットとします。Raspberry Pi 2/3/4(32bitモード)向けのバイナリが生成されます。
ツールチェーンのインストール
Ubuntu / Debian
32ビットARM Linux向けのクロスコンパイラは以下でインストールできます。
sudo apt update
sudo apt install gcc-arm-linux-gnueabihf
macOS
brew install arm-linux-gnueabihf-binutils
# GCC本体はHomebrewのtapから
brew tap messense/macos-cross-toolchains
brew install arm-unknown-linux-gnueabihf
インストール確認
arm-linux-gnueabihf-gcc --version
# arm-linux-gnueabihf-gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
動作確認はQEMUで
実機がなくてもQEMUでARMバイナリを実行できます。
# Ubuntu
sudo apt install qemu-user
# macOS
brew install qemu
# ARMバイナリをx86_64マシン上で実行
qemu-arm ./hello
サンプルプロジェクト
第1〜3回と同じ構成を使います。
sample/
├── include/
│ └── greet.h
└── src/
├── main.c
└── greet.c
Cのソースコードは変更不要です。ビルドシステム側の設定だけを変えます。
1. Makefileでクロスコンパイル
Makefileでのクロスコンパイルは非常にシンプルです。CC 変数をクロスコンパイラに差し替えるだけです。
方法①:変数を直接書き換える
# ターゲット名
TARGET = hello
# クロスコンパイラを指定
CC = arm-linux-gnueabihf-gcc
CFLAGS = -Wall -Wextra -I./include
DEPFLAGS = -MMD -MP
SRCS = src/main.c src/greet.c
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d)
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
-include $(DEPS)
clean:
rm -f $(TARGET) $(OBJS) $(DEPS)
方法②:コマンドラインから上書きする(推奨)
Makefileはネイティブビルド用のままにしておき、実行時に CC を上書きする方法が柔軟で使いやすいです。
# ネイティブビルド(従来通り)
make
# クロスコンパイル(CCを上書き)
make CC=arm-linux-gnueabihf-gcc
方法③:CROSS_COMPILE変数を使う
Linuxカーネルのビルドなどで広く使われている慣習です。
CROSS_COMPILE ?=
CC = $(CROSS_COMPILE)gcc
# ネイティブビルド
make
# クロスコンパイル
make CROSS_COMPILE=arm-linux-gnueabihf-
CROSS_COMPILE にプレフィックスを設定すると $(CROSS_COMPILE)gcc が arm-linux-gnueabihf-gcc になります。
ビルドと動作確認
make CC=arm-linux-gnueabihf-gcc
# ARMバイナリであることを確認
file hello
# hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV)...
# QEMUで実行
qemu-arm ./hello
# Hello, World!
# Hello, Qiita!
2. Autotoolsでクロスコンパイル
Autotoolsは ./configure に --host オプションを渡すだけでクロスコンパイルに対応できます。これがAutotoolsの大きな利点のひとつです。
クロスコンパイル時は --host オプションに実行するターゲットマシンを指定します。
./configure --host=arm-linux-gnueabihf
make
--host を指定すると ./configure は自動的に arm-linux-gnueabihf-gcc をCコンパイラとして使用します。configure.ac や Makefile.am は変更不要です。
実際の手順
# Autotoolsファイルを生成(開発者側の作業)
autoreconf -i
# out-of-sourceビルドディレクトリを作成
mkdir build_arm && cd build_arm
# クロスコンパイル向けにconfigureを実行
../configure --host=arm-linux-gnueabihf
# ビルド
make
# 確認
file src/hello
# src/hello: ELF 32-bit LSB executable, ARM...
# QEMUで実行
qemu-arm src/hello
--build / --host / --target の違い
Autotoolsには似た名前のオプションが3つあります。
| オプション | 意味 | 今回の例 |
|---|---|---|
--build |
ビルドを実行するマシン |
x86_64-linux-gnu(省略可、自動検出) |
--host |
生成したバイナリが動くマシン | arm-linux-gnueabihf |
--target |
コンパイラ自体が生成するコードの対象 | コンパイラをビルドする場合のみ使用 |
通常のクロスコンパイルでは --host だけ指定すれば十分です。
3. CMakeでクロスコンパイル
CMakeのクロスコンパイルはツールチェーンファイル(.cmake ファイル)を使うのが標準的なやり方です。
ツールチェーンファイルはCMakeにターゲットプラットフォームのすべての情報を伝えるための別ファイルです。CMAKE_SYSTEM_NAME、コンパイラへのパス、CMAKE_FIND_ROOT_PATH の設定が必須の要素です。
ツールチェーンファイルを作成する
# toolchain-arm-linux-gnueabihf.cmake
# ターゲットOSとCPUアーキテクチャを指定
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
# クロスコンパイラを指定
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
# find_*コマンドの検索先をターゲット環境に限定する
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) # ツール類はホスト環境を使う
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) # ライブラリはターゲット環境のみ
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) # ヘッダはターゲット環境のみ
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) # パッケージはターゲット環境のみ
ツールチェーンファイルは -DCMAKE_TOOLCHAIN_FILE オプションで指定します。最初の cmake 実行時に1度指定すれば、以降はキャッシュから読み込まれます。
ビルド手順
# ツールチェーンファイルを指定してconfigureステップを実行
cmake -S . -B build_arm \
-DCMAKE_TOOLCHAIN_FILE=toolchain-arm-linux-gnueabihf.cmake
# ビルド
cmake --build build_arm
# 確認
file build_arm/src/hello
# build_arm/src/hello: ELF 32-bit LSB executable, ARM...
# QEMUで実行
qemu-arm build_arm/src/hello
# Hello, World!
# Hello, Qiita!
CMakeLists.txt は変更不要です。ツールチェーンファイルを差し替えるだけで別のターゲットに対応できます。
ツールチェーンファイルを使う利点
ツールチェーンファイルを使うことで、CMakeLists.txt はクロスコンパイルの存在を意識せずに書けます。if(WIN32) や if(UNIX) といった条件分岐もツールチェーンファイルの設定に基づいて正しく評価されます。
複数のターゲット向けにビルドする場合も、ターゲットごとにツールチェーンファイルを用意するだけです。
toolchains/
├── toolchain-arm-linux-gnueabihf.cmake # ARM 32bit Linux
├── toolchain-aarch64-linux-gnu.cmake # ARM 64bit Linux
└── toolchain-x86_64-linux-gnu.cmake # x86_64 Linux(ネイティブ)
3つのビルドシステムの比較
| 項目 | Makefile | Autotools | CMake |
|---|---|---|---|
| 設定方法 |
CC 変数を上書き |
--host オプション |
ツールチェーンファイル |
| ソースの変更 | 不要(方法②③の場合) | 不要 | 不要 |
| 記述量 | 最小 | 最小 | ツールチェーンファイルが必要 |
| 複数ターゲット対応 | 工夫が必要 | オプションで切り替え | ファイルを差し替えるだけ |
| 再利用性 | 低い | 中程度 | 高い(ファイルを使い回せる) |
Makefileは手軽ですが、複数ターゲットを管理するとMakefileが複雑になります。CMakeのツールチェーンファイルはターゲットごとに独立しているため、プロジェクトが大きくなるほど管理しやすくなります。
まとめ
| ビルドシステム | クロスコンパイルのキーワード |
|---|---|
| Makefile |
CC=arm-linux-gnueabihf-gcc または CROSS_COMPILE=arm-linux-gnueabihf-
|
| Autotools | ./configure --host=arm-linux-gnueabihf |
| CMake | -DCMAKE_TOOLCHAIN_FILE=toolchain-arm-linux-gnueabihf.cmake |
どのビルドシステムも CMakeLists.txt や Makefile.am などのビルド定義ファイルを変更せずにクロスコンパイルできる点が重要なポイントです。「どこでビルドするか」と「何をビルドするか」を分離するのがクロスコンパイル設定の基本的な考え方です。