Вступление
С началом учебного года в мой техникум пришла такая штука, как Код будущего. Я и пара моих приятелей решили записаться на курсы по программированию на 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)
DSolodukhin
22.11.2023 13:26+2Не, ну это не серьезно, только настроился на интересное чтиво, а статья уже и закончилась.
Yura_FX Автор
22.11.2023 13:26Да, статья короткая, но зато без воды. Как раз для быстрого чтения в перерывах между работой например)
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 и.т.п. Можно представить регистры как кучу и адресовать его соответственным образом.
bodyawm
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;
hello_my_name_is_dany
22.11.2023 13:26+1Такое можно делать только в некоторых языках, да и придётся помнить про Big/Little Endian
ImagineTables
22.11.2023 13:26Я бы вообще предпочёл union или type… забыл термин. Когда
(byte*) &my_word
. Но с современными стандартами и компиляторами, куда ни плюнь — всюду UB.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.
DvoiNic
22.11.2023 13:26опкоды лучше (особенно в учебных целях) разбирать побитно - становится понятно, как процессор дешифрует команды.
mark_ablov
22.11.2023 13:26+3Я не знаю, какая ISA взята за основу, но это точно не i4004. Регистра B в i4004 не существует (в i8008 - есть), а регистры просто индексные - rr0..rr15. Опкоды левые. Некоторых инструкций даже в теории нет в i4004 (HLT появился только в i4040).
Запись в память куда сложнее, чем просто одна инструкция. Да и память не линейна, о организована в виде банков и регистров памяти.
Если что, у писал 2 эмулятора (i4004 и i4040), которые совместимы с железом с точностью до тактов.
Yura_FX Автор
22.11.2023 13:26Скажите пожалуйста, верные ли опкоды для инструкций я нашёл?
NOP (No Operation): опкод - 0000.
ADD (Сложение): опкод - 0001.
SUB (Вычитание): опкод - 0100.
AND (Логическое И): опкод - 0111.
OR (Логическое ИЛИ): опкод - 1000.
Загрузка числа в аккумулятор (acc): опкод - 1001.
Загрузка числа в регистр: опкод - 1101.mark_ablov
22.11.2023 13:26Нет, неверные.
Советую почитать релевантные доки - MCS-4_Assembly_Language_Programming_Manual_Dec73 / MCS-4_UsersManual_Feb73 / MCS4_Data_Sheet_Nov71. Или даже просто datasheet на процессор.
Yura_FX Автор
22.11.2023 13:26Спасибо за информацию. В datasheet я мельком заглядывал сегодня, ещё раз загляну :)
unclegluk
22.11.2023 13:26Такой простой эмулятор можно реализовать даже без классов, если не стоит задача расширять его функционал
добесконечности.
Neu256
22.11.2023 13:26+1Можно даже попробовать сделать для этого эмулятора свой язык программррвания
Yura_FX Автор
22.11.2023 13:26Знаете, это было бы очень здорово))
DvoiNic
22.11.2023 13:26я б посоветовал сделать нормальный эмулятор, а для него - написать свой ассемблер. Бесполезно в плане применения, но даст хорошую практику.
Neu256
22.11.2023 13:26Что-то потипу смеси питона и плюсов. Ну либо 0х00 можно только через return 0 сделать нормально, а если автоматически как в питоне, я это даже не представляю как реализовать
Paradius
22.11.2023 13:26+1Спасибо тебе дружище за эту статью. Очень давно интересовала тема ассемблера и как оно все работает на самом низком уровне, но никак не мог подступиться. Увяз в чтении литературы, подобной Таненбауму и т. д. Вместо того, чтобы просто взять и начать что-то делать, мне всегда кажется что еще рано, мало знаний, надо еще что-то почитать. Твоя статья и твой до неприличия простой и понятный код, с простым примером вдохновили меня. Оказывается вот оно как просто. Машина просто шаг за шагом берет и читает инструкцию, выполняет ее, а потом переходит к следующей. Благодаря тебе, я скачал эмулятор 8086 и начал изучать ассемблер, жутко интересно. Очень хочется уловить момент перехода с ассемблера на более-менее сносный компилятор какого-нибудь простого ЯП.
DvoiNic
22.11.2023 13:26Очень хочется уловить момент перехода с ассемблера на более-менее сносный компилятор какого-нибудь простого ЯП
Найдите книгу Хендрикс "Компилятор Small-c для микро-ЭВМ" (изд. Радио и связь, 1989) - там это объясняется практически на примере. Только кодогенерация для ассемблера 8080.
DvoiNic
вам еще учиться и учиться..
Yura_FX Автор
Да, это так, но я уверен, что с дальнейшей практикой и обучением я смогу достичь ещё более высокого уровня :)
DvoiNic
Прежде всего нужно научиться читать даташиты (например, прочитать, что есть не только аккумулятор и указатель команд, но и регистр переносов, регистр стека, регистры общего назначения). Освоить двоичную арифметику (что б не было "вы не можете получить число выше 15"). А уж потом браться за эмуляторы процессоров. Ничего сложного нет, но вы слишком рано начали делать эмулятор.
Минусовать не стал чисто из-за того, чтоб не отбить желание программировать. Но плюсов вы точно не заслужили.
Yura_FX Автор
Спасибо, что уделили внимание моей статье и что даёте различные советы на будущее)
DvoiNic
Да пожалуйста. Если нужна будет конкретная помощь - обращайся в личку, свяжемся, например, в телеге.