Edited at

たのしい組み込みCMake

この記事は NTTコミュニケーションズ Advent Calendar 2018 の19日目です。

先週土日風邪で寝込んでいて大幅に遅れてしまいました。すみません。

今回は楽しい楽しいCMakeを組み込みで使っていくための話をしようと思います。


TL;DR


  • 組み込みMakefile : 素直で簡単。でも1から全て書く必要があるので大変

  • 組み込みCMake : 多機能で便利。でもCMakeが何を自動でやってくれてるか一つずつ暴いて変更していく必要があって大変

  • 適材適所で使い分けると良さそう


CMakeとは

CMakeは数あるビルド自動化ツールのうちの一つです。

有名どころではLLVMやOpenCVなどで使われています。


なぜCMakeなのか

Makefileは素直で小回りの効く良いビルド自動化ツールだと思います。技術としてよく枯れていて(ほめことば)資料も多いです。

しかし、たくさん書いていると以下のような点が気になってきます。


  • 用意されてる仕組みが少ないので書くのが大変

  • それぞれが独自の仕組みを用意することになるので、他人からは理解が大変。覚えても使いまわしが...

  • autotoolsは設定ファイルがたくさんできちゃう

CMake(及びその他Mesonなどのビルドシステム)はこの点を解決できる可能性のあるツールです。

CMakeは様々な便利な仕組みが予め提供されています、そのため、例えばC言語の簡単なプログラムをビルドしたいだけであれば、CMakeList.txtにたった3行を書くだけで行えてしまいます。


CMakeLists.txt

cmake_minimum_required(VERSION 3.13)

project(Hello)
add_executable(Hello main.c)

この他にも様々な機能が用意されており、例えば「デバッグとリリースビルドを分ける」みたいなこともCMakeの用意した仕組みを使えば比較的簡単に行なえます(方法については割愛します。「CMAKE_BUILD_TYPE」などで調べてください)。これらの仕組みをうまく活用することで、Makefileに比べ少ない記述量でビルドが行えるようになります。そして、その仕組みはCMakeを使っているプロジェクトでは基本共通なので、公式のマニュアルや多くの人が出しているサンプルを読めば理解はしやすいですし、一度覚えれば多くの場所で役立つでしょう。


CMakeの便利ポイント

先程挙げた以外に、個人的に気がついたCMakeのメリットを3つ挙げます。


  • out-of-sourceビルド

  • クロスコンパイル用設定の分離(後述)

  • autotoolsに比べ必要なファイルが少ない(CMakeList.txtを一つ置くだけで良くなる。説明省略)

ここでは最初のひとつだけ解説を行います。

out-of-sourceなビルドは、ソースコードのあるパスとは異なる場所でビルドを行う方法です。中間生成ファイルなどはビルドディレクトリに集まるので、ソースディレクトリはキレイなままですし、生成物を削除したいときもビルドディレクトリを削除するだけでよくなります。

一方、in-sourceビルドは中間生成ファイルをソースディレクトリに置くので、ソースディレクトリが汚れます。とても丁寧にMakefileを作らなければ、ソースディレクトリをキレイにできないという問題もあります。

そのため、これから新規に開発する際には可能な限り最初からout-of-sourceでビルドできるようにすべきでしょう。

CMakeではこのout-of-sourceビルドを簡単に行う方法が提供されています。

試しにC言語のHelloWorldをout-of-sourceでビルドしてみましょう。コードは以下を参照です。

CMakeでは-Bオプションを使うことで簡単にビルドディレクトリを変更できます。

cmakeとninjaをインストールした後、helloworldディレクトリに移動して、以下のコマンドを実行してください。


$ cmake -H. -Bbuild -GNinja
$ cmake --build build/

ビルド後のディレクトリを見てみると、buildディレクトリの中でout-of-sourceなビルドが行なえていることがわかります。

$ tree -L 2

.
├── build
│ ├── build.ninja
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── cmake_install.cmake
│ ├── Hello
│ └── rules.ninja
├── CMakeLists.txt
└── main.c

もちろん、autotools + Makefileでもout-of-sourceなビルドはできます。ただし、そのためには開発者がきちんとout-of-sourceビルドを意識してMakefile.inなどを作り込んでおく必要があります。

