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

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

LLVM言語 学習メモ (1) - 戻り値を返すだけのmain関数からHello Worldまで

コンパイラを作る前にLLVM言語を勉強する。

本記事はその自分用メモ。 第三者にわかりやすく解説する意図はない。 そういう記事はいつか書くかもしれないし書かないかもしれない。

公式のリファレンスを参考にする。

LLVM IRではHello Worldを標準出力することすら初学者にとって難易度が高い。 https://releases.llvm.org/10.0.0/docs/LangRef.html#module-structureにサンプルコードがあるが、C言語の知識がある程度だと、わかるようなわからないような内容。

まずはただ値を返すだけのmain関数を定義するところからはじめて、Hello Worldを標準出力できるところを目指す。

最適化に関するものなどはひとまず無視して、最低限動くものだけを作る。

目次

開発環境

最小のプログラム

まずは戻り値を返すだけのmain関数を一つだけ作成してみる。

以下の内容のテキストファイルを、拡張子.llで保存。

define i32 @main() {
    ret i32 111
}

以下のコマンドでコンパイル、実行、戻り値の確認。

llvm-as (ファイル名).ll
lli (ファイル名).bc
$lastexitcode

実行結果

111

https://releases.llvm.org/10.0.0/docs/LangRef.html#functionsによると、関数定義の構文は以下。

define [linkage] [PreemptionSpecifier] [visibility] [DLLStorageClass]
        [cconv] [ret attrs]
        <ResultType> @<FunctionName> ([argument list])
        [(unnamed_addr|local_unnamed_addr)] [AddrSpace] [fn Attrs]
        [section "name"] [comdat [($name)]] [align N] [gc] [prefix Constant]
        [prologue Constant] [personality Constant] (!name !N)* { ... }

何やら大量に書けるようだが、ひとまず今はdefine <ResultType> @<FunctionName> ([argument list]) { ... }にだけ注目する。

先のソースコードでは、戻り値<ResultType>i32、関数名<FunctionName>main、引数リスト[argument list]()つまり引数なし。

i32は32ビット整数。

関数呼び出し

以下は、関数func1を定義し、呼び出すサンプルコード。

define i32 @main() {
    %result = call i32 @func1()
    ret i32 %result
}

define i32 @func1() {
    ret i32 222
}

%result = call i32 @func1()で関数を呼んでいる。

関数定義は、関数を呼ぶ位置より下でもいいらしい。

https://releases.llvm.org/10.0.0/docs/LangRef.html#i-callによると、関数呼び出しの構文は以下。

<result> = [tail | musttail | notail ] call [fast-math flags] [cconv] [ret attrs] [addrspace(<num>)]
        <ty>|<fnty> <fnptrval>(<function args>) [fn attrs] [ operand bundles ]

難しそうな部分はひとまず無視して、今回使うのは<result> = call <ty> <fnptrval>(<function args>)の部分。

<result>が戻り値を受け取るレジスタを指すローカル識別子。変数のようなものだが、ローカル変数と言ってしまうとレジスタではなくスタック領域に配置されるものと紛らわしいため、公式ドキュメントのLocal identifierの直訳、ローカル識別子と呼ぶことにする。

ローカル識別子は%<名前>で定義する。上のコードでは%result

<ty>が戻り値の型。上のコードではi32

<fnptrval>が関数名。上のコードではfunc1

https://releases.llvm.org/10.0.0/docs/LangRef.html#i-callによると<fnptrval>

An LLVM value containing a pointer to a function to be called.

呼び出される関数へのポインターを含むLLVM値。

らしい。 fnptrvalfunction pointer valueの略語と思われる。 とりあえずここでは関数名をそのまま指定すればよいと理解しておく。

<function args>が引数リスト。上のコードでは引数なし。

引数付き関数の定義

先ほどは引数なしのコードを書いた。 次は引数ありの関数定義とその呼び出し。

define i32 @func1(i32 %x) {
    ret i32 %x
}

define i32 @main() {
    %result = call i32 @func1(i32 333)
    ret i32 %result
}

1行目のi32 %xが引数リスト。xが変数名。 ドキュメントで説明を見つけられなかったが、<function args>型 値形式と思われる。複数の場合はカンマ区切り。

6行目のi32 333が引数。 C言語と違い、関数呼び出し時の引数にも型を書く必要がある。

グローバル変数、定数

https://releases.llvm.org/10.0.0/docs/LangRef.html#global-variablesによると、グローバル変数の定義の構文は以下。

@<GlobalVarName> = [Linkage] [PreemptionSpecifier] [Visibility]
                [DLLStorageClass] [ThreadLocal]
                [(unnamed_addr|local_unnamed_addr)] [AddrSpace]
                [ExternallyInitialized]
                <global | constant> <Type> [<InitializerConstant>]
                [, section "name"] [, comdat [($name)]]
                [, align <Alignment>] (, !name !N)*

難しそうな部分はひとまず無視して、今回使うのは@<GlobalVarName> = <global | constant> <Type> [<InitializerConstant>]の部分。

グローバル変数を定義し、参照するコードは以下。

@g_x = constant i32 444

define i32 @main() {
    %result = load i32, i32* @g_x
    ret i32 %result
}

変数名がg_x、定数にするのでconstant、型がi32、初期値が444

ret i32 @g_xのようにグローバル変数を直接returnしようとすると、コンパイル時にerror: global variable reference must have pointer typeというエラーになる。 グローバル変数の参照はポインタ型でなければならないらしい。

