vm2gol v2 製作メモ(14) 複数の引数を渡す / スタックオーバーフロー対策 / 返り値



前回は引数を 1個だけ渡すサブルーチン呼び出しができるようになりましたが、 今回はそれを 2個にしてみます。

それから、返り値の渡し方を調べたところ、 とりあえずは reg_a にセットすればよさそうでした。 それなら簡単にできそうです。ついでにやってしまいましょうか。

これらを組み合わせると、 たとえば「引数1 と引数2 を受け取って足した結果を返すサブルーチン」 なんてものが作れそうです。 今回はこれを目標にしましょう。

引数を2つ渡す

一度にやると大変なので、まずは引数を2個渡して 無事戻ってくるとこまでやります。

CDECL によると、引数は後ろのものから先にスタックに積むそうなので、 前回のをちょっと修正してこうします。

# 14_two_args_return.vga.txt

  push 34   # 引数2 を先に push
  push 12   # その次に引数1 を push
  call sub
  add_sp 2  # 引数の数だけスタックポインタを戻す
  exit

label sub
  push bp
  cp sp bp

  # サブルーチンの処理本体
  cp [bp+2] reg_a

  cp bp sp
  pop bp
  ret

引数を渡す順番と、サブルーチンから戻った後の後片付けで スタックポインタを引数の数だけ戻すところがポイントです。

これを動かすと……無事 exit までたどり着いて終了するのですが、 途中で spbp が負の値になり、スタックの天井を突き抜けてしまいます (Ruby では負の添字は配列の末尾側へのアクセスになるので、 スタックの底の方に戻って不正に書き込みが行われます)。 これはまずい。

スタックオーバーフロー対策

スタックのサイズは Memory のコンストラクタで渡していたので、 そこを増やしてやればいいのですが、 その前にせっかくなのでスタックオーバーフロー(stack overflow)対策をやりましょう! スタックオーバーフロー対策チャンスです!


Vm#sp に 0 より小さい数をセットしようとしたら例外を出すようにします。

スタックオーバーフローが発生したことに気づければいいだけなので、 こんなので十分でしょう。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -110,6 +110,11 @@ class Vm
     @bp = 3
   end
 
+  def set_sp(addr)
+    raise "Stack overflow" if addr < 0
+    @sp = addr
+  end
+
   def load_program(path)
     @mem.main = YAML.load_file(path)
   end

Vm#sp に値をセットしている箇所を set_sp() の呼び出しに書き換え。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -158,7 +158,7 @@ class Vm
         add_ac()
         @pc += pc_delta
       when "add_sp"
-        @sp += @mem.main[@pc + 1]
+        set_sp(@sp + @mem.main[@pc + 1])
         @pc += pc_delta
       when "compare"
         compare()
@@ -172,14 +172,14 @@ class Vm
         addr = @mem.main[@pc + 1]
         jump_eq(addr)
       when "call"
-        @sp -= 1 # スタックポインタを1減らす
+        set_sp(@sp - 1) # スタックポインタを1減らす
         @mem.stack[@sp] = @pc + 2 # 戻り先を記憶
         next_addr = @mem.main[@pc + 1] # ジャンプ先
         @pc = next_addr
       when "ret"
         ret_addr = @mem.stack[@sp] # 戻り先アドレスを取得
         @pc = ret_addr # 戻る
-        @sp += 1 # スタックポインタを戻す
+        set_sp(@sp + 1) # スタックポインタを戻す
       when "push"
         arg = @mem.main[@pc + 1]
         val_to_push =
@@ -191,7 +191,7 @@ class Vm
           else
             raise not_yet_impl("push", arg)
           end
-        @sp -= 1
+        set_sp(@sp - 1)
         @mem.stack[@sp] = val_to_push
         @pc += pc_delta
       when "pop"
@@ -202,7 +202,7 @@ class Vm
         else
           raise not_yet_impl("pop", arg)
         end
-        @sp += 1
+        set_sp(@sp + 1)
         @pc += pc_delta
       else
         raise "Unknown operator (#{op})"
@@ -232,7 +232,7 @@ class Vm
     when "bp"
       @bp = src_val
     when "sp"
-      @sp = src_val
+      set_sp(src_val)
     else
       raise not_yet_impl("copy dest", arg2)
     end

サイズ4 のかわいいスタックのまま動かすと、 スタックオーバーフローを検出して例外が発生するはず!

$ ./run.sh 14_two_args_return.vga.txt 

(略)

================================
reg_a(0) reg_b(0) reg_c(0) zf(0)
---- memory (main) ----
      00   ["push", 34]
      02   ["push", 12]
      04   ["call", 11]
      06   ["add_sp", 2]
      08   ["exit"]
      09 ["label", "sub"]
pc => 11   ["push", "bp"]
      13   ["cp", "sp", "bp"]
      16   ["cp", "[bp+2]", "reg_a"]
      19   ["cp", "bp", "sp"]
      22   ["pop", "bp"]
      24   ["ret"]
---- memory (stack) ----
sp    => 0 6
         1 12
         2 34
   bp => 3 0

vgvm.rb:114:in `set_sp': Stack overflow (RuntimeError)
        from vgvm.rb:194:in `block in start'
        from vgvm.rb:126:in `loop'
        from vgvm.rb:126:in `start'
        from vgvm.rb:316:in `<main>'

発生しました! いいですねー。 sp がすでに 0 の位置にあるのに、そこでさらに push しようとしたため 期待どおりに エラーになってくれました。


確認できて気が済んだので、 スタック領域のサイズを増やします。

vm2gol-v1 を作っていたときは足りなくなるたびにその都度伸ばしていましたが、 煩雑なのでここで一気に 50 まで引き上げておきます。 かわいいスタックはここで卒業です。

@@ -90,7 +90,7 @@ class Memory
 end

 class Vm
-  def initialize(mem)
+  def initialize(mem, stack_size)
     # program counter
     @pc = 0

@@ -104,9 +104,9 @@ class Vm

     @mem = mem
     # スタックポインタ
-    @sp = 3
+    @sp = stack_size - 1
     # ベースポインタ
-    @bp = 3
+    @bp = stack_size - 1
     end

     def set_sp(addr)
@@ -320,8 +320,9 @@ end

 bin_file = ARGV[0]

-mem = Memory.new(4)
-vm = Vm.new(mem)
+stack_size = 50
+mem = Memory.new(stack_size)
+vm = Vm.new(mem, stack_size)
 vm.load_program(bin_file)

 vm.start

スタック領域のサイズを十分に大きくしたので、 せっかく実装したスタックオーバーフロー検知の処理は この先出番がなくなってしまうのでした……。

まあ、残しておいても邪魔にはならないですし、 試行錯誤しているときにまた発生しないとも限らない、ということにして、 このまま残しておきます。

ケチケチせずにサイズを 100 とか 500 とかにしてしまってもいいのですが。


ちなみに、v1 のときはその都度必要な分だけスタック領域を大きくする、 というやり方で進めていたので、実際必要だったんですよ…… そこらへんも手探りでした。

「最初からスタック領域のサイズを十分大きくしておけばよい」 という発想は、 後から振り返るとそりゃそうだって感じがしますが、 「あ、これも削れるじゃん」と気づいたのは v1 を完成させた後でした。


( ところで、この検知の処理って CPU でやるべきことなの? という点は謎ですね。普通はどこでやるんでしょうね? (まだ調べてない) )


脱線しましたが、えーと……あ、そうそう、引数 2個渡して戻る、 というのをやっていたのでした。

動かしてみると……。

================================
reg_a(12) reg_b(0) reg_c(0) zf(0)
---- memory (main) ----
      00   ["push", 34]
      02   ["push", 12]
      04   ["call", 11]
      06   ["add_sp", 2]
pc => 08   ["exit"]
      09 ["label", "sub"]
      11   ["push", "bp"]
      13   ["cp", "sp", "bp"]
      16   ["cp", "[bp+2]", "reg_a"]
      19   ["cp", "bp", "sp"]
      22   ["pop", "bp"]
      24   ["ret"]
---- memory (stack) ----
         41 0
         42 0
         43 0
         44 0
         45 49
         46 6
         47 12
         48 34
sp bp => 49 0

exit

うん、いいですね。

返り値

残りの「引数2つを足して返す」をやっつけましょう。 「サブルーチンの処理本体」の部分をこんな感じに書けば良さそうです。

cp [bp+2] reg_a # 引数1 を reg_a にセット
cp [bp+3] reg_b # 引数2 を reg_b にセット
add_ab          # reg_a と reg_b を足して結果を reg_a にセット

add_ab 命令はたしか前に作ってたはず…… ありますね。これ使えばいいですね。

結果を返すのはどこにいったのかというと、 結果は reg_a に入れて返す(これも CDECL に合わせています) ということにしているので、 add_ab の場合は 特別に何かやる必要はないのです。

はい、ではアセンブリのコードを修正します!

--- a/14_two_args_return.vga.txt
+++ b/14_two_args_return.vga.txt
@@ -9,7 +9,9 @@ label sub
   cp sp bp
 
   # サブルーチンの処理本体
-  cp [bp+2] reg_a
+  cp [bp+2] reg_a # 引数1
+  cp [bp+3] reg_b # 引数2
+  add_ab
 
   cp bp sp
   pop bp

実行。

vgvm.rb:250:in `num_args_for': Invalid operator (add_ab) (RuntimeError)

抜けてました。追加します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -244,7 +244,7 @@ class Vm
       2
     when "set_reg_a", "set_reg_b", "label", "call", "push", "pop", "add_sp"
       1
-    when "ret", "exit"
+    when "ret", "exit", "add_ab"
       0
     else
       raise "Invalid operator (#{operator})"

