Perl で 8ビット CPU を作る

CPU を作る、と言ってもハードではなくソフト、仮想機械です。

2001 年から UNIX USER で連載されていた西田亙さんの「gccプログラミング工房」。いまさらながら、バックナンバーを取り寄せて初回から順番に読んでいます。とてもためになる連載です。

この連載中で第10回から数回に分けて開発されていた octopus という 8 ビット CPU の仮想機械があります。オリジナルは C 言語で書かれていたのですが、その設計を見て、これは他の言語でも作れるのではないか、と思い Perl に移植してみたところなんとか動作させることができました。以下の URL にコードを公開します。(西田さんに確認を取ったところ、オリジナルのソースは Public Domain とのことでした。オリジナルは http://www.skyfree.org/jpn/unixuser/ からダウンロード可能です。)

以下、octopus.pl の実行例です。コマンドライン引数に octopus の機械語を与えると、その機械語を解釈して命令を実行します。実行された命令の内容と、命令実行後のメモリの内容を端末に出力します。

% perl octopus.pl 37 32 33 16 144 7 248 41 1 144 12 152 41 2 152
  Memory = 25 20 21 10 90 07 F8 29 01 90 0C 98 29 02 98 F8
           00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
----------------------------------------------------------
00: movi 0x20, r5
02: movi 0x10, r1
04: call 0x07
07: addi 0x01, r1
09: call 0x0C
0C: addi 0x02, r1
0E: ret
0B: ret
06: hlt
## System is halted
----------------------------------------------------------
            0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
  Memory = 25 20 21 10 90 07 F8 29 01 90 0C 98 29 02 98 F8
           00 00 00 00 00 00 00 00 00 00 00 00 00 00 0B 06
    Port = FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
           FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
Register = 00 13 00 00 00 20 00 07 00 00 00 00 00 00 00 00
    Flag = NZ
    Step = 8

256 バイトのメモリと 16 個の 8 ビットレジスタ。スタックを使った CALL / RET 命令によりサブルーチンがネストされて呼び出されている様子が見えます。

オリジナルの octopus では I/O ポートが実装されており、OUT 命令によりキーボードの LED を光らせながらビープ音を鳴らすことができるのですが、今回は外界とのインタフェースは省略して、四則演算やサブルーチン呼び出しで必要とされる基本的なインストラクションを実装することを目的にしました。実装したのは lib/Octopus/Instruction.pm の @Inst 配列にある 21 個の命令です。

また、C言語のソースそのままで移植するのではなく、自分なりに設計をアレンジして書いてみました。大きな違いとしては、設計に OO を持ち込んだことと、例外処理を用いたあたりです。CPU の基本設計 (5 + 3 ビットインストラクション、8ビットオペランド、16 レジスタ、256バイトメモリ等々) はオリジナルに習った作りになっています。

コードの断片を紹介します。例えばサブルーチンの戻りアドレスをスタックに積み、任意のアドレスへジャンプする CALL 命令は

package Octopus::Instruction::CALL;
use strict;
use warnings;
use base qw/Octopus::Instruction/;
use Octopus::Util;

sub process {
    my ($self, $vm) = @_;
    my $dst;

    if ($self->mode == 0) {
        $dst = $vm->text->[ $vm->pc++ ];
        msg "0x%02X", $dst;
    } else {
        $dst = $vm->reg->[ $self->mode ];
        msg "r%d", $self->mode;
    }

    $vm->text->[ --$vm->sp ] = $vm->pc;
    $vm->pc = $dst;
}

1;

というコードになります。$vm は仮想機械のインスタンスですが、このインスタンス上に用意したメモリ領域 (text) や レジスタ(reg)、スタックポインタ (sp)、プログラムカウンタ (pc) を操作することで所定の動作となります。このようにして一つ一つの CPU 命令を実装していきます。$vmexecute メソッドを実行すると、メモリ領域に配置された機械語のうちプログラムカウンタが指すバイトを解釈して、命令が実装されたモジュールに処理をディスパッチして、ロジックを実行します。

仮想機械を作る、というのが初めての経験でしたが、これまで本などで書かれたその通りに受け取ることしかできなかった CPU の世界を実際に自分の手で実装してみることで、プログラムカウンタを進めながら機械語を解釈し、スタックを使ってサブルーチンを呼び出すといった動作原理をコードで理解することができました。一つ一つの基本的な命令は「1バイトを移動する」といったほんの小さな命令ですが、それらの命令が組み合わさることで任意の処理となって生き物のように動きます。これは感動です。アセンブリコードをハンドアセンブルで命令を一つずつ機械語に落としていく作業もまた楽しい。この仮想機械用のアセンブラを実装してみたくなりました。

西田さん、それから(残念ながら休刊となってしまいましたが) UNIX USER / オープンソースマガジンの編集スタッフのみなさま、素晴らしい記事をありがとうございます。