LLVMについて調べたことまとめ

本記事は 自作OS Advent Calendar の14日目の記事となります。

adventar.org

みなさま、ご無沙汰しております。

ここ最近、なかなか自作OS活動に時間を割くことができずにいましたが、リハビリを兼ねて、ClangやRustのバックエンドでもあり、自作OS界隈でも徐々に名前を聞くようになってきた(気がする)、 LLVM について、改めて調べてみました。

目次

The LLVM compiler Infrastructure project

The LLVM compiler Infrastructure project (以下LLVMプロジェクト) はコンパイラ技術群(toolchain)だと自身を紹介しています。 LLVM という言葉自体は割と皆さんご存知かと思います。昔は Low Level Virtual Machine の略語だった気がしますが、現在はそれだけではない広範囲な領域を扱っているといえます(むしろVirtual Machineという単語がもたらすイメージが逆に勘違いを誘いそうです)。

私は普段iOSアプリを書く仕事をしているのでとりわけ目にする機会が多いのかもしれませんが、XcodeiOSアプリやmac OS用のアプリを作る場合にも、裏側でLLVMが動いていることをよく目にします(Xcode上でアプリのデバッグをする際に立ち上がるデバッガはLLDBです)。

また、私は昨年から今年にかけて、Rustを使って色んなことをやっていましたが、Rustのバックエンドで動いているのもLLVMです。

Primary sub projects

コンパイラと直接関係ないものもありますが、LLVMプロジェクトがプライマリと位置付けるプロジェクトには次のようなものがあります。

  1. LLVM Core libraries
  2. Clang
  3. The LLDB project
    • デバッガ。
  4. The libc++ and libc++ ABI projects
    • 標準C++ライブラリ(gccにおけるlibstdc++と同じ)。
  5. The compiler-rt project
    • gccにおけるlibgccのようなもの(浮動小数関連の処理などが組み込ま出た基本ライブラリ)。Clangや Rustでベアメタルなプログラムを書く場合にはとてもお世話になる。
  6. The OpenMP subproject
    • 並列計算用の基盤
  7. The polly project
    • 最適化基盤。
  8. The libclc project
  9. The klee project
  10. The SAFECode project
  11. The lld project
    • リンカ(gccにおけるldのようなもの)。

OS自作観点で言うと、LLVM Core librariesやClangはもちろんのこと、 compiler-rtlld もおさえておくと良いと思います。個人的には自作OSにJITコンパイラ組み込んでみるのもおもしろうそうかなと考えています。

バックエンドとフロントエンド

LLVMも含めたコンパイラの処理全体のフローで見ると、我々が普段直に接するClangやRustなどのコンパイラフロントエンド と呼ばれます。

フロントエンドは主にC言語などの各言語から LLVM IR という中間言語コンパイルするところまでを担当しています。

フロントエンドに対して、 バックエンド は、LLVM IR から各CPU向けのバイナリを生成するところまでを担います。

また、ClangにしてもRustにしても、フロントエンドを実行すると、各CPU向けのバイナリを出力するところまでやってくれます。これは、各フロントエンドが裏側でLLVMを呼び出したりしてくれるためです。コンパイルのプロセスを制御してくれるという意味で、 コンパイラドライバ と呼ぶこともできます。

LLVM IRとbitcode

LLVMが扱う中間言語LLVM IR と呼ばれます。大抵は拡張子 .ll として扱われます。ファイルの中身は、実はテキストファイルとなっており、これをバイナリに変換したものが bitcode と呼ばれます。

LLVM IR はアーキテクチャ独立の中間言語で、ターゲットの仮想アーキテクチャは無限個のレジスタを持つ特徴があります。

LLVM IR の言語仕様は LLVM Language Reference Manual — LLVM 6 documentation 、bitcodeのファイルフォーマットは LLVM Bitcode File Format — LLVM 6 documentation で公開されています。

実際に動かしてみる

さて、ここからはClangとLLVMを使ってC言語ソースコードから各CPU向けのバイナリが生成される過程をみていきたいと思います。

※ 以下の実行結果については、全て macOS Sierra 上での実行結果となります。Linux上での実行結果については別途機会があれば紹介できればと思います。

LLVMのインストール

mac OSの場合はHomebrewを使えばすぐにインストールすることができます。

brew install --with-toolchain llvm

インストールができたら、PATHを設定する必要があります。

export PATH="/usr/local/opt/llvm/bin/:$PATH"

x86_64 (Mach-O) 向けバイナリが生成されるまで

以下のような簡単なC言語のコードをx86_64向けバイナリにコンパイルしてみます。

#include <stdio.h>

int main() {
  printf("Hello, world!");
  return 0;
}

まずはClangを使いLLVM IRを出力してみます。コマンドは次のとおりです。

clang -emit-llvm -S -o hello.ll hello.c

出力されたLLVM IRのコードは次のとおりです。

; ModuleID = 'hello.c'
source_filename = "hello.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.12.0"

@.str = private unnamed_addr constant [14 x i8] c"Hello, World\0A\00", align 1

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main() #0 {
  %1 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  %2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0))
  ret i32 0
}

declare i32 @printf(i8*, ...) #1

attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0, !1}
!llvm.ident = !{!2}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"PIC Level", i32 2}
!2 = !{!"clang version 5.0.0 (tags/RELEASE_500/final)"}

今回はLLVM IRの詳細な解説は行いませんが、 @main と書かれている箇所がmain関数の処理になります。

生成したLLVM IRをさらに各CPU向けのオブジェクトファイルへ変換してみましょう。使用するツールは llc というツールです。

llc -filetype=obj -o hello.o hello.ll

これでオブジェクトファイルを生成することができました。最後にこのオブジェクトファイルに標準ライブラリ等をリンクして実行形式ファイルを作成します。

使用するのは lld ・・・といきたいところですが、なぜか自分の環境ではlldを用いてリンクすることができませんでした。。

なので、ここではldを使ってリンクを行います。環境にもよりますが、以下のコマンドを実行することで実行形式ファイルを得ることができます。

ld -demangle -lto_library /path/to/llvm/5.0.0/lib/libLTO.dylib -dynamic -arch x86_64 -macosx_version_min 10.12.0 -o hello hello.o -lSystem /path/to/llvm/5.0.0/lib/clang/5.0.0/lib/darwin/libclang_rt.osx.a

上記コマンドは clang -o hello hello.o で得られる結果と同一の結果です。

もちろん、普通に実行できます。

 $ ./hello
Hello, World

以上がコンパイルの流れとなります。コンパイル設定によってはこれらのプロセスの間に最適化などの処理をはさむことになると思います。

今回は手作業で各ツールを実行していきましたが、これらのプロセスは通常コンパイラドライバの役割を持つClangがすべてやってくれます。

bitcodeの生成、実行

少し脱線しますが、LLVM IR のソースコードからbitcodeを生成してみましょう。

bitcodeはClangを使って生成することができます。

clang -c -emit-llvm hello.c

bitcodeは lli というbitcode実行環境を使ってbitcodeのまま実行することができます。実行にはJITコンパイラ、またはインタプリタを選択することができます(lli - directly execute programs from LLVM bitcode — LLVM 6 documentation)。

$ lli hello.bc
Hello, World

RustでのLLVM IR・bitcode生成方法 (ただし実行はできず)

Rustのコンパイラ(rustc)についても、LLVM IRやbitcodeを生成することができます。使用したRustのコードは次のとおりです。

fn main() {
    println!("Hello, World");
}

LLVM IR の生成は次のコマンドでできます。

rustc --emit=llvm-ir hello.rs

bitcodeの生成は次のコマンドです。

rustc --emit=llvm-bc hello.rs

ただし、LLVM IRもbitcodeも、生成自体はできるのですが、どうやらRustの標準ライブラリが別途必要になるらしく、ldでのリンクやlliで実行することはできませんでした。

ld -demangle -lto_library /usr/local/Cellar/llvm/5.0.0/lib/libLTO.dylib -dynamic -arch x86_64 -macosx_version_min 10.12.0 -o hello hello.o -lSystem /usr/local/Cellar/llvm/5.0.0/lib/clang/5.0.0/lib/darwin/libclang_rt.osx.a
Undefined symbols for architecture x86_64:
  "std::io::stdio::_print::hfe7c7aedc93efc1e", referenced from:
      hello::main::h85c057a2a5f42fc8 in hello.o
  "std::rt::lang_start::h6e7c6ad302fab11a", referenced from:
      _main in hello.o
ld: symbol(s) not found for architecture x86_64
lli hello.bc
LLVM ERROR: Program used external function '__ZN3std2rt10lang_start17h6e7c6ad302fab11aE' which could not be resolved!

さらにマングリングもされちゃってるので、マングリングしないようにオプション指定してあげる必要がありそうですね。

残念ながらここで時間切れとなってしまったので、今度時間ができたらこの続きができればと思います。

まとめ

時間の都合上、概要を説明するだけになってしまいましたが、LLVMGCCと並ぶ有力な選択肢として、今後OS自作界隈でLLVMの知見が深まっていくことを大いに期待しています。

私もRustやXcodeを使っている以上、LLVMと向き合わざるを得ないですが、今回調べた中で、LLVMは改めておもしろそうだと感じたので、今後もLLVMプロジェクトの動向には注目していきたいと思います。

参考文献(と簡単な紹介)

余談:お知らせと来年に向けて

本記事とはあまり関連しないですが、この場をお借りしてお知らせを。

昨年末から今年前半にかけ、同人誌の出版やQiitaの記事などのアウトプットを行いました。

qiita.com

secret-lab.booth.pm

おかげさまで記事に対するコメントや同人誌のサンプルコードに対するプルリクなどをいただきました(ありがとうございました)が、技術書典2終了後、色々と忙しい時期が続いた(今も続いている)ことにより、返信や対応が遅くなってしまい、大変申し訳ありません。

いただいたものについては徐々にフォローしていきたいと考えていますので、(既に解決済みかもしれませんが)今しばらくお待ちいただければと思います。

最後に、来年の抱負を少し。

同人誌作りは、今年のハイライトとなる出来事であったとともに、自分にとって忘れられない貴重な経験となりました。絶対にお約束できるわけではありませんが、来年もまた書く機会があれば書きたいと思っています。

そして、自作OSについては、技術書典2以降、すっかり滞ってしまっているので、落ち着いた段階でまた再開したいです。

github.com

というわけで、みなさま、良いお年を!