まあこうやってその都度必要な分を修正していけばいいんです(そういう方針です)。

vgvm.rb:237:in `copy': Not yet implemented ("copy dest") ("reg_b") (RuntimeError)

reg_b へのコピーもまだでしたね。 Vm#copy() に追加します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -229,6 +229,8 @@ class Vm
     case arg2
     when "reg_a"
       @reg_a = src_val
+    when "reg_b"
+      @reg_b = src_val
     when "bp"
       @bp = src_val
     when "sp"

どうでしょう。

================================
reg_a(46) reg_b(34) reg_c(0) zf(0)
---- memory (main) ----
      00   ["push", 34]
      02   ["push", 12]
      04   ["call", 11]
      06   ["add_sp", 2]
pc => 08   ["exit"]
      09 ["label", "sub"]
      11   ["push", "bp"]
      13   ["cp", "sp", "bp"]
      16   ["cp", "[bp+2]", "reg_a"]
      19   ["cp", "[bp+3]", "reg_b"]
      22   ["add_ab"]
      23   ["cp", "bp", "sp"]
      26   ["pop", "bp"]
      28   ["ret"]
---- memory (stack) ----
         41 0
         42 0
         43 0
         44 0
         45 49
         46 6
         47 12
         48 34
sp bp => 49 0

exit

できました! 足し算の結果の 12 + 34 = 46 が reg_a にセットされ、 sp, bp がスタックの底に戻っています!

2個でできたので、3個以上の場合も

  • 後ろの引数から順に push
  • 引数の数だけスタックポインタを戻す

という点を押さえておけば同じ要領でできるでしょう。



vm2gol v2 製作メモ(13) サブルーチンに引数を1個渡す / add_sp



前々回と前回でいろいろ片付いたので引数やります!

引数はスタックに置いて渡すそうです!

まず引数を1個だけ使うのをやってみます。 次のようにサブルーチン呼び出し(call)の直前でスタックに push します。

(※間違いがあります。後述)

サブルーチン呼び出し前:

sp bp 2
      3

引数を1個 push: push {引数}

sp    1 {引数}
   bp 2
      3

サブルーチン呼び出し: push bp

sp    0 2
      1 {引数}
   bp 2
      3

サブルーチン呼び出し: cp sp bp

sp bp 0 2
      1 {引数}
      2
      3

で、サブルーチン内でこの引数にアクセスする場合は bp + 1 の位置を見ればよい(※間違いがあります。後述)と。

ふむふむ。やってみましょう。 例として、サブルーチンの中で引数1 を reg_a にセットするだけ、 というのをやってみます。

# 13_one_arg.vga.txt

  push 11 # 引数1
  call sub
  exit

label sub
  push bp
  cp sp bp

  # サブルーチンの処理本体
  cp [bp+1] reg_a

  cp bp sp
  pop bp
  ret

いろいろ片付いたはずでしたが実際動かすとまだ足りないところがあります。 まず最初の push ができない。

vgvm.rb:182:in `block in start': Not yet implemented ("push") (11) (RuntimeError)

オペランドとして即値を受け取れていませんでした。対応させます。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -176,6 +176,8 @@ class Vm
         arg = @mem.main[@pc + 1]
         val_to_push =
           case arg
+          when Integer
+            arg
           when "bp"
             @bp
           else

次は……

vgvm.rb:216:in `copy': Not yet implemented ("copy src") ("[bp+1]") (RuntimeError)

うん、そんな気はしました。 cp のコピー元のとこを修正します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -212,6 +212,8 @@ class Vm
         @sp
       when "bp"
         @bp
+      when /^\[bp\+(\d+)\]$/
+        @mem.stack[@bp + $1.to_i]
       else
         raise not_yet_impl("copy src", arg1)
       end

@bp + $1.to_i を返すのではなく、 「スタックの @bp + $1.to_i の位置にある値」を返したいので @mem.stack[@bp + $1.to_i] となります。

正規表現が若干ハードコーディング気味ですが動いたので気にせず進みます。 YAGNI です。

お次はこう:

vgvm.rb:227:in `copy': Not yet implemented ("copy dest") ("reg_a") (RuntimeError)

cp のコピー先を修正!

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -219,6 +219,8 @@ class Vm
       end
 
     case arg2
+    when "reg_a"
+      @reg_a = src_val
     when "bp"
       @bp = src_val
     when "sp"

エラーが出なくなりましたが、動きがおかしいですね。

(終了時の状態)

================================
reg_a(4) reg_b(0) reg_c(0) zf(0)
---- memory (main) ----
      00   ["push", 11]
      02   ["call", 7]
pc => 04   ["exit"]
      05 ["label", "sub"]
      07   ["push", "bp"]
      09   ["cp", "sp", "bp"]
      12   ["cp", "[bp+1]", "reg_a"]
      15   ["cp", "bp", "sp"]
      18   ["pop", "bp"]
      20   ["ret"]
---- memory (stack) ----
         0 3
         1 4
sp    => 2 11
   bp => 3 0

exit

引数として指定した 11 が reg_a にセットされてほしいのに、 4 がセットされています。

(しばし調べる)

分かりました。 サブルーチン呼び出しの際には引数だけじゃなくて 戻り先アドレスもスタックに積んでいたのでした。

0 3
1 4  ← 戻り先アドレス
2 11 ← 引数
3 0

なので、[bp+1] じゃなくて [bp+2] としないといけないのでした。

--- a/13_one_arg.vga.txt
+++ b/13_one_arg.vga.txt
@@ -7,7 +7,7 @@ label sub
   cp sp bp
 
   # サブルーチンの処理本体
-  cp [bp+1] reg_a
+  cp [bp+2] reg_a
 
   cp bp sp
   pop bp

アセンブリコードを修正して動かすと、終了時の状態がこうなります。

================================
reg_a(11) reg_b(0) reg_c(0) zf(0)
---- memory (main) ----
      00   ["push", 11]
      02   ["call", 7]
pc => 04   ["exit"]
      05 ["label", "sub"]
      07   ["push", "bp"]
      09   ["cp", "sp", "bp"]
      12   ["cp", "[bp+2]", "reg_a"]
      15   ["cp", "bp", "sp"]
      18   ["pop", "bp"]
      20   ["ret"]
---- memory (stack) ----
         0 3
         1 4
sp    => 2 11
   bp => 3 0

exit

こんどは reg_a にちゃんと 11 がセットされました! されましたが!!

spbp の位置がずれてますね……。 これはずれていてはいけない (サブルーチン呼び出し前と同じ状態になっていないといけない)はずです。

呼び出し規約は CDECL を参考にすることにしたので、ちょっと調べてみると、 CDECL では ret で呼び出し元に戻った後に sp の位置を戻すようで、 sp に 1 を足すためにまた専用命令である add_sp をシュッと追加します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -152,6 +152,9 @@ class Vm
       when "add_ac"
         add_ac()
         @pc += pc_delta
+      when "add_sp"
+        @sp += @mem.main[@pc + 1]
+        @pc += pc_delta
       when "compare"
         compare()
         @pc += pc_delta
@@ -234,7 +237,7 @@ class Vm
     case operator
     when "cp"
       2
-    when "set_reg_a", "set_reg_b", "label", "call", "push", "pop"
+    when "set_reg_a", "set_reg_b", "label", "call", "push", "pop", "add_sp"
       1
     when "ret", "exit"
       0

アセンブリコードの方にも add_sp を追加。

--- a/13_one_arg.vga.txt
+++ b/13_one_arg.vga.txt
@@ -1,5 +1,6 @@
   push 11 # 引数1
   call sub
+  add_sp 1
   exit
 
 label sub

またまた動かすとこんどは……

================================
reg_a(11) reg_b(0) reg_c(0) zf(0)
---- memory (main) ----
      00   ["push", 11]
      02   ["call", 9]
      04   ["add_sp", 1]
pc => 06   ["exit"]
      07 ["label", "sub"]
      09   ["push", "bp"]
      11   ["cp", "sp", "bp"]
      14   ["cp", "[bp+2]", "reg_a"]
      17   ["cp", "bp", "sp"]
      20   ["pop", "bp"]
      22   ["ret"]
---- memory (stack) ----
         0 3
         1 4
         2 11
sp bp => 3 0

exit

sp が元の位置に戻りました! めでたしめでたし!!

修正後のアセンブリコードも改めて貼っておきます。

# 13_one_arg.vga.txt

  push 11 # 引数1
  call sub
  add_sp 1
  exit

label sub
  push bp
  cp sp bp

  # サブルーチンの処理本体
  cp [bp+2] reg_a

  cp bp sp
  pop bp
  ret


vm2gol v2 製作メモ(12) リファクタリング / cp



道具が用意できたので引数渡しをやりたい! のですが、今回はリファクタリング回にします。

前回やっつけで追加した copy_sp_to_bpcopy_bp_to_sp を整理して、 せっかくのリファクタリング回なので他の部分もあわせて整理しておきます。


まず、

case arg
when "bp"
  @bp
else
  raise "push: not yet implemented (#{arg})"
end

のように、 case 式で分岐させ、 else で「その値に関する処理はまだ実装してないよ」と例外を投げている箇所が ありますが、以後も出てくるので共通化しておきます。

これは別ファイル common.rb に抽出します。

# common.rb

def p_e(*args)
  args.each{ |arg| $stderr.puts arg.inspect }
end

def pp_e(*args)
  args.each{ |arg| $stderr.puts arg.pretty_inspect }
end

def not_yet_impl(*args)
  "Not yet implemented" +
    args
    .map{ |arg| " (#{ arg.inspect })" }
    .join("")
end

