Хабрахабр

Виртуальная машина своими руками

Иногда в голову приходит какая-то мысль избавиться от которой очень сложно. Такое произошло и со мной.

Если вы заинтересовались, то вперёд под кат! Я решил создать виртуальную машину (VM), учитывая то, что на тот момент у меня не было идей, мне показалось, что это прекрасная мысль.

Теория

Для начала немного теории. Что вообще такое виртуальная машина? Это программа или набор программ позволяющий эмулировать какую — нибудь аппаратную платформу, проще говоря эмулятор компьютера.

Сами по себе виртуальные машины бывают разные, к примеру Virtual Box – это классическая виртуальная машина позволяющая эмулировать самый настоящий компьютер, а вот к примеру JVM (виртуальная машина Java) такого не может.

Мой вариант VM будет чем — то схож с JVM просто потому, что это более обучающий проект, нежели направленный на создание мощной VM.

Память

Итак, а теперь давайте разберёмся с памятью. Для создания памяти я решил использовать массив unsigned int. Размер массива определим при помощи макроса, в моём варианте размер памяти равен 4096 байт (в массиве 1024 элемента, а так — как на большинстве платформ под данные типа unsigned int выделяется 4 байта то 1024*4 = 4096), помимо прочего определим 8 регистров по 8 ячеек в каждом это будет уже 256 байт (8*8*4 = 256). Выглядит это так:

#define MEMSIZE 1024
unsigned int memory[MEMSIZE];
unsigned int reg[8][8];

Программирование

Память у нас есть, а как теперь писать код под нашу VM? Сейчас мы этим вопросом и займёмся, для начала определим команды которые наша машина будет исполнять:

enum commands { /* Список комманд / List of commands */ CRG = 1, /* Change ReGister - Выбрать регистр [1] */ CRC, /* Change Register Cell [2] */ PRG, /* Put in ReGister - положить данные в нулевую ячейку регистра [3] */ PRC /* Put Register Cell Положить данные в ячейку [4] */
};

Каждая команда имеет свой флаг, определяющий некоторые дополнительные параметры
опишем флаги:

enum flags { /* Список флагов / List of flags */ STDI = 1, /* Стандартный флаг / Standard flag */ STDA /* Адресный флаг / Address flag */
};

Стандартная команда имеет вид: [команда] [флаг] [данные] (вид некоторых команд может отличаться), основываясь на этом напишем простой интерпретатор:

if (memory[cell] == CRG && memory[cell + 1] == STDI) { indxX = memory[cell + 2]; cell++;
}
else if (memory[cell] == CRC && memory[cell + 1] == STDI) { indxY = memory[cell + 2]; cell++;
}
else if (memory[cell] == PRG && memory[cell + 1] == STDI) { reg[indxX][0] = memory[cell + 2]; cell++;
}
else if (memory[cell] == PRC && memory[cell + 1] == STDI) { reg[indxX][indxY] = memory[cell + 2]; cell++;
}

indxX & indxY – это переменные хранящие текущую позицию курсора в регистре reg.
сell – это переменная хранящая текущую позицию курсора в массиве memory.

Я понимаю что написание asm посредством макросов это не очень хорошо, но данное решение временное. Но программирование цифрами это не слишком удобно поэтому при помощи препроцессора C опишем наш ассемблер.

Код нашего asm выглядит так:

/* Команды */
#define $CRG
#define $CRC {memory[memIndx++] = CRC;}
#define $PRG {memory[memIndx++] = PRG;}
#define $PRC {memory[memIndx++] = PRC;}
/* Флаги */
#define _$STDI {memory[memIndx++] = STDI;}
#define _$STDA {memory[memIndx++] = STDA;}
/* Данные */
#define _$DATA memory[memIndx++] =

memIndx – это переменная хранящая текущую позицию курсора в массиве memory.

А вот код на нашем asm кладущий 123 в регистр по адресу [1][0] (первый регистр, нулевая ячейка):

$CRG /* Выбираем регистр */ _$STDI /* Используем флаг STDI */ _$DATA 1; /* Передаём данные */
$CRC /* Выбираем ячейку */ _$STDI _$DATA 0;
$PRC /* Кладём значение */ _$STDI _$DATA 123;

Поздравляю, теперь у нас есть подобие asm для нашей машины!

Запуск программ

Нам удалось заставить нашу машину исполнять программы, но коду не хватает переносимости с одной машины на другую, поэтому сейчас мы займёмся созданием генератора машинного кода из asm (а я напомню что в отличие от настоящих компьютеров наша машина имеет машинный код представленный не в виде двоичных, а десятичных чисел), в принципе это не так сложно, но для начала давайте продумаем реализацию.

Сначала у нас есть asm код, теперь нам надо его перевести в числа, потом записать полученный машинный код в файл .ncp (numeric code program, по факту это текстовый файл, но чтобы его отличать от всего прочего я придумал собственное расширение), после этого нам надо запустить .ncp файл, сделать это просто, так как написанный нами ранее, интерпретатор распознаёт именно числа на нужно только извлекать данные из файла и превращать их в числа с помощью atoi().

Перейдём от слов к делу:

Чтение кода и запись его в файл:

if (memory[i] == CRG && memory[i + 1] == STDI) { fprintf(code, "%d %d ", CRG, STDI); i++;
}
else if (memory[i] == CRC && memory[i + 1] == STDI) { fprintf(code, "%d %d ", CRC, STDI); i++;
}
else if (memory[i] == PRG && memory[i + 1] == STDI) { fprintf(code, "%d %d ", PRG, STDI); i++;
}
else if (memory[i] == PRC && memory[i + 1] == STDI) { fprintf(code, "%d %d ", PRC, STDI); i++;
}

Код является частью тела функции ncpGen().

Чтение файла и его исполнение:

if (prog != NULL) { fread(txt, 1, len, prog); tok = strtok(txt, " "); while (tok != NULL) { memory[i] = atoi(tok); tok = strtok(NULL, " "); if (argc == 3 && strcmp(argv[2], "-m") == 0) { printf("%d\n", memory[i]); } i++; } memInter();
}
else { perror("Fail");
}

А теперь определим макрос для того чтобы вместо интерпретации asm код превращался в .ncp:

#define _toNCP(name) {strcpy(filename, name);} {ncpGen();}

Если что, то в статье представлен не весь код, а только его небольшая часть!

Полный код есть в репозитории проекта.

Спасибо большое за прочтение!

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть