オペコード配列を自動生成する

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

前回は命令セットシミュレータの骨格を作りました。
そこで出てきたdecode関数の中にop_tableという配列があったのですが、今回はその配列の要素を生成する方法について書いてみます。

配列要素を別ファイルにする

op_table配列の要素数は65,536もありますので、atmega328.cに直に書くと大変なことになってしまいます。
そこで、配列の要素を定義する部分だけを別のファイルにしようと思います。

配列の要素を定義するファイルは「opcode.inc」と名前にします。
そして、次のようにインクルードしてあげればop_tableの初期化子にすることができます。

static operation_t *decode(atmega328_t *cpu, uint16_t op)
{
  static operation_t* const op_table[0x10000] =
  {
#include "opcode.inc"
  };

  return op_table[op >> 12];
}

opcode.incの自動生成

さて、opcode.incを分離したのはいいですが、さすがに65,536要素を手作業で入力していくのは大変です。
中には命令としては同じでオペランドだけが異なるものも多数ありますので、そういうのはプログラムを書いて自動生成させる方が簡単です。

opcode.incを自動生成するプログラムはCで書いてもいいんですが、こういうのはスクリプト言語を使った方が便利です。

スクリプト言語は何でもかまいません。
私はあまりいろんなスクリプト言語を使えませんので、一番手に馴染んでいるPHPを使うことにしました。
PHPはWebだけでなく、こういうコマンドラインツールを作ることも簡単にできます。

というわけでPHPで次のようなスクリプトを書きました。

<?php
// オペコード配列を準備
$opcode_table = [];

// いったん全部未定義命令として初期化
for ($i = 0; $i < 0x10000; $i++)
  $opcode_table[$i] = 'undefined';

// 命令を登録する。
$opcode_table[0b0000_0000_0000_0000] = 'nop';

// できあがった配列を出力
for ($i = 0; $i < 0x10000; $i++)
  echo "${opcode_table[$i]},\n";

このスクリプトを実行すると標準出力にopcode.incの内容が出力されます。

それではスクリプトの内容を順を追ってみていきます。

配列の準備

最初に$opcode_tableという空の配列を準備しています。
PHPでは[]を使って配列を表しますが、[]の中に何も書かなければ空の配列になります。

未定義命令で初期化

次に、先ほどの配列$opcode_tableに65,536個(16進で0x10000個)の全要素に’undefined’を埋め込みます。
いったん全要素を未定義命令として、あとから未定義ではない命令を登録していく形を取ります。
こうすることで少なくとも未初期化の要素はなくなります。

‘undefined’というのはCの関数名になります。
PHPでは関数名は単なる文字列として扱うしかないので、引用符で囲んで文字列にしています。

実際の命令を登録する

先ほどは全要素を’undefined’で埋めましたが、未定義でない命令をひとつずつ登録してあげる必要があります。

今回はオペコード0x0000に相当するnop命令だけ登録しています。

nop命令というのは「No OPeration」の意味で、何もしない命令です。
「AVR®命令一式手引書」からnop命令の説明を引用しておきますね。

せっかくなので、命令の説明の見方も説明しておくことにします。

「動作」と項目がこの命令の具体的な振る舞いになります。
nop命令は何もしない命令ですので、動作は「なし」になっています。

「書式」というのはアセンブリ言語での書き方になります。

「オペランド」というのは命令のパラメータのことだと思ってください。
nop命令は何もしないのでオペランドはありません。

「プログラムカウンタ」の項目はこの命令を実行したあとのプログラムカウンタ(PC)がどうなるかを定義しています。
nop命令は何もしませんが、実行したあとは次の命令を読み込むようにしないといけませんのでプログラムカウンタはインクリメントされます。

「機械語」の項目はこの命令のオペコードが2進数で書かれています。
このようにオペコードは2進数で表記されますので、スクリプトの中でも配列の添数の表記に2進数を使いました。

// 命令を登録する。
$opcode_table[0b0000_0000_0000_0000] = 'nop';

「ステータスレジスタ(SREG)とブール式」の項目は、この命令を実行することで、ステータスレジスタのどのフラグがどのように変化するかを表しています。
nop命令を実行してもステータスレジスタはまったく変化しませんので、すべてのフラグが「-」になっています。

最後に「命令語数」ですが、この命令が何ワードかを表しています。
括弧内にバイト数も併記されていますね。
AVRの命令はワード(16ビット)単位ですので、ワード数で表記しているんですね。
「ワード」と「語」は同じ意味だと思ってください。

できあがった配列を出力

できあがった配列$opcode_tableは、順に標準出力に出力します。
Cの配列の初期化子にしないといけませんので、各要素のあとにカンマを付けるのを忘れてはいけません。

出力した内容をopcode.incにリダイレクトしてあげればファイルが完成します。

php opcode.php > opcode.inc

undefned関数とnop関数

スクリプトが自動生成したopcode.incにはundefined関数とnop関数が含まれていましたので、これらの関数を定義しておきます。

次のコードをatmega328.cのdecode関数の前に挿入します。

static void undefined(atmega328_t *cpu, uint16_t op)
{
  fprintf(stderr, "undefined opcode: 0x%04x\n", op);
}

static void nop(atmega328_t *cpu, uint16_t op)
{
  // 何もしない
}

これでコンパイルが通るようになりまりました。


今回は今までとはちょっと変わった雰囲気になったと思います。
Cでプログラムを書く場合でも、こんな感じでコードを自動生成するスクリプトを書くことはよくあると思います。

次回からは順に命令を追加していくことにします。
必ずしもオペコードの順には登録していきませんので、そこのところはあらかじめご了承ください。

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