- PVSM.RU - https://www.pvsm.ru -

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

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

5.25" Floppy

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

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

Инструменты

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

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

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

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

Поскольку файл картинки и данных загружается без заголовочного блока (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 [5], которая содержит начальный адрес области бейсика. Таким образом, точка входа в загрузчик смещена на 91 байт относительно области бейсика.

  4. Ещё один способ посмотреть на загрузчик описан в статье Desativando a autoexecução de um programa BASIC [6]. В отладчике Fuse [7] нужно поставить точку останова 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 [4]. Если задаться целью, с её помощью игру можно разобрать до последнего винтика (сообщения, спрайта, звука). Как это делается, подробно описано в документации [8].

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

  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 [9]). Похоже, такой подход реализует какую-то защиту от копирования, т.к. стандартной процедурой этот файл не загружается и некоторые копировщики его не понимают.

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

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

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

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

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

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

Итого

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

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

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

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

  1. Профлицей «ТРУЪ Спектрумист» [12].
  2. Reverse engineering ZX Spectrum (Z80) games [13].
  3. Adaptação de jogos de fita para Beta 48 [14].

Автор: DeadMoroz

Источник [15]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/317270

Ссылки в тексте:

[1] Pac-Man: http://www.worldofspectrum.org/infoseekid.cgi?id=0003581

[2] Pac-Man.tzx: http://www.worldofspectrum.org/pub/sinclair/games/p/Pac-Man.tzx.zip

[3] Fuse: https://sourceforge.net/projects/fuse-emulator/

[4] SkoolKit: http://skoolkit.ca/

[5] PROG: https://skoolkid.github.io/rom/asm/5C53.html

[6] Desativando a autoexecução de um programa BASIC: http://cantinhotk90x.blogspot.com/2012/06/desativando-autoexecucao-de-um-programa.html

[7] отладчике Fuse: http://manpages.ubuntu.com/manpages/bionic/man1/fuse.1.html#monitor/debugger

[8] документации: https://skoolkit.readthedocs.io/en/v7.1/diy.html

[9] LD-BYTES: https://skoolkid.github.io/rom/asm/0556.html

[10] результат дизассемблирования: https://github.com/morozov/pacman/tree/master/skool

[11] репозитории проекта: https://github.com/morozov/pacman

[12] Профлицей «ТРУЪ Спектрумист»: https://zxrainbow.wordpress.com

[13] Reverse engineering ZX Spectrum (Z80) games: https://mrcook.uk/reverse-engineering-zx-spectrum-games

[14] Adaptação de jogos de fita para Beta 48: http://cantinhotk90x.blogspot.com/2012/07/adaptacao-de-jogos-de-fita-para-beta-48.html

[15] Источник: https://habr.com/ru/post/451174/?utm_campaign=451174