LoginSignup
2
0

CrystalでLLVM-IRを出力して楽しむ

Last updated at Posted at 2023-12-01

はじめに

あー。やっぱり、誰もCrystalの記事を投稿しないじゃないですか。
あらかじめ予想できたことですが、実際に起こってみると悲しいものがありますよー。

そこで、短時間で書ける記事を適当に生成して投稿してきますよ。
これを読んでるキミ、アドベントカレンダーに投稿したまえ。

CrystalでLLVM-IRを出力する

最近Crystalで面白いなと思っているのが、LLVM-IRの出力機能です。
このLLVM-IRというのは何かというと、日本語ではLLVM中間表現っていって、プログラミング言語の一種だそうですね。といっても人間が書くことはほとんどなくて、いろいろな言語のコンパイラが書くプログラミング言語です。CrystalのコンパイラはCrystal言語で書かれたコードをLLVM-IRに変換します。

LLVM-IRを出力すれば、LLVMが、お使いのCPUに合わせた実行形式のファイルを生成してくれます。つまり、Crystal言語がやっているのは、私達の書いたコードをLLVM-IRに変換することだけで、それよりも低いレイヤーは、clangコンパイラの一部であるLLVMがやってくれるというわけです。うまいことフロントエンドとバックエンドで分業しているわけですね。

このLLVM-IRを出力するのはとても簡単です。

crystal build --emit llvm-ir piyo.cr

これだけです。piyo.ll が出力されます。でも、実は、Crystalくんって、普通にコンパイルすると、結構な量の標準ライブラリを読み込んでしまうんですよね。これだとすごく長い(14万行ぐらい)ファイルが出力されてしまいます。だから、LLVM-IRを鑑賞したいだけの場合は次のようにします。

crystal build --emit llvm-ir --prelude empty piyo.cr

この --prelude empty は事前に何も読み込まないという指定で、これをやってしまうとビルドできないスクリプトも多いのですが、下のような簡単なコードであればビルドできます。

piyo.cr
a = 0
b = 0.1
c = "neko"

これを

crystal build --emit llvm-ir --prelude empty piyo.cr

としてビルドしますと、ちょっと長いですが、次のようなLLVM-IRが得られます。

; ModuleID = 'e'
source_filename = "main_module"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

%String = type { i32, i32, i32, i8 }

@ARGC_UNSAFE = internal global i32 0
@ARGV_UNSAFE = internal global i8** null
@"Crystal::BUILD_COMMIT" = internal global %String* null
@"'1908c816f'" = private constant { i32, i32, i32, [10 x i8] } { i32 1, i32 9, i32 9, [10 x i8] c"1908c816f\00" }
@"Crystal::BUILD_DATE" = internal global %String* null
@"'2023-07-19'" = private constant { i32, i32, i32, [11 x i8] } { i32 1, i32 10, i32 10, [11 x i8] c"2023-07-19\00" }
@"Crystal::CACHE_DIR" = internal global %String* null
@"'/home/kojix2/.cache...'" = private constant { i32, i32, i32, [28 x i8] } { i32 1, i32 27, i32 27, [28 x i8] c"/home/kojix2/.cache/crystal\00" }
@"Crystal::DEFAULT_PATH" = internal global %String* null
@"'$ORIGIN/../share/cr...'" = private constant { i32, i32, i32, [29 x i8] } { i32 1, i32 28, i32 28, [29 x i8] c"$ORIGIN/../share/crystal/src\00" }
@"Crystal::DESCRIPTION" = internal global %String* null
@"'Crystal 1.9.2 [1908...'" = private constant { i32, i32, i32, [90 x i8] } { i32 1, i32 89, i32 89, [90 x i8] c"Crystal 1.9.2 [1908c816f] (2023-07-19)\0A\0ALLVM: 14.0.0\0ADefault target: x86_64-pc-linux-gnu\0A\00" }
@"Crystal::PATH" = internal global %String* null
@"'lib:/usr/local/bin/...'" = private constant { i32, i32, i32, [40 x i8] } { i32 1, i32 39, i32 39, [40 x i8] c"lib:/usr/local/bin/../share/crystal/src\00" }
@"Crystal::LIBRARY_PATH" = internal global %String* null
@"'/usr/local/bin/../l...'" = private constant { i32, i32, i32, [30 x i8] } { i32 1, i32 29, i32 29, [30 x i8] c"/usr/local/bin/../lib/crystal\00" }
@"Crystal::LIBRARY_RPATH" = internal global %String* null
@"''" = private constant { i32, i32, i32, [1 x i8] } { i32 1, i32 0, i32 0, [1 x i8] zeroinitializer }
@"Crystal::VERSION" = internal global %String* null
@"'1.9.2'" = private constant { i32, i32, i32, [6 x i8] } { i32 1, i32 5, i32 5, [6 x i8] c"1.9.2\00" }
@"Crystal::LLVM_VERSION" = internal global %String* null
@"'14.0.0'" = private constant { i32, i32, i32, [7 x i8] } { i32 1, i32 6, i32 6, [7 x i8] c"14.0.0\00" }
@"'neko'" = private constant { i32, i32, i32, [5 x i8] } { i32 1, i32 4, i32 4, [5 x i8] c"neko\00" }

define %String* @__crystal_main(i32 %argc, i8** %argv) !dbg !3 {
alloca:
  %a = alloca i32, align 4, !dbg !9
  %b = alloca double, align 8, !dbg !9
  %c = alloca %String*, align 8, !dbg !9
  br label %entry

entry:                                            ; preds = %alloca
  store i32 %argc, i32* @ARGC_UNSAFE, align 4, !dbg !12
  store i8** %argv, i8*** @ARGV_UNSAFE, align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [10 x i8] }* @"'1908c816f'" to %String*), %String** @"Crystal::BUILD_COMMIT", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [11 x i8] }* @"'2023-07-19'" to %String*), %String** @"Crystal::BUILD_DATE", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [28 x i8] }* @"'/home/kojix2/.cache...'" to %String*), %String** @"Crystal::CACHE_DIR", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [29 x i8] }* @"'$ORIGIN/../share/cr...'" to %String*), %String** @"Crystal::DEFAULT_PATH", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [90 x i8] }* @"'Crystal 1.9.2 [1908...'" to %String*), %String** @"Crystal::DESCRIPTION", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [40 x i8] }* @"'lib:/usr/local/bin/...'" to %String*), %String** @"Crystal::PATH", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [30 x i8] }* @"'/usr/local/bin/../l...'" to %String*), %String** @"Crystal::LIBRARY_PATH", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [1 x i8] }* @"''" to %String*), %String** @"Crystal::LIBRARY_RPATH", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [6 x i8] }* @"'1.9.2'" to %String*), %String** @"Crystal::VERSION", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [7 x i8] }* @"'14.0.0'" to %String*), %String** @"Crystal::LLVM_VERSION", align 8, !dbg !12
  store i32 0, i32* %a, align 4, !dbg !9
  store double 1.000000e-01, double* %b, align 8, !dbg !14
  store %String* bitcast ({ i32, i32, i32, [5 x i8] }* @"'neko'" to %String*), %String** %c, align 8, !dbg !15
  ret %String* bitcast ({ i32, i32, i32, [5 x i8] }* @"'neko'" to %String*), !dbg !15
}

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

; Function Attrs: uwtable
define i32 @main(i32 %argc, i8** %argv) #0 !dbg !16 {
entry:
  %0 = call %String* @__crystal_main(i32 %argc, i8** %argv), !dbg !18
  ret i32 0, !dbg !18
}

attributes #0 = { uwtable }

!llvm.dbg.cu = !{!0}
!llvm.module.flags = !{!2}

