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

Хочешь попрактиковаться в кодинге на ассемблере? Давай вместе шаг за шагом создадим интерпретатор бейсика и запустим его прямо из загрузочного сектора твоего компьютера. Для этого придется задействовать перекрывающиеся подпрограммы с разветвленной рекурсией, иначе бейсик не уместится в 512 байт. Скорее всего, это будет самая сложная программа в твоей жизни. Когда ты создашь ее своими руками, сможешь без зазрения совести называть себя хакером.

 

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

По сути, написав бейсик для бутсектора, мы превратим твой ПК в аналог старых домашних компьютеров типа 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_statement я поставил дополнительный вход в ту же самую подпрограмму: @@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_statement возвращаться не будем. Нужный ret выполнит сама функция input.

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

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *