clang + cmake で aarch64 linux 向けに C/C++ アプリをクロスコンパイルするメモ


背景

  • レイトレーシングや機械学習など, aarch64(arm64) linux のプログラムを書いていたり, ライブラリをビルドしていたりする
  • 市販で入手しやすい Native 環境(e.g. Raspberry Pi 4, Android スマホ + termux や, Jetson AGX とか. だいたい aarch64 4 コア + 8 GB mem な環境) だとビルドが遅かったり, 並列ビルドすると out-of-memory になりつらいので, つよつよ host PC(Threadripper とか)で cross-compile でやりたい
    • 特に embree-aarch64 で ARM のビルドだと, 同等の性能の x86 の 5~10 倍はコンパイル遅いような現象が出て辛い
    • AWS とかでつよつよ ARM インスタンス借りる手もあるが, レイトレーシングなど画面に出したりなどのアプリだとめんどいし, またコンパイルうまく行かずにソースコード直したり原因突き止めたりとかで時間消費しても課金されるのでおつらい.
  • gcc だと aarch64-***-gcc みたいにコマンドが別れていて, cross-compile 設定がやりやすいが, clang だと基本 triple 指定なので cmake 記述がめんどい
  • ネットにある情報(as of Jul 2020)だと, arm 32bit を想定しているのばかりで, aarch64 環境向けの設定がまったくない.

情報

Stackoverflow を参考にして, toolchain file を作って対応していきます.

方法

ターゲットは Raspberry Pi(64bit) や Jetson AGX(aarch64 Ubuntu 18.04)などを想定します.

ホスト環境は

  • cmake 3.17
  • Ubuntu 18.04(x64)
  • clang 10(apt や, prebuilt で入るもの)

を仮定します. ターゲットとホストで同じ Ubuntu バージョンの環境(glibc, libstdc++ のバージョンが同じ環境)を使っているものとします.

cmake および clang は, あまり古すぎると aarch64 cross-compile 対応が未成熟でうまくいかないかもです.

libc, libstdc++ など

clang のパッケージ自体には libc(glibc) など含まれていないので(llvm-libc が開発中の模様ではある), gcc の libc, libstdc++(C++ STL)を流用します.
これは gcc のパッケージを入れるのが手っ取り早いと思います.

Ubuntu 18.04 には幸いにも aarch64 cross-compile 用 gcc パッケージがあります.

$ sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
$ sudo apt install g++-multilib
$ sudo apt install libstdc++-8-dev-arm64-cross

(g++-multilib を入れれば, gcc-aarch64-linux-gnu g++-aarch64-linux-gnu は不要かも?)

/usr/aarch64-linux-gnu に inc, lib が配置されます.
/usr/lib/gcc-cross/aarch64-linux-gnu/ にもクロスコンパイル用のなにか(crtbegin.o などがあるのでランタイム周り?)が配置されます.

libstdc++-8-dev-arm64-cross は aarch64 用の libstdc++ が入ります.
入れないと, コンパイル時に bits/c++-config.h が見つからないエラーがでます。
(/usr/aarch64-linux-gnu/include/c++/8/aarch64-linux-gnu/bits/c++config.h ファイルができているのを確認しましょう)

今回は 8 を使いましたが, Ubuntu のバージョンなどに応じて変更する必要があるかもです
(また, apt には pic 版もありました. コンパイルうまくいかなかったら pic 版を試すのもいいかも)

最小限のクロスコンパイル

まずは, 最小限の構成で, clang を用い C++ コードが aarch64 にクロスコンパイルできるかをチェックしましょう.

triple

clang の場合, --target でコンパイル先のアーキテクチャを指定することでクロスコンパイルします.

clang --target=aarch64-linux-gnu

linker も, clang コマンドを使います.

sysroot, gnu-toolchain

クロスコンパイルでは, コンパイル先の環境のヘッダやライブラリなどの検索パスを設定する必要があります.

--sysroot でシステムヘッダ(?)のパスをしています. 今回は --sysroot=/usr/aarch64-linux-gnu になります.

別途 --gnu-toochain で glibc, libstdc++ 関連(?)のパスを設定します.
実際にはここで指定したパスに triple(aarch64-linux-gnu) が付与されたりして検索されるようなので, --gnu-toolchain=/usr になります.
内部的では -L... -lgcc_s ... などに展開されます.
複数 aarch64 gcc 環境(version 7.5, 8 がインストールされているなど)がある場合, よろしくバージョンを選択してくれるようです.

また, --gnu-toolchain の代わりに -B flag を使うでも OK そうです: https://gcc.gnu.org/onlinedocs/gcc/Directory-Options.html

URL は gcc のヘルプですが, clang でもサポートされています.

pthread

-lpthread でもいけるかもですが, Linux(posix) の推奨(?)である -pthread を使います.
C++11 thread など使う場合は現状 pthread 必須ですから(Posix 環境の場合), だいたい現在のナウでヤングな C++11 or later コードでは -pthread 必須と考えておいたほうがいいでしょう.