ついでにデバッグ用の p_e, pp_e というべんりメソッドも足しておきました。

not_yet_impl() の中で例外を投げるとこまでやってしまおうかとも 考えましたが、 なるべく単機能(例外用のメッセージを組み立てるだけ)になっている方がよいかな? とか、 呼び出し元を読んだときにraise していることが分かった方が明示的でよいかな? などと考えて控えめにしておきました。


呼び出し元を修正します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -2,6 +2,8 @@
 require 'pp'
 require 'yaml'
 
+require './common'
+
 module TermColor
   RESET  = "\e[m"
   RED    = "\e[0;31m"
@@ -174,7 +176,7 @@ class Vm
           when "bp"
             @bp
           else
-            raise "push: not yet implemented (#{arg})"
+            raise not_yet_impl("push", arg)
           end
         @sp -= 1
         @mem.stack[@sp] = val_to_push
@@ -185,7 +187,7 @@ class Vm
         when "bp"
           @bp = @mem.stack[@sp]
         else
-          raise "pop: not yet implemented (#{arg})"
+          raise not_yet_impl("pop", arg)
         end
         @sp += 1
         @pc += 2

次に、値のコピーまわりを整理します。

前回はとにかく早く動かしたかったので bp, sp 間のコピー用に専用の命令を追加してしまいましたが、 コピー用の命令は今後もいろいろ使うので、 この段階で汎用化しておきます。


一般的な(?)アセンブリ言語では こういうとき mov(move)という命令を使うようです。 ただし、自分が見聞きした範囲でも

  • move(移動)じゃなくてコピーでは?
  • オペランドの順番(コピー元、先)が Intel 記法と AT&T 記法で違っていて混乱する

という意見がちらほらあるようで(どのくらいポピュラーかは分かりませんが……)、個人的にも微妙に感じたので、 それならいっそのこと cp (copy) にしてはどうか、となりました。

MOV命令の元になった英語は「move」で、移動という意味なのですが、 代入って移動とは似ているけどちょっと違いますよね。

(中略)

すなおにコピー命令とかだったら説明が簡単だったのになあ。 どうしてMOV命令ということになったのかは、筆者にも分かりません。

30日でできる! OS自作入門 p32)

オペランドの順番問題については、 これは UNIX の cp コマンドと同じインターフェイスにしてみます。 つまり、コピー元、コピー先、の順番です。 分かりやすいですね? 自分にとって分かりやすいのでこれでいいということにします。 よく知られた既存のインターフェイスに合わせることで覚える手間をなくそう、余分な負荷を削ろうという工夫のつもりでもあります。

どうせ自分向けに作っているオレオレのものなので、 こういうのは「自分が覚えやすいかどうか」を基準にして決めてしまえばよい、と割り切っています(jump_eq のような他の命令の名前なんかもそういう基準で適当に決めています)。


適当に決めたので適当に修正しましょう。

まず cp 命令を追加。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -143,6 +143,12 @@ class Vm
       when "copy_sp_to_bp"
         @bp = @sp
         @pc += 1
+      when "cp"
+        copy(
+          @mem.main[@pc + 1],
+          @mem.main[@pc + 2]
+        )
+        @pc += 3
       when "add_ab"
         add_ab()
         @pc += 1
@@ -200,8 +206,31 @@ class Vm
     end
   end
 
+  def copy(arg1, arg2)
+    src_val =
+      case arg1
+      when "sp"
+        @sp
+      when "bp"
+        @bp
+      else
+        raise not_yet_impl("copy src", arg1)
+      end
+
+    case arg2
+    when "bp"
+      @bp = src_val
+    when "sp"
+      @sp = src_val
+    else
+      raise not_yet_impl("copy dest", arg2)
+    end
+  end
+
   def self.num_args_for(operator)
     case operator
+    when "cp"
+      2
     when "set_reg_a", "set_reg_b", "label", "call", "push", "pop"
       1
     when "ret", "exit", "copy_bp_to_sp", "copy_sp_to_bp"

copy_bp_to_spcopy_sp_to_bp はもう使わないので消しておきます。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -137,12 +137,6 @@ class Vm
         n = @mem.main[@pc + 1]
         @reg_c = n
         @pc += 2
-      when "copy_bp_to_sp"
-        @sp = @bp
-        @pc += 1
-      when "copy_sp_to_bp"
-        @bp = @sp
-        @pc += 1
       when "cp"
         copy(
           @mem.main[@pc + 1],
@@ -233,7 +227,7 @@ class Vm
       2
     when "set_reg_a", "set_reg_b", "label", "call", "push", "pop"
       1
-    when "ret", "exit", "copy_bp_to_sp", "copy_sp_to_bp"
+    when "ret", "exit"
       0
     else
       raise "Invalid operator (#{operator})"

前回のアセンブリコードを修正。

--- a/11_bp_push_pop.vga.txt
+++ b/11_bp_push_pop.vga.txt
@@ -6,13 +6,13 @@
 label sub
   # 前処理
   push bp
-  copy_sp_to_bp
+  cp sp bp
 
   # サブルーチン本体の処理
   set_reg_a 2
 
   # 後片付け
-  copy_bp_to_sp
+  cp bp sp
   pop bp
 
   ret

修正後のアセンブリコードが

cp bp sp

となって、アセンブリのコードなんだかシェルスクリプトなんだか よく分からない感じになって楽しくなりましたね(?)。


そういえば特に何も言わずにやってますが、 VM の命令名とアセンブリニーモニックが一致しているので こういう場合もアセンブラは修正なしです。

つくづく影の薄いアセンブラ……。


あと気になっていたのが、命令ごとのオペランド(引数)の数に関する知識が vgvm.rb のコード中に散らばっていることです。

せっかく Vm.num_args_for がすでにあるので、これを使いまわしましょう。

(今のうちに対処しておかないとこの先辛いってわけではなく、 とりあえず DRY にしときましょうか、という程度のノリです。)

@@ -118,6 +118,9 @@ class Vm
     loop do
       # operator
       op = @mem.main[@pc]
+
+      pc_delta = 1 + Vm.num_args_for(op)
+
       case op
       when "exit"
         $stderr.puts "exit"
@@ -125,32 +128,32 @@ class Vm
       when "set_reg_a"
         n = @mem.main[@pc + 1]
         set_reg_a(n)
-        @pc += 2
+        @pc += pc_delta
       when "set_reg_b"
         n = @mem.main[@pc + 1]
         set_reg_b(n)
-        @pc += 2
+        @pc += pc_delta
       when "set_reg_c"
         n = @mem.main[@pc + 1]
         set_reg_c(n)

以下同様

修正した 11_bp_push_pop.vga.txt を実行してみます。

$ ./run.sh 11_bp_push_pop.vga.txt 
(略)
================================
reg_a(2) reg_b(3) reg_c(0) zf(0)
---- memory (main) ----
      00   ["set_reg_a", 1]
      02   ["call", 9]
      04   ["set_reg_b", 3]
pc => 06   ["exit"]
      07 ["label", "sub"]
      09   ["push", "bp"]
      11   ["cp", "sp", "bp"]
      14   ["set_reg_a", 2]
      16   ["cp", "bp", "sp"]
      19   ["pop", "bp"]
      21   ["ret"]
---- memory (stack) ----
         0 0
         1 3
         2 4
sp bp => 3 0

exit
$

問題ないですね! 次回は引数渡しに戻ります!!!!



vm2gol v2 製作メモ(11) 引数渡しの準備 / bp, push, pop



さて、ダンプ表示が改善されたところで次に進みます。

call, ret で(多段の)サブルーチン呼び出しができたところまでやったので…… 次はサブルーチンに引数で値を渡せるようにします (ここらへんから「C言語のあれをやるには何が必要なんだっけ?」 みたいな感じで進んでいきます)。

(先に言っておくと、今回含め準備があるので実際に引数を渡せるようになるのは次の次の回です)

どうやるかというと、引数はスタックに置いて渡します。 スタックに引数を置いて、呼びだされたサブルーチン側ではそれを見ます。

ここらへんからだんだん難しくなって……というか知らない部分だったので調べました。


第六話:EBPとESP、スタック領域の使われ方|トリコロールな猫|note
https://note.mu/nekotricolor/n/n2a247c808275

x86アセンブリ言語での関数コール
https://vanya.jp.net/os/x86call/


詳しくはリンク先に書いてますので説明は略(ひどい!)!! こんな感じでやっていきましょう!

この、ベースポインタとスタックポインタを使って あれこれやっていく仕組みを知らなかったので、 今回知って、「うおーこんな風になってたのかー」 「スタックと spbp だけでこういうことができるのかー」 と結構感動しました。 今まで(高級言語で)数えきれないほど関数呼び出しやってきたわけですが、 ブラックボックスだった部分が明らかになってうおーってなりました。

呼び出し規約は CDECL っぽくします (適当なので細かい部分は違ってるかもしれません)。


今回動かしたいアセンブリコードはこう。 ★ の箇所が今回の新しい要素です。

# 11_bp_push_pop.vga.txt

  set_reg_a 1
  call sub
  set_reg_b 3
  exit

label sub
  # 前処理
  push bp       # ★
  copy_sp_to_bp # ★

  # サブルーチン本体の処理
  set_reg_a 2

  # 後片付け
  copy_bp_to_sp # ★
  pop bp        # ★

  ret

あれこれ一度にやると大変なので一歩ずつやります。

やることは

  • (1) レジスタを追加: ベースポインタ
  • (2) 命令を追加: copy_sp_to_bp, copy_bp_to_sp
  • (3) 命令を追加: push, pop

ですね。

(1) レジスタを追加: ベースポインタ

まずは bp を追加。

一緒にダンプ表示を修正して bp も表示されるようにします。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -98,6 +98,8 @@ class Vm
     @mem = mem
     # スタックポインタ
     @sp = 3
+    # ベースポインタ
+    @bp = 3
   end
 
   def load_program(path)
--- a/vgvm.rb
+++ b/vgvm.rb
@@ -64,7 +64,7 @@ class Memory
     }.join("\n")
   end
 
