前回までで、LLVM IRを作成したりオブジェクトファイルを出力したりする方法を説明したが、プログラム本体は戻り値を返すだけのものだった。
それだけだとまともなプログラムを作成できないので、今回は標準出力にhello world
を出力するLLVM IRを作成する。
また、出力したオブジェクトファイルをリンクして実行可能ファイルを作成する方法も説明する。今回はC標準ライブラリの関数を利用するので、前回よりリンクに手間がかかる。
※この手順でLLVM 10.0.0をビルドした環境で、Visual Studioのプロジェクトをこの内容に設定してビルド確認している。
目次
目的のLLVM IRコード
以下に相当するLLVM IRを作成するためのC++プログラムを作成する。
※後述のC++コードで出力できるものとは若干違うが、気にする必要はない。
C++コード
解説
puts関数の宣言
C標準ライブラリのputs
関数を使うため、その関数を宣言しておく。
これはC言語における関数のプロトタイプ宣言に相当する。
- 引数リストを表すArrayRefオブジェクトを作成する
- 関数の型を表す
FunctionType
オブジェクトを作成する - ModuleにgetOrInsertFunction関数で、関数の宣言を追加する
引数リストを作成
std::vector<llvm::Type*> typesForArgsPuts = { builder.getInt8PtrTy() }; llvm::ArrayRef<llvm::Type*> argsPuts(typesForArgsPuts);
今回は引数がi8*
型ひとつだけなので、builderからgetInt8PtrTy関数で取得したTypeオブジェクトをひとつvectorに入れる。
そのvectorをもとに、引数リストを表すArrayRefクラスのオブジェクトを作成する。
ちなみに、vectorを使わずにArrayRefを作成するコンストラクタも用意されている(それらについてはArrayRefのdoxygenを参照)。
関数の型を作成
llvm::FunctionType* ftPuts = llvm::FunctionType::get(builder.getInt32Ty(), argsPuts, false);
FunctionTypeのget関数の引数に、戻り値の型i32
、引数リスト、可変長引数を使わないことを表すfalse
を指定して、関数の型FunctionType
のオブジェクトを作成する。
Modueに関数の宣言を追加
llvm::FunctionCallee fPuts = module.getOrInsertFunction("puts", ftPuts);
第一引数は関数の名前。 第二引数は関数の型。
main関数を定義
llvm::FunctionType* ftMain = llvm::FunctionType::get(llvm::Type::getInt32Ty(context), false); llvm::Function* fMain = llvm::Function::Create(ftMain, llvm::Function::ExternalLinkage, "main", module); llvm::BasicBlock* basicBlock = llvm::BasicBlock::Create(context, "entry", fMain); builder.SetInsertPoint(basicBlock);
ここらへんは以前説明したものと同様。
文字列定数"hello world"を定義
llvm::Constant* strHelloWorld = builder.CreateGlobalStringPtr("hello world");
CreateGlobalStringPtr関数でグローバル識別子として文字列定数を作成できる。
この関数はCreateGlobalString関数と同様に文字列定数を作成するが、戻り値としてi8
配列型ではなくi8*
型を返す。
puts関数の引数はi8*
型なのでCreataGlobalStringPtr
関数のほうが都合がよい。
これを使うと自分でCreateGEP関数を使ってgetelementptr命令を追加しなくてもよくなる。
CreateGlobalStringPtr
関数はSetInsertPoint
関数より後に実行しないとエラーになる。
main
関数内のBasicBlock内に追加するわけでもないのに、なぜかそうしなければならない。
puts関数を実行
llvm::Value* result = builder.CreateCall(fPuts, strHelloWorld);
CreateCall関数でcall
命令を追加する。
第一引数にcall
したい関数を表すFunction
オブジェクトのポインタを指定する。
第二引数に関数に渡す引数を表すValue
オブジェクトのポインタを指定する。ここで指定しているstrHelloWorld
はConstant
クラスのオブジェクトだが、Constant
クラスはValue
クラスのサブクラスなので指定できる。
CreateCall
関数の戻り値としてcall
した関数の戻り値が返される。今回は、それを以下のようにmain
関数の戻り値として返すようにしている。
builder.CreateRet(result);
実行可能ファイルの作成(リンク)
今回作成したC++プログラムでは、a.o
というファイル名でオブジェクトファイルを出力する。
それをC標準ライブラリ等とリンクして実行可能ファイルを作成する。
lld-link
コマンドでリンクする場合、以下のようにコマンドを実行する。
lld-link a.o -defaultlib:libcmt "-libpath:C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.25.28610\lib\x64" "-libpath:C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\ucrt\x64" "-libpath:C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\um\x64"
上記の各フォルダパスはVisual Studio 2019を標準のインストール先にインストールした場合のもの。 また、64ビットアプリケーションとしてリンクすることを前提としている。
自分の環境の各ライブラリのパスを調べたい場合、clang
コマンドに-v
を指定して実行すればclang
が内部でリンク時にコマンドに指定するオプションを見ることができる。
ただし、clang
にlld-link
を使わせるためには-fuse-ld=lld
オプションも指定する必要がある。
指定しなかった場合は、Visual Studioのリンカlink
が使われる。
link
の使い方を参考にしたい場合は-fuse-ld=lld
を指定しなければよい。
-defaultlib
オプションで指定しているlibcmt
というのはVisual StudioのC標準ライブラリの名前。
現状、Windows環境でC標準ライブラリを利用する場合は、Visual Studioに含まれるものを使うしかなさそう。
LLVMがC標準ライブラリを作成することを計画しているようなので将来に期待。
Windows環境においては何をするにしてもWindows APIを利用する必要がある。 Visual StudioのC標準ライブラリも利用しており、そのために以下のライブラリに依存している。
- kernel32.lib
- libucrt.lib
- uuid.lib
これらのライブラリのパスも-libpath
で指定する必要がある。
このため、コンパイラフロントエンドを自作したとしても、ユーザーにVisual Studioをインストールしてもらわないとリンクができない。 標準Cライブラリを使わなかったとしても、Windows APIの利用は避けられないので結局Windows SDKは必要になる。
以下で解説されている方法で直接syscall
を呼べば上記はいらなくなるかもしれないが、標準ライブラリを自作しなければならなくなるので多大な労力が必要になると思われる。