- 目次ページに戻る / 前 / 次
- 前回からの差分をまとめて見る
さて、ダンプ表示が改善されたところで次に進みます。
call
, ret
で(多段の)サブルーチン呼び出しができたところまでやったので……
次はサブルーチンに引数で値を渡せるようにします
(ここらへんから「C言語のあれをやるには何が必要なんだっけ?」
みたいな感じで進んでいきます)。
(先に言っておくと、今回含め準備があるので実際に引数を渡せるようになるのは次の次の回です)
どうやるかというと、引数はスタックに置いて渡します。 スタックに引数を置いて、呼びだされたサブルーチン側ではそれを見ます。
ここらへんからだんだん難しくなって……というか知らない部分だったので調べました。
第六話:EBPとESP、スタック領域の使われ方|トリコロールな猫|note
https://note.mu/nekotricolor/n/n2a247c808275
x86アセンブリ言語での関数コール
https://vanya.jp.net/os/x86call/
詳しくはリンク先に書いてますので説明は略(ひどい!)!! こんな感じでやっていきましょう!
この、ベースポインタとスタックポインタを使って
あれこれやっていく仕組みを知らなかったので、
今回知って、「うおーこんな風になってたのかー」
「スタックと sp
と bp
だけでこういうことができるのかー」
と結構感動しました。
今まで(高級言語で)数えきれないほど関数呼び出しやってきたわけですが、
ブラックボックスだった部分が明らかになってうおーってなりました。
呼び出し規約は 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
push
、pop
の前に簡単な方からやっつけます。
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_for
に set_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 のマニュアルやオプションにもフレームポインタという名前がよく登場します。