2011/01/31

LLVMのアセンブリコードによるプログラム

LLVM [Wikipedia] はコンパイラのための基盤記述であり、clangなどに用いられています。

この記事では今さらですが、LLVMのアセンブリコードによるプログラムをいくつか試してみます。 なお、LLVMのアセンブリコードの詳細については「LLVM Assembly Language Reference Manual」を参照してください。

次の例は単純なHello worldの例です (helloworld.ll)。

@HW = internal constant [14 x i8] c"Hello world!\0A\00"

declare i32 @puts(i8*)

define i32 @main() {
  %cast_hw = getelementptr [14 x i8]* @HW, i32 0, i32 0
  call i32 @puts(i8* %cast_hw)
  ret i32 0
}

ここで、@で始まる識別子はグローバル変数、 %で始まる識別子はローカル変数であり、 「i+数値」はそのビット数の整数を表現しています。

また、declareでは@putsを外部関数として宣言しており、main内でそれを用いています。 getelementptrは配列や構造体からアドレスを取得するための命令であり、 この例では単に文字列の先頭のアドレスを得ているだけです。 具体的には、第1引数はベースとなるアドレス、 第2引数以降はインデックスを指定します。この例では 第2引数は[14 x i8]に、第3引数はi8に対するインデックスとなります。

ここから、実行ファイルを作成して、実行するには次のようにします。

> llc helloworld.ll && gcc helloworld.s && ./a.out
Hello world!

なお、.llからllvmのビットコード(bc)にして、それを用いて実行するには次のようにします。

> llvm-as helloworld.ll && lli helloworld.bc 
Hello world!

次の例ではループを利用して0から9までをprintfで表示します。

@MSG = internal constant [4 x i8] c"%d\0A\00"

declare i32 @printf(i8*, ...)

define i32 @main() {
head:
  %cast_msg = getelementptr [4 x i8]* @MSG, i32 0, i32 0
  %i = alloca i32
  store i32 0, i32* %i
  br label %loop

loop:
  %val = load i32* %i
  call i32 (i8*, ...)* @printf(i8* %cast_msg, i32 %val)
  %ival = add i32 %val, 1
  store i32 %ival, i32* %i
  %b = icmp sle i32 %ival, 9
  br i1 %b, label %loop, label %tail

tail:
  ret i32 0
}

LLVMのアセンブリコードは、「静的単一代入」 と呼ばれる形式であり、各変数に再代入ができない形式になっています。 そのため、alloc命令でスタック上に領域を確保し、 load命令とstore命令によって値を保存したり取り出したりしています。

なお、loop:ラベルの直前に無意味なbr命令がありますが、 ラベルを付ける場所が関数の先頭かbr命令の直後だけのようなので、その制限回避のためです。

ちなみに、上のプログラムはphi命令を利用するとより簡単に書くことができます。

@MSG = internal constant [4 x i8] c"%d\0A\00"

declare i32 @printf(i8*, ...)

define i32 @main() {
head:
  %cast_msg = getelementptr [4 x i8]* @MSG, i32 0, i32 0
  br label %loop

loop:
  %val = phi i32 [ 0, %head ], [ %nextval, %loop ]
  %nextval = add i32 %val, 1
  call i32 (i8*, ...)* @printf(i8* %cast_msg, i32 %val)
  %b = icmp sle i32 %nextval, 9
  br i1 %b, label %loop, label %tail

tail:
  ret i32 0
}

phi命令は「現在のブロックの直前のブロック」によって結果の値を変えることができる命令です。 上のプログラムでは、1回目にphi命令に来たときにはheadラベルから来ているので %valに0が代入され、それ以降はtailラベルの直前のbr命令から来ており 結果的に直前のブロックがloopとなるため%valには%nextvalが代入されます。 phi命令の次の行では%valに1足した値を%nextvalに代入しているので、これによって値が変更されていきます。

また (当然でしょうが)、ループのプログラムの例は出力される結果が異なります。ふたつめの例では、スタックを一時変数に用いずにレジスタだけを利用していることがわかります。

> diff -u =(llc loop1.ll -o - -x86-asm-syntax=intel) =(llc loop2.ll -o - -x86-asm-syntax=intel)
--- /tmp/zshAjFUQG    2011-01-31 22:35:56.000000000 +0900
+++ /tmp/zshw9GEZr    2011-01-31 22:35:56.000000000 +0900
@@ -10,19 +10,17 @@
 Ltmp1:
     sub RSP, 8
 Ltmp2:
-    mov DWORD PTR [RSP + 4], 0
-    lea RBX, QWORD PTR [RIP + _MSG]
+    xor EBX, EBX
+    lea R14, QWORD PTR [RIP + _MSG]
     .align  4, 0x90
 LBB0_1:                                 ## %loop
                                         ## =>This Inner Loop Header: Depth=1
-    mov R14D, DWORD PTR [RSP + 4]
-    mov RDI, RBX
-    mov ESI, R14D
+    mov RDI, R14
+    mov ESI, EBX
     xor AL, AL
     call    _printf
-    inc R14D
-    mov DWORD PTR [RSP + 4], R14D
-    cmp R14D, 10
+    inc EBX
+    cmp EBX, 10
     jl  LBB0_1
 ## BB#2:                                ## %tail
     xor EAX, EAX

なお、llvm-gccにオプション--emit-llvmと-Sを付けることでC言語からLLVMのアセンブリコードを得ることができます。

llvm-gcc foobar.c -o foobar.ll -S --emit-llvm

まとめ

LLVMのアセンブリコードによるプログラムを試してみました。

関連記事

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。