-  def dump_stack(sp)
+  def dump_stack(sp, bp)
     lines = []
     @stack.each_with_index do |x, i|
       addr = i
@@ -72,7 +72,13 @@ class Memory
       head =
         case addr
         when sp
-          "sp    => "
+          if sp == bp
+            "sp bp => "
+          else
+            "sp    => "
+          end
+        when bp
+          "   bp => "
         else
           "         "
         end
@@ -190,7 +196,7 @@ class Vm
 ---- memory (main) ----
 #{ @mem.dump_main(@pc) }
 ---- memory (stack) ----
-#{ @mem.dump_stack(@sp) }
+#{ @mem.dump_stack(@sp, @bp) }
     EOB
   end
 

この修正により、こんな感じで bp の位置が表示されるようになります。

---- memory (stack) ----
         0 0
sp    => 1 2
   bp => 2 3
         3 0

(2) 命令を追加: copy_sp_to_bp, copy_bp_to_sp

pushpop の前に簡単な方からやっつけます。

copy_sp_to_bp, copy_bp_to_sp は、名前の通り、 sp の値を bp にコピーするのと、その逆です。 ここは後で整理するのですが、とりあえず素早く曳光弾を通して動かしたい(せっかちなので……)ので 軽率に専用の命令を追加しました。

これらは何も難しいことはないですね。 命令を追加するときは Vm.num_args_for も忘れずに修正します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -135,6 +135,12 @@ class Vm
         n = @mem.main[@pc + 1]
         @reg_c = n
         @pc += 2
+      when "copy_bp_to_sp"
+        @sp = @bp
+        @pc += 1
+      when "copy_sp_to_bp"
+        @bp = @sp
+        @pc += 1
       when "add_ab"
         add_ab()
         @pc += 1
@@ -174,7 +180,7 @@ class Vm
     case operator
     when "set_reg_a", "label", "call"
       1
-    when "ret", "exit"
+    when "ret", "exit", "copy_bp_to_sp", "copy_sp_to_bp"
       0
     else
       raise "Invalid operator (#{operator})"

(3) 命令を追加: push, pop

次は push

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -167,6 +167,18 @@ class Vm
         ret_addr = @mem.stack[@sp] # 戻り先アドレスを取得
         @pc = ret_addr # 戻る
         @sp += 1 # スタックポインタを戻す
+      when "push"
+        arg = @mem.main[@pc + 1]
+        val_to_push =
+          case arg
+          when "bp"
+            @bp
+          else
+            raise "push: not yet implemented (#{arg})"
+          end
+        @sp -= 1
+        @mem.stack[@sp] = val_to_push
+        @pc += 2
       else
         raise "Unknown operator (#{op})"
       end
@@ -178,7 +190,7 @@ class Vm
 
   def self.num_args_for(operator)
     case operator
-    when "set_reg_a", "label", "call"
+    when "set_reg_a", "label", "call", "push"
       1
     when "ret", "exit", "copy_bp_to_sp", "copy_sp_to_bp"
       0

いきなり pop まで作って動かすのが不安なので、 ここまでの分だけで動作確認してみます。

# 11_test_pop.vga.txt

  push bp
  copy_sp_to_bp
  push bp
  copy_bp_to_sp
  exit

良さそうなので pop に進みます。


--- a/vgvm.rb
+++ b/vgvm.rb
@@ -179,6 +179,16 @@ class Vm
         @sp -= 1
         @mem.stack[@sp] = val_to_push
         @pc += 2
+      when "pop"
+        arg = @mem.main[@pc + 1]
+        case arg
+        when "bp"
+          @bp = @mem.stack[@sp]
+        else
+          raise "pop: not yet implemented (#{arg})"
+        end
+        @sp += 1
+        @pc += 2
       else
         raise "Unknown operator (#{op})"
       end
@@ -190,7 +200,7 @@ class Vm
 
   def self.num_args_for(operator)
     case operator
-    when "set_reg_a", "label", "call", "push"
+    when "set_reg_a", "label", "call", "push", "pop"
       1
     when "ret", "exit", "copy_bp_to_sp", "copy_sp_to_bp"
       0

はい、これで冒頭のコード( 11_bp_push_pop.vga.txt )を動かしてみると……

