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

Я решил создать виртуальную машину (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)


  1. whitemonkey
    08.03.2019 16:09
    +4

    Продолжение будет? Просто запасся большой тарелкой с рыбой, и прочитал неуспев даже коснуться её. А заголовок так вдохновил…


    1. Centrix2132 Автор
      08.03.2019 16:10
      +1

      Точно сказать что «Продолжение будет» или «Продолжения не будет» я сказать не могу.



  1. iliazeus
    08.03.2019 16:19

    Про интерпретаторы байткода на Хабре недавно был очень хороший пост. Мне кажется, он будет вам интересен, в том числе как пример хорошей обучающей статьи на данную тему.


    А код плохой, да. Удручают, например, определения функций в заголовочных файлах, или каскады else if вместо одного switch.


    1. Centrix2132 Автор
      08.03.2019 16:40

      Спасибо приму к сведению.


    1. Centrix2132 Автор
      08.03.2019 17:15

      А что не так с определениями функций в заголовочных файлах?


      1. iliazeus
        08.03.2019 17:37
        +1

        Во-первых, с точки зрения организации кода, в заголовочных файлах обычно определяется только интерфейс какого-то модуля программы — объявления типов и функций, документирующие комментарии и т.д. Детали реализации помещают в файлы .c

        Во-вторых, если такой заголовочный файл подключить в два раздельно компилирующихся файла, то она скомпилируется в каждом из них, что приведет к ошибке на стадии компоновки — нескольким определениям одного и того же имени (функции).


        1. Centrix2132 Автор
          08.03.2019 17:59

          Понял, принял.


          1. da-nie
            09.03.2019 12:11

            Но шаблонные функции определяются именно в заголовочных файлах.


        1. humbug
          10.03.2019 09:09

          что приведет к ошибке на стадии компоновки

          Поэтому есть ключевое слово inline. Хотя лично мне нравится определение функций в .c/.cpp файлах, а не в заголовках.


  1. hobo-mts
    08.03.2019 16:39

    Я заметил 4 пунктуационных ошибки до #habracut


    1. Centrix2132 Автор
      08.03.2019 16:39
      +1

      Буду рад если вы мне на них укажете.


  1. KonstantinSpb
    08.03.2019 16:51
    +1

    Если эта тема интересна, то имеет смысл прочитать
    Advanced Design and Implementation of Virtual Machines
    www.amazon.com/Advanced-Design-Implementation-Virtual-Machines/dp/146658260X


  1. qw1
    08.03.2019 19:02
    +1

    #define $CRG
    Офигеть! А что, так можно было?


    1. Centrix2132 Автор
      08.03.2019 19:04

      В именах или в качестве имён макросов можно использовать "$" & "_".


      1. qw1
        08.03.2019 19:07
        +2

        С подчёркиванием понятно, но $ было неожиданно, спасибо.


    1. iliazeus
      08.03.2019 19:17
      +1

      Implementation-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, например, можно.


  1. gdt
    08.03.2019 19:17

    Код к сожалению не идеален, но для начала вполне неплохо. Я бы порекомендовал вам написать простенький транслятор с C-подобного псевдоязыка в байт-код вашей машины (это далеко не так сложно, как кажется, смотрите рекурсивный спуск, и раз уж вы пишете на C — вам могут помочь такие утилиты как lex и yacc, например). Когда у вас будет больше одного типа данных, поддержка строк и рекурсии — вы сами поймёте, что не так с вашим кодом, и как его можно улучшить, это нормально. Продолжайте в том же духе!


    1. Centrix2132 Автор
      08.03.2019 19:20

      Спасибо.



  1. MaxVetrov
    09.03.2019 08:17

    Вроде реализация функций не должна быть в заголовочных файлах.
    PS. Ааа, уже писали. Ну напомню еще раз.