ライブラリをコンパイルしてLLVM-IRを出力する方法
皆さんご存知、LLVM-IRはコンパイラ基盤であるLLVMの中間生成物であり、emscriptenやPROCESS WARPはLLVM-IRを読み込んで動かすことができるプラットフォームです。 現代のプログラミングでは既存ライブラリを利用することにより開発期間の短縮や品質向上を図っています。前述のプラットフォームでプログラムを動かす場合、自分のプログラムをLLVM-IRに変換するわけですが、その過程で依存ライブラリも同様にLLVM-IRに変換する必要があります。
$ clang -emit-llvm -c hoge.c
上記コマンドでC/C++からLLVM-IRへの変換はできていましたが、autoconfなどのビルドツールでは同様の方法では対応できませんでした。このような場合には-fltoオプションを使うとうまくいくようです。
The LLVM gold plugin — LLVM 3.8 documentation
GMPというライブラリを例にLLVM-IRの塊を取り出してみます。 GMPはC/C++で任意精度の整数、小数演算を行うためのライブラリです。標準C/C++ライブラリ以外にほとんど依存しません。
前提
- OS X Yosemite Version 10.10.5
- clang Apple LLVM version 7.0.0 (clang-700.1.76)
- LLVM 3.6.2 (homebrewでインストル済み)
- GMP 6.1.0 (.tar.bz2ファイルをダウンロード、解凍しておく)
コンパイル作業
$ cd <gmpの展開先> $ CC="clang -flto" CXX="clang++ -flto" ./configure --disable-shared <省略> $ make <省略>
GMPは通常、共有ライブラリと静的ライブラリの両方をコンパイルします。LLVM-IRを取り出す場合、静的ライブラリだけあればOKなので--disable-sharedオプションを指定しています。 GMPの場合、.libフォルダ以下にコンパイル済みライブラリが格納されています。
$ cd .lib $ ls libgmp.a libgmp.la libgmp.lai
静的ライブラリlibgmp.aにはLLVM-IRのBitCodeが含まれています。一旦arを展開し、中のBitCodeを取り出します。
$ mkdir work $ cd work $ llvm-ar x ../libgmp.a $ ls <libgmp.aに格納されていた.oファイルが展開されているはず>
展開された.oファイルはただのオブジェクトファイルではなくBitCodeです。 llvm-disコマンドでヒューマンリーダブルな.llファイルに変換できます。
$ find *.o -exec llvm-dis {} \;
あとはllvm-linkコマンドで1つにまとめて終わりです。
$ llvm-link -o ../libgmp.bc *.ll $ cd .. $ llvm-dis libgmp.bc $ ls libgmp.a libgmp.bc libgmp.la libgmp.lai libgmp.ll work
失敗談
fltoオプションの存在に気づくまで、emscriptenのようにCCにラッパを流し込んでなんとかしようとしていました。 基本動作は以下のとおりでスクリプトを組んだのですが、autoconfの関数有無の判定はプログラムがリンクまで正常に行えるかを基準にしています。 LLVM-IRの出力までで処理を止めた場合、関数がなくともリンク相当のコマンドが正常終了し、存在しない関数が有ると判定しmakeで止まるとうい問題が発生しました。 その時の調査で-fltoオプションを見つけて無事ライブラリのLLVM-IRを出力できることが分かりました。 ここまで来るのに2日ほどかけてgcc/clang/emcc/autoconfの動作をトレースしたんですけどね… 以下は必要なくなったラッパプログラムです。
#!/usr/bin/env python # coding:utf-8 import subprocess import sys import re import os import stat i_name = False o_name = False o_name_idx = False is_c = False is_e = False is_o = False is_s = False s_idx = False # 起動引数を読み取って、-c, -o, -S関連の有無と場所を確認 for idx, arg in enumerate(sys.argv): if is_o == True and (not o_name): o_name = arg o_name_idx = idx matched = re.match(r"^(.*)\.(c|i|cpp|cxx|cc|c\+\+|ii|s)$", arg) if matched: i_name = matched.group(1) if arg == "-c": is_c = True if arg == "-E": is_e = True if arg == "-o": is_o = True if arg == "-S": is_s = True s_idx = idx # 起動引数をコピーしてコマンドを作成 command = sys.argv[:] # command[0] = "clang" # -Sオプションの場合は-cで上書きする if is_s: command[s_idx] = "-c" # プリプロセッサ出力の場合を除き、出力形式に合わせて出力ファイル名を決定 if not is_e: if is_o: # 実行可能形式でのコンパイルをしようとした場合は、hoge.00に出力 if not is_c and not is_s: command[o_name_idx] = o_name + ".00" else: command.insert(1, "-o") if is_s: # アセンブラやオブジェクトファイルの場合は # .s, .oファイルを出力(中身はLLVM-IR) command.insert(2, i_name + ".s") elif is_c: command.insert(2, i_name + ".o") else: # 実行可能形式でファイル名が指定されない場合はa.out.00に出力 o_name = "a.out" command.insert(2, o_name + ".00") # コンパイルオプションに-emit-llvmを付加する command.insert(1, "-emit-llvm") command.insert(2, "-c") command.insert(3, "-fno-vectorize") # set target command.insert(1, "-m64") proc = subprocess.Popen( command, shell = False, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE) stdout_data, stderr_data = proc.communicate() print stdout_data print stderr_data # 実行可能形式でのコンパイルの場合、LLVM-IRを # 実行するクッションスクリプトを作成 # @todo リンクオプションをlliに渡すようにする if not is_c and not is_e and not is_s: f = open(o_name, "w") f.write("#!/usr/bin/env sh\n") f.write("/usr/local/Cellar/llvm/3.6.2/bin/lli " + o_name + ".00 \"$@\"\n") f.close() os.chmod(o_name, stat.S_IRWXU) sys.exit(proc.returncode)