Вступление

С началом учебного года в мой техникум пришла такая штука, как Код будущего. Я и пара моих приятелей решили записаться на курсы по программированию на Python, но этот процесс длился очень долго, к сожалению так ничего и не началось :( Из-за двух каких-то, извиняюсь за выражение, дебилов, которые что-то и где-то не успели вовремя заполнить и сдать, пришлось свернуть эту программу в моём техникуме. Я не особо расстроился, наоборот, это даже дало мне хороший толчок для изучения данного языка программирования самостоятельно дома, как я и делал со всеми предыдущими языками :) Изучив достаточно материала, я осмелился на написание эмулятора CPU, конкретно Intel 4004 (дедушку современных микропроцессоров) с очень урезанным функционалом.

Как всё писалось?

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

Начал я с реализации памяти, в которую будут записываться результаты программ:

memory = bytearray(256) # 256 байт памяти

Затем я создал класс I4004, в который стал добавлять функции. Первой стала функция инициализации:

def __init__(self, program, memory):
        self.program = program
        self.memory = memory

        self.acc = 0 # Аккумулятор
        self.pc = 0 # Счётчик команд

Имеется Аккумулятор (acc) — регистр процессора, в котором сохраняются результаты выполнения арифметических и логических команд, а также Счётчик команд (pc) - регистр процессора, который указывает, какую команду нужно выполнять следующей.

Следующей стала функция run, которая отвечала за запуск:

def run(self):
        while True:
            opcode = self.program[self.pc]

            if opcode == 0x00: # Остановка
                return

            elif opcode == 0xA2: # Загрузка числа в аккумулятор
                self.acc = self.program[self.pc + 1]
                self.pc += 2

            elif opcode == 0xA4: # Загрузка числа в регистр 0
                self.rr0 = self.program[self.pc + 1]
                self.pc += 2

            elif opcode == 0x58: # Сложение
                self.acc += self.rr0
                self.pc += 1
                if self.acc > 15:
                    self.acc = 15
                    print("Вы не можете получить число выше 15!")

            elif opcode == 0x29: # Вычитание
                self.acc -= self.rr0
                self.pc += 1
                if self.acc < 0:
                    self.acc = 0
                    print("Вы не можете получить число ниже 0!")

            # и так далее...

            else:
                print("Неизвестная инструкция!")
                return

Для составления программ используется опкод — часть машинного языка, называемая инструкцией и определяющая операцию, которая должна быть выполнена. Вот таблица опкодов и соответствующая им операция:

Опкод

Операция

0x00

Остановка программы

0xA2

Загрузка числа в аккумулятор

0xA4

Загрузка числа в регистр 0

0x58

Сложение

0x29

Вычитание

0x16

Логическая операция И

0x08

Логическая операция ИЛИ

0xD0

Сохранение результата в памяти

Как составляется программа?

Программу надо составлять прямо в коде, а конкретно в program.py. Вот пример программы сложения чисел 5 и 3:

program = [   
    0xA2, 0x05,
    0xA4, 0x03,
    0x58,
    0xD0, 0x10,
    0x00,
]
Результат программы
Результат программы

Заключение

Надеюсь, что кому-нибудь эта статья станет полезной, возможно она подтолкнёт кого-то на написание чего-то похожего.

Полный код можно посмотреть на моём GitHub.

С вами был Yura_FX. Спасибо, что дочитали данную статью до конца. Не забывайте делиться своим мнением в комментариях :)

