Programming
Assembler
Reverse engineering
Old hardware
May 11

Адаптация программ для ZX Spectrum к TR-DOS современными средствами. Часть 1

Tutorial

В отличие от современных компьютеров, на спектрумах понятия файловой системы не было как такового. Это значит, что загрузка с каждого типа носителя требовала отдельной реализации и в большинстве случаев программу нельзя было просто так скопировать с кассеты на дискету. В случаях, когда загрузчик программы был написан на бейсике, его можно было адаптировать к TR-DOS довольно простой доработкой. Однако ситуация осложнялась тем, что во многих играх (как фирменных так и взломанных) загрузчики были написаны в машинных кодах и иногда содержали защиту от копирования.


5.25" Floppy


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


В этой статье я расскажу, как выполнить такую адаптация на примере игры Pac-Man, а именно, оригинального образа Pac-Man.tzx.


Инструменты


Несмотря на то, что в былые времена вся такая работа делалась непосредственно на ZX Spectrum (за отсутствием других вариантов), я буду адаптировать игру с использованием эмулятора и утилит командной строки. Основная причина в том, что особенно поначалу процесс адаптации состоит из большого количества проб и ошибок, и он проходит гораздо менее болезненно, если его автоматизировать. Всё то же самое можно проделать и непосредственно на спектруме.


В первой части мы будем использовать следующие инструменты:


  1. Эмулятор Fuse для отладки и тестирования.
  2. SkoolKit для дизассемблирования.

Отключение автозапуска в загрузчике


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


Есть несколько способов посмотреть на код загрузчика:


  1. Самый простой — начать загружать программу, дождаться, пока загрузчик запустится, и остановить его нажатием клавиши Space. Во многих случаях это работает, но в случае с Pacman, как и во многих других, это приводит к сбросу.


  2. Следующий способ — загрузить программу с использованием MERGE "" вместо LOAD "". В отличие от LOAD, MERGE игнорирует автозапуск программы. В случае с Pac-Man загрузка через MERGE приводит к зависанию компьютера с характерным сдвигом экрана влево. Это связано с тем, что вместо того, чтобы выполнять программу построчно, MERGE пытается разобрать её целиком и слить с уже загруженной программой. Однако, если в программе есть блок с машинными кодами, который нарушает синтаксис программы, это приводит к сбою.


  3. Если не хочется ломать голову, можно преобразовать образ ленты из 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 байт относительно области бейсика.


  4. Ещё один способ посмотреть на загрузчик описан в статье 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 и начать загружать программу. Как только загрузчик начнёт выполняться, эмулятор остановится:


Debugger


В случае, когда загрузчик совсем простой, достаточно посмотреть на дизассемблированный код в средней панели и понять, что куда загружается. Обычно код загрузки беззаголовочного файла выглядит приблизительно так:


LD IX, $8000 ; начальный адрес загрузки
LD DE, $4000 ; длина загружаемого файла
LD A,  $FF   ; индикатор тела файла
CALL   $0556 ; вызов LD-BYTES
JP     $8000 ; переход в программу

В более сложном случае с выполнением кода нужно разбираться по шагам и делать пометки. Для этого хорошо подходит набор утилит SkoolKit. Если задаться целью, с её помощью игру можно разобрать до последнего винтика (сообщения, спрайта, звука). Как это делается, подробно описано в документации.


Если кратко, нужно сделать следующее:


  1. Сделать снапшот Pac-Man.z80 памяти компьютера, используя tap2sna.py или возможности эмулятора.
  2. Создать контрольный файл Pac-Man.ctl с начальным набором инструкций для дизассемблирования:
    i 16384 Ignore for now
    c $5D26 Loader
  3. Запустить дизассемблирование: sna2skool.py -H -c Pac-Man.ctl Pac-Man.z80 > Pac-Man.skool.
  4. В ходе изучения кода добавлять новые инструкции и комментарии в контрольный файл.
  5. Повторять до полного просветления.

В результате, после первого прохода получаем следующее (комментарии мои, адреса опущены):


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, то к этот момент расшифровка уже выполнится мы увидим расшифрованный загрузчик:


Loader


Эта программа уже гораздо более похожа на типичный случай, упомянутый выше. В регистры IX и DE загружается значение $4000 (16384), делается что-то ещё и передаётся управление подпрограмме ПЗУ по адресу $055A (это на несколько байт ниже чем стандартная точка входа в LD-BYTES). Похоже, такой подход реализует какую-то защиту от копирования, т.к. стандартной процедурой этот файл не загружается и некоторые копировщики его не понимают.


Точка входа в программу


Осталось разобраться, как же вызывается программа после загрузки. Вместо привычного CALL LD-BYTES и JP здесь используется LD SP, XXXX и JP LD-BYTES. Первый (обычныйы) вариант работает следующим образом:


  1. CALL кладёт на стек текущее значение программного счётчика (PC).
  2. Управление передаётся вызываемой подпрограмме.
  3. При возврате из подпрограммы (RET) значение со стека снимается и происходит переход в вызывающую программу.

Почему здесь сделано иначе? Дело в том, что Pac-Man совместим с ZX Spectrum 16K и занимает абсолютно всю оперативную память (см. размер файла выше). Таким образом, загружаясь, программа затирает собой и загрузчик, и стек, где бы они ни находились. Если бы мы хотели перейти из ПЗУ в загрузчик с использованием стека и далее вызывать загруженную программу через JP, на момент окончания загрузки ни адреса, по которому находится JP, ни самой инструкции в памяти уже не было бы.


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


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


Итого


В результате изучения загрузчика мы выяснили следующее:


  1. Беззаголовочный файл длиной 16384 байт загружается по адресу 16384 (в экранную область, что в общем-то очевидно в процессе загрузки).
  2. По окончании загрузки указатель стека находится по адресу $5D7C, куда и передаётся управление.

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


Ссылки по теме:


  1. Профлицей «ТРУЪ Спектрумист».
  2. Reverse engineering ZX Spectrum (Z80) games.
  3. Adaptação de jogos de fita para Beta 48.
+34
8.3k 46
Comments 51
Top of the day