LLD linker

bfd(/usr/lib/gcc-cross/aarch64-linux-gnu/8/../../../../aarch64-linux-gnu/bin/ld) だと, pthread 関連で,

/usr/lib/gcc-cross/aarch64-linux-gnu/8/../../../../aarch64-linux-gnu/bin/ld: cannot find /usr/aarch64-linux-gnu/lib/libpthread.so.0 inside /usr/aarch64-linux-gnu
/usr/lib/gcc-cross/aarch64-linux-gnu/8/../../../../aarch64-linux-gnu/bin/ld: cannot find /usr/aarch64-linux-gnu/lib/libpthread_nonshared.a inside /usr/aarch64-linux-gnu

とエラーがでてしまいます(どうも内部でのパス探索がうまくいっていないっぽい?) gold の場合もだめでした.

-fuse-ld=lld として, LLD リンカーを使いましょう
(apt で clang を入れている場合は, sudo apt install lld-10 などとして別途インストールが必要)

Minimal sample

#include <thread>
#include <iostream>

void fn(void) {
  std::cout << "bora\n";
}

int main(int argc, char **argv)
{
  std::thread th(fn);

  th.join();

  return EXIT_SUCCESS;
}
/mnt/data/local/clang+llvm-10.0.0-x86_64-linux-gnu-ubuntu-18.04/bin/clang++ -v \
  --target=aarch64-linux-gnu \
  -std=c++14 \
  --gcc-toolchain=/usr \
  --sysroot=/usr/aarch64-linux-gnu \
  main.cc \
  -fuse-ld=lld -pthread

Voala!

できたバイナリを aarch64 linux マシンにコピって動けば成功です!

あとはこの設定をうまく cmake で行えばよさそうです.

うまく行かない場合

-v -H オプションなどで, 以下のようなヘッダのサーチパスやインクルード履歴など見れますのでがんばってデバッグしてみましょう.

#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc-cross/aarch64-linux-gnu/8/../../../../aarch64-linux-gnu/include/c++/8
 /usr/lib/gcc-cross/aarch64-linux-gnu/8/../../../../aarch64-linux-gnu/include/c++/8/aarch64-linux-gnu
 /usr/lib/gcc-cross/aarch64-linux-gnu/8/../../../../aarch64-linux-gnu/include/c++/8/backward
 /usr/lib/llvm-9/lib/clang/9.0.0/include
 /usr/aarch64-linux-gnu/include
End of search list.
. /usr/lib/gcc-cross/aarch64-linux-gnu/8/../../../../aarch64-linux-gnu/include/c++/8/cstddef
.. /usr/lib/gcc-cross/aarch64-linux-gnu/8/../../../../aarch64-linux-gnu/include/c++/8/aarch64-linux-gnu/bits/c++config.h
... /usr/lib/gcc-cross/aarch64-linux-gnu/8/../../../../aarch64-linux-gnu/include/c++/8/aarch64-linux-gnu/bits/os_defines.h
.... /usr/aarch64-linux-gnu/include/features.h
..... /usr/aarch64-linux-gnu/include/stdc-predef.h
..... /usr/aarch64-linux-gnu/include/sys/cdefs.h
...... /usr/aarch64-linux-gnu/include/bits/wordsize.h
...... /usr/aarch64-linux-gnu/include/bits/long-double.h
..... /usr/aarch64-linux-gnu/include/gnu/stubs.h
...... /usr/aarch64-linux-gnu/include/bits/wordsize.h
...... /usr/aarch64-linux-gnu/include/gnu/stubs-lp64.h
... /usr/lib/gcc-cross/aarch64-linux-gnu/8/../../../../aarch64-linux-gnu/include/c++/8/aarch64-linux-gnu/bits/cpu_defines.h
.. /usr/lib/llvm-9/lib/clang/9.0.0/include/stddef.h
... /usr/lib/llvm-9/lib/clang/9.0.0/include/__stddef_max_align_t.h

bits/c++config.h が見つからないエラー?

しかし、環境によっては(たとえば GitHub Actions での Ubuntu 18.04 ビルド環境), --sysroot-B(--gcc-toolchain)hではうまく aarch64 クロスコンパイル環境のヘッダを探索してくれないケースがあります.

たとえば, x86 のヘッダなどが優先されてしまい, bits/c++config.h が見つからないエラーが出てしまう.

その場合は, -I で直接指定するしかなさそうです(Clang cross compile で推奨の方法っぽい. -isystem でもいいかも)
-nostdinc++(C++ ヘッダを標準パス?から探索しない)も追加しておくとよいでしょう.

-nostdinc++ -I/path/to/c++ -I/path/to/c++/aarch64-linux-gnu/

など.

Cmake 設定

を参考にして toolchain ファイルを作ります.

# Based on https://stackoverflow.com/questions/54539682/how-to-set-up-cmake-to-cross-compile-with-clang-for-arm-embedded-on-windows
set(CMAKE_CROSSCOMPILING TRUE)
SET(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)

if(DEFINED ENV{GCC_ARM_TOOLCHAIN})
    set(GCC_ARM_TOOLCHAIN $ENV{GCC_ARM_TOOLCHAIN})
else()
    # Assume `/usr/aarch64-linux-gnu/` exists
    # (e.g. though `sudo apt install gcc-aarch64-linux-gnu` on Ubuntu)
    set(GCC_ARM_TOOLCHAIN "/usr")
endif()

# Clang target triple
SET(TARGET_TRIPLE aarch64-linux-gnu)

# specify the cross compiler
# TODO(LTE): Read clang path from environment variable
SET(CMAKE_C_COMPILER_TARGET ${TARGET_TRIPLE})
SET(CMAKE_C_COMPILER /mnt/data/local/clang+llvm-10.0.0-x86_64-linux-gnu-ubuntu-18.04/bin/clang)
SET(CMAKE_CXX_COMPILER_TARGET ${TARGET_TRIPLE})
SET(CMAKE_CXX_COMPILER /mnt/data/local/clang+llvm-10.0.0-x86_64-linux-gnu-ubuntu-18.04/bin/clang++)
SET(CMAKE_ASM_COMPILER_TARGET ${TARGET_TRIPLE})
SET(CMAKE_ASM_COMPILER /mnt/data/local/clang+llvm-10.0.0-x86_64-linux-gnu-ubuntu-18.04/bin/clang++)

# Don't run the linker on compiler check
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)


# or we can use `--gcc-toolchain` for clang
# this flag is required to find libc(glibc) headers, crtbegin.o, etc
set(CMAKE_C_FLAGS_INIT " -B${GCC_ARM_TOOLCHAIN}")
set(CMAKE_CXX_FLAGS_INIT " -B${GCC_ARM_TOOLCHAIN} ")

# C/C++ toolchain
set(GCC_ARM_SYSROOT "${GCC_ARM_TOOLCHAIN}/${TARGET_TRIPLE}")
set(CMAKE_SYSROOT ${GCC_ARM_SYSROOT})
#set(CMAKE_FIND_ROOT_PATH ${GCC_ARM_SYSROOT})

# Search for programs in the build host directories
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# For libraries and headers in the target directories
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
  • -B(or --gcc-toolchain)のパスは CMAKE_C_FLAGS_INIT, CMAKE_CXX_FLAGS_INIT に指定する
  • linker には clang を使う

lld 設定は, CMakeLists.txt あたりに, CMAKE_LINK_FLAGS-fuse-ld=lld を使記述します(Toolchain cmake で記載してもいいかも)

set(CMAKE_CROSSCOMPILING TRUE)

pthread

-B and --sysroot がきちんと設定されていれば, find_package(Threads) で pthread 見つかります.

ただ, cross-compiling 環境で, THREADS_PREFER_PTHREAD_FLAG On にして, find_package(Threads) すると, CMAKE_THREAD_LIBS_INIT にはなにもセットされないバグがあるようです(cmake 3.17.3 で確認)

work around として, -pthread を明示的に linker flag などに設定しましょう.

cross compile のモードかどうかは, CMAKE_CROSSCOMPILING 変数で判定できます.

IF (CMAKE_CROSSCOMPILING)
  LIST(APPEND CMAKE_LINK_FLAGS "-pthread ")
ENDIF()

など.

ただ, cross compile モードの場合, UNIX フラグは立たないようです.

Windows への cross compile のケースなどと区別したい場合,
CMAKE_SYSTEM_PROCESSOR や, toolchain file で設定した変数(e.g. TARGET_TRIPLE)を export して(MARK_AS_ADVANCED?), 外側の CmakeLists.txt でチェックできるようにするとよさそうです.

実例

embree-aarch64 を参照ください.
(現在 branch にあるが, いずれ master にマージ予定)

まとめ

めんどいです. pthread や glibc(libc) 周りでいろいろワナがありますので注意です.

TODO

  • できたバイナリファイルの aarch64 実機へのコピーを楽にする方法を探す
    • rsync で watch and 同期?
    • aarch64 実機のストレージを NFS, CIFS(samba) マウントでもよいが, 10 GbE 以上でないとネットワーク速度律速になりそう
  • libc++(-stdlib=libc++)でのクロスコンパイルを試す
  • llvm-libc が出てきたら, llvm-libc でいけるか試す.
  • 優秀な aarch64 + C/C++ 開発若人さまが, clang + cmake で aarch64 クロスコンパイル環境を整えることで, 人類史上最速で究極の aarch64 + C/C++ 開発若人さまへと昇華なされるスキームを確立する旅に出たい