一方、CMakeを使えば、先程のたった3行の記述でもout-of-source対応になるのでとても簡単です。


組み込みCMake

前置きが長くなりました。ここからはこのCMakeを組み込みプログラムのビルドに使う方法について解説していこうと思います。

昔のことはわかりませんが、最近のCMakeは組み込みのビルドにも利用できます。ただし、最初にTL;DRにも書いたようにここからはCMakeが気を利かせて何をやってくれていたかを調べて変更を加えていくという、茨の道になっていきます。頑張っていきましょう。


ターゲットの設定

今回はAArch64向けのUEFIアプリ(PEバイナリ)をclangとlld-link(lld)を使ってクロスビルドする方法について考えていきます。

ビルドの流れについては以前Makefileを使って書いたものがあるので、以下のリポジトリを参照してください。


CMAKE_TOOLCHAIN_FILE

CMakeを使ってクロスビルドを行う際、クロスビルドに必要なtoolchainやオプションの設定をtoolchainファイルという別のファイルに分離して置いておくことができます、クロスコンパイルを行う際は、cmakeコマンドに -D オプションで CMAKE_TOOLCHAIN_FILE 定数を使ってその分離したファイルのパスを指定するだけです。

例えば、aarch64-uefi.cmake というファイルにAArch64でUEFIアプリをクロスビルドする際の設定を書いたとすると、以下のコマンドで読み込んでクロスビルドができます。

$ cmake -H. -Bbuild -GNinja -DCMAKE_TOOLCHAIN_FILE=./aarch64-uefi.cmake

$ cmake --build build/

このように分離しておくことで、1つのCMakeLists.txtの中に様々なアーキテクチャ向けの設定をたくさん書く必要がなくなり見通しが良くなりますし、クロスコンパイルのターゲットを増やすことも作り込み次第ですが簡単にできるかもしれません。


CMAKE_TOOLCHAIN_FILEを書く

ここからはAArch64なUEFIアプリを作るためのtoolchainファイルを実際に書きながら、CMakeでクロスコンパイルする際に何をするにはどうすればよいかを説明していきたいと思います。

なお、公式のドキュメントはこちらにあります。併せてお読みください。

https://gitlab.kitware.com/cmake/community/wikis/doc/cmake/CrossCompiling


toolchainの指定

クロスコンパイラに使うコンパイラ等を設定します。特に解説は必要ないでしょう。

set(CMAKE_C_COMPILER clang)

set(CMAKE_AR llvm-ar CACHE FILEPATH "Arhiver")
set(CMAKE_RANLIB llvm-ranlib CACHE FILEPATH "Ranlib")
set(CMAKE_AS llvm-as)
set(CMAKE_NM llvm-nm)
set(CMAKE_OBJDUMP llvm-objdump)

arとranlibは少し特殊な修正が必要です。理由までは追いきれていません(すみません)。

https://stackoverflow.com/questions/40210347/cmake-build-failed-with-cmake-ar-notfound-cr-exe-not-found

一緒にここでリンカも設定したいところですが、リンカの設定はこのように簡単にはできないようなので、あとで設定を行います。


target設定

クロスビルドのターゲット設定を行います。

set(CMAKE_SYSTEM_NAME Generic)

set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER_TARGET aarch64-pc-windows-coff)

CMakeでBareMetal向けのビルドを行うには、CMAKE_SYSTEM_NAMEにGenericに設定します。

ここに設定する値によって、予めそのプラットフォーム向けの設定が読み込まれるようです。Genericの場合は以下の設定が読まれて共有ライブラリのビルドが禁止されます(今回は必要ありませんが、BareMetalで共有ライブラリをビルドする必要がある場合はこのオプションをあえて無効にする必要があります)。

https://github.com/Kitware/CMake/blob/master/Modules/Platform/Generic.cmake

CMAKE_SYSTEM_PROCESSORはターゲットのアーキテクチャを設定します。今回はAArch64なのでaarch64をセットしています。

CMAKE_C_COMPILER_TARGETはCコンパイラにClangを使う際に必要な設定です。clangでクロスコンパイルする際に --triple オプションで渡す設定を入れてください。(余談 : オプション一つでクロスコンパイルできちゃうのがclangのいいところですよね)

https://cmake.org/cmake/help/v3.0/manual/cmake-toolchains.7.html


sysrootの設定

クロスコンパイルターゲット向けのライブラリ等の入ったsysrootディレクトリを指定します。

# sysroot

set(CMAKE_SYSROOT $ENV{HOME}/opt/llvm/sysroot-aarch64-elf)
set(CMAKE_INCLUDE_PATH ${CMAKE_SYSROOT}/usr/include)
set(CMAKE_LIBRARY_PATH ${CMAKE_SYSROOT}/usr/lib)
set(CMAKE_INSTALL_PREFIX ${CMAKE_SYSROOT}/usr)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}/usr)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

私は $HOME/opt/llvm/sysroot-aarch64-elf にsysroot環境があるのでこのように設定しました。

CMakeでは$ENV{変数名}とやるとシステム変数が取得できます。


CMAKE_TRY_COMPILE_TARGET_TYPE設定

CMakeではビルド前にコンパイラが正しく動作するかをチェックしてくれますが、クロスコンパイルでは共有ライブラリがビルドできなかったり、ビルドができても実行できなくてエラーになる場合があります。

そういうことを防ぐために、静的ライブラリをビルドしてテストを通してもらえるように設定を行います。

set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)


includeディレクトリの追加

includeで探してくるディレクトリを追加したいときはこのようにします。

include_directories(${CMAKE_SYSROOT}/usr/include/efi)

include_directories(${CMAKE_SYSROOT}/usr/include/efi/${CMAKE_SYSTEM_PROCESSOR})


CFLAGSの追加

Cコンパイラに渡すCFLAGSを追加します。

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-stack-protector -fshort-wchar")

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11 -Wall -Wextra -Wpedantic")

コンパイラに渡すフラグを設定する際には、CMAKE_<LANG>_FLAGSを使います。<LANG>のところには設定したい言語名が入ります。

https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_FLAGS.html

次に細かいところに注目しましょう。

先程書いたCFLAGS設定は、Makefileなら以下のように書けますが、CMakeではちょっと違う書き方が必要になります。

CFLAGS += -fno-stack-protector \

-fshort-wchar \
-std=c11 \
-Wall \
-Wextra \
-Wpedantic

まず、CMakeではMakefileでつかっていた += による設定の追加は、以下のように変数を展開して再設定する必要があります。

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} 追加したい設定")

また、CMakeでは設定したい値は " で囲うのが大切です。そうしないと、スペースが;に置換されてビルド時に入ってしまいます。

そして、\ をつかって改行をはさみつつ複数行に書く方法ですが、関数によっては改行が意味を持つ場合があるためか、今の所CMake同じようなことをする方法が分かっていません。うまくやる方法があれば教えていただけると嬉しいですが、今回はそのままにします。


リンカの設定

CMakeでリンカの設定をするためには、CMakeの仕組みと、リンクのフェーズで何を行っているかを簡単に説明する必要があります。


リンカフラグの設定

まず、CMakeはビルドするバイナリが以下のどれかによって、ビルドの設定を別の変数にセットする必要があります。


  • 実行バイナリ(EXE)

  • 共有ライブラリ(SHARED_LIBRARY)

  • 静的ライブラリ(STATIC_LIBARY)

  • その他(省略)

今回はCMakeLists.txtでadd_executable関数を使ってビルドする、つまり実行バイナリ(EXE)を作ろうとしてるのでCMAKE_EXE_LINKER_FLAGSにリンカフラグを設定する必要があります。ちなみに共有ライブラリのリンカフラグはCMAKE_SHARED_LINKER_FLAGSに、静的ライブラリはCMAKE_STATIC_LINKER_FLAGSに設定します。

set(ld_flags "-subsystem:efi_application -dll -entry:efi_main")

set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${ld_flags}")


リンカの変更

CMakeでリンカの変更を行うのは少し大変です。

CMakeのデフォルトでは、リンクの処理をgccやclangなどのコンパイラ経由で行っています。

コンパイラ経由でリンクを行っているのであれば -fuse-ld=ld.lld のようにすれば簡単にリンカが変えられるのではと思いますが、これは少なくとも私の環境では動作しませんでした。

個人的にはリンクの処理をコンパイラ経由で行うのはあまりやらないので、リンカとコンパイラは分けて使いたいところです。

しかし、CMakeにはリンカだけを変更するという設定はありません。代わりに、リンクのコマンドをそのままそっくり置き換えることは可能です。ここでもバイナリの種類によって設定する変数が変わります。名前の一貫性はありません。つらい。


  • 実行バイナリ : CMAKE_<LANG>_LINK_EXECUTABLE

  • 共有ライブラリ : CMAKE_<LANG>_CREATE_SHARED_LIBRARY

  • 静的ライブラリ : CMAKE_<LANG>_CREATE_STATIC_LIBRARY

今回はCの実行バイナリをlld-linkでリンクして作るので CMAKE_C_LINK_EXECUTABLE を以下のように設定します。

set(CMAKE_C_LINK_EXECUTABLE "/usr/bin/lld-link <LINK_FLAGS> <OBJECTS> <LINK_LIBRARIES> -out:<TARGET>")

<OBJECTS> などはCMakeの方で設定したものが入ります。<>で囲われた変数に何があって、どういった中身が入ってるかをまとめたサイトは私もまだ見つけられてません。もしご存知の方がいましたら教えてください。


toolchainファイルの全体

以上でtoolchainファイルは完成です。最終的にこんな感じになりました。


aarch64-uefi.cmake

# toolchain

set(CMAKE_C_COMPILER clang)
set(CMAKE_AR llvm-ar CACHE FILEPATH "Arhiver")
set(CMAKE_RANLIB llvm-ranlib CACHE FILEPATH "Ranlib")
set(CMAKE_AS llvm-as)
set(CMAKE_NM llvm-nm)
set(CMAKE_OBJDUMP llvm-objdump)

# to pass compiler test
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

# target
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(triple aarch64-pc-windows-coff)
set(CMAKE_C_COMPILER_TARGET ${triple})

# sysroot
set(CMAKE_SYSROOT $ENV{HOME}/opt/llvm/sysroot-aarch64-elf)
set(CMAKE_INCLUDE_PATH ${CMAKE_SYSROOT}/usr/include)
set(CMAKE_LIBRARY_PATH ${CMAKE_SYSROOT}/usr/lib)
set(CMAKE_INSTALL_PREFIX ${CMAKE_SYSROOT}/usr)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}/usr)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

# set include directories
include_directories(${CMAKE_SYSROOT}/usr/include/efi)
include_directories(${CMAKE_SYSROOT}/usr/include/efi/${CMAKE_SYSTEM_PROCESSOR})

# flags
# C
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-stack-protector -fshort-wchar")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11 -Wall -Wextra -Wpedantic")

# linker
set(ld_flags "-subsystem:efi_application -dll -entry:efi_main")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${ld_flags}")

set(CMAKE_C_LINK_EXECUTABLE "/usr/bin/lld-link <LINK_FLAGS> <OBJECTS> <LINK_LIBRARIES> -out:<TARGET>")



toolchainファイルを使ったビルド

早速できたtoolchainファイルを使ってUEFIアプリをビルドしてみましょう。

コードは以下のuefi_helloディレクトリ以下を参照してください。

ビルドは以下のコマンドで行えます(ビルドには sysroot以下にあらかじめgnu-efiのヘッダがインストールされている必要があります)。

$ cmake -H. -Bbuild -GNinja -DCMAKE_TOOLCHAIN_FILE=./aarch64-uefi.cmake

$ cmake --build build/

うまく行けばbuildディレクトリ以下にuefi_main.efiができているはずです。


おわりに

以上で組み込み環境でのCMake解説をおわります。

CMakeは使いこなせるようになればout-of-sourceビルドが簡単にできるようになるなど、様々な恩恵があります。これから大きめなプロジェクトを開発する予定がある場合は、予めCMakeなどの今どきのビルドシステムを使って書いていくべきでしょう。

一方、組み込みでCMakeを使うにはCMakeの仕組みをよく理解して、変数を置き換えたりコマンドを置き換えたりしていく必要があり、結構大変です。実際に私がここまでたどり着くのには、趣味の時間を一ヶ月ほど費やしました。作ろうとしているプロジェクトが非常に小さく完成度より開発速度を求めたい場合や、アクロバティックなビルドをしなければ行けない場合は、素直なMakefileを使ってさっと書いてしまうのも良いと思います。

以上、本記事がみなさまの開発のお役に立てれば幸いです。