!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !1, producer: "Crystal", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug)
!1 = !DIFile(filename: "main_module", directory: ".")
!2 = !{i32 2, !"Debug Info Version", i32 3}
!3 = distinct !DISubprogram(name: "__crystal_main", linkageName: "__crystal_main", scope: !4, file: !4, type: !5, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition, unit: !0, retainedNodes: !8)
!4 = !DIFile(filename: "??", directory: ".")
!5 = !DISubroutineType(types: !6)
!6 = !{!7}
!7 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
!8 = !{}
!9 = !DILocation(line: 1, column: 1, scope: !10)
!10 = distinct !DILexicalBlock(scope: !3, file: !11, line: 1, column: 1)
!11 = !DIFile(filename: "piyo.cr", directory: "/home/kojix2/Crystal/tmp")
!12 = !DILocation(line: 0, scope: !13)
!13 = distinct !DILexicalBlock(scope: !3, file: !4, line: 1, column: 1)
!14 = !DILocation(line: 2, column: 1, scope: !10)
!15 = !DILocation(line: 3, column: 1, scope: !10)
!16 = distinct !DISubprogram(name: "main", linkageName: "main", scope: !17, file: !17, line: 11, type: !5, scopeLine: 11, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !8)
!17 = !DIFile(filename: "empty.cr", directory: "/usr/local/share/crystal/src")
!18 = !DILocation(line: 12, column: 3, scope: !16)

まあ長いんですけど、別にビビる必要はないんですよね。
僕も正直、1週間前にLLVM-IRについてChatGPTに聞いて学んだだけですが、それでも楽しむことは可能です。

では順番に見ていきましょう。

最初の

; ModuleID = 'e'
source_filename = "main_module"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

