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 のマニュアルやオプションにもフレームポインタという名前がよく登場します。