LiteCoding

Заметки о программировании

Антиотладочные приёмы в Sinclair Basic

2 комментария

A screenshot from Rebelstar, a well-known Spectrum game

0. Введение
Sinclair Basic появился на свет в 1979 году и первоначально был разработан для ZX80. Удачная реализация и небольшие размеры позволила ему практически в неизменном виде перекочевать сначала на ZX Spectrum 16/48k, а затем и на более совершенные модели линейки ZX Spectrum. Эта статья посвящена антиотладочным приемам, которые широко использовались в программах, написанных на бейсике.

Прежде всего, стоит пояснить, что программа, записанная в ПЗУ «Спектрума», не содержит в себе никаких дополнительных средств, помогающих отладке. Поэтому исследование программы на бейсике банально сводились к загрузке с кассеты, нажатию клавиши BREAK (или SHIFT + пробел) и методичному исследованию исходного кода с помощью команды LIST. Временами это срабатывало, но для изучения большинства программ требуется хотя бы минимальный инструментарий. В настоящее время у нас есть эмуляторы, имеющие в своем составе дизассемблер, отладчик, редактор памяти (например, EmuZWin).

1. Sinclair Basic снаружи и изнутри
Программ, написанных непосредственно на бейсике, не так уж и много. К слову, когда у меня в 1994 году появился ZX Spectrum и пять кассет с играми, то из примерно сотни игр на бейсике была написана только одна. Но почти все остальные игры имели маленький загрузчик, который грузил блоки машинного кода и передавал управление. Как раз этот загрузчик был написан на бейсике. Естественно, разновидностей таких загрузчиков было довольно много. Мы разберем некоторые из них ниже.

Четырехкилобайтный размер интерпретатора не мог не добавить ряд ограничений. Разработчикам в процессе проектирования и реализации пришлось решить немало технических трудностей. Первое, от чего пришлось отказаться — синтаксический анализатор. Создатели бейсика предложили весьма элегантное решение — байткод. Причем, этот байткод генерируется «на лету». Налицо выигрыш процессорного времени во время введения команды и исполнения программы.

Каждое ключевое слово в бейсике имеет свой код в знакогенераторе. Саму таблицу можно посмотреть в википедии. Для нас же важно знать, что каждое ключевое слово занимает в памяти ровно 1 байт.

Также стоит обратить внимание на пятибайтный формат представления чисел. Каждой пятибайтной записи предшествует байт-маркер 0x0e. Целые числа от -65535 до 65535 кодируются по следующей маске: 0x00 0x00 LSB MSB 0x00. Вы видите, что знак целого числа тут не фигурирует, он берется из текстового представления числа. Но для наших целей вполне достаточно и положительных целых чисел.

Каждая строка программы на бейсике имеет следующий формат:
2 байта - номер строки (big endian)
2 байта - длина строки программы в байтах (little endian, без учета первых двух полей)
n байт - строка программы
1 байт - 0x0d

Строка программы содержит данные как для отображения на экране, так и для вычисления. Например, строка
10 LET a=32768
имеет следующее представление в байткоде:
0x00 0x0a - строка 10
0x0f 0x00 - длина равна 15 байт
0xf1 - LET
0x61 0x3d 0x33 0x32 0x37 0x36 0x36 - a=32768
0x0e 0x00 0x00 0x00 0x80 0x00 - 32768 в пятибайтном формате
0x0d - конец строки

А теперь начинается самое интересное. Что же можно сделать, зная внутреннюю кухню Sinclair Basic?

2. Строка номер ноль.
Строку с номером 0 ввести нельзя, как бы мы ни старались. Если она есть, то в листинге ее видно, но отредактировать не удастся. Однако, есть системная переменная, которая в документации называется PROG (в ZX Spectrum 48k она находится по адресу 0x5c53), в которой хранится адрес загруженной в память программы на бейсике. И весьма нехитрая манипуляция с этим адресом позволяет поменять номер у строки. Например, так:

10 LET addr=PEEK(23635) + (PEEK(23636) * 256)
20 POKE addr, 0
30 POKE addr + 1, 0

После запуска этой программы в ее листинге первая строка будет иметь номер 0.

3. Неупорядоченный листинг
На основе предыдущего примера можно перемешать номера строк, как заблагорассудится. Нет абсолютно никаких препятствий, чтобы после строки 10 шла строка 1, а за ней строка 600 и т.д. Выполняться программа будет по порядку размещения строк в памяти.

4. Выполняем машинный код внаглую
Есть такое зарезервированное слово — USR. Оно позволяет выполнить подпрограмму, находящуюся в памяти. Напрямую его использовать нельзя, но существует великое множество обходных путей. Например:

RANDOMIZE USR addr
PRINT USR addr
LET a=USR addr

А загрузить машинный код можно с использованием ключевых слов READ, POKE и DATA. Вот один из подобных загрузчиков, встречающихся в дикой природе:

1 REM FANTASY WORLD DIZZY
10 CLEAR 24319
20 FOR j=24576 TO 24594
30 READ a
40 POKE j,a
50 NEXT j
60 RANDOMIZE USR 24576
70 DATA 17,0,1,221,33,198,92,62,255,55,205,86,5,212,0,0,195,198,92

В этом фрагменте область памяти, используемая бейсиком, ограничивается адресом 24319. После этого оставшуюся память можно свободно использовать. В цикле с 20-й по 50-ю строку грузятся данные из 70-й строки. Эти данные записываются по адресу 24576, а затем туда передается управление.

5. Прячем машинный код в комментариях
Собственно говоря, ничто не мешает хранить машинный код в комментариях. Конечно, с клавиатуры его не введешь. Но можно сначала зарезервировать достаточную область памяти в строке с ключевым словом REM, а затем вписать машинный код с помощью команды POKE. Я встречал этот прием вместе с приемом «неупорядоченный листинг». Дело было так:

28725 REM [тут 3 пробела для маскировки, а дальше машинный код]
20 CLEAR 24499: BORDER 0: PAPER 0: INK 0: CLS: RANDOMIZE USR 23875

Этот пример взят из загрузчика Dizzy 3.5, сконвертированного в файл для запуска в эмуляторе. Не знаю, был ли этот код на диске, или он был добавлен после конвертации, но пример все равно довольно типичный. Для каждой программы на бейсике задается точка входа (номер строки, с которой начинается исполнение). В данном случае она была задана жестко — 28725. В принципе, тут мало отличий от предыдущего случая, за исключением того, что машинный код уже в памяти, и на него можно просто передать управление.
Гораздо интереснее, откуда взялся такой адрес для перехода. Чтобы сделать универсальный загрузчик, нужно взять значение из переменной PROG, затем прибавить 8 (2 байта на номер строки, 2 на длину строки, 1 на REM, 3 на пробелы), и уж затем переходить по полученному адресу. Но мы знаем, в каких условиях будет производиться запуск программы. Она грузится с дискеты в TR-DOS, следовательно, у нас есть дисковод. А это означает, что PROG будет показывать на адрес 23867, а не 23755 (по переменным TR-DOS есть хороший документ).

6. Написано «Ливерпуль», а читается «Манчестер»
Помните, чуть выше мы рассмотрели пятибайтный формат хранения чисел? Этот прием использует двойную запись числа во всей красе. Дело в том, что числа конвертируются в пятибайтный формат и добавляются в байткод самим интерпретатором бейсика. Но мы всегда можем поменять отображение числа в листинге или его значение. Существует целый класс загрузчиков, который сначала загружает машинный код в память (например, по 4-му или 5-му сценарию), а затем совершает настоящее самоубийство незамысловатым способом:

RANDOMIZE USR 0

(если вы не в курсе, в нормальных условиях эта команда оказывает эффект, аналогичный перезагрузке компьютера).

Но если внимательно посмотреть байткод, то там будет что-то вроде

0xf9 0xc0 0x30 - RANDOMIZE USR 0
0x0e 0x00 0x00 0x43 0x5d 0x00 - отображаем ноль, а переход будет на 23875

Этот прием хорошо срабатывает против невнимательного исследователя кода, если заметный ноль заменить на более традиционный адрес, по которому будет более-менее осмысленный машинный код.

7. Заключение
Конечно, это далеко не все, что можно сказать о трюках. Можно придумать (да и наверняка уже было придумано) множество способов противодействия исследованию кода, и часть из них описана в этой статье. Также информацию по антиотладочным приемам (не только на бейсике, но и для ZX Spectrum в целом) можно найти в интереснейшем руководстве How to Hack on the ZX Spectrum. И не ленитесь сами время от времени смотреть, что скармливаете эмулятору, а помогут вам в этом утилиты с сайта http://www.zxmodules.de/. Уверяю, вас ждет множество интересных открытий.

Изначально эта статья была опубликована мной на Хабрахабре.

Share and Enjoy:
  • Print
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks
  • LinkedIn
  • Tumblr

Written by Дмитрий Воробьев

Четверг, Сентябрь 23rd, 2010 at 08:35

Posted in Статьи

Tagged with ,

2 комментария to 'Антиотладочные приёмы в Sinclair Basic'

Subscribe to comments with RSS or TrackBack to 'Антиотладочные приёмы в Sinclair Basic'.

  1. Жалко, что данный компьютер умер.
    Хорошее время было…
    Сам себе его собирал, увеличивал память, подключал муз. сопроцессор.

    google.com Konstantin Kosarev

    16 Мар 11 at 20:39

  2. Зато сейчас можно вволю поностальгировать, запустив любимые игрушки на эмуляторе. Для меня — это серия про Dizzy. Именно она предопределила мою любовь к квестам и приключенческим играм.

Leave a Reply

You must be logged in to post a comment.