https://releases.llvm.org/10.0.0/docs/LangRef.html#load-instructionload命令を使い、%result = load i32, i32* @g_xでいったん変数にコピーしてからreturnするとうまくいった。

グローバル変数レジスタではなく静的領域に配置されるため、そこからレジストにロードしてから使わなければならないということだろう。

Hello World

Hello Worldを標準出力に出力するコードは以下。 https://releases.llvm.org/10.0.0/docs/LangRef.html#module-structureにあるサンプルコードよりいろいろと削っている。

@hello_world = constant [12 x i8] c"hello world\00"

declare i32 @puts(i8*)

define i32 @main() {
    %ptr = getelementptr [12 x i8], [12 x i8]* @hello_world, i32 0, i32 0
    call i32 @puts(i8* %ptr)
    ret i32 0
}

文字列のグローバル定数

@hello_world = constant [12 x i8] c"hello world\00"が文字列定数のグローバル変数の定義。

文字列はC言語と同じで\0で終わる8bit値の配列。LLVMでは\00のように必ず二桁で書かなければならない。

https://releases.llvm.org/10.0.0/docs/LangRef.html#array-typeによると、LLVM IRの配列の構文は以下。

[<# elements> x <elementtype>]

<# elements>が要素数<elementtype>が要素の型。

LLVM IRでは、配列の要素数も型に含むらしい。

文字列なら[\00を含む文字数 x i8]となる。Hello World\00なら[12 x i8]

https://releases.llvm.org/10.0.0/docs/LangRef.html#complex-constantsArray Constantsの説明によると、文字列定数はc""で囲むらしい。 Hello Worldという文字列定数ならc"Hello World\00"

puts関数の宣言

declare i32 @puts(i8*)が関数の宣言。

公式のリファレンス内にdeclareというキーワードの説明を見つけられなかったが、英単語の意味やサンプルコードでの使われ方からして、C言語における関数のプロトタイプ宣言のようなものと思われる。

puts関数を定義しなくても動いていることから、定義は外部にあるものと思われる。 lliコマンドで実行するまでにリンクのようなことをしなかったがなぜか動いた。

puts関数は、C言語の同名の関数?

配列の先頭アドレスの取得

C言語puts関数は、char配列の先頭アドレス、つまり8bit整数のポインタ型を引数にとる。 declare i32 @puts(i8*)i8型のポインタを引数にとる。

配列のメンバ変数のポインタを取得するにはgetelemntptrを使う。 get element pointerの意味だと思われる。

getelemntptrは、配列にかぎらずaggregate型の要素アドレスを取得するのにも使う。例えば、構造体のメンバ変数のアドレスなど。

%ptr = getelementptr [12 x i8], [12 x i8]* @hello_world, i32 0, i32 0グローバル変数hello_worldの0番目の要素のポインタ、つまり配列の先頭アドレスを取得する。

https://releases.llvm.org/10.0.0/docs/LangRef.html#getelementptr-instructionによると、構文は以下。

<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*

1つ目の<ty>

The first argument is always a type used as the basis for the calculations.

最初の引数は、常に計​​算の基礎として使用されるタイプです。

何を言っているのかわからないが、サンプルコードを見た限り、文字列"Hello World"の型、つまり[12 x i8]型を指定すれば良さそう。

2つ目の<ty>* <ptrval>

The second argument is always a pointer or a vector of pointers, and is the base address to start from.

2番目の引数は常にポインターまたはポインターのベクトルであり、開始元のベースアドレスです。

今、puts関数の引数に渡すために文字列の先頭アドレス、つまりi8の配列の先頭のアドレスを取得したい。

グローバル変数@hello_worldのベースアドレス、つまりi8配列の先頭位置のアドレスを取得したいので、<ptrval>には@hello_worldを指定する。 <ty>*には@hello_worldの型のポインタ、つまり[12x i8]*を指定する。

残りの{, [inrange] <ty> <idx>}*

The interpretation of each index is dependent on the type being indexed into. The first index always indexes the pointer value given as the second argument, the second index indexes a value of the type pointed to.

各インデックスの解釈は、インデックスが作成される型に依存します。最初のインデックスは、常に2番目の引数として指定されたポインタ値にインデックスを付け、2番目のインデックスは、ポイントされた型の値にインデックスを付けます。

これも何を言っているのかわからない。

今回作成したコードでいうと、%ptr = getelementptr [12 x i8], [12 x i8]* @hello_world, i32 0, i32 0i32 0, i32 0に該当する。

2番目の引数で指定したポインタ値というのは、[12 * i8]* @hello_world。 これにインデックスをつけるというのは @hello_worldのベースアドレス + ( [12 * i8]のサイズ * インデックス値 ) の位置のアドレスを取得することを意味すると思われる。

今回は@hello_worldの先頭アドレスが欲しいので、最初のインデックスにはi32 0を指定する。

これで得られる値の型は[12 * i8]*型である。 欲しいのは、puts関数に渡すためのi8*型なので、2番目のインデックスを指定する必要がある。

2番目のインデックスが、[12 x i8]型に対するインデックス、つまり普通の配列のインデックスに相当すると思われる。 今回、配列の先頭ポインタが欲しいので0を指定する。 もしここを1にすると、Hello Worldではなくello Worldが出力される。

こうやって取得したポインタをcall i32 @puts(i8* %ptr)puts関数に渡すと、文字列が標準出力に出力される。

なお、getelementptrは構造体のメンバ変数のポインタを取得する場合にも使う。 それについての説明はこわくないLLVM入門! - Qiitaの説明がわかりやすい。

また、公式にQAがある。 The Often Misunderstood GEP Instruction — LLVM 10 documentation