Категория > Новости > МикроБ. Пишем бейсик на ассемблере и умещаем в 512 байт - «Новости»

МикроБ. Пишем бейсик на ассемблере и умещаем в 512 байт - «Новости»


13-05-2020, 16:09. Автор: Bradshaw
Хочешь попрактиковаться в кодинге на ассемблере? Давай вместе шаг за шагом создадим интерпретатор бейсика и запустим его прямо из загрузочного сектора твоего компьютера. Для этого придется задействовать перекрывающиеся подпрограммы с разветвленной рекурсией, иначе бейсик не уместится в 512 байт. Скорее всего, это будет самая сложная программа в твоей жизни. Когда ты создашь ее своими руками, сможешь без зазрения совести называть себя хакером.
МикроБ. Пишем бейсик на ассемблере и умещаем в 512 байт - «Новости»
INFO

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



Как пользоваться интерпретатором


По сути, написав бейсик для бутсектора, мы превратим твой ПК в аналог старых домашних компьютеров типа Commodore 64 или ZX Spectrum, которые имели этот язык в ПЗУ и позволяли программировать на нем сразу после загрузки.


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


Интерпретатор работает в двух режимах: интерактивном и обычном. В интерактивном режиме он выполняет команды сразу после ввода.




В обычном режиме сначала надо занести исходник программы в память и затем дать команду run.


Если нужно удалить строку из исходника, просто введи в командной строке ее номер.


Как интерпретатор узнаёт, в каком режиме обрабатывать текст из командной строки? Если строка начинается с номера, интерпретатор обрабатывает ее в обычном режиме. Если не с номера — в интерактивном.


Максимальный размер программы — 999 строчек. Максимальная длина строки — 19 символов. Обрати внимание, что клавиша Backspace функционирует как надо. Хоть на экране символ и не затирается, в буфере все в порядке.


В распоряжении у программиста:


  • три команды: run (стирает программу), list (выводит РёСЃС…РѕРґРЅРёРє РЅР° экран), new (запускает программу);

  • 26 переменных (от a до z): двухбайтовые целые числа без знака;

  • выражения, которые могут включать в себя: числа, четыре арифметические операции, скобки, переменные;

  • три оператора: if, goto, =;

  • РґРІРµ функции: print, input.

Вот языковые конструкции, которые понимает наш интерпретатор:


  • var=expr присваивает значение expr переменной var (от a до z);

  • print expr выводит значение expr и переводит курсор на следующую строку;

  • print expr; выводит значение expr и оставляет курсор на текущей строке;

  • print "][ello" печатает строку Рё переводит РєСѓСЂСЃРѕСЂ РЅР° следующую строку;

  • print "][ello"; печатает строку и оставляет курсор на текущей строке;

  • input var считывает значение СЃ клавиатуры, помещает его РІ переменную var (a..z);

  • goto expr переходит РЅР° указанную строку программы;

  • if expr1 goto expr2 — если expr1 РЅРµ 0, прыгнуть РЅР° строку expr2, иначе на следующую после if.

Пример: if c-5 goto 2 (если c-5 не 0, прыгаем на строку 2).


Начинаем делать интерпретатор


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


  • буфер для текста из командной строки;

  • буфер для хранения исходника программы;

  • массив для хранения переменных (от a до z);

  • указатель на текущую строку программы.



Все сегментные регистры нацеливаем на CS. Затем сбрасываем «флаг направления», чтобы строки обрабатывались слева направо, Р° РЅРµ наоборот (РєРѕРіРґР° будем обращаться Рє инструкциям РІСЂРѕРґРµ stosb). Буфер, который предназначен для исходника программы, заполняем символом 0x0D (СЃРёРјРІРѕР» возврата каретки, более известный как клавиша Enter).


Р?СЃС…РѕРґРЅРёРє программы РЅР° бейсике будем обрабатывать как двумерный символьный массив: 1000 Г— 20.


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


 

Запускаем главный рабочий цикл


Здесь сначала восстанавливаем указатель стека (регистр SP). На тот случай, если программа на бейсике обрушилась из-за ошибки.


Затем сбрасываем указатель running (текущая строка программы). Потом вызываем подпрограмму input_line, которая ждет, РїРѕРєР° программист что-РЅРёР±СѓРґСЊ напечатает. Подпрограмма сохраняет полученную строку РІ регистр SI.


Дальше смотрим, начинается строка с номера или нет. Если с номера, нам надо записать ее в буфер, который отведен под исходник. Для этого сначала вычисляем адрес, куда записывать строку. За это у нас отвечает подпрограмма find_address (результат кладет РІ регистр DI). Определив нужный адрес, копируем туда строку: rep movsb.


