ここでは、Mae-Bにおける仮想マシン(BVM: B Virtual Machine)について説明します。
BVMは、実行環境のint型を1ワードとして動作します。
BVMがひとつのプログラムのために割り当てる領域は64Kワードです。その中の配置は下図の通り。

getvec()関数による動的メモリ割り当てで割り当てられる領域です。下端をheap_end_addressというレジスタで保持しており、現在の領域で足りなくなればこれの値が増えてヒープ領域が下に伸びます。
※「スタック」欄の凡例:
| No. | 命令 | オペランド | 意味 | スタック |
|---|---|---|---|---|
| 1 | UNREACHABLE | なし | 絶対に通らないところに配置する不正命令。この命令が実行されたらエラーメッセージを出力して終了する。 | [] → [] |
| 2 | PUSH | pushする整数値 | 整数をスタックにpushする。 | [] → [op1] |
| 3 | PUSH_AUTO | pushする変数のベースレジスタからのオフセット | 自動変数の値をスタックにpushする。 | [] → [自動変数の値] |
| 4 | PUSH_STATIC | pushする値を格納しているアドレス。 | 指定したアドレスの内容をスタックにpushする。 | [] → [アドレスの内容] |
| 5 | POP_AUTO | pop先の変数のベースレジスタからのオフセット | スタックのトップの値を自動変数にpopする。 | [value] → [] |
| 6 | POP_STATIC | pop先のアドレス | スタックトップの値を指定したアドレスにpopする。 | [value] → [] |
| 7 | PUSH_AUTO_ADDRESS | 対象の変数のベースレジスタからのオフセット | 自動変数のアドレスをスタックにpushする。 | [] → [自動変数のアドレス] |
| 8 | PUSH_BY_STACK | なし | スタックトップに格納されているアドレスに格納されている値をpushする。 | [アドレス] → [左のアドレスの内容] |
| 9 | POP_BY_STACK | なし | スタックトップのアドレスに、スタックの2番目に格納されている値をpopする。 | [value, ポップ先のアドレス] → [] |
| 10 | POP | なし | スタックトップの値をひとつ捨てる。 | [value] → [] |
| 11 | PUSH_N | スタックを進める数 | オペランドの数だけスタックポインタを進める(オペランドの数だけダミーの値をpushするような動作。ただしスタックの内容自体は変化しない)。 | [] → [dummy1, dummy2, dummy3, ...(op1の数だけ)] |
| 12 | POP_N | スタックを戻す数 | オペランドの数だけスタックポインタを戻す(オペランドの数だけダミーの値をpopするような動作)。 | [dummy1, dummy2, dummy3, ...(op1の数だけ)] → [] |
| 13 | DUPLICATE | なし | スタックトップの値をスタックトップにpushする。 | [value] → [value, value] |
| 14 | ADD | なし | スタックトップのふたつの値を加算してスタックに積む。 | [value1, value2] → [value1 + value2] |
| 15 | SUB | なし | スタックの2番目の値からスタックトップの値を減算してスタックに積む。 | [value1, value2] → [value1 - value2] |
| 16 | MUL | なし | スタックトップのふたつの値を乗算してスタックに積む。 | [value1, value2] → [value1 * value2] |
| 17 | DIV | なし | スタックの2番目の値をスタックトップの値で除算してスタックに積む。 | [value1, value2] → [value1 / value2] |
| 18 | MOD | なし | スタックの2番目の値をスタックトップの値で割った余りをスタックに積む。 | [value1, value2] → [value1 % value2] |
| 19 | MINUS | なし | スタックトップの値の符号を逆転させた値をスタックに積む。 | [value] → [-value] |
| 20 | LEFT_SHIFT | なし | スタックの2番目の値をスタックトップの値だけ左シフトした値をスタックに積む。 | [value1, value2] → [value1 << value2] |
| 21 | RIGHT_SHIFT | なし | スタックの2番目の値をスタックトップの値だけ右シフトした値をスタックに積む。 | [value1, value2] → [value1 >> value2] |
| 22 | EQ | なし | スタックの2番目の値とスタックトップの値を比較し、等しければ1を、等しくなければ0をスタックに積む。 | [value1, value2] → [value1 == value2] |
| 23 | NE | なし | スタックの2番目の値とスタックトップの値を比較し、等しければ0を、等しくなければ1をスタックに積む。 | [value1, value2] → [value1 != value2] |
| 24 | GT | なし | スタックの2番目の値とスタックトップの値を比較し、2番目の値 > スタックトップの値であれば1を、そうでなければ0をスタックに積む。 | [value1, value2] → [value1 > value2] |
| 25 | GE | なし | スタックの2番目の値とスタックトップの値を比較し、2番目の値 >= スタックトップの値であれば1を、そうでなければ0をスタックに積む。 | [value1, value2] → [value1 >= value2] |
| 26 | LT | なし | スタックの2番目の値とスタックトップの値を比較し、2番目の値 < スタックトップの値であれば1を、そうでなければ0をスタックに積む。 | [value1, value2] → [value1 < value2] |
| 27 | LE | なし | スタックの2番目の値とスタックトップの値を比較し、2番目の値 <= スタックトップの値であれば1を、そうでなければ0をスタックに積む。 | [value1, value2] → [value1 <= value2] |
| 28 | LOGICAL_NOT | なし | スタックトップの値が0であれば1を、0でなければ0をスタックに積む。 | [value] → [!value] |
| 29 | BIT_AND | なし | スタックの2番目の値とスタックトップの値の論理積を取得しスタックに積む。 | [value1, value2] → [value1 & value2] |
| 30 | BIT_OR | なし | スタックの2番目の値とスタックトップの値の論理和を取得しスタックに積む。 | [value1, value2] → [value1 | value2] |
| 31 | BIT_XOR | なし | スタックの2番目の値とスタックトップの値の排他的論理和を取得しスタックに積む。 | [value1, value2] → [value1 ^ value2] |
| 32 | BIT_NOT | なし | スタックトップの値のビットNOTを取得しスタックに積む。 | [value] → [~value] |
| 33 | JUMP | ジャンプ先のアドレス | オペランドで指定されたアドレスにジャンプする。 | [] → [] |
| 34 | JUMP_IF_TRUE | ジャンプ先のアドレス | スタックトップの値が非ゼロなら、オペランドで指定されたアドレスにジャンプする。 | [value] → [] |
| 35 | JUMP_IF_FALSE | ジャンプ先のアドレス | スタックトップの値がゼロなら、オペランドで指定されたアドレスにジャンプする。 | [value] → [] |
| 36 | JUMP_STACK | なし | スタックトップのアドレスにジャンプする。 | [value] → [] |
| 37 | CALL | なし | スタックトップのアドレスの関数を呼び出す。詳細は「関数呼び出し」を参照。 | [argn, ..., arg2, arg1] →[argn, ..., arg2, arg1, nargs, base, pc] |
| 38 | SAVE_RETURN_VALUE | なし | スタックトップの値を、戻り値用レジスタに退避する。 | [] → [value] |
| 39 | RETURN | なし | 関数から戻る。実行中の関数がmain()関数だったら処理を終了する。詳細は「関数呼び出し」を参照。 | [argn, ..., arg2, arg1, nargs, base, pc] →[argn, ..., arg2, arg1, nargs] |
| 40 | PUSH_RETURN_VALUE | なし | 戻り値用レジスタの値をpushする。 | [] → [value] |
BVMは仮想的なCPUなので、当然レジスタもあります(実態は単なるローカル変数またはファイル内static変数ですが)。
Bから、組み込み関数以外の、Bで作られた関数を呼ぶ場合、以下のような動きになります。
図にするとスタックは以下のようになっています(これは、関数が呼び出されて、呼び出された側のプログラムがちょっと動いて計算のためにスタックが使われている状態だと思ってください)。
こうなっていれば、ローカル変数と引数は、すべてbaseからの固定の相対距離(オフセット)で参照できます。命令の中のPUSH_AUTOとかPOP_AUTOとかオペランドはこのオフセットです。

関数の実行が終わってリターンするときの動きは以下。
こうした組み込み関数は、以下のような形式で定義されていて、BVMから呼び出されます(これはputnumb()の例)。
static int
fun_putnumb(int argc, int *args, int *memory)
{
return fprintf(st_current_output, "%d", args[0]);
}
argcは引数の数、argsは引数の配列で、args[0], args[1], ... と指定することで順に引数が取り出せます。memoryは、BVMが確保している64Kワードの領域そのものの先頭アドレスです。たとえば最初の引数でポインタが渡されたら、memory[args[0]]でそのポインタの指す先の内容を取り出すことができます。
getvec(), rlsevec()はCのmalloc()とfree()に相当する関数です。
ただし、Cのfree()は開放すべき領域のポインタだけ渡せばよいですが、Bのrlsevec()は開放すべき領域へのポインタと、その領域のサイズも必要です。
なぜこうなっているのか、どんな実装であるべきなのか、いろいろ考えました。
まず、MH-TSS版のリファレンスのrlsevec()の説明を見ると、こんなことが書いてあります。
-- this function releases the n+1 words of the static vector v back to the system. Calling this routine with an automatic vector v as argument results in immediate or eventual disaster!
訳)-- この関数は静的ベクトル v の n+1 ワードをシステムに解放する。自動ベクトル v を引数としてこのルーチンを呼び出すと、即時または遅延した災害を引き起こす!
そりゃまあ自動変数の領域を開放したらろくでもないことが起きるでしょうが、じゃあ静的変数ならいいの? というのが気になります。そもそも「この関数は静的ベクトルvの~」とか書いてあるし。
それどころか、GCOS8版のマニュアルを見ると、こんなことが書いてあります。
All memory allocation is done by manipulating a linked list of free memory called the free list. The free list initially includes the so-called "core hole". You can use RLSEVEC to return any area of memory which is not on the free list, as long as the address of the memory is greater than the load address of RLSEVEC. If you attempt to release memory which is already on the free list, in whole or in part, RLSEVEC immediately aborts your program.
訳)すべてのメモリ割り当ては、フリーリストと呼ばれる空きメモリの連結リストを操作することで行われます。フリーリストには初期状態ではいわゆる「コアホール」が含まれています。RLSEVECを使用すれば、そのメモリのアドレスがRLSEVECのロードアドレスより大きい限り、フリーリスト上にない任意のメモリ領域を返すことができます。フリーリスト上に既に存在するメモリを、全体または一部を問わず解放しようとすると、RLSEVECは直ちにプログラムを中止します。
「任意のメモリ領域を返すことができます」と書いてある……
このあたりをいろいろ考えた結果、以下のようにしました。
coalescingが要るかどうかは迷いましたが、この方法でcoalescingなしだと、たとえば「連結リストを作るので固定サイズの領域をたくさんgetvec()する」という(たいへんよくある)コードを書いたとき、rlsevec()で領域を解放しても、その領域はgetvec()した領域よりヘッダ分狭いので、結局再利用できません。さすがにそれでは使えないよなあ、ということでcoalescingありとしました。GCOS8のマニュアルを見ると「フリーリスト上に既に存在するメモリを、全体または一部を問わず解放しようとすると、RLSEVECは直ちにプログラムを中止します」とあり、こんなことができるならrlsevec()の度にフリーリストをひととおりスキャンしているということでもありますし。
そして、この方式だと、普通の静的変数の配列を、rlsevec()で以降のgetvec()で使えるように「返す」ことができます。
公開日: 2026/01/18
間違い等ありましたら、掲示板にご連絡願います。