Комментарии (27)


  1. DvoiNic
    22.11.2023 13:26
    +7

    вам еще учиться и учиться..


    1. Yura_FX Автор
      22.11.2023 13:26

      Да, это так, но я уверен, что с дальнейшей практикой и обучением я смогу достичь ещё более высокого уровня :)


      1. DvoiNic
        22.11.2023 13:26
        +1

        Прежде всего нужно научиться читать даташиты (например, прочитать, что есть не только аккумулятор и указатель команд, но и регистр переносов, регистр стека, регистры общего назначения). Освоить двоичную арифметику (что б не было "вы не можете получить число выше 15"). А уж потом браться за эмуляторы процессоров. Ничего сложного нет, но вы слишком рано начали делать эмулятор.

        Минусовать не стал чисто из-за того, чтоб не отбить желание программировать. Но плюсов вы точно не заслужили.


        1. Yura_FX Автор
          22.11.2023 13:26

          Спасибо, что уделили внимание моей статье и что даёте различные советы на будущее)


          1. DvoiNic
            22.11.2023 13:26

            Да пожалуйста. Если нужна будет конкретная помощь - обращайся в личку, свяжемся, например, в телеге.


  1. DSolodukhin
    22.11.2023 13:26
    +2

    Не, ну это не серьезно, только настроился на интересное чтиво, а статья уже и закончилась.


    1. Yura_FX Автор
      22.11.2023 13:26

      Да, статья короткая, но зато без воды. Как раз для быстрого чтения в перерывах между работой например)


  1. bodyawm
    22.11.2023 13:26
    +4

    Ну, elif для всех опкодов использовать эт конечно мощно :)) Я когда-то в 14 лет написал интерпретатор BASIC-подобного языка и похожим образом реализовывал команды, причём таким образом были реализованы и основные конструкции (объявление функций, if/else, циклы), но так делать не стоит.

    От абстрактного эмулятора процессора толку не очень много. Но гораздо интереснее эмулировать уже готовые устройства: с экранчиком, кнопочками или звуком, можете даже что-то своё придумать. Потом этот "эмулятор" можно воплотить в настоящее аппаратное устройство - например, на ESP32 ;)

    По советам: выделите отдельный словарь (коллекцию вида ключ = значение) для каждого опкода и реализацию для него, а-ля так (Python не знаю, но код должен быть понятен):

    void opMovImmediate(BinaryReader reader)
      {
        byte reg1 = reader.ReadByte(), reg2 = reader.ReadByte();
    
        ...
      }
    
    Dictionary<byte, Action<BinaryReader>> opcodes;
    
    ...
    
    opcodes.Add(I4004.MovImmediate, opMovImmediate);
    
    byte opcode = reader.ReadByte();
    
    if(opcodes.ContainsKey(opcode))
      opcodes[opcode](reader);
    else
      throw new ArgumentException("Unimplemented opcode " + opcode);

    Сами номера опкодов лучше выделить в отдельное перечисление. Можно вот так:

    enum I4004
    {
      Nop = 0,
      MovImmediate = 0x2
    }

    Регистры можно реализовывать по разному, однако учтите, что в x86 и некоторых других архитектурах с подходом а-ля один регистр - одна переменная не прокатит, поскольку bl/bh - младшие разряды "большого" 16-битного регистра bx и.т.п. Можно представить регистры как кучу и адресовать его соответственным образом.


    1. bodyawm
      22.11.2023 13:26
      +1

      Плюсик всё равно поставил. Пилить эмуляторы и интерпретаторы - дико увлекательное и интересное занятие на самом деле.


    1. Yura_FX Автор
      22.11.2023 13:26

      Благодарю за Ваши советы. Постараюсь их учесть :)


    1. ImagineTables
      22.11.2023 13:26
      +2

      одна переменная не прокатит, поскольку bl/bh - младшие разряды "большого" 16-битного регистра bx

      Почему же?

      struct registry_x
      {
          byte l;
          byte h;
      
          operator word () { return h << 8 + l; };
          operator = (word w) { l = word & 0xff; h = word >> 8; };
      };
      
      registry_x bx; // Одна переменная )))
      
      bx.l; = 1;
      bx.h; = 2;
      bx = 3;
      


      1. hello_my_name_is_dany
        22.11.2023 13:26
        +1

        Такое можно делать только в некоторых языках, да и придётся помнить про Big/Little Endian


        1. ImagineTables
          22.11.2023 13:26

          Я бы вообще предпочёл union или type… забыл термин. Когда (byte*) &my_word. Но с современными стандартами и компиляторами, куда ни плюнь — всюду UB.


          1. vesper-bot
            22.11.2023 13:26
            +1

            В Паскале это кажется называлось absolute, или case-type, типа такого:

            type registersplit=record
              case x of { неважно что писать в case..of, вплоть до бреда }
                1: l:byte; h:byte;
                2: x: word;
                3: ex: longint; { а не было dword в турбо-6.0 }
              end;
            end;
            var a:registersplit;
            begin
               a.h=32;
               writeln(a.x); { выведет 32*256=8192 ибо DS инициализируется нулями, a.l=0 }
            end.
            

            Не знаю, есть ли это вот ещё в Delphi/Lazarus, но если есть, точно не должно вести себя как UB.


    1. DvoiNic
      22.11.2023 13:26

      опкоды лучше (особенно в учебных целях) разбирать побитно - становится понятно, как процессор дешифрует команды.


  1. mark_ablov
    22.11.2023 13:26
    +3

    Я не знаю, какая ISA взята за основу, но это точно не i4004. Регистра B в i4004 не существует (в i8008 - есть), а регистры просто индексные - rr0..rr15. Опкоды левые. Некоторых инструкций даже в теории нет в i4004 (HLT появился только в i4040).

    Запись в память куда сложнее, чем просто одна инструкция. Да и память не линейна, о организована в виде банков и регистров памяти.

    Если что, у писал 2 эмулятора (i4004 и i4040), которые совместимы с железом с точностью до тактов.


    1. Yura_FX Автор
      22.11.2023 13:26

      Скажите пожалуйста, верные ли опкоды для инструкций я нашёл?
      NOP (No Operation): опкод - 0000.
      ADD (Сложение): опкод - 0001.
      SUB (Вычитание): опкод - 0100.
      AND (Логическое И): опкод - 0111.
      OR (Логическое ИЛИ): опкод - 1000.
      Загрузка числа в аккумулятор (acc): опкод - 1001.
      Загрузка числа в регистр: опкод - 1101.


      1. mark_ablov
        22.11.2023 13:26

        Нет, неверные.

        Советую почитать релевантные доки - MCS-4_Assembly_Language_Programming_Manual_Dec73 / MCS-4_UsersManual_Feb73 / MCS4_Data_Sheet_Nov71. Или даже просто datasheet на процессор.


        1. Yura_FX Автор
          22.11.2023 13:26

          Спасибо за информацию. В datasheet я мельком заглядывал сегодня, ещё раз загляну :)


  1. unclegluk
    22.11.2023 13:26

    Такой простой эмулятор можно реализовать даже без классов, если не стоит задача расширять его функционал до бесконечности.


  1. Neu256
    22.11.2023 13:26
    +1

    Можно даже попробовать сделать для этого эмулятора свой язык программррвания


    1. Yura_FX Автор
      22.11.2023 13:26

      Знаете, это было бы очень здорово))


      1. DvoiNic
        22.11.2023 13:26

        я б посоветовал сделать нормальный эмулятор, а для него - написать свой ассемблер. Бесполезно в плане применения, но даст хорошую практику.


      1. Neu256
        22.11.2023 13:26

        Что-то потипу смеси питона и плюсов. Ну либо 0х00 можно только через return 0 сделать нормально, а если автоматически как в питоне, я это даже не представляю как реализовать


  1. Paradius
    22.11.2023 13:26
    +1

    Спасибо тебе дружище за эту статью. Очень давно интересовала тема ассемблера и как оно все работает на самом низком уровне, но никак не мог подступиться. Увяз в чтении литературы, подобной Таненбауму и т. д. Вместо того, чтобы просто взять и начать что-то делать, мне всегда кажется что еще рано, мало знаний, надо еще что-то почитать. Твоя статья и твой до неприличия простой и понятный код, с простым примером вдохновили меня. Оказывается вот оно как просто. Машина просто шаг за шагом берет и читает инструкцию, выполняет ее, а потом переходит к следующей. Благодаря тебе, я скачал эмулятор 8086 и начал изучать ассемблер, жутко интересно. Очень хочется уловить момент перехода с ассемблера на более-менее сносный компилятор какого-нибудь простого ЯП.


    1. Yura_FX Автор
      22.11.2023 13:26

      Я очень рад, что Вам понравилось. Я старался :)


    1. DvoiNic
      22.11.2023 13:26

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

      Найдите книгу Хендрикс "Компилятор Small-c для микро-ЭВМ" (изд. Радио и связь, 1989) - там это объясняется практически на примере. Только кодогенерация для ассемблера 8080.