Создаем свой ассемблер

В одной из статей было рассказано, как создать свой собственный виртуальный 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;
}

Добавить комментарий