Хабрахабр

[Из песочницы] Пишем никому не нужный эмулятор

Доброго времени суток.

Довольно давно имелось желание написать эмулятор какого-нибудь процессора.
А что может быть лучше, чем изобрести велосипед?

Имя велосипеду — V16, от склеивания слова Virtual и, собственно, разрядности.

С чего начать?

А начать нужно, разумеется, с описания процессора.

1. В самом начале, я планировал написать эмулятор DCPU-16, но таких чудес на просторах Интернета хватает с лихвой, поэтому я решил остановиться только на "слизывании" самого основного с DCPU-16 1.

Архитектура

Память и порты

  • V16 адресует 128Kb (65536 слов) оперативной памяти, которая также может использоваться как буферы устройств и стек.
  • Стек начинается с адреса FFFF, следовательно, RSP имеет стандартное значение 0xFFFF
  • Портов ввода-вывода V16 имеет 256, все они имеют длину в 16 бит. Чтение и запись из них осуществляется через инструкции IN b, a И OUT b, a.

Регистры

V16 имеет два набора регистров общего назначения: основной и альтернативный.
Работать процессор может только с одним набором, поэтому между наборами можно переключаться при помощи инструкции XCR.

Инструкции

Все инструкции имеют максимальную длину в три слова и полностью определяются первым
Первое слово делится на три значения: младший байт — опкод, старший байт в виде двух 4-битных значений — описание операндов.

Прерывания

Если значение адреса равно нулю, то прерывание не делает ничего, просто обнуляет флаг HF. Прерывания здесь — не более чем таблица с адресами, на которые процессор дублирует инструкцию CALL.

Пример псевдокода и слов, в которые все это должно странслироваться:

MOV RAX, 0xABCD ; 350D ABCD
MOV [RAX], 0x1234 ; 354D 1234

Cycles (Такты)

Каждое обращение к оперативной памяти это один отдельный такт. V16 может выполнять одну инструкцию за 1, 2 или 3 такта. Инструкция это не такт!

Начнем писать!

Реализация основных структур процессора

  1. Регистров всего четыре, но ситуацию улучшает то, что таких наборов в процессоре целых два. Набор регистров. Переключение происходит при помощи инструкции XCR.

    typedef struct regs_t {
    uint16_t rax, rbx; //Primary Accumulator, Base Register
    uint16_t rcx, rdx; //Counter Register, Data Register
    } regs_t;

  2. В отличии от DCPU-16, V16 имеет условные переходы, вызовы подпрограмм и возвраты оттуда же. Флаги. На данный момент процессор имеет 8 флагов, 5 из которых — флаги условий.

    //Чтобы было красиво, нужно включить заголовок stdbool.h
    typedef struct flags_t {
    bool IF, IR, HF;
    bool CF, ZF;
    bool EF, GF, LF;
    } flags_t;

  3. Здесь также описана таблица адресов прерываний, что вполне можно назвать дескрипторами и найти ещё одну отсылку на 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;

  4. При получении значений, нам необходимо сначала прочитать, затем изменить, а затем записать значение туда, откуда мы его взяли. Операнд.

    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)
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)

Показать больше

Похожие публикации

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

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

Кнопка «Наверх»