これはメタデータ的なやつっすね(笑) (テキトー

次の構造体

%String = type { i32, i32, i32, i8 }

これが、Crystalの型です。今回はStringしか使っていないので、Stringしか使っていませんが、他にもClassを使ったり宣言したりすると、ここにいろいろ追加されます。

このあとまたメタデータ的なやつが続きます。ヘッダーだと思って無視して良いでしょう。

ARGC_UNSAFE = internal global i32 0
@ARGV_UNSAFE = internal global i8** null
@"Crystal::BUILD_COMMIT" = internal global %String* null
@"'1908c816f'" = private constant { i32, i32, i32, [10 x i8] } { i32 1, i32 9, i32 9, [10 x i8] c"1908c816f\00" }
@"Crystal::BUILD_DATE" = internal global %String* null
@"'2023-07-19'" = private constant { i32, i32, i32, [11 x i8] } { i32 1, i32 10, i32 10, [11 x i8] c"2023-07-19\00" }
@"Crystal::CACHE_DIR" = internal global %String* null
@"'/home/kojix2/.cache...'" = private constant { i32, i32, i32, [28 x i8] } { i32 1, i32 27, i32 27, [28 x i8] c"/home/kojix2/.cache/crystal\00" }
@"Crystal::DEFAULT_PATH" = internal global %String* null
@"'$ORIGIN/../share/cr...'" = private constant { i32, i32, i32, [29 x i8] } { i32 1, i32 28, i32 28, [29 x i8] c"$ORIGIN/../share/crystal/src\00" }
@"Crystal::DESCRIPTION" = internal global %String* null
@"'Crystal 1.9.2 [1908...'" = private constant { i32, i32, i32, [90 x i8] } { i32 1, i32 89, i32 89, [90 x i8] c"Crystal 1.9.2 [1908c816f] (2023-07-19)\0A\0ALLVM: 14.0.0\0ADefault target: x86_64-pc-linux-gnu\0A\00" }
@"Crystal::PATH" = internal global %String* null
@"'lib:/usr/local/bin/...'" = private constant { i32, i32, i32, [40 x i8] } { i32 1, i32 39, i32 39, [40 x i8] c"lib:/usr/local/bin/../share/crystal/src\00" }
@"Crystal::LIBRARY_PATH" = internal global %String* null
@"'/usr/local/bin/../l...'" = private constant { i32, i32, i32, [30 x i8] } { i32 1, i32 29, i32 29, [30 x i8] c"/usr/local/bin/../lib/crystal\00" }
@"Crystal::LIBRARY_RPATH" = internal global %String* null
@"''" = private constant { i32, i32, i32, [1 x i8] } { i32 1, i32 0, i32 0, [1 x i8] zeroinitializer }
@"Crystal::VERSION" = internal global %String* null
@"'1.9.2'" = private constant { i32, i32, i32, [6 x i8] } { i32 1, i32 5, i32 5, [6 x i8] c"1.9.2\00" }
@"Crystal::LLVM_VERSION" = internal global %String* null
@"'14.0.0'" = private constant { i32, i32, i32, [7 x i8] } { i32 1, i32 6, i32 6, [7 x i8] c"14.0.0\00" }
@"'neko'" = private constant { i32, i32, i32, [5 x i8] } { i32 1, i32 4, i32 4, [5 x i8] c"neko\00" }

そして __crystal_main という名前の関数が来ます。

define %String* @__crystal_main(i32 %argc, i8** %argv) !dbg !3 {
alloca:
  %a = alloca i32, align 4, !dbg !9
  %b = alloca double, align 8, !dbg !9
  %c = alloca %String*, align 8, !dbg !9
  br label %entry

entry:                                            ; preds = %alloca
  store i32 %argc, i32* @ARGC_UNSAFE, align 4, !dbg !12
  store i8** %argv, i8*** @ARGV_UNSAFE, align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [10 x i8] }* @"'1908c816f'" to %String*), %String** @"Crystal::BUILD_COMMIT", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [11 x i8] }* @"'2023-07-19'" to %String*), %String** @"Crystal::BUILD_DATE", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [28 x i8] }* @"'/home/kojix2/.cache...'" to %String*), %String** @"Crystal::CACHE_DIR", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [29 x i8] }* @"'$ORIGIN/../share/cr...'" to %String*), %String** @"Crystal::DEFAULT_PATH", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [90 x i8] }* @"'Crystal 1.9.2 [1908...'" to %String*), %String** @"Crystal::DESCRIPTION", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [40 x i8] }* @"'lib:/usr/local/bin/...'" to %String*), %String** @"Crystal::PATH", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [30 x i8] }* @"'/usr/local/bin/../l...'" to %String*), %String** @"Crystal::LIBRARY_PATH", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [1 x i8] }* @"''" to %String*), %String** @"Crystal::LIBRARY_RPATH", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [6 x i8] }* @"'1.9.2'" to %String*), %String** @"Crystal::VERSION", align 8, !dbg !12
  store %String* bitcast ({ i32, i32, i32, [7 x i8] }* @"'14.0.0'" to %String*), %String** @"Crystal::LLVM_VERSION", align 8, !dbg !12
  store i32 0, i32* %a, align 4, !dbg !9
  store double 1.000000e-01, double* %b, align 8, !dbg !14
  store %String* bitcast ({ i32, i32, i32, [5 x i8] }* @"'neko'" to %String*), %String** %c, align 8, !dbg !15
  ret %String* bitcast ({ i32, i32, i32, [5 x i8] }* @"'neko'" to %String*), !dbg !15
}

すると alloca というあたりで %a, %b, %c が宣言されます(より正確にはメモリが確保されます)。
この % ついているやつはローカル変数的な存在らしいっす。

alloca:
  %a = alloca i32, align 4, !dbg !9
  %b = alloca double, align 8, !dbg !9
  %c = alloca %String*, align 8, !dbg !9

ちゃんと、 %a が i32 %b が double %c%String になっているところに注目してください。

entry の最後のところでメモリ領域に値が書き込まれていることがわかります。

  store i32 0, i32* %a, align 4, !dbg !9
  store double 1.000000e-01, double* %b, align 8, !dbg !14
  store %String* bitcast ({ i32, i32, i32, [5 x i8] }* @"'neko'" to %String*), %String** %c, align 8, !dbg !15
  ret %String* bitcast ({ i32, i32, i32, [5 x i8] }* @"'neko'" to %String*), !dbg !15