$ ./run.sh 11_bp_push_pop.vga.txt 
vgvm.rb:208:in `num_args_for': Invalid operator (set_reg_b) (RuntimeError)
        from vgvm.rb:25:in `dump_main'
        from vgvm.rb:225:in `dump_v2'
        from vgvm.rb:116:in `start'
        from vgvm.rb:274:in `<main>'

あ、、、num_args_forset_reg_b を追加しそこねてましたね。 追加します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -200,7 +200,7 @@ class Vm
 
   def self.num_args_for(operator)
     case operator
-    when "set_reg_a", "label", "call", "push", "pop"
+    when "set_reg_a", "set_reg_b", "label", "call", "push", "pop"
       1
     when "ret", "exit", "copy_bp_to_sp", "copy_sp_to_bp"
       0

これで動くようになりました。 以下は終了時の状態です。

================================
reg_a(2) reg_b(3) reg_c(0) zf(0)
---- memory (main) ----
      00   ["set_reg_a", 1]
      02   ["call", 9]
      04   ["set_reg_b", 3]
pc => 06   ["exit"]
      07 ["label", "sub"]
      09   ["push", "bp"]
      11   ["copy_sp_to_bp"]
      12   ["set_reg_a", 2]
      14   ["copy_bp_to_sp"]
      15   ["pop", "bp"]
      17   ["ret"]
---- memory (stack) ----
         0 0
         1 3
         2 4
sp bp => 3 0

exit

メモ

ふつうのコンパイラをつくろう』 p324

なお、他のアーキテクチャではベースポインタと同様の役割を フレームポインタ(frame pointer)と呼ぶほうが一般的です。 そのため、 gcc のマニュアルやオプションにもフレームポインタという名前がよく登場します。



vm2gol v2 製作メモ(10) ステップ実行 / ダンプ表示の改善



さて、多段のサブルーチン呼び出しなんかやったりして、だんだん育ってきました。 それに連れてちょっとずつプログラムも長くなってきて、実行時の動きに不満が出てきましたので、 今回は本題の方をいったん脇に置いておいてそこを修正します。

何が不満かというと、1ステップごとに1秒スリープするというところです。 スリープ時間が長いとトライ&エラーを繰り返すときにかったるいし、 短くすると、結局最後まで終わってからゆっくり眺めることになって、 それだったらスリープなしでいいじゃんみたいになります。

そこで思いついたのがステップ実行方式への変更です。 何の気無しに思いついてやってみたところ思った以上に良いもので、 やってる本人としてはなかなかのブレイクスルー感がありました。


ステップ実行にすることによって

  • 次にこれを実行します、と表示して止まる
  • その表示を見て「それなら次はこうなるはずだ」と予測する。 たとえば 「reg_a の値が◯◯になるはずだ」「sp の値が 1 減って戻り先アドレスがスタックに積まれるはずだ」 のように。
  • 実際に1ステップ実行させる
  • 予想どおりだったら「よしよし」といって次へ(くりかえす)
  • そうでなければバグなので、デバッグする。 もしくは自分の予測が間違っているので、そっちを改める。

こういうサイクルが回るようになり、それによって脳内に動作モデルみたいなのが出来上がっていく感触がありました。

書籍などで解説を読むにしても、説明の文章や掲載されているコードをただ読んで頭の中だけで動作を想像するより、 実際に上記のように動かしてサイクルを回した方が圧倒的に理解しやすかったです。 たとえば x86アセンブリ言語での関数コール(vanya.jp.net) ではウェブページ上でステップ実行して試せるようになっているのですが、これがとても分かりやすくて参考になりました。これは紙の書籍ではできないことですね。

この後ベースポインタや引数やローカル変数が加わってスタックの扱いがさらに複雑になっていくのですが、 これがないと辛かっただろうな……と思います。


長々と書きましたが変更は数行です。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -28,6 +28,7 @@ class Vm
 
   def start
     dump() # 初期状態
+    $stdin.gets
 
     loop do
       # operator
@@ -78,14 +79,13 @@ class Vm
         raise "Unknown operator (#{op})"
       end
 
-      # 1命令実行するごとにダンプしてちょっと待つ
       dump()
-      sleep 1
+      $stdin.gets
     end
   end
 
   def dump
-    puts "%- 10s | pc(%2d) | reg_a(%d) b(%d) c(%d) | zf(%d) | sp(%d,%d)" % [
+    print "%- 10s | pc(%2d) | reg_a(%d) b(%d) c(%d) | zf(%d) | sp(%d,%d)" % [
       @mem[@pc],
       @pc,
       @reg_a, @reg_b, @reg_c,

今回これで 1回使ってしまうのも何なので、ダンプ表示部分を全面的に書きなおして 一気に最終形に近いところまで修正してしまいます。 (実際には完成までの間にその都度細かく修正していましたが、ここでまとめて盛り込んでしまいます)


まずは前準備としてメモリを別クラスに分離します。 実際のコンピュータでも CPU とメモリは物理的に別ユニットだから、 という素朴な発想からですが、まあこれでやってみます。

まずは Memory クラスを新たに作り、 インスタンスVm のコンストラクタで渡すようにしました。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -2,8 +2,11 @@
 require 'pp'
 require 'yaml'
 
+class Memory
+end
+
 class Vm
-  def initialize
+  def initialize(mem)
     # program counter
     @pc = 0
 
@@ -134,7 +137,8 @@ end
 
 exe_file = ARGV[0]
 
-vm = Vm.new
+mem = Memory.new
+vm = Vm.new(mem)
 vm.load_program(exe_file)
 
 vm.start

それから、 Vm#mem だったところを Memory#main に置き換えていきます。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -3,6 +3,11 @@ require 'pp'
 require 'yaml'
 
 class Memory
+  attr_accessor :main
+
+  def initialize
+    @main = []
+  end
 end
 
 class Vm
@@ -18,7 +23,7 @@ class Vm
     # flag
     @zf = 0
 
-    @mem = []
+    @mem = mem
     # スタック領域
     @stack = Array.new(4, 0)
     # スタックポインタ
@@ -26,7 +31,7 @@ class Vm
   end
 
   def load_program(path)
-    @mem = YAML.load_file(path)
+    @mem.main = YAML.load_file(path)
   end
 
   def start
@@ -35,21 +40,21 @@ class Vm
 
     loop do
       # operator
-      op = @mem[@pc]
+      op = @mem.main[@pc]
       case op

(以下、同様に @mem を @mem.main に置き換え)

同じようにスタックも Memory クラスに移動させます。 スタックのサイズは Memory.new のときに渡すようにしました。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -3,10 +3,13 @@ require 'pp'
 require 'yaml'
 
 class Memory
-  attr_accessor :main
+  attr_accessor :main, :stack
 
-  def initialize
+  def initialize(stack_size)
     @main = []
+
+    # スタック領域
+    @stack = Array.new(stack_size, 0)
   end
 end
 
@@ -24,8 +27,6 @@ class Vm
     @zf = 0
 
     @mem = mem
-    # スタック領域
-    @stack = Array.new(4, 0)
     # スタックポインタ
     @sp = 3
   end
@@ -76,11 +77,11 @@ class Vm
         jump_eq(addr)
       when "call"
         @sp -= 1 # スタックポインタを1減らす
-        @stack[@sp] = @pc + 2 # 戻り先を記憶
+        @mem.stack[@sp] = @pc + 2 # 戻り先を記憶
         next_addr = @mem.main[@pc + 1] # ジャンプ先
         @pc = next_addr
       when "ret"
-        ret_addr = @stack[@sp] # 戻り先アドレスを取得
+        ret_addr = @mem.stack[@sp] # 戻り先アドレスを取得
         @pc = ret_addr # 戻る
         @sp += 1 # スタックポインタを戻す
       else
@@ -99,7 +100,7 @@ class Vm
       @pc,
       @reg_a, @reg_b, @reg_c,
       @zf,
-      @sp, @stack[@sp]
+      @sp, @mem.stack[@sp]
     ]
   end
 
@@ -142,7 +143,7 @@ end
 
 exe_file = ARGV[0]
 
-mem = Memory.new
+mem = Memory.new(4)
 vm = Vm.new(mem)
 vm.load_program(exe_file)

ダンプ表示を改善します。

以下にコードをいろいろ貼ってますが、 修正後の動いている様子を先に見てもらった方が話が早いと思うので貼っておきます。

f:id:sonota88:20190521050134g:plain

要するにこんな見た目+動きにしたい。


Vm クラスに num_args_for() というメソッドを追加。 _for はなくても良かったかな。

VM 命令に対応する引数の数を教えてくれるというもの。 新しいダンプ表示で使います。

# class Vm

  def self.num_args_for(operator)
    case operator
    when "set_reg_a", "label", "call"
      1
    when "ret", "exit"
      0
    else
      raise "Invalid operator (#{operator})"
    end
  end

dump_main()dump_stack()Memory クラスに追加。 ここでさっきの Vm.num_args_for() を呼び出しています。

# class Memory

  def dump_main(pc)
    vmcmds = []
    addr = 0
    while addr < @main.size
      operator = @main[addr]
      num_args = Vm.num_args_for(operator)
      vmcmds << {
        addr: addr,
        xs: @main[addr .. addr + num_args]
      }
      addr += 1 + num_args
    end

    vmcmds.map{ |vmcmd|
      head =
        if vmcmd[:addr] == pc
          "pc =>"
        else
          "     "
        end

      operator = vmcmd[:xs][0]

      "%s %02d %s" % [
        head,
        vmcmd[:addr],
        vmcmd[:xs].inspect
      ]
    }.join("\n")
  end

  def dump_stack(sp)
    lines = []
    @stack.each_with_index do |x, i|
      addr = i
      next if addr < sp - 8
      head =
        case addr
        when sp
          "sp    => "
        else
          "         "
        end
      lines << head + "#{addr} #{x.inspect}"
    end
    lines.join("\n")
  end

Vm#dump_regVm#dump_v2 を追加して、 Vm#dump の呼び出し箇所を書き換え。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -94,7 +94,7 @@ class Vm
   end
 
   def start
-    dump() # 初期状態
+    dump_v2() # 初期状態
     $stdin.gets
 
     loop do
@@ -147,7 +147,7 @@ class Vm
       end
 
       # 1命令実行するごとにダンプしてちょっと待つ
-      dump()
+      dump_v2()
       $stdin.gets
     end
   end
@@ -173,6 +173,25 @@ class Vm
     ]
   end
 
+  def dump_reg
+    [
+      "reg_a(#{ @reg_a.inspect })",
+      "reg_b(#{ @reg_b.inspect })",
+      "reg_c(#{ @reg_c.inspect })"
+    ].join(" ")
+  end
+
+  def dump_v2
+    puts <<-EOB
+================================
+#{ dump_reg() } zf(#{ @zf })
+---- memory (main) ----
+#{ @mem.dump_main(@pc) }
+---- memory (stack) ----
+#{ @mem.dump_stack(@sp) }
+    EOB
+  end
+
   def set_mem(addr, n)
     @mem.main[addr] = n
   end

古い方の Vm#dump はもう使わないので消しておきます。


この段階ではこんな見た目。

================================
reg_a(2) reg_b(0) reg_c(0) zf(0)
---- memory (main) ----
      00 ["set_reg_a", 1]
      02 ["call", 9]
      04 ["set_reg_a", 5]
      06 ["exit"]
      07 ["label", "sub1"]
      09 ["set_reg_a", 2]
      11 ["call", 18]
      13 ["set_reg_a", 4]
      15 ["ret"]
      16 ["label", "sub2"]
pc => 18 ["set_reg_a", 3]
      20 ["ret"]
---- memory (stack) ----
         0 0
sp    => 1 13
         2 4
         3 0

ふむ……ラベルは、アセンブリのソースと同じようにインデントされているといいのでは? と考えてちょっと修正。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -35,9 +35,17 @@ class Memory
 
       operator = vmcmd[:xs][0]
 
-      "%s %02d %s" % [
+      indent =
+        if operator == "label"
+          ""
+        else
+          "  "
+        end
+
+      "%s %02d %s%s" % [
         head,
         vmcmd[:addr],
+        indent,
         vmcmd[:xs].inspect
       ]
     }.join("\n")
================================
reg_a(4) reg_b(0) reg_c(0) zf(0)
---- memory (main) ----
      00   ["set_reg_a", 1]
      02   ["call", 9]
pc => 04   ["set_reg_a", 5]
      06   ["exit"]
      07 ["label", "sub1"]
      09   ["set_reg_a", 2]
      11   ["call", 18]
      13   ["set_reg_a", 4]
      15   ["ret"]
      16 ["label", "sub2"]
      18   ["set_reg_a", 3]
      20   ["ret"]

お、やっぱりちょっといいですね。


さらに、ジャンプ系の命令(jump, jump_eq, call, ret)と exit は他と区別ができると見やすいかなと思って色を付けました。

インデントと同じくこれも見た目の問題ですが、 色が付くと楽しくなってモチベーションが上がるのと、 見やすくなってデバッグ効率的にもよいはず。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -2,6 +2,11 @@
 require 'pp'
 require 'yaml'
 
+module TermColor
+  RESET  = "\e[m"
+  RED    = "\e[0;31m"
+end
+
 class Memory
   attr_accessor :main, :stack
 
@@ -35,6 +40,14 @@ class Memory
 
       operator = vmcmd[:xs][0]
 
+      color =
+        case operator
+        when "exit", "call", "ret", "jump", "jump_eq"
+          TermColor::RED
+        else
+          ""
+        end
+
       indent =
         if operator == "label"
           ""
@@ -42,7 +55,7 @@ class Memory
           "  "
         end
 
-      "%s %02d %s%s" % [
+      "%s %02d #{color}%s%s#{TermColor::RESET}" % [
         head,
         vmcmd[:addr],
         indent,

というわけで、このようになりました(上に貼ったのと同じ画像です)。 ステップごとに Enter キーを押してステップ実行しています。

f:id:sonota88:20190521050134g:plain

そこはかとなくデバッガっぽい雰囲気のするダンプ表示になりましたね。

こんなふうに可視化(?)すると、 なんか機械がガチャガチャ動いてる感じがして楽しい!!!!

コードがそこそこ増えましたが、 それを上回る改善効果が得られたような気がします。 たった数十行でこれができるなら安い!!

追記 2019-12-25

case 式を使って Vm.num_args_for() を書いていますが、 1行に複数の命令を並べているため、この後で命令の変更が発生したときの diff が見にくくなってしまいました。 今思えば、1命令が1行になるように、次のようにハッシュで書いておけばよかったですね…… (※ 実装としてどちらが良いかという話ではなく、diff と記事を見やすくするための都合です) 。

class Vm
  NUM_ARGS_MAP = {
    "ret"  => 0,
    "exit" => 0,
    "set_reg_a" => 1,
    "label"     => 1,
    "call"      => 1
  }
end


vm2gol v2 製作メモ(9) 2段以上のサブルーチン呼び出し / スタック領域とスタックポインタ



前回簡単なサブルーチンをやりましたが、 今回はサブルーチンからまた別のサブルーチンを呼ぶ、 というのをやってみたいです。 これも普通にできてほしい。

今の状態ではこれはできません。 なぜかというと、 ret で戻るときの戻り先アドレスを reg_c に入れているからです。 このまま2段目のサブルーチン呼び出しをやってしまうと、 call のときに reg_c が上書きされてしまい、2段目からは戻れても 1段目からは正しく戻れなくなってしまいます。

そこで、スタック領域というものを用意します。

メインメモリの末尾の方をスタック領域として使うのが一般的っぽいのですが、 ここではメインメモリとは別にスタック領域を用意します。 なぜならその方が簡単そうだから(どっちでもあんまり変わらない?)。 我々(?)は初心者なので難しそうなことをやってはいけません。

まあ、いったんこれで作っておいて後から変えることもできなくはなさそうです。


はい。ではやります。

まずスタック領域を用意します。 メインメモリと同じくただの配列です。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -16,6 +16,8 @@ class Vm
     @zf = 0
 
     @mem = []
+    # スタック領域
+    @stack = Array.new(4, 0)
   end
 
   def load_program(path)

スタック領域のサイズはとりあえず 4 としました。かわいいスタックです。

それから、スタック領域の先頭がどこなのかを記憶しておく必要があるので、 その位置を保存しておくレジスタ、スタックポインタを追加します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -18,6 +18,8 @@ class Vm
     @mem = []
     # スタック領域
     @stack = Array.new(4, 0)
+    # スタックポインタ
+    @sp = 3
   end
 
   def load_program(path)

スタック領域のサイズを 4 としたので、 スタックポインタの初期値は、スタックの「底」である 3 を指すようにしておきます。 次の図のようなイメージです。

f:id:sonota88:20190519121326p:plain
図 9-1 スタック領域の初期状態


それから、 callret を書き換えて reg_c ではなくスタック領域を使うようにします。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -66,11 +66,14 @@ class Vm
         addr = @mem[@pc + 1]
         jump_eq(addr)
       when "call"
-        @reg_c = @pc + 2 # 戻り先を記憶(call ではなく、その次の命令になるように)
+        @sp -= 1 # スタックポインタを1減らす
+        @stack[@sp] = @pc + 2 # 戻り先を記憶
         next_addr = @mem[@pc + 1] # ジャンプ先
         @pc = next_addr
       when "ret"
-        @pc = @reg_c
+        ret_addr = @stack[@sp] # 戻り先アドレスを取得
+        @pc = ret_addr # 戻る
+        @sp += 1 # スタックポインタを戻す
       else
         raise "Unknown operator (#{op})"
       end
  • sp から 1 を引いて戻り先アドレスをセット
  • 戻り先アドレスを取り出して sp に 1 を足す

が対になっているのがミソですね。


あわせてダンプ表示をいじります。 追加で @mem[@pc], @sp, @stack[@sp] も表示します。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -85,10 +85,12 @@ class Vm
   end
 
   def dump
-    puts "pc(%2d) | reg_a(%d) b(%d) c(%d) | zf(%d)" % [
+    puts "%- 10s | pc(%2d) | reg_a(%d) b(%d) c(%d) | zf(%d) | sp(%d,%d)" % [
+      @mem[@pc],
       @pc,
       @reg_a, @reg_b, @reg_c,
-      @zf
+      @zf,
+      @sp, @stack[@sp]
     ]
   end
 

機械語コードを書いて……

# 09_nested_call.vge.yaml

[
  "set_reg_a", 1,
  "call", 9, # sub1
  # 4 sub1 からここに戻って来るはず
  "set_reg_a", 5,
  "exit",

  # 7
  "label", "sub1",
  # 9
  "set_reg_a", 2,
  "call", 18, # sub2
  # 13 sub2 からここに戻って来るはず
  "set_reg_a", 4,
  "ret",

  # 16
  "label", "sub2",
  # 18
  "set_reg_a", 3,
  "ret"
]

動かしてみます。

$ ruby vgvm.rb 09_nested_call.vge.yaml
set_reg_a  | pc( 0) | reg_a(0) b(0) c(0) | zf(0) | sp(3,0)
call       | pc( 2) | reg_a(1) b(0) c(0) | zf(0) | sp(3,0)
set_reg_a  | pc( 9) | reg_a(1) b(0) c(0) | zf(0) | sp(2,4)  … call されて @pc が変更され、
                                                               スタック領域に sub1 からの戻り先アドレスがセットされた
call       | pc(11) | reg_a(2) b(0) c(0) | zf(0) | sp(2,4)
set_reg_a  | pc(18) | reg_a(2) b(0) c(0) | zf(0) | sp(1,13) … call されて @pc が変更され、
                                                               スタック領域に sub2 からの戻りアドレスがセットされた
ret        | pc(20) | reg_a(3) b(0) c(0) | zf(0) | sp(1,13)
set_reg_a  | pc(13) | reg_a(3) b(0) c(0) | zf(0) | sp(2,4)  … ret により sub2 から戻った
ret        | pc(15) | reg_a(4) b(0) c(0) | zf(0) | sp(2,4)
set_reg_a  | pc( 4) | reg_a(4) b(0) c(0) | zf(0) | sp(3,0)  … ret により sub1 から戻った
exit       | pc( 6) | reg_a(5) b(0) c(0) | zf(0) | sp(3,0)
exit

いいですねー。 reg_a の値も 1, 2, 3, 4, 5 と予想どおりに変わっています。

ではアセンブリコードからやってみましょう。 今回はジャンプまわりの変更がないのでアセンブラの修正は不要でそのまま動くはず。

# 09_nested_call.vga.txt

  set_reg_a 1
  call sub1   # sub1 を呼び出し
  set_reg_a 5 # sub1 からここに戻って来るはず
  exit

label sub1
  set_reg_a 2
  call sub2   # sub2 を呼び出し
  set_reg_a 4 # sub2 からここに戻って来るはず
  ret

label sub2
  set_reg_a 3
  ret
$ ./run.sh 09_nested_call.vga.txt
set_reg_a  | pc( 0) | reg_a(0) b(0) c(0) | zf(0) | sp(3,0)
call       | pc( 2) | reg_a(1) b(0) c(0) | zf(0) | sp(3,0)
set_reg_a  | pc( 9) | reg_a(1) b(0) c(0) | zf(0) | sp(2,4)
call       | pc(11) | reg_a(2) b(0) c(0) | zf(0) | sp(2,4)
set_reg_a  | pc(18) | reg_a(2) b(0) c(0) | zf(0) | sp(1,13)
ret        | pc(20) | reg_a(3) b(0) c(0) | zf(0) | sp(1,13)
set_reg_a  | pc(13) | reg_a(3) b(0) c(0) | zf(0) | sp(2,4)
ret        | pc(15) | reg_a(4) b(0) c(0) | zf(0) | sp(2,4)
set_reg_a  | pc( 4) | reg_a(4) b(0) c(0) | zf(0) | sp(3,0)
exit       | pc( 6) | reg_a(5) b(0) c(0) | zf(0) | sp(3,0)
exit

さっきと同じ結果になっています。OK!

いやー、スタック領域を導入するだけで入れ子の呼び出しができてしまいました。 意外と簡単ですね?



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

いいですね!

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

EDSAC

(2024-01-06 追記)

EDSAC ではジャンプ命令のオペランドを直接書き換えて戻り先アドレスを記憶させていたそうです。

当時、プログラムの命令とデータはまったく同じようにメモリに記録されていました。 そのため変数に値を代入するような気軽さで、プログラム自身を書き換えることができました。 関数を呼んだあと元の位置に戻ってくることを、ジャンプ命令のジャンプ先を書き換えて実現していました。

コーディングを支える技術――成り立ちから学ぶプログラミング作法』 p46



vm2gol v2 製作メモ(7) ラベルとアセンブラ



さて、機械語コードをいくつか書いてきて、 ジャンプ先のアドレスを調べて修正するのに慣れてきた一方で、だんだん面倒にもなってきました。 プログラムを修正するたびに調べるのも面倒だし、書きなおすのも面倒。 こんなの人間のやる仕事じゃねえ!

で、なんか自動化できそうなのでやってみます。

そのためにラベルという命令?   命令というか目印みたいなのを導入します。

ラベルを使って、プログラムをこんな感じで書けないでしょうか?

[
  "set_reg_a", 0,
  "set_reg_b", 0,
  "compare",
  "jump_eq", "then", # ラベルを指定してジャンプ
  "set_reg_c", 3,
  "jump", "endif",   # ラベルを指定してジャンプ
  "label", "then",   # ラベル
  "set_reg_c", 2,
  "label", "endif",  # ラベル
  "set_reg_a", 4,
  "exit"
]

これを変換して、 "then" とか "endif" の部分を数字(アドレス)にすれば、 これまで手で書いてきた機械語コードと同じ位置付けのものになる (VM に渡せるようになる)、という寸法です。

この「変換前のプログラム」のフォーマットも紆余曲折(JSON にしたり)があり、 vm2gol-v1 のときはこんな感じの YAML ファイルに落ち着きました。

- set_reg_a 0
- set_reg_b 0
- compare
- jump_eq then
- set_reg_c 3
- jump endif
- label then
- set_reg_c 2
- label endif
- set_reg_a 4
- exit

ですが、ラベルがインデントされているとやはり見やすいということで、 ちょっと工夫して、たとえばこんなフォーマットを考えてみます。

-   set_reg_a 0
-   set_reg_b 0
-   compare
-   jump_eq then
-   set_reg_c 3
-   jump endif
- label then
-   set_reg_c 2
- label endif
-   set_reg_a 4
-   exit

これを YAML としてパースすると、先頭の余分な空白は無視されるので、 内容的には上のインデントなしと同じになります。

あ、でもこれだと、 このファイルをプログラムで自動生成するときに困りそうかな……。

というわけで、 vm2gol-v2 では YAML はやめてオレオレフォーマットにします。

  set_reg_a 0
  set_reg_b 0 # ここを書き換えて動作確認する
  compare
  jump_eq then
  set_reg_c 3
  jump endif

label then
  set_reg_c 2

label endif
  set_reg_a 4
  exit

これを機械語コードに変換するプログラムを作りました。これです。

# vgasm.rb

# coding: utf-8
require 'pp'
require 'yaml'

def parse(src)
  alines = []
  src.each_line do |line|
    words = line.sub(/#.*/, "").strip.split(/ +/)
    unless words.empty?
      alines << words
    end
  end
  alines
end

def create_label_addr_map(alines)
  map = {}

  addr = 0
  alines.each do |aline|
    head, *rest = aline

    case head
    when "label"
      name = rest[0]
      map[name] = addr
      addr += 2
    else
      addr += 1
      addr += rest.size
    end
  end

  map
end

src = File.read(ARGV[0])
alines = parse(src)

# key: ラベル名、 value: アドレス のマッピングを作る
label_addr_map = create_label_addr_map(alines)
# pp label_addr_map

words = []
alines.each do |aline|
  head, *rest = aline

  words << head

  case head
  when "label"
    words << rest[0]
  when "jump", "jump_eq"
    label_name = rest[0]
    words << label_addr_map[label_name]
  else
    words += rest.map{ |arg|
        (/^\-?\d+$/ =~ arg) ? arg.to_i : arg
      }
  end
end

puts YAML.dump(words)

2パス(2段階)の処理に分かれていて、 まず最初のパスでラベル名とアドレスのマッピングを作り、 2パス目で jumpjump_eq で指定されているラベルをアドレスに置き換えます。

あれっなんかアセンブラができてしまいましたね(わざとらしく)。 そしてこの「変換前のプログラム」はアセンブリのコードですね(わざとらしく)?

拡張子は .vga.txt にします。アセンブリ(Assembly)の a です。

さっきの「変換前のコード」を 07_if.vga.txt に保存して、 実行します!

$ cat 07_if.vga.txt 
  set_reg_a 0
  set_reg_b 0 # ここを書き換えて動作確認する
  compare
  jump_eq then
  set_reg_c 3
  jump endif

label then
  set_reg_c 2

label endif
  set_reg_a 4
  exit

$ ruby vgasm.rb 07_if.vga.txt > 07_if.vge.yaml

$ cat 07_if.vge.yaml
---
- set_reg_a
- 0
- set_reg_b
- 0
- compare
- jump_eq
- 11
- set_reg_c
- 3
- jump
- 15
- label
- then
- set_reg_c
- 2
- label
- endif
- set_reg_a
- 4
- exit

jump, jump_eq の引数がそれぞれ 11, 15 となっていて、 うまく変換できてますね。

これを VM に与えると…

$ ruby vgvm.rb 07_if.vge.yaml
pc( 0) | reg_a(0) b(0) c(0) | zf(0)
pc( 2) | reg_a(0) b(0) c(0) | zf(0)
pc( 4) | reg_a(0) b(0) c(0) | zf(0)
pc( 5) | reg_a(0) b(0) c(0) | zf(1)
pc(11) | reg_a(0) b(0) c(0) | zf(1)
vgvm.rb:63:in `block in start': Unknown operator (label) (RuntimeError)
        from vm.rb:28:in `loop'
        from vm.rb:28:in `start'
        from vm.rb:135:in `<main>'

おっと VM の修正を忘れていました。 label の場合は何もする必要がなくて、単に次に進むだけとします。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -53,6 +53,8 @@ class Vm
       when "compare"
         compare()
         @pc += 1
+      when "label"
+        @pc += 2
       when "jump"
         addr = @mem[@pc + 1]
         @pc = addr

では再度実行。

  $ ruby vgvm.rb 07_if.vge.yaml
  pc( 0) | reg_a(0) b(0) c(0) | zf(0)
  pc( 2) | reg_a(0) b(0) c(0) | zf(0)
  pc( 4) | reg_a(0) b(0) c(0) | zf(0)
  pc( 5) | reg_a(0) b(0) c(0) | zf(1) … compare を実行した
  pc(11) | reg_a(0) b(0) c(0) | zf(1) … ラベル "then" の位置にジャンプした
  pc(13) | reg_a(0) b(0) c(0) | zf(1) … label なので何もせず進んだ
  pc(15) | reg_a(0) b(0) c(2) | zf(1) … then句を実行した
  pc(17) | reg_a(0) b(0) c(2) | zf(1) … endif にジャンプした
  pc(19) | reg_a(4) b(0) c(2) | zf(1)
  exit

良さそうですね。

……いや、ちょっと待ってください。 正しい動きではあるんですが、 ラベルそのものがあるアドレスにジャンプしても label の時は何もせず進むだけなので、無駄な感じがしますね。

なので、ジャンプするときはラベルの次の位置にジャンプさせると良いのでは?

やってみましょうか。

@@ -41,7 +41,7 @@ alines.each{ |line|
     words << rest[0]
   when "jump", "jump_eq"
     label_name = rest[0]
-    words << label_addr_map[label_name]
+    words << label_addr_map[label_name] + 2
   else
     words.concat(
       rest.map{ |arg|
$ ruby vgasm.rb 07_if.vga.txt > 07_if.vge.yaml

$ cat 07_if.vge.yaml 
---
- set_reg_a
- 0
- set_reg_b
- 0
- compare
- jump_eq
- 13
- set_reg_c
- 3
- jump
- 17
- label
- then
- set_reg_c
- 2
- label
- endif
- set_reg_a
- 4
- exit

$ ruby vgvm.rb 07_if.vge.yaml
pc( 0) | reg_a(0) b(0) c(0) | zf(0)
pc( 2) | reg_a(0) b(0) c(0) | zf(0)
pc( 4) | reg_a(0) b(0) c(0) | zf(0)
pc( 5) | reg_a(0) b(0) c(0) | zf(1)
pc(13) | reg_a(0) b(0) c(0) | zf(1) … ラベルの次の位置にジャンプした
pc(15) | reg_a(0) b(0) c(2) | zf(1)
pc(17) | reg_a(0) b(0) c(2) | zf(1)
pc(19) | reg_a(4) b(0) c(2) | zf(1)
exit

特に問題なさそう。

(こうすると VM に追加した label は用済みな気が…… まあ残したままにしておきましょうか)


なんかあっさりとアセンブラができてしまいました。

といっても命令も値も機械語と同じで、 仕事らしい仕事といえばラベルの変換くらいなので、 アセンブラと言ってしまってよいものかという感じはしますが。

※ ちなみに、この後もアセンブラ部分はほとんど変わりません。これでほぼ完成形。


あ、そうだ、今回のでアセンブルVM での実行の 2段階に分かれて、 実行するのがちょっと面倒になってきたので、 いっぺんに実行させるための簡単な Bash スクリプトを用意しておきます。

#!/bin/bash

set -o errexit

file="$1"
bname=$(basename $file .vga.txt)
exefile=tmp/${bname}.vge.yaml

ruby vgasm.rb $file > $exefile
ruby vgvm.rb $exefile

アセンブラによって生成された .vge.yaml ファイルは tmp/ というディレクトリに入るようにしました。

vm2gol-v1 のときは生成されたファイルも全部 git のリポジトリに入れていましたが、 今回は tmp/ ディレクトリごと無視します。

--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+tmp/

run.sh という名前で保存して chmod で実行権限を付けて、 こいつにアセンブリのファイルを渡すと、 VM での実行までやってくれます。

$ chmod u+x run.sh

$ ./run.sh 07_if.vga.txt 
pc( 0) | reg_a(0) b(0) c(0) | zf(0)
pc( 2) | reg_a(0) b(0) c(0) | zf(0)
pc( 4) | reg_a(0) b(0) c(0) | zf(0)
(略)


vm2gol v2 製作メモ(6) if文っぽい条件分岐



前回 comparejump_eq を使っていきなりカウンタを作ってしまいましたが、 この2つがあれば、

a = 0
b = 0
if a == b
  c = 2
else
  c = 3
end
a = 4

みたいな普通の if 文っぽいことができそうです。ちょっとやってみましょう。

BASIC風に書くとこう。

10 set_reg_a 0
20 set_reg_b 0
30 compare
40 jump_eq 70

# else
50 set_reg_c 3
60 jump 80

# then
70 set_reg_c 2

# endif
80 set_reg_a 4

a と b が等しい場合は c に 2 をセットし、 そうでない場合は c に 3 をセットし、 どちらの場合でもその後に a に 4 をセットする。

else 節の後に then 節、という順番が慣れない感じですが、 なんかできそうですね。機械語にしてみます。

# 06_if.vge.yaml

[
  # 0
  "set_reg_a", 0,
  # 2
  "set_reg_b", 0, # ここを書き換えて動作確認する
  # 4
  "compare",
  # 5
  "jump_eq", 11,
  # 7
  "set_reg_c", 3,
  # 9
  "jump", 13,
  # 11
  "set_reg_c", 2,
  # 13
  "set_reg_a", 4,
  # 15
  "exit"
]

まずはこれをそのまま実行します。

$ ruby vgvm.rb 06_if.vge.yaml
pc( 0) | reg_a(0) b(0) c(0) | zf(0)
pc( 2) | reg_a(0) b(0) c(0) | zf(0)
pc( 4) | reg_a(0) b(0) c(0) | zf(0)
pc( 5) | reg_a(0) b(0) c(0) | zf(1) … a == b なので zf == 1 になった
pc(11) | reg_a(0) b(0) c(0) | zf(1) … then 節にジャンプした
pc(13) | reg_a(0) b(0) c(2) | zf(1) … then 節を実行した
pc(15) | reg_a(4) b(0) c(2) | zf(1) … 最後に reg_a に 4 をセットした
exit

いいですね。では、プログラムを書き換えて a == b とならない場合を試してみます。

"set_reg_b", 0,

"set_reg_b", 1,

に書き換えて実行。

$ ruby vgvm.rb 06_if.vge.yaml
pc( 0) | reg_a(0) b(0) c(0) | zf(0)
pc( 2) | reg_a(0) b(0) c(0) | zf(0)
pc( 4) | reg_a(0) b(1) c(0) | zf(0)
pc( 5) | reg_a(0) b(1) c(0) | zf(0) … a != b なので zf == 0 のまま
pc( 7) | reg_a(0) b(1) c(0) | zf(0) … ジャンプせずに次の命令へ進んだ
pc( 9) | reg_a(0) b(1) c(3) | zf(0) … else 節を実行した
pc(13) | reg_a(0) b(1) c(3) | zf(0) … then 節を飛び越えて最後にジャンプした
pc(15) | reg_a(4) b(1) c(3) | zf(0) … 最後に reg_a に 4 をセットした
exit

いいですね!



vm2gol v2 製作メモ(5) 条件分岐(compare, jump_eq)



チンタラやっててなかなか進みませんが一歩ずつやっていきます。

ループができるようになったら、次は条件分岐ですよ! プログラムといえば条件分岐!

条件分岐はどうやるかというと…… comparejump_eq という命令を導入します。 こいつを使って前回のカウンタを改造して、 値が 3 になったら 0 に戻ってまた 1 ずつ増えるというカウンタを作りましょう。

BASIC 風に書くとこうでしょうか:

10 set_reg_c 1  # これは最初に1回だけ実行される
20 set_reg_b 3  # これは最初に1回だけ実行される
30 set_reg_a 0
40 add_ac       # a + c の結果を a にセット
50 compare      # reg_a と reg_b を比較してフラグをセットする
60 jump_eq 30   # reg_a と reg_b が等しかったら、30番地に戻って reg_a の値を 0 に戻す
                # そうでなければ何もせず次に進む
70 jump 40      # reg_a と reg_b が等しくない場合

機械語コードに翻訳します。

# 05_compare_jump_eq.vge.yaml

[
  # 0
  "set_reg_c", 1,
  # 2
  "set_reg_b", 3,
  # 4
  "set_reg_a", 0,
  # 6
  "add_ac",
  "compare",
  "jump_eq", 4,
  "jump", 6
]

まず、レジスタreg_a, reg_b の2つだけだとめんどくさくなりそうな気がしたので、 reg_c を使うようにしてみました。

reg_a に足すべき値である 1 を reg_c にセットしておき、 add_acreg_c の値を reg_a に足します。

set_reg_cadd_ac はまだないので追加します。 set_reg_aadd_ab とほとんど同じです。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -35,6 +35,10 @@ class Vm
         n = @mem[@pc + 1]
         @reg_b = n
         @pc += 2
+      when "set_reg_c"
+        n = @mem[@pc + 1]
+        @reg_c = n
+        @pc += 2
       when "add_ab"
         add_ab()
         @pc += 1
--- a/vgvm.rb
+++ b/vgvm.rb
@@ -42,6 +42,9 @@ class Vm
       when "add_ab"
         add_ab()
         @pc += 1
+      when "add_ac"
+        add_ac()
+        @pc += 1
       when "jump"
         addr = @mem[@pc + 1]
         @pc = addr
@@ -74,6 +77,10 @@ class Vm
   def add_ab
     @reg_a = @reg_a + @reg_b
   end
+
+  def add_ac
+    @reg_a = @reg_a + @reg_c
+  end
 end
 
 exe_file = ARGV[0]

add_abadd_ac も 1行しかないのでメソッドにしなくてよかったかもしれませんね…… まあ、いちいち直していくのも煩雑なのでこのまま進めます)


準備ができたところで今回の目玉である comparejump_eq の登場です!

compare の動作はこう:

reg_a と reg_b の値を比較し、
  等しければ:     ゼロフラグに 1 をセット
  等しくなければ: ゼロフラグに 0 をセット

jump_eq の動作はこう:

ゼロフラグの値が
  0 だったら: 何もせず次の命令に進む(プログラムカウンタを素直に進める)
  1 だったら: 引数で指定されたアドレスにジャンプする

比較とジャンプの 2ステップに別れるんですね。ふむふむー。

で、この 2つのステップを仲介しているのがゼロフラグ。 ゼロフラグもレジスタの一種で、これも今回追加します。


まずゼロフラグ。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -12,6 +12,9 @@ class Vm
     @reg_b = 0
     @reg_c = 0
 
+    # flag
+    @zf = 0
+
     @mem = []
   end

comparejump_eq を追加。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -48,9 +48,15 @@ class Vm
       when "add_ac"
         add_ac()
         @pc += 1
+      when "compare"
+        compare()
+        @pc += 1
       when "jump"
         addr = @mem[@pc + 1]
         @pc = addr
+      when "jump_eq"
+        addr = @mem[@pc + 1]
+        jump_eq(addr)
       else
         raise "Unknown operator (#{op})"
       end
@@ -84,6 +90,18 @@ class Vm
   def add_ac
     @reg_a = @reg_a + @reg_c
   end
+
+  def compare
+    @zf = (@reg_a == @reg_b) ? 1 : 0
+  end
+
+  def jump_eq(addr)
+    if @zf == 1
+      @pc = addr
+    else
+      @pc += 2
+    end
+  end
 end
 
 exe_file = ARGV[0]

ステップ数が増えて、今のままだとダンプ出力が長くなってしまいます。 ここに長々としたのを貼るのもなんなので、 dump というメソッドを作ってダンプ出力を見やすくしました。

--- a/vgvm.rb
+++ b/vgvm.rb
@@ -23,6 +23,8 @@ class Vm
   end
 
   def start
+    dump() # 初期状態
+
     loop do
       # operator
       op = @mem[@pc]
@@ -62,11 +64,19 @@ class Vm
       end
 
       # 1命令実行するごとにダンプしてちょっと待つ
-      pp self
+      dump()
       sleep 1
     end
   end
 
+  def dump
+    puts "pc(%2d) | reg_a(%d) b(%d) c(%d) | zf(%d)" % [
+      @pc,
+      @reg_a, @reg_b, @reg_c,
+      @zf
+    ]
+  end
+
   def set_mem(addr, n)
     @mem[addr] = n
   end
@@ -108,6 +118,5 @@ exe_file = ARGV[0]
 
 vm = Vm.new
 vm.load_program(exe_file)
-pp vm # 初期状態
 
 vm.start

結果です! いい感じですね!

$ ruby vgvm.rb 05_compare_jump_eq.vge.yaml 
pc( 0) | reg_a(0) b(0) c(0) | zf(0)
pc( 2) | reg_a(0) b(0) c(1) | zf(0)
pc( 4) | reg_a(0) b(3) c(1) | zf(0)
pc( 6) | reg_a(0) b(3) c(1) | zf(0)
pc( 7) | reg_a(1) b(3) c(1) | zf(0)
pc( 8) | reg_a(1) b(3) c(1) | zf(0)
pc(10) | reg_a(1) b(3) c(1) | zf(0)
pc( 6) | reg_a(1) b(3) c(1) | zf(0)
pc( 7) | reg_a(2) b(3) c(1) | zf(0)
pc( 8) | reg_a(2) b(3) c(1) | zf(0)
pc(10) | reg_a(2) b(3) c(1) | zf(0)
pc( 6) | reg_a(2) b(3) c(1) | zf(0)
pc( 7) | reg_a(3) b(3) c(1) | zf(0)
pc( 8) | reg_a(3) b(3) c(1) | zf(1) … ゼロフラグが立った!
pc( 4) | reg_a(3) b(3) c(1) | zf(1) … 4番地にジャンプしている!
pc( 6) | reg_a(0) b(3) c(1) | zf(1) … reg_a が 0 にリセットされた!
pc( 7) | reg_a(1) b(3) c(1) | zf(1) … ↓ 以下くりかえしている!
pc( 8) | reg_a(1) b(3) c(1) | zf(0)
pc(10) | reg_a(1) b(3) c(1) | zf(0)
pc( 6) | reg_a(1) b(3) c(1) | zf(0)
pc( 7) | reg_a(2) b(3) c(1) | zf(0)
pc( 8) | reg_a(2) b(3) c(1) | zf(0)
pc(10) | reg_a(2) b(3) c(1) | zf(0)
pc( 6) | reg_a(2) b(3) c(1) | zf(0)
pc( 7) | reg_a(3) b(3) c(1) | zf(0)
pc( 8) | reg_a(3) b(3) c(1) | zf(1)
pc( 4) | reg_a(3) b(3) c(1) | zf(1)
pc( 6) | reg_a(0) b(3) c(1) | zf(1)
pc( 7) | reg_a(1) b(3) c(1) | zf(1)
(略)

同じ項目が縦に並んで、変化が見やすくなりました。