- 目次ページに戻る / 前 / 次
- 前回からの差分をまとめて見る
前回は引数を 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
までたどり着いて終了するのですが、
途中で sp
と bp
が負の値になり、スタックの天井を突き抜けてしまいます
(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
- 引数の数だけスタックポインタを戻す
という点を押さえておけば同じ要領でできるでしょう。