vm2gol v2 製作メモ(8) サブルーチンの呼び出し(call, ret)



どんどんプログラムっぽくしていきましょう。

プログラムといえばサブルーチン! サブルーチンやりましょう!

最初は簡単なのがいいので、 reg_a に値をセットするだけの サブルーチンをやってみます。

callret という命令を追加します。

# 08_call_ret.vga.txt

  set_reg_b 1
  call sub
  set_reg_b 3
  exit

label sub
  set_reg_a 2
  ret

call でサブルーチン sub にジャンプし、 サブルーチンの処理が終わったら ret で元の場所に戻ります。

順番としては、

set_reg_b 1
set_reg_a 2
set_reg_b 3

のように動いてほしい。

call にはジャンプ先のラベル名が与えられていて、 普通のジャンプと同じようにすれば良さそうです。

問題は ret です。 サブルーチンから戻るには「どこに戻るか」が分からないと戻れません。

ひとまず、 reg_c が空いているので reg_c に戻り先のアドレスをセットするようにしましょうか。 いつセットするかというと、 call するときですね。 戻り先アドレスを覚えておくというところが、単純な jump と異なるところです。

まずは VM に命令を追加して…

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -61,6 +61,12 @@ class Vm
       when "jump_eq"
         addr = @mem[@pc + 1]
         jump_eq(addr)
+      when "call"
+        @reg_c = @pc + 2 # 戻り先を記憶(call ではなく、その次の命令になるように)
+        next_addr = @mem[@pc + 1] # ジャンプ先
+        @pc = next_addr
+      when "ret"
+        @pc = @reg_c
       else
         raise "Unknown operator (#{op})"
       end

アセンブラも修正しないといけないんですが、いっぺんにやると分からなくなりそうなので まずは機械語コードを自分で書きます。 せっかくアセンブラ作ったのにまだ手で機械語コード書くのか……という気もしますが、 一歩ずつということで。

# 08_call_ret.vge.yaml

[
  "set_reg_b", 1, # サブルーチン呼び出し前
  "call", 9,      # サブルーチン呼び出し
  # 4
  "set_reg_b", 3, # サブルーチン呼び出しから戻った後
  "exit",

  # 7
  # ここからサブルーチン
  "label", "sub",
  # 9
  "set_reg_a", 2,
  "ret"
]
$ ruby vgvm.rb 08_call_ret.vge.yaml
pc( 0) | reg_a(0) b(0) c(0) | zf(0)
pc( 2) | reg_a(0) b(1) c(0) | zf(0) … b=1 がセットされた
pc( 9) | reg_a(0) b(1) c(4) | zf(0) … reg_c に戻り先アドレスがセットされ、
                                       サブルーチンにジャンプした
pc(11) | reg_a(2) b(1) c(4) | zf(0) … a=2 がセットされた
pc( 4) | reg_a(2) b(1) c(4) | zf(0) … 呼び出し元(の次の命令の位置)に戻った
pc( 6) | reg_a(2) b(3) c(4) | zf(0) … b=3 がセットされた
exit

いいですね!

良さそうなので、アセンブラを修正してさっき手で書いた 機械語コードと同じものが出力されるようにします。 といってもこれだけ。

--- a/vgasm.rb
+++ b/vgasm.rb
@@ -50,7 +50,7 @@ alines.each do |aline|
   case head
   when "label"
     words << rest[0]
-  when "jump", "jump_eq"
+  when "jump", "jump_eq", "call"
     label_name = rest[0]
     words << label_addr_map[label_name] + 2
   else

まずはアセンブルだけやってみて出力を確認します。

アセンブラが出力した機械語コードはアドレスの情報がなくて自分で数えるのが面倒なので、 確認用の簡単なスクリプトを書きました。

# dump_exe.rb

lineno = -2

while line = $stdin.gets do
  lineno += 1
  puts "% 4d %s" % [lineno, line]
end

これを使って見てみると…

$ ruby vgasm.rb 08_call_ret.vga.txt | ruby dump_exe.rb
  -1 ---
   0 - set_reg_b
   1 - 1
   2 - call
   3 - 9
   4 - set_reg_b
   5 - 3
   6 - exit
   7 - label
   8 - sub
   9 - set_reg_a
  10 - 2
  11 - ret

call subcall 9 に期待したとおりに置き換えられていて、問題なさそうです。


サッと書いてしまったものの、番号を表示するだけならnl コマンドでもよかったですね……。

$ ruby vgasm.rb 08_call_ret.vga.txt | nl -v -1
    -1  ---
     0  - set_reg_b
     1  - 1
     2  - call
     3  - 9
     4  - set_reg_b
     5  - 3
     6  - exit
     7  - label
     8  - sub
     9  - set_reg_a
    10  - 2
    11  - ret

良さそうなので run.sh でまとめて実行しましょう。

$ ./run.sh 08_call_ret.vga.txt
pc( 0) | reg_a(0) b(0) c(0) | zf(0)
pc( 2) | reg_a(0) b(1) c(0) | zf(0)
pc( 9) | reg_a(0) b(1) c(4) | zf(0)
pc(11) | reg_a(2) b(1) c(4) | zf(0)
pc( 4) | reg_a(2) b(1) c(4) | zf(0)
pc( 6) | reg_a(2) b(3) c(4) | zf(0)
exit

いいですね!

初めて機械語アセンブリ言語でサブルーチン呼び出しを書きました(オレオレの機械語アセンブリ言語ですが……)! 新鮮!