0002: виртуальная стековая машина (интерпретатор байт-кода a.k.a движок)
Я написал движок на С без ++ для DOS -- я хотел попробовать полностью
написать GUI на Форте, для этого мне нужен был прямой доступ к видеопамяти,
а с DirectX или SDL (http://sdl.sf.net) я решил пока не разбираться.
Для адаптации для других хост-систем он требует минимальной адаптации --
исходник очень маленький, и это одна из причин, по которой я использую tiny.
Полные исходники tinyVM брать с http://akps.ssau.ru/forth/tiny/
или писать свой вариант на любом языке
config.h
=============
// tinyVM (c) Dmitry Ponyatov <forth@km.ru>, public domain
// файл конфигурации
// размеры структур ВМ (памяти и стеков)
#define Msz 0x1000
#define Rsz 0x100
#define Dsz 0x10
#define Lsz 0x10
// консольный ввод/вывод
#define CONIO_ext
// отладка
#define DEBUG_ext
// графическое расширение
#define GRAPH_ext
// ===================================================================
vm.cpp
=============
// tinyVM (c) Dmitry Ponyatov <forth@km.ru>, public domain
#include "config.h"
#include
#include
#include
#define uint unsigned int
int M[Msz]; // память
uint R[Rsz]; uint Rp=0; // стек возвратов
int D[Dsz]; uint Dp=0; // стек данных
/*
регистры Rp и Dp указывают на первый свободный элемент
поместить число в стек D[Dp++]=n
взять число из стека n=D[--Dp]
*/
uint op; uint Ip=0; // опкод, указатель команд
/*
все команды ВМ реализованы в виде функций void CMD() ("микрокод")
*/
// команда с опкодом op не определена
void UNDEF() { printf("\nUNDEF %X:%X\n",--Ip,op);
void NOP() { }
void JMP() { Ip=M[Ip]; }
void qJMP() { if (D[--Dp]) Ip++; else JMP(); }
void CALL() { R[Rp++]=Ip+1; JMP(); }
void RET() { Ip=R[--Rp]; }
void LIT() { D[Dp++]=M[Ip++]; }
// ВМ поддерживает циклы со счетчиком
struct {
int count,to; // текущее, конечное значения счетчика
uint addr; // адрес после команды do
} L[Lsz]; uint Lp=0;
void DO() { assert(Lp0); L[Lp-1].count++;
if (L[Lp-1].count0); D[Dp++]=L[Lp-1].count; }
void J() { assert(Lp>1); D[Dp++]=L[Lp-2].count; }
void K() { assert(Lp>2); D[Dp++]=L[Lp-3].count; }
// код остальных команд см. исходник
/*
для декодирования опкодов (запуска "микрокода" по опкоду) чаще всего
используются таблицы переходов (jump table), содержащие указатели на
соответствующие функции, или указатели на функцию UNDEF()
если вы будете писать свой вариант движка на другом языке, учтите что
некоторые языки в принципе не поддерживают такие таблицы и выполнение функции
или процедуры по указателю, или делают это с большими накладными расходами
(например Java -- в FidoNet-конференции RU.JAVA мой вопрос "как написать
аналог вот такого кода" породил большой флейм, и все идет к тому что тему
объявят offtopicом)
в этом случае вам придется делать декодирование опкода на switchах или
аналогичных структурах вашего языка
*/
// jump table
void (*JT[])()={
// 0x
NOP, JMP, qJMP, CALL,
RET, LIT, UNDEF, UNDEF,
DO, LOOP, I, J,
K, UNDEF, UNDEF, UNDEF,
/ остальное см. исходник /
UNDEF};
// функция выполняет одну команду ВМ
void step()
{
// проверка регистров ВМ
assert(Ip:<опкод> и аварийно завершит
интерпретатор
*/
}
/*
main() выполняет загрузку байт-кода и однозадачное его выполнение
main() для варианта запуска из командной строки -- первым параметром
должно идти имя файла с байт-кодом, сгенерированным целевым компилятором
загрузку можно усложнить:
- загружать не только полное содержимое M[], но и стеки (что-то типа hibernate
в выгрузкой состояния ВМ и последующей загрузкой на этом компьютере или с
передачей по сети на другой -- миграция задачи),
- хранить байт-код в сжатом виде (для мелких программ в нем очень много
"мертвых нулей" в старших байтах команд и констант)
- сделать tiny-сервер: демон принимает по сети пакет с байт-кодом и запускает
параллельно себе интерпретатор
- добавить к байт-коду заголовки с информацией типа
UNIX-сигнатуры /usr/bin/tinyVM в начале файла, размером cell и порядком
байт в нем, требований программы к памяти, прав доступа, запуском нескольких
параллельных нитей в одном куске байт-кода и т.п.
больше всего всяких вкусностей обещает многозадачное выполнение -- для этого
нужно определить M[] R[] и D[] как *M, *R, *D и периодически или по событиям
переключать контекст (*M, *R, *D, *L, Ip, контекст системы ввода/вывода и
защиты задач)
особо извращенный вариант -- запускать несколько tinyVM на нескольких
компьютерах и делать распределенную систему с балансировкой нагрузки и(ли)
резервированием и т.п.
*/
int main(int argc, char *argv[])
{
// проверка параметров ком.строки
assert(argc>1);
// загрузка байт-кода
FILE *img=fopen(argv[1],"rb"); assert(img!=NULL);
assert(fread(M,sizeof(M[0]),Msz,img)!=NULL); fclose(img);
// однозадачный планировщик
for (;;) step();
// фиктивный return, чтобы компилер не ругался что из main нет return
return 0;
}
PS:
использование таких интерпретаторов с разным форматом и функциональностью
машинного кода идеально подходит для обучения студентов по курсам типа
"микропроцессоры и ЭВМ": с одной стороны существуют огромные возможности
и удобство отладки (студентом пишется Сшный отладочный код и движок
пересобирается, можно ввести отображение состояния компонентов ВМ, добавить
кнопки, свистелки и перделки, запуск программ не приводит к перезагрузке
компа, и в любой момент можно сбросить или модифицировать состояние ВМ), и с
другой стороны студент получает непаханное поле для написания учебных
операционных систем, компиляторов, отладчиков, сетевых глюкалок и т.п.
ну и в порядке ознакомления или в полном объеме можно показать и
применение (и написание своего) Форта -- например на случай, если нужно
будет написать хитрый ассемблер с навороченными макросами.
================
http://akps.ssau.ruforth@km.ru
FidoNet SU.FORTH 2:5057/18.29
tel.: +7 8462 28 9910 (work), 15 4313 (home)