Адаптация программ для ZX Spectrum к TR-DOS современными средствами. Часть 1
В отличие от современных компьютеров, на спектрумах понятия файловой системы не было как такового. Это значит, что загрузка с каждого типа носителя требовала отдельной реализации и в большинстве случаев программу нельзя было просто так скопировать с кассеты на дискету. В случаях, когда загрузчик программы был написан на бейсике, его можно было адаптировать к TR-DOS довольно простой доработкой. Однако ситуация осложнялась тем, что во многих играх (как фирменных так и взломанных) загрузчики были написаны в машинных кодах и иногда содержали защиту от копирования.
Несмотря на наличие «волшебной кнопки», которая просто делала полный дамп памяти компьютера и позволяла хоть как-то сохранить программу на дискету, среди специалистов считалось хорошим тоном создавать дисковые версии игр с сохранением оригинальной загрузочной картинки и прочих атрибутов.
В этой статье я расскажу, как выполнить такую адаптация на примере игры Pac-Man, а именно, оригинального образа Pac-Man.tzx.
Инструменты
Несмотря на то, что в былые времена вся такая работа делалась непосредственно на ZX Spectrum (за отсутствием других вариантов), я буду адаптировать игру с использованием эмулятора и утилит командной строки. Основная причина в том, что особенно поначалу процесс адаптации состоит из большого количества проб и ошибок, и он проходит гораздо менее болезненно, если его автоматизировать. Всё то же самое можно проделать и непосредственно на спектруме.
В первой части мы будем использовать следующие инструменты:
- Эмулятор Fuse для отладки и тестирования.
- SkoolKit для дизассемблирования.
Отключение автозапуска в загрузчике
Поскольку файл картинки и данных загружается без заголовочного блока (17 байт с именем и типом файла), это означает, что загрузчик написан в машинных кодах. Нужно найти, где эти коды располагаются и с какого адреса запускаются.
Есть несколько способов посмотреть на код загрузчика:
Самый простой — начать загружать программу, дождаться, пока загрузчик запустится, и остановить его нажатием клавиши
Space
. Во многих случаях это работает, но в случае с Pacman, как и во многих других, это приводит к сбросу.Следующий способ — загрузить программу с использованием
MERGE ""
вместоLOAD ""
. В отличие отLOAD
,MERGE
игнорирует автозапуск программы. В случае с Pac-Man загрузка черезMERGE
приводит к зависанию компьютера с характерным сдвигом экрана влево. Это связано с тем, что вместо того, чтобы выполнять программу построчно,MERGE
пытается разобрать её целиком и слить с уже загруженной программой. Однако, если в программе есть блок с машинными кодами, который нарушает синтаксис программы, это приводит к сбою.Если не хочется ломать голову, можно преобразовать образ ленты из TZX в TAP и воспользоваться утилитой
listbasic
, которая поставляется вместе с Fuse:$ tzx2tap Pac-Man.tzx $ listbasic Pac-Man.tap 1 RANDOMIZE USR (PEEK 23635+256*PEEK 23636+91)
Адрес
23635
($5C53
) соответствует системной переменнойPROG
, которая содержит начальный адрес области бейсика. Таким образом, точка входа в загрузчик смещена на 91 байт относительно области бейсика.Ещё один способ посмотреть на загрузчик описан в статье Desativando a autoexecução de um programa BASIC. В отладчике Fuse нужно поставить точку останова
br 2053
, загрузить программу, а когда загрузка закончится и выполнение кода прервётся, выполнить записатьset 23619 128
. Это предотвратит автозапуск программы и позволит выйти в бейсика.
Дизассемблирование загрузчика
Зная смещение точки входа относительно области бейсика, можно рассчитать её абсолютный адрес. В случае с ZX Spectrum 48К без загруженной TR-DOS, область бейсика начинается с адреса 23755
($5CCB
). Следовательно, загрузчик будет начинаться с адреса 23755 + 91 = 23846
($5D26
).
Для начала достаточно поставить точку останова на начальном адресе и посмотреть на машинные коды. В Fuse можно сделать br 23846
и начать загружать программу. Как только загрузчик начнёт выполняться, эмулятор остановится:
В случае, когда загрузчик совсем простой, достаточно посмотреть на дизассемблированный код в средней панели и понять, что куда загружается. Обычно код загрузки беззаголовочного файла выглядит приблизительно так:
LD IX, $8000 ; начальный адрес загрузки
LD DE, $4000 ; длина загружаемого файла
LD A, $FF ; индикатор тела файла
CALL $0556 ; вызов LD-BYTES
JP $8000 ; переход в программу
В более сложном случае с выполнением кода нужно разбираться по шагам и делать пометки. Для этого хорошо подходит набор утилит SkoolKit. Если задаться целью, с её помощью игру можно разобрать до последнего винтика (сообщения, спрайта, звука). Как это делается, подробно описано в документации.
Если кратко, нужно сделать следующее:
- Сделать снапшот
Pac-Man.z80
памяти компьютера, используяtap2sna.py
или возможности эмулятора. - Создать контрольный файл
Pac-Man.ctl
с начальным набором инструкций для дизассемблирования:i 16384 Ignore for now c $5D26 Loader
- Запустить дизассемблирование:
sna2skool.py -H -c Pac-Man.ctl Pac-Man.z80 > Pac-Man.skool
. - В ходе изучения кода добавлять новые инструкции и комментарии в контрольный файл.
- Повторять до полного просветления.
В результате, после первого прохода получаем следующее (комментарии мои, адреса опущены):
ORG $5D26 ; те самые 23846, определённые выше
; Запрет прерываний
DI
IM 1
; Расшифровка загрузчика
LD D, IYh ;
LD E, IYl ;
LD B, $25 ; Длина зашифрованного загрузчика
EX DE, HL ;
LD DE, $0019 ;
ADD HL, DE ; На этом этапе HL содержит $5C53 (адрес переменной PROG)
LD E, (HL) ; Загружаем значение PROG в DE и IX
INC HL ;
LD D, (HL) ;
LD IXh, D ;
LD IXl, E ;
LD A, (IX+$7F) ; Загружаем ключ расшифровки в аккумулятор (находится в $7F-м байте
; относительно PROG)
LD HL, $0035 ; Начало зашифрованного загрузчика ($35 байт относительно PROG)
ADD HL, DE ;
PUSH HL ; Сохраняем адрес загрузчика на стеке
XOR (HL) ; Цикл расшифровки загрузчика
LD (HL), A ;
INC HL ;
DJNZ $5D43 ; Конец цикла
AND (HL) ;
RET NZ ; По окончании расшифровки переходим в загрузчик по адресу на стеке
; Ключ для расшифровки
DEFB $77
Расшифровка загрузчика
Всё, что из этого действительно важно, это то, что расшифрованный загрузчик находится по адресу PROG + $35
. Это значит, что если мы поставим точку останова br 23808
, то к этот момент расшифровка уже выполнится мы увидим расшифрованный загрузчик:
Эта программа уже гораздо более похожа на типичный случай, упомянутый выше. В регистры IX
и DE
загружается значение $4000
(16384
), делается что-то ещё и передаётся управление подпрограмме ПЗУ по адресу $055A
(это на несколько байт ниже чем стандартная точка входа в LD-BYTES
). Похоже, такой подход реализует какую-то защиту от копирования, т.к. стандартной процедурой этот файл не загружается и некоторые копировщики его не понимают.
Точка входа в программу
Осталось разобраться, как же вызывается программа после загрузки. Вместо привычного CALL LD-BYTES
и JP
здесь используется LD SP, XXXX
и JP LD-BYTES
. Первый (обычныйы) вариант работает следующим образом:
CALL
кладёт на стек текущее значение программного счётчика (PC
).- Управление передаётся вызываемой подпрограмме.
- При возврате из подпрограммы (
RET
) значение со стека снимается и происходит переход в вызывающую программу.
Почему здесь сделано иначе? Дело в том, что Pac-Man совместим с ZX Spectrum 16K и занимает абсолютно всю оперативную память (см. размер файла выше). Таким образом, загружаясь, программа затирает собой и загрузчик, и стек, где бы они ни находились. Если бы мы хотели перейти из ПЗУ в загрузчик с использованием стека и далее вызывать загруженную программу через JP
, на момент окончания загрузки ни адреса, по которому находится JP
, ни самой инструкции в памяти уже не было бы.
Вместо этого указатель стека перемещается на область памяти, по которому после загрузки окажется адрес точки входа в программу, и процессор, не заметив подмены, снимет его со стека по новому указателю и перейдёт по указанному адресу.
Полный результат дизассемблирования можно посмотреть в репозитории проекта на гитхабе.
Итого
В результате изучения загрузчика мы выяснили следующее:
- Беззаголовочный файл длиной 16384 байт загружается по адресу 16384 (в экранную область, что в общем-то очевидно в процессе загрузки).
- По окончании загрузки указатель стека находится по адресу
$5D7C
, куда и передаётся управление.
В следующих частях я расскажу, о том как подготовить файлы для записи на диск и написать загрузчик моноблочного файла на ассемблере.
Ссылки по теме:
- Профлицей «ТРУЪ Спектрумист».
- Reverse engineering ZX Spectrum (Z80) games.
- Adaptação de jogos de fita para Beta 48.