В одной из статей было рассказано, как создать свой собственный виртуальный Forth-процессор, пригодный для исполнения некоторых команд, упрощающих процесс создания компиляторов/интерпретаторов стековых языков программирования. В этой же статье была продемонстрирована простейшая программа в форме шестнадцатеричных кодов, которые легко понимаются эмулятором того самого виртуального процессора, но написание более сложных программ (даже простейших нетривиальных процедур) является крайне сложным процессом.
В этот раз попробуем упростить написание программ для J1, используя предложение из той самой статьи о написании собственного ассемблера…
Создание ассемблера крайне трудоемкое дело, но только не в нашем случае: у нас слишком простой процессор — именно под такой мотивацией мы и будем создавать ассемблер, который будет транслировать некоторое подобие ассемблерных команд (они называются ассемблерным мнемониками или просто мнемониками) в их шестнадцатеричное представление, которое мы будем записывать в некоторый удобный файловый формат для последующего запуска.
Для начала создадим удобный синтаксис нашего ассемблера с учетом того, что этот синтаксис должен легко читаться, а также легко интерпретироваться транслятором. Обеспечивая минимальное множество поддерживаемых команд, которое при необходимости может быть расширено, синтаксис ассемблера будет выглядеть так:
<мнемоника_1> <мнемоника_2> ... tag <имя_метки_1> <мнемоника_1> <мнемоника_2> ... <имя процедуры>: <мнемоника_1> <мнемоника_2> ...
В таком ассемблере поддерживаются базовые мнемоники, которые обеспечивают исполнение минимальных инструкций процессора J1, условные/безусловные переходы на некоторую именованную метку, создание и вызов именновых процедур и размещение литералов в стек. Данные особенности транслятора освобождают от необходимости вручную расчитывать адреса переходов и размещения процедур, поскольку именование меток/процедур обеспечивает сокрытие адресов от программиста ценой невозможности ручного указания передачи управления по адресу (хотя это не особо и проблема: при необходимости можно добавить и интерпретацию числовых адресов вместо меток).
В ассемблере будут поддерживаться следующие инструкции:
nop нет операции add сложение xor исключающее или and побитовое и or побитовое или invert побитовое инвертирование eq равенство lt меньше ult беззнаковое меньше swap обмен значений стека dup дублирование вершины стека drop удалить вершину стека over поместить на вершину предпоследнее число из стека nip удаление предпоследнего числа из стека pushr поместить вершину стека данных в стек вызовов popr поместить вершину стека вызовов в стек данных load загрузить в стек значение из ячейки памяти store сохранить в ячейку памяти значение из стека dsp глубина стека данных lsh сдвиг влево rsh сдвиг вправо decr декремент up увеличить указатель стека данных на 1 down уменьшить указатель стека данных на 1 copy копирование halt останов процессора
Теперь приступим к реализации ассемблера и для начала определим необходимые «заимствования» из стандартной библиотеки и ряд псевдонимов для описания предметной области ассемблера:
import std.algorithm; import std.conv; import std.range; import std.stdio; import std.string; alias Instruction = ushort; alias Command = Instruction[]; alias TranslationTable = Command[string]; enum TranslationTable MNEMONICS = [ "nop" : [0x6000], "add" : [0x6202], "xor" : [0x6502], "and" : [0x6302], "or": [0x6402], "invert" : [0x6600], "eq" : [0x6702], "lt" : [0x6802], "ult" : [0x6f02], "swap" : [0x6180], "dup" : [0x6081], "drop" : [0x6102], "over" : [0x6181], "nip" : [0x6002], "pushr" : [0x6146], "popr" : [0x6b89], "load" : [0x6c00], "store" : [0x6022, 0x6102], "dsp" : [0x6e81], "lsh" : [0x6d02], "rsh" : [0x6902], "decr" : [0x6a00], "up" : [0x6001], "down" : [0x6002], "copy" : [0x6100], "halt": [0xffff] ]; alias Name = string; alias Address = ushort; alias JumpTable = Address[Name];
Псевдонимы обьеспечивают удобство для обозначения таких понятий как инструкция (т.е элементарная шестнадцатеричная команда J1), команда (т.е ряд последовательных шестнадцатеричных инструкций), а также трансляционную таблицу (таблица, которая определяет в какую последовательность инструкций транслируется каждая из поддерживаемых мнемоник). Также, мы определяем три псевдонима, которые будут описывать таблицу переходов по меткам/процедурам.
Алгоритм транслятора очень прост: загружаем исходный текст программы на ассемблере в транслятор, выполняем обработку исходного текста, заменяя мнемоники ассемблера на шестнадцатеричные команды и обрабатывая инструкции условного/безусловного перехода (заменяем эти команды заранее вычисленными шестнадцатеричными значениями), выполнив все подстановки и вычисления, сохраняем результат в файл в удобном формате.
Для выполнения намеченного алгоритма на первом этапе нужно выполнить своеобразное препроцессирование файла, вычислив адреса именнованных меток и адреса именованных процедур, поскольку необходимые «теги» (так условно назовем имена процедур/меток) могут встречаться как в начале файла с исходным кодом, так и в его конце, а это может усложнить процесс трансляции файла. Препроцессирование в этом случае будет означать генерирование таблицы переходов, которая представляет собой ассоциативный массив, в котором роль ключей будут выполнять «теги», а в роли значений — адреса «тегов». Генерирование таблицы переходов выглядит так: поскольку каждая команда ассемблера расположена с новой строки (это по сути дела обязательный элемент синтаксиса ассемблера), то сначала выполняется разбиение листинга на строки и инициализация счетчика адресов нулем. Далее, идет просмотр списка строк с постепенным увеличением на 1 счетчика адресов в том случае, если встреченная строка содержит или мнемонику или элементарную инструкцию (у нас 4 таких инструкции: push/jmp/jz/call), в противном же случае — увеличение счетчика адресов не произойдет, но может произойти добавление «тега» в таблицу переходов на основании его типа (т.е добавление адресов именованной метки отличается о добавления именованной процедуры). В остальных случаях препроцессирование игнорирует увеличение счетчика адресов, считая, что строка в таких случаях не несет никакой смысловой нагрузки. Также важный момент: приращение счетчика адреса в случае наличия мнемоники в трансляционной таблице осуществляется не на единицу, а на длину (т.е на количество элементарных шестнадцатеричных команд) мнемоники в инструкциях.
Код всей процедуры выглядит так:
// подготовка таблицы переходов auto createJumpTable(string assemblerListing, ref JumpTable table) { string[] preparedListing = assemblerListing.strip.splitLines; Address address = 0; foreach (assemblerMnemonic; preparedListing) { string mnemonic = assemblerMnemonic.strip.split[0].strip; // обработка управляющих инструкций if ((mnemonic == "push" ) || (mnemonic == "jmp" ) || (mnemonic == "jz" ) || (mnemonic == "call" )) { address++; } // обработка процедур if (mnemonic.endsWith(":")) { string procedureName = mnemonic[0..$-1]; table[procedureName] = address; } // обработка меток if (mnemonic == "tag") { string tagName = assemblerMnemonic.strip.split[1].strip; table[tagName] = address; } // обработка обычных инструкций if (mnemonic in MNEMONICS) { address += MNEMONICS[mnemonic].length; } } }
Следующий шаг ассемблера после получения таблицы переходов — это выполнение генерации последовательности шестнадцатеричных команд на основе таблицы переходов. Разбор ассемблерного листинга осуществляется следующим образом: производится разбиение исходного листинга на массив строк, далее из каждой строки извлекается мнемоника и/или базовая команда и ее аргумент, после чего происходит поиск мнемоники в трансляционной таблице и выборка из нее нужного кода и помещение его в массив шестнадцатеричных команд (т.е в итоговый результат ассемблерования). В случае, если окажется что мнемоника представляет собой одну из элементарных инструкций, то произойдет разбор инструкции и превращение ее в одну шестнадцатеричную команду, а дальнейшие операции совпадают с описанным ранее случаем.
Выглядит трансляция в шестнадцатеричные коды вот так:
// транслировать в шестнадцатеричные коды auto toAssemblerCodes(string assemblerListing, JumpTable table) { Command command; string mnemonic, argument; string[] preparedListing = assemblerListing.strip.splitLines; foreach (assemblerMnemonic; preparedListing) { mnemonic = assemblerMnemonic.strip.split[0].strip; if (assemblerMnemonic.strip.split.length > 1) { argument = assemblerMnemonic.strip.split[1].strip; } else { argument = ""; } switch (mnemonic) { case "push": command ~= 0x8000 | to!Instruction(argument); break; case "jmp": command ~= 0x0000 | table[argument]; break; case "jz": command ~= 0x2000 | table[argument]; break; case "call": command ~= 0x4000 | table[argument]; break; case "ret": auto lastCommand = command[$-1]; command[$-1] = 0x1000 | lastCommand; break; default: break; } if (mnemonic in MNEMONICS) { command ~= MNEMONICS[mnemonic]; } } return command; }
Теперь самое интересное: нас интересует итоговый набор шестнадцатеричных кодов после ассемблирования необходимо преобразовать в файл удобного формата.
Помните, в статье про J1 я упоминал про то, что существуют и аппаратные его реализации в виде прпоектов для FPGA/ASIC ?
Файл удобного формата был бы очень кстати, если бы предполагалась загрузка кодов процессора прямо в плату FPGA, и тут стоит вспомнить то, что J1 работает с памятью некоторого размера и не имеет портов ввода/вывода. Это обстоятельство позволяет предположить, что тут удобнее всего был файл, который представляет собой нечто вроде слепка памяти, который можно напрямую загрузить в плату. Именно такие раздумья меня привели к поиску максимально простого формата файла, который легко бы разбирался и при этом его можно было бы напрямую загрузить в плату (хотя я таким и не занимался)…
К счастью, формат нашелся и называется он Memory Initialization File (или сокращенно MIF). Используется данный формат для инициализации модулей памяти, которые работают внутри FPGA плат фирмы Altera (кстати, уже давно эта контора является подразделением компании Intel) и он очень простой — это текстовой файл (не бинарный) с простым человекочитаемым заголовком и его очень просто разбирать.
Вот примерно так выглядит этот файл:
-- Quartus II generated Memory Initialization File (.mif) WIDTH=<ширина значений, помещаемых в память>; DEPTH=<количество значений>; ADDRESS_RADIX=HEX; DATA_RADIX=HEX; CONTENT BEGIN <первый адрес> : <значение>; ... <последний адрес> : <значение>; END;
В нашем случае, WIDTH=16 (так как у нас 16-битные значения), DEPTH=16384 (так как у нас память включает в себя именно столько значений), ADRESS_RADIX и DATA_RADIX указывают на то в каком формате будут указаны адреса и значения, в нашем случае это шестнадцатеричные коды из 4х цифр.
Реализация записи полученных шестнадцатеричных команд в альтеровский файл инициализации памяти может быть описана следующим образом:
// превратить в файл инициализации Альтеры auto toMIF(Command command, string filename, ushort depth = 16_384) { enum string HEADER = `-- Quartus II generated Memory Initialization File (.mif) WIDTH=16; DEPTH=%d; ADDRESS_RADIX=HEX; DATA_RADIX=HEX; CONTENT BEGIN `; while (command.length < depth) { command ~= cast(ushort) 0xFFFF; } File file; file.open(filename, "w"); file.writef(HEADER, depth); for (ushort i = 0; i < command.length; i++) { string index = format("%0.4x", i).toUpper; string data = format("%0.4x", command[i]).toUpper; file.writefln("\t%s : %s;", index, data); } file.write("END;"); }
Объединить процедуры в единый механизм транслятора, который будет обрабатывать произвольный файл с листингом и записывать в любой иной файл можно следующим образом (не претендуя на полноту реализации, конечно):
import std.algorithm; import std.conv; import std.range; import std.stdio; import std.string; alias Instruction = ushort; alias Command = Instruction[]; alias TranslationTable = Command[string]; enum TranslationTable MNEMONICS = [ "nop" : [0x6000], "add" : [0x6202], "xor" : [0x6502], "and" : [0x6302], "or": [0x6402], "invert" : [0x6600], "eq" : [0x6702], "lt" : [0x6802], "ult" : [0x6f02], "swap" : [0x6180], "dup" : [0x6081], "drop" : [0x6102], "over" : [0x6181], "nip" : [0x6002], "pushr" : [0x6146], "popr" : [0x6b89], "load" : [0x6c00], "store" : [0x6022, 0x6102], "dsp" : [0x6e81], "lsh" : [0x6d02], "rsh" : [0x6902], "decr" : [0x6a00], "up" : [0x6001], "down" : [0x6002], "copy" : [0x6100], "halt": [0xffff] ]; alias Name = string; alias Address = ushort; alias JumpTable = Address[Name]; // подготовка таблицы переходов auto createJumpTable(string assemblerListing, ref JumpTable table) { string[] preparedListing = assemblerListing.strip.splitLines; Address address = 0; foreach (assemblerMnemonic; preparedListing) { string mnemonic = assemblerMnemonic.strip.split[0].strip; // обработка управляющих инструкций if ((mnemonic == "push" ) || (mnemonic == "jmp" ) || (mnemonic == "jz" ) || (mnemonic == "call" )) { address++; } // обработка процедур if (mnemonic.endsWith(":")) { string procedureName = mnemonic[0..$-1]; table[procedureName] = address; } // обработка меток if (mnemonic == "tag") { string tagName = assemblerMnemonic.strip.split[1].strip; table[tagName] = address; } // обработка обычных инструкций if (mnemonic in MNEMONICS) { address += MNEMONICS[mnemonic].length; } } } // транслировать в шестнадцатеричные коды auto toAssemblerCodes(string assemblerListing, JumpTable table) { Command command; string mnemonic, argument; string[] preparedListing = assemblerListing.strip.splitLines; foreach (assemblerMnemonic; preparedListing) { mnemonic = assemblerMnemonic.strip.split[0].strip; if (assemblerMnemonic.strip.split.length > 1) { argument = assemblerMnemonic.strip.split[1].strip; } else { argument = ""; } switch (mnemonic) { case "push": command ~= 0x8000 | to!Instruction(argument); break; case "jmp": command ~= 0x0000 | table[argument]; break; case "jz": command ~= 0x2000 | table[argument]; break; case "call": command ~= 0x4000 | table[argument]; break; case "ret": auto lastCommand = command[$-1]; command[$-1] = 0x1000 | lastCommand; break; default: break; } if (mnemonic in MNEMONICS) { command ~= MNEMONICS[mnemonic]; } } return command; } // превратить в файл инициализации Альтеры auto toMIF(Command command, string filename, ushort depth = 16_384) { enum string HEADER = `-- Quartus II generated Memory Initialization File (.mif) WIDTH=16; DEPTH=%d; ADDRESS_RADIX=HEX; DATA_RADIX=HEX; CONTENT BEGIN `; while (command.length < depth) { command ~= cast(ushort) 0xFFFF; } File file; file.open(filename, "w"); file.writef(HEADER, depth); for (ushort i = 0; i < command.length; i++) { string index = format("%0.4x", i).toUpper; string data = format("%0.4x", command[i]).toUpper; file.writefln("\t%s : %s;", index, data); } file.write("END;"); } void main(string[] args) { import std.file; auto assemblerListing = cast(string) std.file.read(args[1]); JumpTable jumps; createJumpTable(assemblerListing, jumps); assemblerListing.toAssemblerCodes(jumps).toMIF(args[2]); }
Теперь для испытаний напишем простой цикл для J1 c помощью которого выполним умножение двух чисел:
push 5 push 5000 store jmp cycle multiply: add ret tag cycle push 1024 call multiply push 5000 load decr dup jz end push 5000 store jmp cycle tag end halt
Что происходит в ассемблерном листинге ?
Для начала размещаем число 5 в ячейке памяти по адресу 5000 (число 5 - это второй множитель) и осуществляем переход на метку cycle, где начинается весь основной цикл. После перехода размещаем первый множитель - число 1024 в стеке, после чего вызываем процедуру умножения (она просто осуществляет сложение двух чисел в стеке, но будет вызывана несколько раз, что и приведет к получению результата умножения), затем размещаем адрес первого множителя в стеке (это число 5000), затем используем адрес для загрузки значения из памяти в стек, выполняем уменьшение на единицу загруженного значения, выполняем дублирование уменьшенного значения, команда jz выполняет сравнение значения на стеке с 0, и если сравнение было успешным, то выполняется переход на метку end (т.е выполянется переход на условие окончания программы). Если переход не удался, то выполняется следующая за jz инструкция, т.о после этого мы помещаем в стек число 5000 (адрес ячейки памяти со вторым множителем) и выполняем переход на метку cycle. Переход на метку cycle фактически дает бесконечный цикл, единственным условием выхода из которого служит наличие нуля на вершине стека - данное условие проверяется инструкцией jz, а конечным сам цикл делает уменьшение на единицу значения ячейки памяти с последующим размещением этого значения в стеке и дублированием его. Дублирование в нашем случае нужно для того, что значение могло быть использовано для последующего выполнения перехода на условие окончание, которым является служебная команда halt.
Прежде чем проводить испытания данного кода, нам потребуется исходный код эмулятора J1, которы можно взять из статьи про виртуальный процессор. Также необходимо внести правку в один из методов класса J1_CPU, а именно в метод executeProgram:
void executeProgram() { // 0xffff = HALT while (RAM[programCounter] != 0xffff) { writefln("{pc : %d, instruction : %0.4x}", programCounter, RAM[programCounter]); // RAM.toMIF("dump.mif"); execute(RAM[programCounter]); print; } }
правка небольшая и обеспечивает несколько иную интерпретацию команды halt, которая теперь имеет шестнадцатеричный код 0xffff (также можно раскоментировать закоментированную строку, чтобы в конце работы программы иметь дамп памяти процессора в удобном виде, правда для этого необходимо скопировать процедуру toMIF из исходных кодов ассемблера). После этого добавляем процедуру загрузки MIF-файла и модифицируем процедуру main для того, чтобы запускать программу, заключенную в MIF-файле:
auto fromMIF(string filename) { ushort[16_384] commands; auto content = cast(string) std.file.read(filename); auto begin = content.indexOf("CONTENT BEGIN") + "CONTENT BEGIN".length; auto end = content.indexOf("END;"); content = content[begin..end].strip; foreach (index, line; content.splitLines) { auto separatorIndex = line.indexOf(":") + 1; auto dataLine = line[separatorIndex..$-1].strip.toLower; commands[index] = parse!ushort(dataLine, 16); } return commands; } void main(string[] args) { J1_CPU j1 = new J1_CPU; uint16[16_384] ram = fromMIF(args[1]); j1.setMemory(ram); j1.executeProgram; }
Теперь можно запускать J1 с описанной выше ассемблерной программой, которая дает вот такой результат:
{pc : 0, instruction : 8005} [rs] : [0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 1, instruction : 9388} [rs] : [0, 5, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 2, instruction : 6022} [rs] : [0, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 3, instruction : 6102} [rs] : [0, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 4, instruction : 0006} [rs] : [0, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 6, instruction : 8400} [rs] : [0, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 7, instruction : 4005} [rs] : [0, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 5, instruction : 7202} [rs] : [1024, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 8, instruction : 9388} [rs] : [1024, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 9, instruction : 6c00} [rs] : [1024, 5, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 10, instruction : 6a00} [rs] : [1024, 4, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 11, instruction : 6081} [rs] : [1024, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 12, instruction : 2011} [rs] : [1024, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 13, instruction : 9388} [rs] : [1024, 4, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 14, instruction : 6022} [rs] : [1024, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 15, instruction : 6102} [rs] : [1024, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 16, instruction : 0006} [rs] : [1024, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 6, instruction : 8400} [rs] : [1024, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 7, instruction : 4005} [rs] : [1024, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 5, instruction : 7202} [rs] : [2048, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 8, instruction : 9388} [rs] : [2048, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 9, instruction : 6c00} [rs] : [2048, 4, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 10, instruction : 6a00} [rs] : [2048, 3, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 11, instruction : 6081} [rs] : [2048, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 12, instruction : 2011} [rs] : [2048, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 13, instruction : 9388} [rs] : [2048, 3, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 14, instruction : 6022} [rs] : [2048, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 15, instruction : 6102} [rs] : [2048, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 16, instruction : 0006} [rs] : [2048, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 6, instruction : 8400} [rs] : [2048, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 7, instruction : 4005} [rs] : [2048, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 5, instruction : 7202} [rs] : [3072, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 8, instruction : 9388} [rs] : [3072, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 9, instruction : 6c00} [rs] : [3072, 3, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 10, instruction : 6a00} [rs] : [3072, 2, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 11, instruction : 6081} [rs] : [3072, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 12, instruction : 2011} [rs] : [3072, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 13, instruction : 9388} [rs] : [3072, 2, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 14, instruction : 6022} [rs] : [3072, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 15, instruction : 6102} [rs] : [3072, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 16, instruction : 0006} [rs] : [3072, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 6, instruction : 8400} [rs] : [3072, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 7, instruction : 4005} [rs] : [3072, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 5, instruction : 7202} [rs] : [4096, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 8, instruction : 9388} [rs] : [4096, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 9, instruction : 6c00} [rs] : [4096, 2, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 10, instruction : 6a00} [rs] : [4096, 1, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 11, instruction : 6081} [rs] : [4096, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 12, instruction : 2011} [rs] : [4096, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 13, instruction : 9388} [rs] : [4096, 1, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 14, instruction : 6022} [rs] : [4096, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 15, instruction : 6102} [rs] : [4096, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 16, instruction : 0006} [rs] : [4096, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 6, instruction : 8400} [rs] : [4096, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 7, instruction : 4005} [rs] : [4096, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 5, instruction : 7202} [rs] : [5120, 1024, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 8, instruction : 9388} [rs] : [5120, 5000, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 9, instruction : 6c00} [rs] : [5120, 1, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 10, instruction : 6a00} [rs] : [5120, 0, 5000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 11, instruction : 6081} [rs] : [5120, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] {pc : 12, instruction : 2011} [rs] : [5120, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [rs] : [0, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Итак, в этой статье был описан простейший вариант ассемблера для виртуального процессора, который транслирует программу в файл инициализации памяти. Данный ассемблер дает код, который может быть пригоден для записи в память, которую можно сформировать внутри FPGA платы фирмы Altera. Конечно, не стоит рассчитывать на то, что описанный транслятор совершенен и что он пригоден для реальных боевых задач. Многое можно улучшить как в коде ассемблерного транслятора, так и в функционале (например можно добавить макродирективы и расширить набор команд), также многим может показаться, что мы напрямую привязаны к платам Altera, но весь приведенный код является экспериментальным и может быть именно вы, наш, читатель сможете внести свой вклад в его улучшение.
Напоследок прикладываю полный код процессора J1 с модификациями:
import std.conv : to; // 16-разрядное беззнаковое целое alias int16 = short; // 16-разрядное беззнаковое целое alias uint16 = ushort; class J1_CPU { private { enum RAM_SIZE = 16_384; // стек данных uint16[33] dataStack; // стек вызовов uint16[32] returnStack; // память uint16[RAM_SIZE] RAM; // указатель на вершину стека данных int16 dataPointer; // указатель на вершину стека вызовов int16 returnPointer; // счетчик инструкций uint16 programCounter; // маски для различения типов инструкций j1 enum J1_INSTRUCTION : uint16 { JMP = 0x0000, JZ = 0x2000, CALL = 0x4000, ALU = 0x6000, LIT = 0x8000 }; // маски для различения аргументов инструкций j1 enum J1_DATA : uint16 { LITERAL = 0x7fff, TARGET = 0x1fff }; // исполнение команд АЛУ auto executeALU(uint16 instruction) { uint16 q; uint16 t; uint16 n; uint16 r; // вершина стека if (dataPointer > 0) { t = dataStack[dataPointer]; } // элемент под вершиной стека if (dataPointer > 0) { n = dataStack[dataPointer - 1]; } // предыдущий адрес возврата if (returnPointer > 0) { r = returnStack[returnPointer - 1]; } // увеличить счетчик инструкций programCounter++; // извлечение кода операции АЛУ uint16 operationCode = (instruction & 0x0f00) >> 8; // опознание операций switch (operationCode) { case 0: q = t; break; case 1: q = n; break; case 2: q = to!uint16(t + n); break; case 3: q = t & n; break; case 4: q = t | n; break; case 5: q = t ^ n; break; case 6: q = to!uint16(~to!int(t)); break; case 7: q = (t == n) ? 1u : 0u; break; case 8: q = (to!int16(n) < to!int16(t)) ? 1u : 0u; break; case 9: q = n >> t; break; case 10: q = to!uint16(t - 1u); break; case 11: q = returnStack[returnPointer]; break; case 12: q = RAM[t]; break; case 13: q = to!uint16(n << t); break; case 14: q = to!uint16(dataPointer + 1u); break; case 15: q = (n < t) ? 1u : 0u; break; default: break; } // код действия с указателем на стек данных // (+1 - увеличить указатель, 0 - не трогать, -1 уменьшить (= 2 в двоичном коде)) uint16 ds = instruction & 0x0003; // код действия с указателем на стек возвратов // (+1 - увеличить указатель, 0 - не трогать, -1 уменьшить (= 2 в двоичном коде)) uint16 rs = (instruction & 0x000c) >> 2; switch (ds) { case 1: dataPointer++; break; case 2: dataPointer--; break; default: break; } switch (rs) { case 1: returnPointer++; break; case 2: returnPointer--; break; default: break; } // флаг NTI if ((instruction & 0x0020) != 0) { RAM[t] = n; } // флаг TR if ((instruction & 0x0040) != 0) { returnStack[returnPointer] = t; } // флаг TR if ((instruction & 0x0080) != 0) { dataStack[dataPointer-1] = t; } // флаг RPC if ((instruction & 0x1000) != 0) { programCounter = returnStack[returnPointer]; } if (dataPointer >= 0) { dataStack[dataPointer] = q; } } } public { auto execute(uint16 instruction) { // опознать тип инструкции uint16 instructionType = instruction & 0xe000; // операнд над которым осуществляется инструкция uint16 operand = instruction & J1_DATA.TARGET; // распознать конкретную инструкцию процессора switch (instructionType) { // безусловный переход case J1_INSTRUCTION.JMP: programCounter = operand; break; // переход на адрес, если на вершине стека 0 case J1_INSTRUCTION.JZ: if (dataStack[dataPointer] == 0) { programCounter = operand; } else { programCounter++; } dataPointer--; break; // передать управление на адрес case J1_INSTRUCTION.CALL: returnPointer++; returnStack[returnPointer] = to!uint16(programCounter + 1); programCounter = operand; break; // выполнить инструкцию АЛУ case J1_INSTRUCTION.ALU: executeALU(operand); break; // положить на стек литерал case J1_INSTRUCTION.LIT: operand = instruction & J1_DATA.LITERAL; dataPointer++; dataStack[dataPointer] = operand; programCounter++; break; default: break; } } this() { this.RAM = new uint16[RAM_SIZE]; this.dataPointer = 0; this.returnPointer = 0; this.programCounter = 0; } void print() { writeln("[rs] : ", dataStack); writeln("[rs] : ", returnStack); } void executeProgram() { // 0xffff = HALT while (RAM[programCounter] != 0xffff) { writefln("{pc : %d, instruction : %0.4x}", programCounter, RAM[programCounter]); // RAM.toMIF("dump.mif"); execute(RAM[programCounter]); print; } } void setMemory(uint16[RAM_SIZE] ram) { this.RAM = ram; } auto getMemory() { return RAM; } } } import std.algorithm; import std.conv; import std.file; import std.range; import std.string; import std.stdio; // превратить в файл инициализации Альтеры auto toMIF(uint16[16_384] command, string filename, ushort depth = 16_384) { enum string HEADER = `-- Quartus II generated Memory Initialization File (.mif) WIDTH=16; DEPTH=%d; ADDRESS_RADIX=HEX; DATA_RADIX=HEX; CONTENT BEGIN `; File file; file.open(filename, "w"); file.writef(HEADER, depth); for (ushort i = 0; i < command.length; i++) { string index = format("%0.4x", i).toUpper; string data = format("%0.4x", command[i]).toUpper; file.writefln("\t%s : %s;", index, data); } file.write("END;"); } auto fromMIF(string filename) { ushort[16_384] commands; auto content = cast(string) std.file.read(filename); auto begin = content.indexOf("CONTENT BEGIN") + "CONTENT BEGIN".length; auto end = content.indexOf("END;"); content = content[begin..end].strip; foreach (index, line; content.splitLines) { auto separatorIndex = line.indexOf(":") + 1; auto dataLine = line[separatorIndex..$-1].strip.toLower; commands[index] = parse!ushort(dataLine, 16); } return commands; } void main(string[] args) { J1_CPU j1 = new J1_CPU; uint16[16_384] ram = fromMIF(args[1]); j1.setMemory(ram); j1.executeProgram; }