Доброго времени суток.
Довольно давно имелось желание написать эмулятор какого-нибудь процессора.
А что может быть лучше, чем изобрести велосипед?
Имя велосипеду — V16, от склеивания слова Virtual и, собственно, разрядности.
С чего начать?
А начать нужно, разумеется, с описания процессора.
В самом начале, я планировал написать эмулятор DCPU-16, но таких чудес на просторах Интернета хватает с лихвой, поэтому я решил остановиться только на "слизывании" самого основного с DCPU-16 1.1.
Архитектура
Память и порты
- V16 адресует 128Kb (65536 слов) оперативной памяти, которая также может использоваться как буферы устройств и стек.
- Стек начинается с адреса FFFF, следовательно, RSP имеет стандартное значение 0xFFFF
- Портов ввода-вывода V16 имеет 256, все они имеют длину в 16 бит. Чтение и запись из них осуществляется через инструкции
IN b, a
ИOUT b, a
.
Регистры
V16 имеет два набора регистров общего назначения: основной и альтернативный.
Работать процессор может только с одним набором, поэтому между наборами можно переключаться при помощи инструкции XCR
.
Инструкции
Все инструкции имеют максимальную длину в три слова и полностью определяются первым
Первое слово делится на три значения: младший байт — опкод, старший байт в виде двух 4-битных значений — описание операндов.
Прерывания
Прерывания здесь — не более чем таблица с адресами, на которые процессор дублирует инструкцию CALL
. Если значение адреса равно нулю, то прерывание не делает ничего, просто обнуляет флаг HF.
Диапазон значений | Описание |
---|---|
0x0...0x3 | Регистр как значение |
0x4...0x7 | Регистр как значение по адресу |
0x8...0xB | Регистр + константа как значение по адресу |
0xC | Константа как значение по адресу |
0xD | Константа как значение |
0xE | Регистр RIP как значение только для чтения |
0xF | Регистр RSP как значение |
Пример псевдокода и слов, в которые все это должно странслироваться:
MOV RAX, 0xABCD ; 350D ABCD
MOV [RAX], 0x1234 ; 354D 1234
Cycles (Такты)
V16 может выполнять одну инструкцию за 1, 2 или 3 такта. Каждое обращение к оперативной памяти это один отдельный такт. Инструкция это не такт!
Начнем писать!
Реализация основных структур процессора
-
Набор регистров. Регистров всего четыре, но ситуацию улучшает то, что таких наборов в процессоре целых два. Переключение происходит при помощи инструкции
XCR
.typedef struct regs_t { uint16_t rax, rbx; //Primary Accumulator, Base Register uint16_t rcx, rdx; //Counter Register, Data Register } regs_t;
-
Флаги. В отличии от DCPU-16, V16 имеет условные переходы, вызовы подпрограмм и возвраты оттуда же. На данный момент процессор имеет 8 флагов, 5 из которых — флаги условий.
//Чтобы было красиво, нужно включить заголовок stdbool.h typedef struct flags_t { bool IF, IR, HF; bool CF, ZF; bool EF, GF, LF; } flags_t;
-
Собственно, сам процессор. Здесь также описана таблица адресов прерываний, что вполне можно назвать дескрипторами и найти ещё одну отсылку на x86.
typedef struct cpu_t { //CPU Values uint16_t ram[V16_RAMSIZE]; //Random Access Memory uint16_t iop[V16_IOPSIZE]; //Input-Output Ports uint16_t idt[V16_IDTSIZE]; //Interrupt vectors table (Interrupt Description Table) flags_t flags; //Flags regs_t reg_m, reg_a; //Main and Alt register files regs_t * reg_current; //Current register file uint16_t rip, rsp, rex; //Internal Registers: Instruction Pointer, Stack Pointer, EXtended Accumulator //Emulator values bool reg_swapped; //Is current register file alt bool running; //Is cpu running uint32_t cycles; //RAM access counter } cpu_t;
-
Операнд. При получении значений, нам необходимо сначала прочитать, затем изменить, а затем записать значение туда, откуда мы его взяли.
typedef struct opd_t { uint8_t code : 4; uint16_t value; uint16_t nextw; } opd_t;
Функции для работы со структурами
Когда все структуры описаны, всплывает необходимость в функциях, которые наделят эти структуры магической силой угашенного кода.
cpu_t * cpu_create(void); //Создаем экземпляр процессора
void cpu_delete(cpu_t *); //Удаляем экземпляр процессора
void cpu_load(cpu_t *, const char *); //Загружаем ROM в память
void cpu_rswap(cpu_t *); //Меняем наборы регистров
uint16_t cpu_nextw(cpu_t *); //RAM[RIP++]. Nuff said
void cpu_getop(cpu_t *, opd_t *, uint8_t); //Читаем операнд
void cpu_setop(cpu_t *, opd_t *, uint16_t); //Пишем операнд
void cpu_tick(cpu_t *); //Выполняем одну инструкцию
void cpu_loop(cpu_t *); //Выполняем инструкции, пока процессор работает
Также я не упомянул большое перечисление с кодами операций, но это необязательно и необходимо только для понимания, что происходит во всей этой каше.
Функция tick()
Также здесь присутствуют вызовы static-функций, предназначенных только для вызова из tick()
.
void cpu_tick(cpu_t *cpu)
{
//Если была выполнена инструкция HLT, то функция ничего не сделает
if(cpu->flags.HF) {
//Если к тому же обнулен флаг прерываний, то паузу снимать уже нечему
if(!cpu->flags.IF) {
cpu->running = false;
}
return;
}
//Получаем следующее слово и декодируем как инструкцию
uint16_t nw = cpu_nextw(cpu);
uint8_t op = ((nw >> 8) & 0xFF);
uint8_t ob = ((nw >> 4) & 0x0F);
uint8_t oa = ((nw >> 0) & 0x0F); //А потому что дизайн кода
//Создаем структуры операндов
opd_t opdB = { 0 };
opd_t opdA = { 0 };
//И читаем их значения
cpu_getop(cpu, &opdB, ob);
cpu_getop(cpu, &opdA, oa);
//Дальше для сокращения и улучшения читабельности кода делаем переменные-значения операндов
uint16_t B = opdB.value;
uint16_t A = opdA.value;
uint32_t R = 0xFFFFFFFF; //Один очень интересный костыль
bool clearf = true; //Будут ли флаги условий чиститься после выполнения инструкции?
//И начинаем творить магию!
switch(op) {
//Здесь мы проходим все возможные опкоды. Те, которые пишут результаты, меняют значение переменной R
}
//Чистим флаги условий
if(clearf) {
cpu->flags.EF = false;
cpu->flags.GF = false;
cpu->flags.LF = false;
}
//Очень интересный костыль, максимальное 32-битное значение при 16-битных операциях
// равно 0xFFFF0000, то есть 0xFFFF << 16
// А поэтому очень удобно для результата использовать 32-битное число
if(R != 0xFFFFFFFF) {
cpu_setop(cpu, &opdB, (R & 0xFFFF));
cpu->rex = ((R >> 16) & 0xFFFF);
cpu->flags.CF = (cpu->rex != 0);
cpu->flags.ZF = (R == 0);
}
return;
}
Что делать дальше?
В попытках найти ответ на сей вопрос, я раз пять переписал эмулятор с C на C++, и обратно.
Однако главные цели можно выделить уже сейчас:
- Прикрутить нормальные прерывания (Вместо простого вызова функции и запрета на прием других прерываний сделать вызов функции и добавление новых прерываний в очередь).
- Прикрутить устройства, а также способы общения с ними, благо опкодов может быть 256.
- Научить
себя не писать всякую ересь на хабрпроцессор работать с определенной тактовой частотой в 200 МГц.
Заключение
Надеюсь, что кому-нибудь эта "статья" станет полезной, кого то подтолкнет на написание чего-то похожего.
Мои куличики можно посмотреть на github.
Также, о ужас, у меня есть ассемблер для старой версии этого эмулятора (Нет, даже не пытайтесь, эмулятор как минимум пожалуется на неправильный формат ROM)
Автор: undbsd