osdev-jpでは、OS開発に有用な情報を収集し公開しています

View My GitHub Profile

OS を C++ で開発する場合,標準ライブラリ無しでは言語のコア機能しか使えず C++ の旨味が出し切れません。 そこで C++ 標準ライブラリ libc++ を自作 OS 向けにビルドする方法を紹介します。 動作確認をした環境は Ubuntu 18.04,ビルドに用いたコンパイラは Clang 7.0.1 です。

libc++ は標準 C ライブラリに依存しています。また,アーキテクチャ依存の一部機能を提供するために libc++abi を必要とします。 本記事では標準 C ライブラリとして Newlib を採用しています。

変数定義

まずは以降の手順で共通に使う変数を定義します。

BASEDIR=/usr/local/src
PREFIX=/usr/local/x86_64-elf
COMMON_CFLAGS="-nostdlibinc -O2 -D__ELF__ -D_LDBL_EQ_DBL -D_GNU_SOURCE -D_POSIX_TIMERS"
CC=clang
CXX=clang++
TARGET_TRIPLE=x86_64-elf

BASEDIR はビルドに必要なファイルを置くディレクトリへのパスです。どこでもいいでしょう。 PREFIX はビルドした生成物を最終的に配置するディレクトリへのパスです。これもどこでも大丈夫です。

COMMON_CFLAGS は基本的にこの通り指定してください。うまくビルドできるように調整してあります。

CCCXX はビルドに用いるコマンドを指定します。システムによってはバージョン付きのコマンド名 clang-7 などを指定する必要があるかもしれません。

TARGET_TRIPLE はクロスコンパイル用の指定です。トリプルというだけあって machine-vendor-operatingsystem という形です。しかし,実は 4 番目がある場合があります。 x86_64-elf は machine=x86_64,vendor=unknown,operatingsystem=none で,4 番目が elf という意味になります。

Newlib のビルド

Newlib のソースコードをダウンロードし,configure して make します。

cd $BASEDIR
git clone --depth 1 --branch fix-build https://github.com/uchan-nos/newlib-cygwin.git

Newlib 公式リポジトリではなく私(uchan)のリポジトリからダウンロードしてきます。公式リポジトリのものをそのまま使うとビルドがエラーになってしまうため,修正を行っています。

cd $BASEDIR
mkdir build_newlib
cd build_newlib
../newlib-cygwin/newlib/configure \
  CC=$CC \
  CC_FOR_BUILD=$CC \
  CFLAGS="-fPIC $COMMON_CFLAGS" \
  --target=$TARGET_TRIPLE --prefix=$PREFIX --disable-multilib --disable-newlib-multithread
make -j 4
make install

ソースコードのツリーの外側にビルド用ディレクトリを作ります。 そうするとビルド用ディレクトリを削除しさえすれば何回でもきれいな環境でビルドし直せます。

--disable-multilib は 32 ビット版と 64 ビット版の両方を同時にビルドする multilib という機能を無効化します。これが有効なままだとビルドに失敗することがあります。

--disable-newlib-multithread はマルチスレッドサポートを無効にします。 マルチスレッドは OS のサポートが必要なので,無効化しておかないと OS にリンクした際にマルチスレッド関連の関数がいくつか要求されてしまいます。

make install が完了すると PREFIX に指定したディレクトリ以下に includelib というディレクトリが生成され,そこにファイルがインストールされるはずです。

libc++abi のビルド

libc++abi のソースコードをダウンロードし,cmake して make します。

cd $BASEDIR
git clone --depth 1 --branch llvmorg-7.0.1 https://github.com/llvm/llvm-project.git

念の為,使用するコンパイラバージョン(7.0.1)に合わせたライブラリをダウンロードしています。 他のバージョンでもそのままか,少し修正することでビルド可能かと思います。

--depth 1 とすることで Git の全履歴ではなく特定のコミットだけを取得することになり,clone が高速になります。

cd $BASEDIR
mkdir build_libcxxabi
cd build_libcxxabi
cmake -G "Unix Makefiles" \
  -DCMAKE_INSTALL_PREFIX=$PREFIX \
  -DCMAKE_CXX_COMPILER=$CXX \
  -DCMAKE_CXX_FLAGS="-I$PREFIX/include $COMMON_CFLAGS -D_LIBCPP_HAS_NO_THREADS" \
  -DCMAKE_C_COMPILER=$CC \
  -DCMAKE_C_FLAGS="-I$PREFIX/include $COMMON_CFLAGS -D_LIBCPP_HAS_NO_THREADS" \
  -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \
  -DCMAKE_BUILD_TYPE=Release \
  -DLIBCXXABI_LIBCXX_INCLUDES="$BASEDIR/llvm-project/libcxx/include" \
  -DLIBCXXABI_ENABLE_EXCEPTIONS=False \
  -DLIBCXXABI_ENABLE_THREADS=False \
  -DLIBCXXABI_TARGET_TRIPLE=$TARGET_TRIPLE \
  -DLIBCXXABI_ENABLE_SHARED=False \
  -DLIBCXXABI_ENABLE_STATIC=True \
  $BASEDIR/llvm-project/libcxxabi

make -j4
make install

Newlib のときと同様,ソースコードのツリーの外側にビルド用ディレクトリを作ります。

CMAKE_CXX_FLAGSCMAKE_C_FLAGS に指定している -I$PREFIX/include は,Newlib のヘッダファイルを libc++abi のビルドで利用できるようにします。 このオプションを付けないと libc++abi のビルド時に標準 C ライブラリが参照できずにエラーになります。

