LoginSignup
11
3

More than 3 years have passed since last update.

独断と偏見で考えた自作スタックマシンの紹介

Last updated at Posted at 2020-12-11

この記事は N高等学校 (1) Advent Calendar 2020 の11日目です。

[前回の記事] C++で競技プログラミングをWindows10でやりたい (@YuuAyakawa)
[次回の記事] ニコニコをダークモード化する拡張機能を作ったが、クソコードになったので改良修正した話。 (@nekozuki_dev)

どうも、N高4期生のがーねっとです。

今回は自作のバイトコードと、それを動かすスタックマシンの仕組みについて紹介します。
中検が終わって最近プログラミング言語の制作を再開したのでこのテーマにしました。
( 内容が去年のアドカレと少し被ってる... )

注意書き

1μmの知識と独断&偏見で作っているのでおかしいところがあるかもしれません。
今後もしかすると仕様を変えるかもしれません。

細かいことは気にしない!

コメントでご感想 / ご指摘などいただけるとうれしいですが、お手柔らかにお願いします (涙目

作る目的

私が現在自作している「Chestnut」というプログラミング言語を動かすためです。
もちろんコンパイラも同時に制作中です。

この仮想マシンはJavaでいうとJVMの部分になります。
コンパイルによって生成したバイトコードを仮想マシンで実行します。

開発環境

  • C++17
  • GCC 4.2.1
  • VSCode

コマンド

未リリースなので紹介する意味は特にありません()

ソースコードをコンパイル (コンパイラのコマンド)
$ chesc -i hello.ches -o hello.chesc
バイトコードを実行
$ ches -i hello.chesc

命令一覧

一部の命令を紹介します。

命令コードは1バイトなので理論上は256個まで作れることになります。
( さすがにそんなには作りませんが() )

命令名 処理内容 その他
add 値の加算
sub 値の減算
mul 値の乗算
div 値の除算
and 論理積の演算
or 論理和の演算
equal 一致の判断
greater 大小の比較
rev ビット反転
join 値の結合
jump 処理部分の移動
jumpif 処理部分の移動 演算スタックのトップ値が1の場合のみジャンプ
push 作業スタックにプッシュ
pop 作業スタックをポップ
load 演算スタックにプッシュ
ret ブロックを抜ける
syscall システム命令を実行

スタックの種類

演算では2種類のスタックを使用します。

名前 英語名 (略称) 役割 プッシュ / ポップ命令
作業スタック Working Stack (WS) 演算スタックに使うデータを保持 push / pop
演算スタック Operation Stack (OS?) 命令に渡すデータを保持 load / -

スタックの種類によって値操作の呼び方が変わります。
( ロード = 演算スタックへのプッシュ )

コード例

push    32      16      ; 16をプッシュ
push    32      8       ; 8をプッシュ
load    2               ; 2つの値をロード

mul             ; ロードされた値を乗算 (16 * 8)

load    1       ; 演算結果をロード (関数の戻り値)

pop     ; 演算結果をポップ
pop     ; 8をポップ
pop     ; 16をポップ

ret     ; ブロックから抜ける

mul命令では、ロードされた値を演算し、その結果を作業スタックにプッシュしています。
( 演算スタックは演算後にクリアされるため、命令によってそれをクリアする必要はない )

バイトコードの区切り

バイトコードはいくつかのパートで区切られています。

ヘッダ部分

ヘッダ部分にはマジックナンバーやバージョン情報などのデータが記述されます。
この部分はファイルの先頭に位置していて、サイズは固定です。
( 今のところは128バイト; ヘッダ情報が大きくなれば増やす予定 )

先頭にはバージョンごとに決められたマジックナンバーが記述されます。
最初のリリースのマジックナンバーは 4F 52 49 4E 43 48 41 4E にする予定です。
( この方法はあまりよくない気がするけど、まあ気にしない気にしない... )

命令部分

ヘッダ部分に続いて命令部分が位置します。
命令部分には文字通り、プログラムの命令が記述されます。
すべての命令の末尾には 0x01 が置かれます。

もちろんこの部分のサイズは固定ではありません。

[参考] ## オペコードとオペランド

識別子部分

ここにはクラス、関数、定数などの情報が記述されます。
識別子部分の開始位置はヘッダ情報から参照できます。

↓は記述される情報の例です。

項目 記述される情報
名前空間 識別子, 名前, その他
クラス 識別子, 名前, その他
関数 識別子, 名前, 開始インデックス
定数 識別子, 名前, 値

オペコードとオペランド

オペコードは1バイトです。
オペランドの長さは命令や値によって変わります。

バイトコード例

「32ビットの値 58971 をプッシュする」という命令を例にとると...

; アセンブリ的な表記
push 32 58971

; Hex表記
12 06 E6 5B

0x12 ... push命令のコード
0x06 ... 値のデータサイズ ( 0x06 = 32ビット = int型 )
0xE6 0x5B * ... 実際にプッシュする値 ( * 58971 )

処理手順

1. バイトコードを読み込む

// this->bytesの初期化などは省略...
// typedef unsigned char Byte;

std::ifstream ifs(filePath);
Byte byte;

do {
    ifs.read((char*)&byte, sizeof(char));
    this->bytes[BYTE_LEN++] = byte;
} while(!ifs.eof());

if(BYTE_LEN > 0)
    this->bytes[--BYTE_LEN] = 0;

ifs.close();

↑はソースコードからいろいろ抜き取ったものです。
ついイン/デクリメントを多用してしまう...

2. 命令を実行する

読み込んだバイトコードから1バイトの命令コードを取得して処理します。


void ches::Interpreter::runNextInst() {
    try {
        if(this->index < HEADER_LEN || this->index >= this->idAreaIndex) {
            /* エラー処理 */
            this->finalize();
        }

        Byte opcode = BYTE;
        Byte opcodeIndex = this->index;

        // opcode分をindexに追加する
        this->index++;

        switch(opcode) {
            case IT_InstDiv: {
                /* エラー処理 */
                this->finalize();
            } break;

            case IT_Add: {
                /* add命令の処理 */
            } break;

            case IT_Equal: {
                /* equal命令の処理 */
            } break;

            case IT_Jump: {
                /* jump命令の処理 */
            } break;

            /* 以降の命令は省略 */

            default: {
                // 無効な命令なのでエラー
                /* エラー処理 */
                this->finalize();
            } break;
        }
    } catch() {
        /* エラー処理 */
        this->finalize();
    }
}

↓命令の処理の例として、push命令を書いておきます。

case IT_Push: {
    #define BYTE                        (this->bytes[this->index])
    #define BYTES_AT(indexPair)         (this->getBytes(indexPair))
    #define STACK_PUSH(size, value)     this->stack.push(size, value);

    Byte size = BYTE;
    this->index++;

    std::pair<int, int> index = GET_DIV_INDEX(this->index));
    STACK_PUSH(size, BYTES_AT(index);
} break;

GET_DIV_INDEX(index) は渡されたインデックスから 0x01 までの長さと開始位置を返します。
引数の index は参照渡しで、終了位置まで自動でインデックスが進んでくれます。

( マクロを使いすぎたりしてるので後で書き直します )

コード例

このChestnutコードをコンパイルすると

main(str[] args)
    str name = "Garnet"
    println("Hello, " ~ name ~ "!")
end

↓こうなります (命令部分のみ)

push    32      "Garnet"
push    32      "Hello, "

load    2
join

push    32      "!"

load    2
join

load    1
jump    

ret

.....時間があれば解読してみてください()

最後に / 今後すること

記事を読んでくださってありがとうございます。

現在はC++でスタックマシンを実装中です。
コンパイラも含めてある程度のものが動くようになればリリースする予定です。
( この記事を書いた人は計画性を備えてないのでいつになるかは未定 )

リリースするまでにはChestnutの公式Twitterも作れたらいいなと。

下にChestnut関連のリンクを貼ったのでもしご興味があればどうぞ。
よろしければコメントなどいただけるとうれしいです。

それでは。

リンク

GitHub ... Garnet3106/chestnut
Twitter ... @Garnet3106

去年のアドカレ記事 ... 自作言語「Chestnut」の仕組みとミスったところ
※ ↑今は仕組みがいろいろ変わってます

11
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
3