minrubyコンパイラをx86-64へ移植
AArch64版(64ビットARMアセンブリ)
x86_64版
事前準備
事前に minruby gem をインストールしておく
code:sh
gem install minruby
先日作成したminrubyコンパイラはApple M1(AArch64)向けのアセンブリコードを出力していた。x86-64の勉強がてら、x86-64向けのアセンブリコードを出力するようにしたい。
入力した整数をプリント
code:minrubyc.rb
# usage:
# ruby minrubyc.rb <filename> > tmp.s
# gcc -z noexecstack tmp.s libminruby.c
# ./a.out
require "minruby"
tree = minruby_parse(ARGV0) puts "\t.intel_syntax noprefix"
puts "\t.text"
puts "\t.globl main"
puts "main:"
puts "\tpush rbp"
puts "\tmov rbp, rsp"
else
end
# 入力した整数をプリントする
puts "\tmov rdi, rax"
puts "\tcall p"
puts "\tmov rsp, rbp"
puts "\tpop rbp"
puts "\tret"
code:libminruby.c
long p(long n) {
printf("%ld\n", n);
return n;
}
コンパイルしてみる。4649が画面に表示されればOK
https://gyazo.com/dc3d410ebf9441335d445750bade3090
四則演算の結果を画面へプリント
四則演算に対応した。スタックへの退避はx86の方がRISC系の命令セットよりも書きやすい。
code:minrubyc.rb
# usage:
# ruby minrubyc.rb <filename> > tmp.s
# gcc -z noexecstack tmp.s libminruby.c
# ./a.out
require "minruby"
def gen(tree)
elsif %w(+ - * /).include?(tree0) # R12とR13をスタックへ退避
puts "\tpush r12"
puts "\tpush r13"
# 左辺を計算してR12へ結果を格納
puts "\tmov r12, rax"
# 右辺を計算してR13へ結果を格納
puts "\tmov r13, rax"
# 演算結果をRAXへ格納
when "+"
puts "\tadd r12, r13"
puts "\tmov rax, r12"
when "-"
puts "\tsub r12, r13"
puts "\tmov rax, r12"
when "*"
puts "\timul r12, r13"
puts "\tmov rax, r12"
when "/"
puts "\tmov rax, r12"
puts "\tcqo"
puts "\tidiv r13"
else
end
# R12とR13をスタックから復元
puts "\tpop r13"
puts "\tpop r12"
else
end
end
tree = minruby_parse(ARGF.read)
puts "\t.intel_syntax noprefix"
puts "\t.text"
puts "\t.globl main"
puts "main:"
puts "\tpush rbp"
puts "\tmov rbp, rsp"
gen(tree)
# 評価した結果を画面へ出力
puts "\tmov rdi, rax"
puts "\tcall p"
puts "\tmov rsp, rbp"
puts "\tpop rbp"
puts "\tret"
複文への対応とプリント関数の導入
以下のような複文を実行できるようにする。合わせてプリント関数 p を導入する。
まずはプリント関数の導入。
code:diff
diff --git a/minrubyc.rb b/minrubyc.rb
index 89bf85d..213aca2 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -43,6 +43,12 @@ def gen(tree)
# R12とR13をスタックから復元
puts "\tpop r13"
puts "\tpop r12"
+ elsif tree0 == "func_call" && tree1 == "p" +
+ # 評価した結果を画面へ出力
+ puts "\tmov rdi, rax"
+ puts "\tcall p"
else
end
@@ -59,10 +65,6 @@ puts "\tmov rbp, rsp"
gen(tree)
-# 評価した結果を画面へ出力
-puts "\tmov rdi, rax"
-puts "\tcall p"
-
puts "\tmov rsp, rbp"
puts "\tpop rbp"
puts "\tret"
diff --git a/test.sh b/test.sh
index 75ee256..9396bda 100755
--- a/test.sh
+++ b/test.sh
@@ -61,10 +61,10 @@ assert() {
# assert 1 'p(1 >= 1)'
# assert 0 'p(0 >= 1)'
-assert 33 '99 / 3'
-assert 200 '10 * 20'
-assert 40 '30 + 20 - 10'
-assert 60 '10 + 20 + 30'
-assert 4649 '4649'
+assert 33 'p 99 / 3'
+assert 200 'p 10 * 20'
+assert 40 'p 30 + 20 - 10'
+assert 60 'p 10 + 20 + 30'
+assert 4649 'p 4649'
echo OK
次に複文を実行できるように。
code:diff
diff --git a/minrubyc.rb b/minrubyc.rb
index 213aca2..d015605 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -49,6 +49,10 @@ def gen(tree)
# 評価した結果を画面へ出力
puts "\tmov rdi, rax"
puts "\tcall p"
+ tree1...each do |statement| + gen(statement)
+ end
else
end
diff --git a/test.sh b/test.sh
index 9396bda..d37e1ef 100755
--- a/test.sh
+++ b/test.sh
@@ -61,10 +61,16 @@ assert() {
# assert 1 'p(1 >= 1)'
# assert 0 'p(0 >= 1)'
+# 複文
+assert 20 '1 + 1; p 10 + 10'
assert 33 'p 99 / 3'
assert 200 'p 10 * 20'
assert 40 'p 30 + 20 - 10'
assert 60 'p 10 + 20 + 30'
assert 4649 'p 4649'
echo OK
変数を導入
code:diff
commit 29a231c8a26b3c2a493f74194bd2bf11c83a6cf3
Author: takashi hatakeyama <takashi.hatakeyama@gmail.com>
Date: Mon Jul 31 12:19:30 2023 +0900
変数を導入
diff --git a/minrubyc.rb b/minrubyc.rb
index d015605..4e7615f 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -5,7 +5,31 @@
require "minruby"
-def gen(tree)
+# tree 内の変数名一覧
+def var_names(arr, tree)
+ if tree0 == "var_assign" + tree1...flat_map do |statement| + var_names(arr, statement)
+ end
+ else
+ arr
+ end
+end
+
+# スタックフレーム上の変数のアドレスをベースポインタ(RBP)からのオフセットとして返す
+# 例:
+# ひとつ目の変数のアドレス = ベースポインタ(RBP) - 0
+# ふたつ目の変数のアドレス = ベースポインタ(RBP) - 8
+# ふたつ目の変数のアドレス = ベースポインタ(RBP) - 16
+# ...
+def var_offset(var, env)
+ # 変数1つにつき8バイトの領域が必要
+ env.index(var) * -8
+end
+
+def gen(tree, env)
elsif %w(+ - * /).include?(tree0) @@ -14,11 +38,11 @@ def gen(tree)
puts "\tpush r13"
# 左辺を計算してR12へ結果を格納
puts "\tmov r12, rax"
# 右辺を計算してR13へ結果を格納
puts "\tmov r13, rax"
# 演算結果をRAXへ格納
@@ -43,15 +67,24 @@ def gen(tree)
# R12とR13をスタックから復元
puts "\tpop r13"
puts "\tpop r12"
+ elsif tree0 == "var_assign" + puts "\t// var_assign: #{tree1}(#{var_offset(tree1, env)})" + offset = var_offset(tree1, env) + elsif tree0 == "var_ref" + puts "\t// var_ref: #{tree1}(#{var_offset(tree1, env)})" + offset = var_offset(tree1, env) elsif tree0 == "func_call" && tree1 == "p" # 評価した結果を画面へ出力
puts "\tmov rdi, rax"
puts "\tcall p"
tree1...each do |statement| - gen(statement)
+ gen(statement, env)
end
else
@@ -59,6 +92,7 @@ def gen(tree)
end
tree = minruby_parse(ARGF.read)
+env = var_names([], tree)
puts "\t.intel_syntax noprefix"
puts "\t.text"
@@ -67,7 +101,13 @@ puts "main:"
puts "\tpush rbp"
puts "\tmov rbp, rsp"
-gen(tree)
+# ローカル変数用の領域をスタック上へ確保
+
+gen(tree, env)
+
+# スタック上に確保したローカル変数用の領域を開放
puts "\tmov rsp, rbp"
puts "\tpop rbp"
diff --git a/test.sh b/test.sh
index d37e1ef..7e78ad8 100755
--- a/test.sh
+++ b/test.sh
@@ -61,6 +61,10 @@ assert() {
# assert 1 'p(1 >= 1)'
# assert 0 'p(0 >= 1)'
+# 変数
+assert 30 'a = 10; b = 20; p a + b'
+assert 10 'a = 10; p a'
+
# 複文
assert 20 '1 + 1; p 10 + 10'
条件分岐と比較演算子
条件分岐を実装する前に比較演算子を実装する。
比較演算子の実装の詳細は「低レイヤを知りたい人のためのCコンパイラ作成入門」を参照のこと。
code:diff
diff --git a/minrubyc.rb b/minrubyc.rb
index 4e7615f..1bd13ba 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -32,7 +32,7 @@ end
def gen(tree, env)
- elsif %w(+ - * /).include?(tree0) + elsif %w(+ - * / == != < <= > >=).include?(tree0) # R12とR13をスタックへ退避
puts "\tpush r12"
puts "\tpush r13"
@@ -60,6 +60,30 @@ def gen(tree, env)
puts "\tmov rax, r12"
puts "\tcqo"
puts "\tidiv r13"
+ when "=="
+ puts "\tcmp r12, r13"
+ puts "\tsete al"
+ puts "\tmovzb rax, al"
+ when "!="
+ puts "\tcmp r12, r13"
+ puts "\tsetne al"
+ puts "\tmovzb rax, al"
+ when "<"
+ puts "\tcmp r12, r13"
+ puts "\tsetl al"
+ puts "\tmovzb rax, al"
+ when "<="
+ puts "\tcmp r12, r13"
+ puts "\tsetle al"
+ puts "\tmovzb rax, al"
+ when ">"
+ puts "\tcmp r12, r13"
+ puts "\tsetg al"
+ puts "\tmovzb rax, al"
+ when ">="
+ puts "\tcmp r12, r13"
+ puts "\tsetge al"
+ puts "\tmovzb rax, al"
else
end
diff --git a/test.sh b/test.sh
index 7e78ad8..21bcc2b 100755
--- a/test.sh
+++ b/test.sh
@@ -45,21 +45,21 @@ assert() {
# assert 33 'p 99 / 3'
# assert 30 'a = 10; b = 20; p a + b'
-# # 真の場合は1、偽の場合は0を返す
-# assert 1 'p(1 == 1)'
-# assert 0 'p(1 == 2)'
-# assert 0 'p(1 != 1)'
-# assert 1 'p(1 != 2)'
-# assert 1 'p(1 < 2)'
-# assert 0 'p(1 < 1)'
-# assert 1 'p(1 <= 2)'
-# assert 1 'p(1 <= 1)'
-# assert 0 'p(1 <= 0)'
-# assert 1 'p(2 > 1)'
-# assert 0 'p(1 > 1)'
-# assert 1 'p(2 >= 1)'
-# assert 1 'p(1 >= 1)'
-# assert 0 'p(0 >= 1)'
+# 真の場合は1、偽の場合は0を返す
+assert 1 'p(1 == 1)'
+assert 0 'p(1 == 2)'
+assert 0 'p(1 != 1)'
+assert 1 'p(1 != 2)'
+assert 1 'p(1 < 2)'
+assert 0 'p(1 < 1)'
+assert 1 'p(1 <= 2)'
+assert 1 'p(1 <= 1)'
+assert 0 'p(1 <= 0)'
+assert 1 'p(2 > 1)'
+assert 0 'p(1 > 1)'
+assert 1 'p(2 >= 1)'
+assert 1 'p(1 >= 1)'
+assert 0 'p(0 >= 1)'
# 変数
assert 30 'a = 10; b = 20; p a + b'
比較演算子ができたので、条件式 if と elseを実装する。
code:diff
diff --git a/minrubyc.rb b/minrubyc.rb
index 1bd13ba..91d5f14 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -110,6 +110,18 @@ def gen(tree, env)
tree1...each do |statement| gen(statement, env)
end
+ # 条件式を評価
+ puts "\tcmp rax, 0"
+ puts "\tje .Lelse#{tree.object_id}"
+ puts "\tjmp .Lend#{tree.object_id}"
+ puts ".Lelse#{tree.object_id}:"
+ gen(tree3, env) if tree3 + puts ".Lend#{tree.object_id}:"
else
end
diff --git a/test.sh b/test.sh
index 21bcc2b..73f7c52 100755
--- a/test.sh
+++ b/test.sh
@@ -33,11 +33,11 @@ assert() {
# # while
# assert 55 'i = 1; sum = 0; while i <= 10; sum = sum + i; i = i + 1; end; p(sum)'
-# # if
-# assert 42 'if (0 == 0); p(42); else p(43); end'
-# assert 43 'if (0 == 1); p(42); else p(43); end'
-# assert 41 'if (0 == 0); p(41); end'
-# assert '' 'if (0 == 1); p(41); end'
+# if
+assert 42 'if (0 == 0); p(42); else p(43); end'
+assert 43 'if (0 == 1); p(42); else p(43); end'
+assert 41 'if (0 == 0); p(41); end'
+assert '' 'if (0 == 1); p(41); end'
# assert 4649 'p 4649'
# assert 40 'p 30 + 20 - 10'
おまけで while も実装する。
code:diff
diff --git a/minrubyc.rb b/minrubyc.rb
index 91d5f14..9e718a9 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -122,6 +122,18 @@ def gen(tree, env)
puts ".Lend#{tree.object_id}:"
+ puts ".L_while_begin#{tree.object_id}:"
+ # 条件式を評価
+ # 真でなければループを抜ける
+ puts "\tcmp rax, 0"
+ puts "\tje .L_while_end#{tree.object_id}"
+ # ループ本体を評価
+ # ループの先頭へジャンプ
+ puts "\tjmp .L_while_begin#{tree.object_id}"
+ puts ".L_while_end#{tree.object_id}:"
else
end
diff --git a/test.sh b/test.sh
index 7823554..9e118a9 100755
--- a/test.sh
+++ b/test.sh
@@ -30,8 +30,8 @@ assert() {
# assert 2 'case 42; when 0; p(0); when 1; p(1); else p(2); end'
# assert 1 'case 42; when 0; p(0); when 42; p(1); else p(2); end'
-# # while
-# assert 55 'i = 1; sum = 0; while i <= 10; sum = sum + i; i = i + 1; end; p(sum)'
+# while
+assert 55 'i = 1; sum = 0; while i <= 10; sum = sum + i; i = i + 1; end; p(sum)'
# if
assert 42 'if (0 == 0); p(42); else p(43); end'
ifとwhileのブロックの中の変数を参照できるようにするのも忘れないように。
code:diff
diff --git a/minrubyc.rb b/minrubyc.rb
index 047da0b..a3b6fd4 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -19,6 +19,10 @@ def var_names(arr, tree)
tmp_arr = tmp_arr + var_names(tmp_arr, statement)
end
tmp_arr
+ arr + var_names(arr, tree2) + arr + var_names(arr, tree2) else
arr
end
diff --git a/test.sh b/test.sh
index 29fb85f..acb8084 100755
--- a/test.sh
+++ b/test.sh
@@ -57,6 +57,8 @@ assert 0 'p(0 >= 1)'
# 変数
assert 30 'a = 10; b = 20; p a + b'
assert 10 'a = 10; p a'
+assert 30 'a = 10; if a == 10; b = 20; p a + b; end'
+assert 55 'i = 1; sum = 0; while i <= 10; one = 1; sum = sum + i; i = i + one; end; p(sum)'
# 複文
assert 20 '1 + 1; p 10 + 10'
組み込み関数の呼び出し
組み込み関数 add を呼び出せるようにする。
libminruby.c へ add 関数を追加して
code:libminruby.c
long p(long n) {
printf("%ld\n", n);
return n;
}
long add(long a, long b) {
return a + b;
}
以下のように呼び出す。
code:test.rb
p add(10, 20)
組み込み関数の呼び出しを実装した。また、p 関数のみ特別扱いしていた箇所は不要になったので削除した。
code:diff
diff --git a/libminruby.c b/libminruby.c
index 51b016d..c2a0732 100644
--- a/libminruby.c
+++ b/libminruby.c
@@ -4,3 +4,7 @@ long p(long n) {
printf("%ld\n", n);
return n;
}
+
+long add(long a, long b) {
+ return a + b;
+}
diff --git a/minrubyc.rb b/minrubyc.rb
index 9e718a9..87d3508 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -5,6 +5,10 @@
require "minruby"
+# 関数の引数で利用するレジスタ
+PARAM_REGISTERS = %w(rdi rsi rdx rcx r8 r9)
+
# tree 内の変数名一覧
def var_names(arr, tree)
@@ -100,12 +104,25 @@ def gen(tree, env)
puts "\t// var_ref: #{tree1}(#{var_offset(tree1, env)})" offset = var_offset(tree1, env) - elsif tree0 == "func_call" && tree1 == "p" + elsif tree0 == "func_call" +
+ # 引数が6個以上の場合はエラー
+ raise "too many arguments (given #{args.size}, expected 6)" if args.size > 6 +
+ # 引数を評価した結果をスタックへ退避
+ args.reverse.each do |arg|
+ gen(arg, env)
+ puts "\tpush rax"
+ end
+
+ # 退避した引数の評価値を引数レジスタへ格納
+ args.each_with_index do |_, i|
+ end
- # 評価した結果を画面へ出力
- puts "\tmov rdi, rax"
- puts "\tcall p"
+ # 関数を呼び出す
tree1...each do |statement| gen(statement, env)
diff --git a/test.sh b/test.sh
index 9e118a9..59bd8e9 100755
--- a/test.sh
+++ b/test.sh
@@ -22,9 +22,8 @@ assert() {
# assert 30 'def hello() a = 10; b = 20; a + b; end; p hello()'
# assert 120 'def hello(a) b = 20; a + b; end; p hello(100)'
-# # func_call
-# assert 30 'p add(10, 20)'
-# assert 30 'print_int(add(10, 20))'
+# func_call
+assert 30 'p add(10, 20)'
# # case
# assert 2 'case 42; when 0; p(0); when 1; p(1); else p(2); end'
ユーザー定義関数
以下のような、ユーザー定義関数の定義と、その呼び出しを行えるようにする。
code:ruby
def hello()
4649
end
p hello()
code:diff
commit 13b783ada0704bbbaad29cb0a5e929264853f3df
Author: takashi hatakeyama <takashi.hatakeyama@gmail.com>
Date: Mon Jul 31 22:57:59 2023 +0900
ユーザー定義関数を実装
diff --git a/minrubyc.rb b/minrubyc.rb
index 87d3508..7fd2980 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -12,25 +12,42 @@ PARAM_REGISTERS = %w(rdi rsi rdx rcx r8 r9)
# tree 内の変数名一覧
def var_names(arr, tree)
+ arr.include?(tree1) ? arr : arr + [tree1] - tree1...flat_map do |statement| - var_names(arr, statement)
+ tmp_arr = arr
+ tree1...each do |statement| + tmp_arr = tmp_arr + var_names(tmp_arr, statement)
end
+ tmp_arr
else
arr
end
end
+def func_defs(hash, tree)
+ hash.merge({
+ })
+ tree1...reduce(hash) do |acc, statement| + func_defs(acc, statement)
+ end
+ else
+ hash
+ end
+end
+
# スタックフレーム上の変数のアドレスをベースポインタ(RBP)からのオフセットとして返す
# 例:
-# ひとつ目の変数のアドレス = ベースポインタ(RBP) - 0
-# ふたつ目の変数のアドレス = ベースポインタ(RBP) - 8
+# ひとつ目の変数のアドレス = ベースポインタ(RBP) - 8
# ふたつ目の変数のアドレス = ベースポインタ(RBP) - 16
+# ふたつ目の変数のアドレス = ベースポインタ(RBP) - 24
# ...
def var_offset(var, env)
# 変数1つにつき8バイトの領域が必要
- env.index(var) * -8
+ (env.index(var) + 1) * -8
end
def gen(tree, env)
@@ -123,6 +140,8 @@ def gen(tree, env)
# 関数を呼び出す
+ elsif tree0 == "func_def" + # ここでは何もしない
tree1...each do |statement| gen(statement, env)
@@ -158,9 +177,51 @@ end
tree = minruby_parse(ARGF.read)
env = var_names([], tree)
+func_defs = func_defs({}, tree)
puts "\t.intel_syntax noprefix"
puts "\t.text"
+
+# ユーザー定義関数
+func_defs.values.each do |func_def|
+ puts "// ... env: #{env || []}" +
+ name, args, body = func_def
+
+ env = var_names(args, body)
+
+ puts "#{name}:"
+ puts "\tpush rbp"
+ puts "\tmov rbp, rsp"
+
+ # ローカル変数用の領域をスタック上へ確保
+
+ # 引数をスタックへ退避
+ args.each_with_index do |arg, i|
+ offset = var_offset(arg, env)
+ end
+
+ gen(body, env)
+ puts "\t# body end"
+
+ # スタック上に確保したローカル変数用の領域を開放
+
+ puts "\tmov rsp, rbp"
+ puts "\tpop rbp"
+ puts "\tret"
+end
+
+# メイン関数
puts "\t.globl main"
puts "main:"
puts "\tpush rbp"
diff --git a/test.sh b/test.sh
index 59bd8e9..3708b6a 100755
--- a/test.sh
+++ b/test.sh
@@ -17,10 +17,10 @@ assert() {
fi
}
-# # func_def
-# assert 4649 'def hello() 4649; end; p hello()'
-# assert 30 'def hello() a = 10; b = 20; a + b; end; p hello()'
-# assert 120 'def hello(a) b = 20; a + b; end; p hello(100)'
+# func_def
+assert 120 'def hello(a) b = 20; a + b; end; p hello(100)'
+assert 30 'def hello() a = 10; b = 20; a + b; end; p hello()'
+assert 4649 'def hello() 4649; end; p hello()'
# func_call
assert 30 'p add(10, 20)'
Docker環境の中でコンパイル
以下のコマンドでDocker環境の中に入り、
code:sh
$ docker run --platform linux/amd64 -it -v pwd:/root -w /root ruby:latest bash
sample/fib.rb を minrubyc でコンパイル & 実行、fib(10) の結果である55が返ればOK
code:sh
# gem install minruby
# ruby minrubyc.rb sample/fib.rb > tmp.s
# gcc -z noexecstack tmp.s libminruby.c
# ./a.out
55
https://gyazo.com/78f30a8b4272d29068d0a4c58a07a77d
M1Macでx86_64のGCCを動かす方法
gccのDockerイメージを --platform linux/amd64 オプション付きで実行すればOK
code:sh
$ docker run --platform linux/amd64 -it -v pwd:/root -w /root gcc:latest gcc -v
C言語のソースからインテル形式のアセンブリコードを出力する方法
code:sh
docker run --platform linux/amd64 -it -v pwd:/root -w /root gcc:latest gcc -S -masm=intel test.c
出力されるインテル形式のアセンブリコード。
code:test.asm
.intel_syntax noprefix
.text
.globl main
main:
push rbp
mov rbp, rsp
mov eax, 255
pop rbp
ret
x86-64のABI
引数と返り値
第1引数:RDI
第2引数:RSI
第3引数:RDX
第4引数:RCX
第5引数:R8
第6引数:R9
返り値:RAX
メモ
minrubyコンパイラで sample/fib.rb をコンパイルしてx86_64アセンブリファイルを出力、
code:sh
ruby minrubyc.rb sample/fib.rb > tmp.s
出力したx86_64アセンブリをDocker環境上でビルド&実行する。fib(10)の結果である55が返ればOK
code:sh
$ docker run --platform linux/amd64 -it -v pwd:/root -w /root ruby:latest bash
(Docker環境に入って)
# gcc -z noexecstack tmp.s libminruby.c -o fib
# ./fib
55