CALL命令、RCALL命令、RET命令の実装

こんにちは、めのんです!

今回はサブルーチンの呼び出しに必要な3つの命令を実装します。

高級言語でいう関数、プロシージャ、メソッドといった機能はすべてサブルーチンが基礎になっています。
機械語のサブルーチンはすごくシンプルなもので、引数、返却値、スコープなどの概念は一切ありません。
本当に行って、帰るだけの機能なんです。

CALL命令の実装

サブルーチンを呼び出すもっとも基本的な命令がCALL命令です。
この命令はJMP命令とPUSH命令を組み合わせたようなものだと考えるといいでしょう。
つまり、戻り先のアドレスをスタックに積んでからサブルーチンの開始アドレスに分岐します。

いつものように「AVR®命令一式手引書」から命令の説明を引用します。

これだけの説明では実装するには不十分で、戻り先のアドレスとして、プログラムカウンタの上位から先に積むのか下位から先に積むのかがわかりません。

これは同じく「AVR®命令一式手引書」の「付録A. 機械語命令の構造」の次の記述から特定することができます。

この説明を読む限りでは、どうやら戻り先アドレスはビッグエンディアンで格納するようですね。

それではコードを見ていきます。

static void call(atmega328_t *cpu, uint16_t op)
{
  int target = fetch(cpu);
  target += (op & 0x0001) << 16 | (op & 0x01f0) << (17 - 4)

  int sp = cpu->sph << 8 | cpu->spl;
  int return_address = cpu->pc;
  cpu->data[sp--] = return_address & 0xff;
  cpu->data[sp--] = return_address >> 8;
  cpu->sph = sp >> 8;
  cpu->spl = sp & 0xff;

  cpu->pc = target;
  cpu->clock += 4;
}

ATmega328は16ビットPCなので2ワード目だけを分岐先にすればいいんですけど、JMP命令と同様に念のため1ワード目に埋め込まれたアドレスも反映しておくことにします。
コード上ではtargetを組み立てている部分ですね。

その後スタック操作に入ります。
戻り先アドレスがビッグエンディアンで格納されるように、プログラムカウンタの下位側から先にスタックに積んでいきます。
スタックポインタの操作はPUSH命令と同じで事後減少です。

戻り先アドレスにはプログラムカウンタをそのまま使っています。
CALL命令を実行するために2回fetch関数を呼ぶことになりますので、プログラムカウンタはすでに次の命令を指しているからです。

最後に先ほど組み立てたtargetをプログラムカウンタに設定して分岐しています。

RCALL命令の実装

RCALL命令はCALL命令とよく似ていますが相対分岐を使います。
ちょうどJMP命令とRJMP命令の関係に相当します。

こちらも「AVR®命令一式手引書」から命令の説明を引用します。

今回は1ワード命令ですね。

それではコードを見ていきます。

static void rcall(atmega328_t *cpu, uint16_t op)
{
  int sp = cpu->sph << 8 | cpu->spl;
  int return_address = cpu->pc;
  cpu->data[sp--] = return_address & 0xff;
  cpu->data[sp--] = return_address >> 8;
  cpu->sph = sp >> 8;
  cpu->spl = sp & 0xff;

  cpu->pc += (op & 0x0fff) - 0x800;
  cpu->clock += 3;
}

最初に戻り先アドレスをスタックに積む操作から行っています。
ここはCALL命令と同じですね。

分岐先アドレスの求め方はRJMP命令とまったく同じです。

RET命令の実装

最後にRET命令と実装します。
この命令はCALL命令またはRCALL命令で呼び出されたサブルーチンから呼び出し元に戻るためのものです。

スタックには戻り先アドレスが積まれているはずですので、それを取り出して分岐することになります。

こちらも「AVR®命令一式手引書」から命令の説明を引用します。

コードを見ていきますね。

static void ret(atmega328_t *cpu, uint16_t op)
{
  int sp = cpu->sph << 8 | cpu->spl;
  int return_address = cpu->data[++sp] << 8;
  return_address |= cpu->data[++sp];
  cpu->sph = sp >> 8;
  cpu->spl = sp & 0xff;

  cpu->pc = return_address;
  cpu->clock += 4;
}

最初にスタックから戻り先アドレスを取り出します。
CALL命令やRCALL命令では戻り先アドレスを下位側からスタックに積みましたので、RET命令は逆に上位側から取り出しています。
スタックポインタはPOP命令と同じで事前増加になります。

op_tableへの登録

次に、いつものようにop_tableに登録します。

opcode.phpに次のコードを追加して実行してあげればOKです。

for ($k = 0; $k < 64; ++$k)
{
  $opcode_table[0b1001_0100_0000_1110 | ($k & 0b11_1110) << 3 | ($k & 0b0001)] = [ 'call', 2 ];
}

for ($k = 0; $k < 0x1000; ++$k)
{
  $opcode_table[0b1101_0000_0000_0000 | $k] = 'rcall';
}

$opcode_table[0b1001_0101_0000_1000] = 'ret';

今回は65,536命令のうち64 + 4,096 + 1 = 4,161個が埋まりました。

それでは次回またお会いしましょう!