前回、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環境の場合、LLVMのWindows用リンカlld-link
やVisual Studioのlink
等の選択肢がある。
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 StudioかLLVM(Clang)を手動でインストールしてもらわなければならない。
LLVMのリンカlld-link
を使う場合、ここのWindows用Pre-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