Следующая новость
Предыдущая новость

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

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

Содержание статьи

  • Как пользоваться интерпретатором
  • Начинаем делать интерпретатор
  • Запускаем главный рабочий цикл
  • Обрабатываем строки программы
  • Выполняем команду list
  • Выполняем функцию input
  • Обрабатываем выражения
  • Подпрограмма: адрес переменной по ее имени
  • Подпрограмма: печать десятичного числа
  • Подпрограмма: из десятичной строки в шестнадцатеричное число
  • Выполняем команды run/goto
  • Подпрограмма: принимаем с клавиатуры строки исходника
  • Выполняем функцию print
  • Подпрограммы: ввод-вывод символов
  • Таблица команд, функций и операторов
  • Тестируем интерпретатор: пишем программу «Треугольник Паскаля»

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

INFO

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

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

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

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

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

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

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

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

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

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

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

Максимальный размер программы — 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);
  • указатель на текущую строку программы.
МикроБ. Пишем бейсик на ассемблере и умещаем в 512 байт

Все сегментные регистры нацеливаем на CS. Затем сбрасываем «флаг направления», чтобы строки обрабатывались слева направо, а не наоборот (когда будем обращаться к инструкциям вроде stosb). Буфер, который предназначен для исходника программы, заполняем символом 0x0D (символ возврата каретки, более известный как клавиша Enter).

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

Исходник программы на бейсике будем обрабатывать как двумерный символьный массив: 1000 × 20.

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

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

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

Затем сбрасываем указатель running (текущая строка программы). Потом вызываем подпрограмму input_line, которая ждет, пока программист что-нибудь напечатает. Подпрограмма сохраняет полученную строку в регистр SI.

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

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

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

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

Строки программы обрабатываем следующим образом. Берем первое слово из строки и последовательно сравниваем его с каждой записью из таблицы @@statements (см. внизу статьи последний кусок кода). В этой таблице общим списком перечислены команды, операторы и функции, которые понимает наш интерпретатор.

Обрати внимание, какую эвристику я здесь использую, чтобы сэкономить байты на обработку условного оператора. Перед точкой входа execute_statment я поставил дополнительный вход в ту же самую подпрограмму: @@if_handler.

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

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

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

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

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

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

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

Дальше проматываем регистр DI к следующей записи таблицы. Каким образом? Прибавляем CX (длина имени текущей команды, оператора или функции плюс еще два байта (адрес обработчика). Потом восстанавливаем значение регистра SI (rep cmpsb перемотала его вперед), чтобы он опять указывал на начало строки, по которой мы выполняем поиск в таблице операторов.

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

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

Если мы прошли всю таблицу, но так и не нашли совпадения, значит, текущая строка — не команда, не оператор и не функция. В таком случае это, скорее всего, конструкция присваивания вроде var=expr. По идее, других вариантов больше нет. Если, конечно, в исходник не закралась синтаксическая ошибка.

Теперь нам надо вычислить выражение expr и поместить результат по адресу, с которым связана переменная var. Подпрограмма get_variable вычисляет нужный нам адрес и кладет его на стек.

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

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

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

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

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

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее

1 год

7690 р.

1 месяц

720 р.

Я уже участник «Xakep.ru»

Источник

Последние новости