...

суббота, 22 июня 2019 г.

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

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

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

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

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

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)

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий