ライブラリをコンパイルしてLLVM-IRを出力する方法

皆さんご存知、LLVM-IRはコンパイラ基盤であるLLVMの中間生成物であり、emscriptenやPROCESS WARPLLVM-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++ライブラリ以外にほとんど依存しません。

The GNU MP Bignum Library

前提

  • 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)