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
  • 引数の数だけスタックポインタを戻す

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