Адаптация программ для ZX Spectrum к TR-DOS современными средствами. Часть 3
Как мы выяснили в предыдущей части, машинные коды игры загрузить с дискеты непосредственно по адресу назначения нельзя. Мы загрузим их в другое место, а после загрузки переместим куда нужно. Кроме этого, мы хотим сделать моноблочный загрузчик, когда и загрузчик и загружаемые данные находятся в одном бейсик-файле. Такой загрузчик можно написать только в машинных кодах. При этом, поскольку файл у нас моноблочный, загрузчик в машинных кодах нужно будет поместить в комментарии к загрузчику на бейсике.
Итого, получается следующая многоходовка:
- Из бейсика передаём управление программе в машинных кодах.
- Программа в машинных кодах переносит загрузчик из области бейсика в другую область, которую не затронут машинные коды игры, и передаёт управление ему.
- Загружаем и распаковываем загрузочную картинку.
- Загружаем машинные коды игры в область, не перекрывающую область системных переменных.
- Переносим машинные коды по адресу назначения.
- Передаём управление программе.
Разработку придётся начать с середины (пункт 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 хранит избыточную информацию о размере файлов:
- Размер в секторах — используется для размещения файлов на дискете и копирования.
- Размер в байтах — используется для загрузки содержимого.
Обычно эти размеры соответствуют друг другу (256 байт на сектор), но это не обязательно. Этим мы и воспользуемся. Если изменить размер boot-файла в секторах на значение, равное суммарному размеру всех файлов, которые мы хотим загрузить, но не изменять размер в байтах, TR-DOS будет копировать все данные как один большой файл, но при этом при загрузке будет загружаться только бейсик-часть.
На настоящем спектруме или в эмуляторе нулевую дорожку можно редактировать программами типа Disk Doctor, например, 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
Теперь точно всё. Процесс адаптации от начала до конце можно найти в репозитории проекта на гитхабе.
Инструменты:
- Pasmo — кросс-ассемблер для Z80.
bas2tap
— кросс-компилятор спектрумовского диалекта бейсика.trd2scl
— конвертер trd-образов в scl.
Ссылки по теме:
- «Адаптация программ к системе TR-DOS» Николая Родионова.
- «Функции TR-DOS» из журнала Info Guide №1.
- «Структура дискеты TR-DOS» из книги «TR-DOS для профессионалов и любителей».
- Справочник по системным переменным и процедурам ПЗУ спектрума.