"neko" は戻り値としても使われているっぽいっすね。
こうやって、私達が書いたコードが、LLVM-IRに変換されていることがわかります。

その下の printf はリンクされたライブラリなどから、printf を呼び出すためのものだと思います。

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

今回なんでここに出てきてしまったのかはいまいちわかりませんね。String内部で使っているのでしょうか。他にもCの関数を呼び出す場合は、こんな感じで追加されます。

その下は、main 関数が宣言されています。これは argv を受け取って crystal_main を呼び出しているだけっぽいですね。

define i32 @main(i32 %argc, i8** %argv) #0 !dbg !16 {
entry:
  %0 = call %String* @__crystal_main(i32 %argc, i8** %argv), !dbg !18
  ret i32 0, !dbg !18
}

その下もメタデータや、デバッグ情報みたいなのが並びます。

attributes #0 = { uwtable }

!llvm.dbg.cu = !{!0}
!llvm.module.flags = !{!2}

!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !1, producer: "Crystal", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug)
!1 = !DIFile(filename: "main_module", directory: ".")
!2 = !{i32 2, !"Debug Info Version", i32 3}
!3 = distinct !DISubprogram(name: "__crystal_main", linkageName: "__crystal_main", scope: !4, file: !4, type: !5, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition, unit: !0, retainedNodes: !8)
!4 = !DIFile(filename: "??", directory: ".")
!5 = !DISubroutineType(types: !6)
!6 = !{!7}
!7 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
!8 = !{}
!9 = !DILocation(line: 1, column: 1, scope: !10)
!10 = distinct !DILexicalBlock(scope: !3, file: !11, line: 1, column: 1)
!11 = !DIFile(filename: "piyo.cr", directory: "/home/kojix2/Crystal/tmp")
!12 = !DILocation(line: 0, scope: !13)
!13 = distinct !DILexicalBlock(scope: !3, file: !4, line: 1, column: 1)
!14 = !DILocation(line: 2, column: 1, scope: !10)
!15 = !DILocation(line: 3, column: 1, scope: !10)
!16 = distinct !DISubprogram(name: "main", linkageName: "main", scope: !17, file: !17, line: 11, type: !5, scopeLine: 11, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !8)
!17 = !DIFile(filename: "empty.cr", directory: "/usr/local/share/crystal/src")
!18 = !DILocation(line: 12, column: 3, scope: !16)

こんな感じで、LLVM-IRを出力する機能は、文法がそっくりな動的言語のRubyには存在しないため(ひょっとすると、JITで似たような感じのあるのかもしれませんが)Rubyが好きな方もかなり楽しむことができると思います。

最近Crystalで面白いなと思っている、LLVM-IRの出力について書きました。

結局記事を書いていたら、途中で中断も入って、一時間ぐらいかかってしまいました。

最後に、--prelude empty.cr で読み込んだ empty.cr を見ておきましょう。これは Crystalのソースコードの中に含まれています。

require "primitives"

{% if flag?(:win32) %}
  @[Link({{ flag?(:preview_dll) ? "msvcrt" : "libcmt" }})] # For `mainCRTStartup`
{% end %}
lib LibCrystalMain
  @[Raises]
  fun __crystal_main(argc : Int32, argv : UInt8**)
end

fun main(argc : Int32, argv : UInt8**) : Int32
  LibCrystalMain.__crystal_main(argc, argv)
  0
end

すると、main という関数があって、そのなかで __crystal_main という関数が呼び出されていることがわかります。先程のLLVM-IRの中に関数が2つあったのは、ここから来ていることがわかりました。

この記事は以上です。


この記事では、LLVMのコードブロックが、crystal 指定のほうが見やすかったという理由で、LLVMではなく、Crystalで指定されています。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0