-D_LIBCPP_HAS_NO_THREADS は libc++ のヘッダファイルを読み込む際にマルチスレッド関連の機能を誤って有効化しないために指定します。

cmake のオプションで LIBCXXABI_ENABLE_THREADS=False と指定しているのでマルチスレッドが無効になって欲しいところですが,これだけではだめなのです。 libc++abi は libc++ のヘッダファイルを利用しますが, LIBCXXABI_ENABLE_THREADS オプションは libc++abi のファイルにしか効果がなく,libc++ のファイルには影響を及ぼさないようなのです。 libc++ のヘッダファイルを読み込む際にマルチスレッドを無効化するために上記の指定が必要です。

CMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY はビルドしたライブラリが正しいかどうかのテストを,静的ライブラリに対して実行するようにするオプションです。 参考にしたページ たのしい組み込み CMake

LIBCXXABI_ENABLE_EXCEPTIONS=FalseLIBCXXABI_ENABLE_THREADS=False は,それぞれ例外とマルチスレッドを無効にする指定です。例外もマルチスレッドも OS のサポートが必要なので,OS そのものの開発時には無効にしておきます。

LIBCXXABI_ENABLE_SHARED=FalseLIBCXXABI_ENABLE_STATIC=True によって静的ライブラリ(.a)のみを生成するようにします。 OS 本体は普通は静的リンクで作るので,共有ライブラリ(.so)は不要です。

libc++ のビルド

ソースコードは libc++abi のダウンロードのときに一緒にダウンロードされているので,改めてダウンロードする必要はありません。 ビルド用ディレクトリを作って cmake して make するだけです。

cd $BASEDIR
mkdir build_libcxx

cmake -G "Unix Makefiles" \
  -DCMAKE_INSTALL_PREFIX=$PREFIX \
  -DCMAKE_CXX_COMPILER=$CXX \
  -DCMAKE_CXX_FLAGS="-I$PREFIX/include $COMMON_CFLAGS" \
  -DCMAKE_CXX_COMPILER_TARGET=$TARGET_TRIPLE \
  -DCMAKE_C_COMPILER=$CC \
  -DCMAKE_C_FLAGS="-I$PREFIX/include $COMMON_CFLAGS" \
  -DCMAKE_C_COMPILER_TARGET=$TARGET_TRIPLE \
  -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \
  -DCMAKE_BUILD_TYPE=Release \
  -DLIBCXX_CXX_ABI=libcxxabi \
  -DLIBCXX_CXX_ABI_INCLUDE_PATHS="$BASEDIR/llvm-project/libcxxabi/include" \
  -DLIBCXX_CXX_ABI_LIBRARY_PATH="$PREFIX/lib" \
  -DLIBCXX_ENABLE_EXCEPTIONS=False \
  -DLIBCXX_ENABLE_FILESYSTEM=False \
  -DLIBCXX_ENABLE_MONOTONIC_CLOCK=False \
  -DLIBCXX_ENABLE_RTTI=False \
  -DLIBCXX_ENABLE_THREADS=False \
  -DLIBCXX_ENABLE_SHARED=False \
  -DLIBCXX_ENABLE_STATIC=True \
  $BASEDIR/llvm-project/libcxx

make -j4
make install

LIBCXX_CXX_ABI=libcxxabi で ABI サポートライブラリとして libc++abi を使うことを指定します。

LIBCXX_CXX_ABI_INCLUDE_PATHSLIBCXX_CXX_ABI_LIBRARY_PATH で libc++abi のヘッダファイル(cxxabi.h)とライブラリファイル(libc++abi.a)が置いてあるパスを指定します。

libc++abi のときと同様に,LIBCXX_ENABLE_機能名=False として OS のサポートが必要になる機能を軒並み無効化していきます。 指定できるオプションは llvm-project/libcxx/CMakeLists.txt から探すことができます。

この記事では例外,ファイルシステム,単調増加クロック,実行時型情報,マルチスレッドを無効化しています。 これらの機能は OS のサポートが必要となるため,OS 自体の開発では無効化しておくのが良いでしょう。

ビルドされたライブラリの使用方法

ビルドされたライブラリを使用する際は,いくつかのオプションをコンパイラおよびリンカに渡す必要があります。 コンパイラに渡すオプションは原則としてライブラリのビルドで用いたオプションと同一のものとします。 ビルド時と使用時にオプションが異なると誤動作の原因となります。

export CPPFLAGS="-I/usr/local/x86_64-elf/include/c++/v1 -I/usr/local/x86_64-elf/include -nostdlibinc -D__ELF__ -D_LDBL_EQ_DBL -D_GNU_SOURCE -D_POSIX_TIMERS"
export LDFLAGS="-L/usr/local/x86_64-elf/lib"

C ライブラリのインクルードディレクトリより先に C++ のインクルードディレクトリを指定するのがポイントです。 こうすることで libc++ のヘッダファイルが先に探索されるようになります。 逆にすると,特に math ライブラリの使用時にエラー(::isinf が見つからないなど)が発生することがあります。

Docker によるビルド

ライブラリのビルドの際はなるべくクリーンな環境を使うのが良いです。ビルドが安定しますし,気づかぬうちにシステム標準のライブラリが参照されてしまう,というような事故を防げます。 この目的は Docker コンテナで簡単に達成できます。 ということでライブラリのビルド用 Docker コンテナを作りましたのでご利用ください。 https://hub.docker.com/r/uchannos/stdlib-builder