Хабрахабр

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

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

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

Имя велосипеду — 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)

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть