Хочешь попрактиковаться в кодинге на ассемблере? Давай вместе шаг за шагом создадим интерпретатор бейсика и запустим его прямо из загрузочного сектора твоего компьютера. Для этого придется задействовать перекрывающиеся подпрограммы с разветвленной рекурсией, иначе бейсик не уместится в 512 байт. Скорее всего, это будет самая сложная программа в твоей жизни. Когда ты создашь ее своими руками, сможешь без зазрения совести называть себя хакером.
С месяц назад я рассказывал, как написать игрушку FloppyBird, которая тоже умещалась в бутсектор. Но по сравнению с тем, что мы с тобой сотворим сейчас, она покажется тебе мелкой шалостью.
По сути, написав бейсик для бутсектора, мы превратим твой ПК в аналог старых домашних компьютеров типа Commodore 64 или ZX Spectrum, которые имели этот язык в ПЗУ и позволяли программировать на нем сразу после загрузки.
Техническое задание (что будет уметь наш интерпретатор) я сформулирую в виде инструкции пользователя. Вот она.
Интерпретатор работает в двух режимах: интерактивном и обычном. В интерактивном режиме он выполняет команды сразу после ввода.
В обычном режиме сначала надо занести исходник программы в память и затем дать команду run
.
Если нужно удалить строку из исходника, просто введи в командной строке ее номер.
Как интерпретатор узнаёт, в каком режиме обрабатывать текст из командной строки? Если строка начинается с номера, интерпретатор обрабатывает ее в обычном режиме. Если не с номера — в интерактивном.
Максимальный размер программы — 999 строчек. Максимальная длина строки — 19 символов. Обрати внимание, что клавиша Backspace функционирует как надо. Хоть на экране символ и не затирается, в буфере все в порядке.
В распоряжении у программиста:
run
(стирает программу), list
(выводит исходник на экран), new
(запускает программу);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).
Начинаем с того, что задаем области памяти, которыми будем пользоваться:
Все сегментные регистры нацеливаем на 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
. Там интерпретатор восстановит указатель стека и сможет работать дальше, несмотря на то что наткнулся на синтаксическую ошибку.
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее
1 год7690 р. |
1 месяц720 р. |
Я уже участник «Xakep.ru»
Читайте также
Последние новости