- 目次ページに戻る / 前 / 次
- 前回からの差分をまとめて見る
どんどんプログラムっぽくしていきましょう。
プログラムといえばサブルーチン! サブルーチンやりましょう!
最初は簡単なのがいいので、 reg_a
に値をセットするだけの
サブルーチンをやってみます。
call
と ret
という命令を追加します。
# 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 sub
が call 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