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

Как мы выяснили в предыдущей части, машинные коды игры загрузить с дискеты непосредственно по адресу назначения нельзя. Мы загрузим их в другое место, а после загрузки переместим куда нужно. Кроме этого, мы хотим сделать моноблочный загрузчик, когда и загрузчик и загружаемые данные находятся в одном бейсик-файле. Такой загрузчик можно написать только в машинных кодах. При этом, поскольку файл у нас моноблочный, загрузчик в машинных кодах нужно будет поместить в комментарии к загрузчику на бейсике.

Floppy 5.25

Итого, получается следующая многоходовка:


  1. Из бейсика передаём управление программе в машинных кодах.
  2. Программа в машинных кодах переносит загрузчик из области бейсика в другую область, которую не затронут машинные коды игры, и передаёт управление ему.
  3. Загружаем и распаковываем загрузочную картинку.
  4. Загружаем машинные коды игры в область, не перекрывающую область системных переменных.
  5. Переносим машинные коды по адресу назначения.
  6. Передаём управление программе.

Разработку придётся начать с середины (пункт 3). Дело в том, что для того, чтобы написать программу перемещения, нужно знать размер перемещаемой программы, а чтобы встроить машинные коды в бейсик, нужно знать размер программы перемещения.


Моноблочный загрузчик (часть в машинных кодах)

В TR-DOS загрузка данных моноблочного файла больше похожа на загрузку беззаголовочного файла с ленты, когда данные заранее известного размера просто читаются с текущей позиции и загружаются в определённую область памяти. За это в TR-DOS отвечает подпрограмма по адресу #3D13. Для начала загрузим и распакуем картинку:

LD DE, ($5CF4)   ; загружаем позицию головки дисковода из системной переменной
LD BC, $0805     ; регистр B содержит кол-во секторов (9)*,
                 ; регистр С — номер подпрограммы #05 (чтение секторов)
LD HL, $8000     ; загружаем по адресу 32768**
CALL $3D13       ; вызываем процедуру TR-DOS
CALL $8000       ; вызываем процедуру распаковщика

* — см. сжатие загрузочной картинки в предыдущей части;
** — распаковщик релоцируемый, так что загружать можно куда угодно.

Аналогичным образом загружаем машинные коды игры:

LD DE, ($5CF4)   ; загружаем позицию головки дисковода из системной переменной
LD BC, $2505     ; регистр B содержит кол-во секторов,
                 ; регистр С — номер подпрограммы #05 (чтение секторов)
LD HL, $6000     ; загружаем по адресу 24576
CALL $3D13       ; вызываем процедуру TR-DOS

На этом этапе TR-DOS нам больше не нужен, можно перенести машинные коды по адресу назначения, используя инструкция процессора LDIR:

LD HL, $6000   ; откуда (адрес, по которому мы загрузили код ранее)
LD DE, $5B00   ; куда
LD BC, $2500   ; количество байт для копирования (размер файла data.bin)
LDIR

Ну и в конце концов передаём управление программе тем же образом, как и в оригинальном загрузчике — через перемещение указателя стека:

LD SP, $5D7C
RET

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

$ pasmo tmp.asm tmp.bin
$ wc -c tmp.bin
44 tmp.bin


Процедура перемещения загрузчика

Загрузчик занимает 44 байта. Теперь нужно написать процедуру перемещения загрузчика из комментариев в бейсике (пункт 2 списка в начале статьи). Заковыка состоит в том, что адрес, по которому располагается область бейсика, может меняться в зависимости от подключённой к компьютеру периферии, поэтому, чтобы определить, откуда нужно переносить данные, нужно ориентироваться или на системную переменную PROG (так же как в оригинальном загрузчике) или на программный счётчик (регистр процессора PC).

К программному счётчику нельзя так просто доступиться — никаких инструкций процессора вроде LD HL, PC не существует. Решение я подсмотрел в Laser Compress и выглядит оно так (не особо целевое использование процедуры UNSTACK_Z):

LD DE, $00   ; пока что мы не знаем, сколько байт займёт процедура перемещения,
             ; поэтому оставляем здесь ноль. впоследствии здесь должен будет
             ; находиться размер минус 1
INC E        ; добавляем 1 к E, чтобы сбросить флаг нуля, и чтобы процедура
             ; ПЗУ пошла по нужной нам ветви. для этого мы однимали 1 выше
CALL $1FC6   ; вызываем процедуру ПЗУ (получаем результат, аналогичный LD HL, PC)
ADD HL, DE   ; прибавляем размер процедуры к её началу
LD DE, $F800 ; адрес перемещения загрузчика
LD BC, $002C ; длина загрузчика, определённая выше (44 байта)
LDIR
JP $F800     ; переход в загрузчик
             ; далее будет располагаться код загрузчика
             ; и именно этот адрес нам нужно определить

На момент вызова процедуры ПЗУ #1FC6 на стеке будет лежать адрес следующей инструкции (ADD HL, DE). Именно он и запишется в результате вызова процедуры в HL. Соответственно, чтобы определить число, которое нужно записать в самую первую строчку, нужно опять скомпилировать кусок от ADD HL, DE до конца и посмотреть, сколько он займёт:

$ pasmo tmp.asm tmp.bin
$ wc -c tmp.bin
12 tmp.bin

Получилось 12 байт. Соответственно, в первую строчку записываем 11 (#0B).

Далее компонуем процедуру перемещения с загрузчиком (см. готовый файл), который она будет перемещать и снова компилируем. Должно получиться 56 байт.

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


Моноблочный загрузчик (часть на бейсике)

Теперь, когда мы знаем размер загрузчика в машинных кодах, можно написать загрузчик на бейсике и собрать всё в моноблочный файл.

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

 1 REM @#$%...
10 RANDOMIZE USR (PEEK 23635+256*PEEK 23636+5)

23635 (#5C53) — это адрес системной переменной PROG, который мы уже упоминали ранее. 5 — это смещение первого символа комментария относительно PROG (2 байта занимает номер строки, 2 байта — длина строки и 1 байт оператор REM). Если вы хотите добавить какие-то ещё комментарии перед машинными кодами, например ваши имя, номер телефона или почтовый адрес, значение 5 нужно будет откорректировать.

Если бы для создания загрузчика мы не использовали никаких дополнительных утилит, нужно было бы после REM ввести произвольные символы в количестве не меньшем, чем длина программы в машинных кодах, которую мы хотим поместить на место комментария (в нашем случае 56 байт). После этого туда можно было бы загрузить программу через LOAD "" CODE PEEK 23635+256*PEEK 23636+5 и сохранить файл.

Однако, утилита bas2tap может значительно облегчить процесс, т.к. она может скомпилировать бейсик-файл и встроить в него двоичные данные, если каждый байт представлен в виде шестнадцатеричного числа в фигурных скобках. Для этого прогоним скомпилированный загрузчик через hexdump:

 $ hexdump -ve '1/1 "{%02x}"' loader.bin
 {11}{0b}{00}{1c}{cd}{c6}{1f}{19}{11}...

Вывод hexdump вставляем на место комментария в первой строке после REM и компилируем загрузчик на бейсике (-sboot — имя файла на ленте, -a10 — номер строки автостарта):

$ bas2tap -sboot -a10 boot.bas boot.tap

Преобразуем загрузчик из формата tap в hobeta через промежуточный формат 0:

$ tapto0 -f boot.tap
$ 0tohob boot.000


Создание моноблочного файла

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

createtrd Pac-Man.trd
hobeta2trd boot.\$$B Pac-Man.trd
hobeta2trd screen.\$$C Pac-Man.trd
hobeta2trd data.\$$C Pac-Man.trd

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

Принцип следующий: TR-DOS хранит избыточную информацию о размере файлов:


  1. Размер в секторах — используется для размещения файлов на дискете и копирования.
  2. Размер в байтах — используется для загрузки содержимого.

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

На настоящем спектруме или в эмуляторе нулевую дорожку можно редактировать программами типа Disk Doctor, например, Hex Disk Editor:

Hex Disk Editor

Но можно сделать и проще: trd-образ — это ничто иное как побайтовая копия всех данных на дискете, поэтому его можно редактировать в любом шестнадцатеричном редакторе:

$ hexdump -C Pac-Man.trd | head -4
00000000  62 6f 6f 74 20 20 20 20  42 d0 00 d0 00 01 00 01  |boot    B.......|
00000010  73 63 72 65 65 6e 20 20  43 40 9c 14 07 08 01 01  |screen  C@......|
00000020  64 61 74 61 20 20 20 20  43 00 5b 00 25 25 09 01  |data    C.[.%%..|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

Как видно, в самом начале дискеты (на нулевой дорожке) содержится таблица размещения файлов, в которой информация о каждом файле занимает 16 байт. Размер в секторах хранится в байте со смещением #0D (третья колонка справа). Размер наших файлов — #01, #08 и #25 секторов, что в сумме составляет #2E. Запишем это значение в соответствующий байт, а остальные заголовки удалим, т.к. они больше не нужны:

$ hexdump -C Pac-Man.trd | head -4
00000000  62 6f 6f 74 20 20 20 20  42 d0 00 d0 00 2E 00 01  |boot    B.......|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

Теперь у нас есть полноценный образ дискеты с моноблочным файлом. Он должен правильно загружаться и целиком копироваться с дискеты на дискету. Осталось только уменьшить размер образа. Поскольку trd-образ — это побайтовая копия, он всегда занимает 640КБ. На практике в большинстве случаев удобнее использовать формат scl, который больше похож на hobeta хранит непосредственно данные файлов:

$ trd2scl Pac-Man.trd Pac-Man.scl

Теперь точно всё. Процесс адаптации от начала до конце можно найти в репозитории проекта на гитхабе.


Инструменты:


  1. Pasmo — кросс-ассемблер для Z80.
  2. bas2tap — кросс-компилятор спектрумовского диалекта бейсика.
  3. trd2scl — конвертер trd-образов в scl.


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


  1. «Адаптация программ к системе TR-DOS» Николая Родионова.
  2. «Функции TR-DOS» из журнала Info Guide №1.
  3. «Структура дискеты TR-DOS» из книги «TR-DOS для профессионалов и любителей».
  4. Справочник по системным переменным и процедурам ПЗУ спектрума.

© Habrahabr.ru