Я решил создать виртуальную машину (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.
Но программирование цифрами это не слишком удобно поэтому при помощи препроцессора C опишем наш ассемблер. Я понимаю что написание asm посредством макросов это не очень хорошо, но данное решение временное.
Код нашего asm выглядит так:
/* Команды */
#define $CRG {memory[memIndx++] = 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();}
Если что, то в статье представлен не весь код, а только его небольшая часть!
Полный код есть в репозитории проекта.
Спасибо большое за прочтение!
Комментарии (21)
iliazeus
08.03.2019 16:19Про интерпретаторы байткода на Хабре недавно был очень хороший пост. Мне кажется, он будет вам интересен, в том числе как пример хорошей обучающей статьи на данную тему.
А код плохой, да. Удручают, например, определения функций в заголовочных файлах, или каскады else if вместо одного switch.
Centrix2132 Автор
08.03.2019 17:15А что не так с определениями функций в заголовочных файлах?
iliazeus
08.03.2019 17:37+1Во-первых, с точки зрения организации кода, в заголовочных файлах обычно определяется только интерфейс какого-то модуля программы — объявления типов и функций, документирующие комментарии и т.д. Детали реализации помещают в файлы .c
Во-вторых, если такой заголовочный файл подключить в два раздельно компилирующихся файла, то она скомпилируется в каждом из них, что приведет к ошибке на стадии компоновки — нескольким определениям одного и того же имени (функции).humbug
10.03.2019 09:09что приведет к ошибке на стадии компоновки
Поэтому есть ключевое слово inline. Хотя лично мне нравится определение функций в .c/.cpp файлах, а не в заголовках.
KonstantinSpb
08.03.2019 16:51+1Если эта тема интересна, то имеет смысл прочитать
Advanced Design and Implementation of Virtual Machines
www.amazon.com/Advanced-Design-Implementation-Virtual-Machines/dp/146658260X
qw1
08.03.2019 19:02+1#define $CRG
Офигеть! А что, так можно было?Centrix2132 Автор
08.03.2019 19:04В именах или в качестве имён макросов можно использовать "$" & "_".
iliazeus
08.03.2019 19:17+1Implementation-defined. Где-то можно, где-то нельзя.
https://en.cppreference.com/w/c/language/identifier
An identifier is an arbitrarily long sequence of digits, underscores, lowercase and uppercase Latin letters, and Unicode characters specified using \u and \U escape notation (since C99).
(...)
It is implementation-defined if raw (not escaped) Unicode characters are allowed in identifiersВ GCC, например, можно.
gdt
08.03.2019 19:17Код к сожалению не идеален, но для начала вполне неплохо. Я бы порекомендовал вам написать простенький транслятор с C-подобного псевдоязыка в байт-код вашей машины (это далеко не так сложно, как кажется, смотрите рекурсивный спуск, и раз уж вы пишете на C — вам могут помочь такие утилиты как lex и yacc, например). Когда у вас будет больше одного типа данных, поддержка строк и рекурсии — вы сами поймёте, что не так с вашим кодом, и как его можно улучшить, это нормально. Продолжайте в том же духе!
MaxVetrov
09.03.2019 08:17Вроде реализация функций не должна быть в заголовочных файлах.
PS. Ааа, уже писали. Ну напомню еще раз.
whitemonkey
Продолжение будет? Просто запасся большой тарелкой с рыбой, и прочитал неуспев даже коснуться её. А заголовок так вдохновил…
Centrix2132 Автор
Точно сказать что «Продолжение будет» или «Продолжения не будет» я сказать не могу.
RoPi0n
Запасайся несколькими большими тарелками:
habr.com/ru/post/434966
habr.com/ru/post/435202
habr.com/ru/post/435258
habr.com/ru/post/435520
habr.com/ru/post/436224
habr.com/ru/post/437632
habr.com/ru/post/442560