Если в начале строки нет номера, сразу выполняем ее: execute_statement.



Обрабатываем строки программы


Строки программы обрабатываем следующим образом. Берем первое слово из строки и последовательно сравниваем его с каждой записью из таблицы @@statements (СЃРј. РІРЅРёР·Сѓ статьи последний РєСѓСЃРѕРє РєРѕРґР°). Р’ этой таблице общим СЃРїРёСЃРєРѕРј перечислены команды, операторы Рё функции, которые понимает наш интерпретатор.


Обрати внимание, какую эвристику СЏ здесь использую, чтобы сэкономить байты РЅР° обработку условного оператора. Перед точкой РІС…РѕРґР° execute_statment я поставил дополнительный вход в ту же самую подпрограмму: @@if_handler.


Зачем? Чтобы РЅРµ надо было писать отдельный обработчик для конструкций РІСЂРѕРґРµ if a-2 goto 10. Если результат выражения (в данном случае a-2) равняется нулю, РјС‹ РЅРµ заходим РІ if, то есть игнорируем остаток строки (в нашем случае goto 10).


РЎ if разобрались. Дальше обрабатываем остальные команды, операторы и функции. Начинаем с того, что пропускаем лишние пробелы, которые программист добавил для своего удобства. Если в строке нет ничего, кроме пробелов, просто игнорируем ее.




Но если строка не пустая, присматриваемся к ней внимательно. Сначала перебираем по порядку таблицу @@statements Рё сверяем СЃРІРѕСЋ строку СЃ каждой записью оттуда. Каким образом сверяем? Считываем размер строки (РІ случае run это 3) Рё затем сравниваем, используя repe / cmpsb.


Если совпадение обнаружилось, то регистр DI теперь указывает РЅР° соответствующий адрес обработчика. Поэтому РјС‹ без лишних телодвижений прыгаем туда: jmp [di]. Чтобы лучше понять, в чем тут прикол, загляни в конец статьи, посмотри, как устроена таблица @@statements. Подсказка: метки, которые начинаются СЃ @@, — это как раз и есть адреса обработчиков.


Если всю таблицу перебрали, но совпадения так и не нашли, значит, текущая строка программы — это не команда, не оператор и не функция. Раз так, может быть, это название переменной? Прыгаем на @@to_get_var, чтобы проверить.


Дальше проматываем регистр DI к следующей записи таблицы. Каким образом? Прибавляем CX (длина имени текущей команды, оператора или функции плюс еще РґРІР° байта (адрес обработчика). Потом восстанавливаем значение регистра SI (rep cmpsb перемотала его вперед), чтобы РѕРЅ опять указывал РЅР° начало строки, РїРѕ которой РјС‹ выполняем РїРѕРёСЃРє РІ таблице операторов.


Теперь DI указывает на следующую запись из таблицы. Если эта запись ненулевая, прыгаем на @@next_entry, чтобы сравнить строку программы, вернее ее начало, СЃ этой записью.


Если РјС‹ прошли РІСЃСЋ таблицу, РЅРѕ так Рё РЅРµ нашли совпадения, значит, текущая строка — РЅРµ команда, РЅРµ оператор Рё РЅРµ функция. Р’ таком случае это, скорее всего, конструкция присваивания РІСЂРѕРґРµ var=expr. По идее, других вариантов больше нет. Если, конечно, в исходник не закралась синтаксическая ошибка.


Теперь нам надо вычислить выражение expr Рё поместить результат РїРѕ адресу, СЃ которым связана переменная var. Подпрограмма get_variable вычисляет нужный нам адрес Рё кладет его РЅР° стек.


После того как адрес найден, проверяем, есть ли после имени переменной оператор присваивания. Если да, нам надо его выполнить. Но в целях экономии байтов мы сделаем это не здесь.


Чуть ниже нам СЃ тобой так Рё так придется реализовывать присваивание внутри функции input. Вот на тот кусок кода мы и прыгнем: @@assign. Целиком нам тут функция input ни к чему. Понадобится только ее финальная часть, вот ее и берем. Обратно в execute_statment возвращаться РЅРµ будем. Нужный ret выполнит сама функция input.


Если знака присваивания нет, печатаем сообщение РѕР± ошибке Рё прекращаем выполнение программы, то есть прыгаем РЅР° @@main_loop. Там интерпретатор восстановит указатель стека и сможет работать дальше, несмотря на то что наткнулся на синтаксическую ошибку.




Перейти обратно к новости