Пишем никому не нужный эмулятор

в 6:22, , рубрики: C, Программирование, Процессоры, эмулятор

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

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

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

Пишем никому не нужный эмулятор - 1

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

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

В самом начале, я планировал написать эмулятор 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 такта. Каждое обращение к оперативной памяти это один отдельный такт. Инструкция это не такт!

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

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

  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)
{
    //Если была выполнена инструкция 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js