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

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

LLVM C++ API 学習メモ(3) - Hello World

前回までで、LLVM IRを作成したりオブジェクトファイルを出力したりする方法を説明したが、プログラム本体は戻り値を返すだけのものだった。

それだけだとまともなプログラムを作成できないので、今回は標準出力にhello worldを出力するLLVM IRを作成する。

また、出力したオブジェクトファイルをリンクして実行可能ファイルを作成する方法も説明する。今回はC標準ライブラリの関数を利用するので、前回よりリンクに手間がかかる。

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

目次

目的のLLVM IRコード

以下に相当するLLVM IRを作成するためのC++プログラムを作成する。

※後述のC++コードで出力できるものとは若干違うが、気にする必要はない。

C++コード

LLVM IR構築部分のみ抜粋。全体はこちら

解説

puts関数の宣言

C標準ライブラリのputs関数を使うため、その関数を宣言しておく。

これはC言語における関数のプロトタイプ宣言に相当する。

LLVM APIでは、以下のような段階を踏む必要がある。

  1. 引数リストを表すArrayRefオブジェクトを作成する
  2. 関数の型を表すFunctionTypeオブジェクトを作成する
  3. 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);

FunctionTypeget関数の引数に、戻り値の型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オブジェクトのポインタを指定する。ここで指定しているstrHelloWorldConstantクラスのオブジェクトだが、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が内部でリンク時にコマンドに指定するオプションを見ることができる。

ただし、clanglld-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を呼べば上記はいらなくなるかもしれないが、標準ライブラリを自作しなければならなくなるので多大な労力が必要になると思われる。

qiita.com