単子葉類プログラマーのメモ

プログラミング関連の自分用メモだけど他の人の役に立つかもしれないので公開しておく感じのブログ

LLVM C++ API 学習メモ(2) - オブジェクトファイルの出力

前回C++LLVM APIで戻り値を返すだけのプログラムを作成し、文字列形式のLLVM IRを出力した。

今回はそれをオブジェクトファイルとして出力する方法と、それをリンクしてexeファイルを作成する方法についてのメモ。

この手順LLVM 10.0.0をビルドした環境で、Visual Studioのプロジェクトをこの内容に設定してビルド確認している。

目次

オブジェクトファイル出力部分のC++コード

一部抜粋。全文はこちら

解説

target tripleを取得

std::string targetTriple = llvm::sys::getDefaultTargetTriple();

getDefaultTargetTriple()で、デフォルトのtarget tripleを取得する。

例えばx86_64-pc-windows-msvcのような文字列が取得できる(何がとれるかは環境によって異なる)。

この情報は、後でTargetやTargetMachineを作成するときに使用する。

この文字列次第で、どの環境用のオブジェクトファイルを出力するかが決まる。

なお、上で示したソースコードでは省略しているが、以下のようにModuleにtarget tripleを設定すると、出力するLLVM IRにもtarget tripleを含めることができる。

module.setTargetTriple(targetTriple);

Targetを取得

llvm::InitializeNativeTarget();
std::string errorMessage;
const llvm::Target* target = llvm::TargetRegistry::lookupTarget(targetTriple, errorMessage);

lookupTarget()Targetを取得する。

Targetとはターゲット固有の情報。

doxygenにこれ以上の詳しい説明が見当たらなかったので、とりあえずtarget tripleに対応する何らかのデータとだけ理解しておく。 TargetはTargetMachineの作成で使用する。

なお、lookupTarget()の前にInitializeNativeTarget()で初期化を行っておく必要がある。

lookupTarget()が失敗した場合、nullptrが返される。 また、第二引数に指定した変数にエラーメッセージが格納される。

TargetMachineの作成

llvm::TargetOptions targetOptions;
llvm::TargetMachine* targetMachine = target->createTargetMachine(
    targetTriple,
    "generic",
    "",
    targetOptions,
    llvm::Optional<llvm::Reloc::Model>()
);

createTargetMachine()TargetMachineを作成する。

doxygenに引数の説明がほとんどないので、適当にexampleのマネをするしかない……。

これも失敗した場合、nullptrが返される。

オブジェクトファイルの出力ストリームをオープン

std::error_code errorCode;
llvm::raw_fd_ostream stream(
    "a.o",
    errorCode
);

ファイルを出力するためのraw_fd_ostreamオブジェクトを作成する。

第一引数に出力するファイル名を指定する。

コンストラクタでファイルがオープンされる。 失敗した場合は、第二引数にエラーコードがセットされる。

PassManagerを作成

llvm::InitializeNativeTargetAsmPrinter();
llvm::legacy::PassManager passManager;
bool error = targetMachine->addPassesToEmitFile(
    passManager,
    stream,
    nullptr,
    llvm::CodeGenFileType::CGFT_ObjectFile
);

ファイルを出力するためにPassManagerを作成する。

LLVMでは、LLVM IRの変換や最適化を行う部分をPassと呼ぶ。それを管理するのがPassManager。

addPassesToEmitFile()で、ファイルを出力するためのPassをPassManagerに追加する。

第二引数に出力先のストリームを指定する。

第三引数については説明が見当たらなかったので不明。

第四引数にCGFT_ObjectFileを指定すると、オブジェクトファイルが出力される。

CGFT_AssemblyFileを指定すると、テキスト形式のアセンブリが出力される。

出力例

 .text
    .def   @feat.00;
    .scl  3;
    .type 0;
    .endef
    .globl    @feat.00
.set @feat.00, 0
    .file ""
    .def   main;
    .scl  2;
    .type 32;
    .endef
    .globl    main
    .p2align  4, 0x90
main:
    movl $111, %eax
    retq

出力

passManager.run(module);
stream.flush();

run()で、PassManagerに追加したPassを実行する。

つまり、オブジェクトファイルを出力する。

flush()でストリームの内容をすべてファイルに書き込む。

実行可能ファイルの作成(リンク)

以下によると、リンク用のAPIはないらしいのでコマンドでリンクする必要がある。

「悪い方が良い」原則と僕の体験談|Rui Ueyama|note

lld v2は単にコマンドとして書くことにした。

clangもVisual Studioのlink.exe等を呼び出しており、APIでリンクしてはいないようである。

Windows環境の場合、LLVMWindows用リンカlld-linkVisual Studiolink等の選択肢がある。

lld-linkを使う場合、コンソールで以下のように実行する(lld-link.exeがある場所にパスを通している環境を前提としている)。

> lld-link /entry:main a.o

今回、C標準ライブラリの機能は使っていないので、C標準ライブラリをリンクしない。

そのため、/entry:mainでエントリポイントをmain関数に指定しないとエラーになり、lld-link: error: <root>: undefined symbol: mainCRTStartupと出力される。

mainCRTStartupというのは、C標準ライブラリを初期化するためにデフォルトのエントリポイントとして設定されている関数。 この関数はC標準ライブラリ内で定義されている。

出力されたexeファイルを実行すると何も表示されないが、コマンドの戻り値を確認すると111が返されていることがわかる。

今後の課題

リンカ

APIでリンクができないので、Windows向けにLLVMコンパイラフロントエンドを作成したとしても、ユーザーにはVisual StudioLLVM(Clang)を手動でインストールしてもらわなければならない。

LLVMのリンカlld-linkを使う場合、ここWindowsPre-Built Binariesをインストールしてもらえばいいだろう。

不便なのでどうにかしたいが、LLVMを使っているRustも以下のようになっているのでどうしようもないのかもしれない。

インストール - The Rust Programming Language

インストールの途中で、Visual Studio2013以降用のC++ビルドツールも必要になるという旨のメッセージが出るでしょう。 ビルドツールを取得する最も簡単な方法は、Visual Studio 2017用のビルドツールをインストールすることです。

lld-linkを自分が配布するファイルに含めるという手段もあるかもしれないが、ライセンスについて調べていないので、現実的な手段なのかどうかは不明。

LegacyでないPassManager

今回、llvm::legacy::PassManagerを使ったがLegacyではないPassManagerも存在する。

ただし、公式ドキュメント、exampleやclangのソースコードではllvm::legacy::PassManagerが使われていて、Legacyでないほうの使い方はわからなかった。

(2021/12/20追記)公式ドキュメントに解説が作成されていた→https://llvm.org/docs/NewPassManager.html