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

habr.png

Иногда в голову приходит какая-то мысль избавиться от которой очень сложно. Такое произошло и со мной.
Я решил создать виртуальную машину (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();}

Если что, то в статье представлен не весь код, а только его небольшая часть!
Полный код есть в репозитории проекта.
Спасибо большое за прочтение!

© Habrahabr.ru