Adaptation of programs for ZX Spectrum to TR-DOS by modern means. Part 1

  • Tutorial

Unlike modern computers, on the spectra the concept of a file system was not as such. This means that downloading from each type of media required a separate implementation, and in most cases the program could not just be copied from tape to disk. In cases where the program loader was written in BASIC, it could be adapted to TR-DOS with a fairly simple revision. However, the situation was complicated by the fact that in many games (both branded and hacked), the loaders were written in machine codes and sometimes contained copy protection.


Despite the presence of a “magic button” that simply did a complete dump of the computer’s memory and made it possible to somehow save the program to a floppy disk, it was considered by experts to create disk versions of games while preserving the original boot image and other attributes.

In this article, I will tell you how to perform such an adaptation on the example of the Pac-Man game , namely, the original Pac-Man.tzx image .


Despite the fact that in the old days all such work was done directly on the ZX Spectrum (in the absence of other options), I will adapt the game using the emulator and command line utilities. The main reason is that especially at first the adaptation process consists of a large number of trial and error, and it goes much less painfully if it is automated. All the same can be done directly on the Spectrum.

In the first part we will use the following tools:

  1. Fuse emulator for debugging and testing.
  2. SkoolKit for disassembling.

Disabling startup in the bootloader

Since the image and data file is downloaded without a header block (17 bytes with the name and type of file), this means that the loader is written in machine codes. You need to find where these codes are located and from what address they are launched.

There are several ways to look at the bootloader code:

  1. The easiest is to start downloading the program, wait for the bootloader to start, and stop it by pressing a key Space. In many cases, this works, but in the case of Pacman, as in many others, this leads to a reset.

  2. The next way is to download the program using MERGE ""instead LOAD "". In contrast LOAD, it MERGEignores the autorun program. In the case of Pac-Man, loading through MERGEcauses the computer to freeze with a characteristic screen shift to the left. This is due to the fact that instead of executing the program line by line, it MERGEtries to parse it in its entirety and merge it with the already loaded program. However, if the program has a block with machine codes that violates the syntax of the program, this will lead to a crash.

  3. If you do not want to rack your brains, you can convert the tape image from TZX to TAP and use the utility listbasicthat comes with Fuse:

    $ tzx2tap Pac-Man.tzx
    $ listbasic Pac-Man.tap
       1 RANDOMIZE USR (PEEK 23635+256*PEEK 23636+91)

    Address 23635( $5C53) corresponds to a system variable PROGthat contains the starting address of the basic area. Thus, the entry point to the bootloader is offset by 91 bytes relative to the BASIC area.

  4. Another way to look at the bootloader is described in the article Desativando a autoexecução de um programa BASIC . In the Fuse debugger, you need to set a breakpoint br 2053, load the program, and when the download is completed and the code execution is interrupted, write set 23619 128. This will prevent the program from starting up and will allow you to exit to BASIC.

Bootloader disassembly

Knowing the shift of the entry point relative to the BASIC area, you can calculate its absolute address. In the case of the ZX Spectrum 48K without loaded TR-DOS, the BASIC area starts with the address 23755( $5CCB). Therefore, the bootloader will start at address 23755 + 91 = 23846( $5D26).

To get started, just put a breakpoint at the starting address and look at the machine codes. In Fuse, you can make br 23846and start downloading the program. As soon as the bootloader starts executing, the emulator will stop:


In the case when the loader is very simple, just look at the disassembled code in the middle panel and understand what is being loaded. Typically, the download code for an headerless file looks something like this:

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

In a more complex case with code execution, you need to understand the steps and make notes. The SkoolKit utility suite is well suited for this . If you set a goal, with its help the game can be parsed to the last screw (message, sprite, sound). How this is done is described in detail in the documentation .

In short, do the following:

  1. Make a snapshot Pac-Man.z80of the computer's memory using tap2sna.pyeither the emulator capabilities.
  2. Create a control file Pac-Man.ctlwith an initial set of instructions for disassembling:
    i 16384 Ignore for now
    c $5D26 Loader
  3. Start disassembly: -H -c Pac-Man.ctl Pac-Man.z80 > Pac-Man.skool.
  4. As you study the code, add new instructions and comments to the control file.
  5. Repeat until completely enlightened.

As a result, after the first pass we get the following (my comments, addresses are omitted):

ORG $5D26      ; те самые 23846, определённые выше
; Запрет прерываний
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

Bootloader Decryption

All that is really important is that the decrypted bootloader is located at PROG + $35. This means that if we set a breakpoint br 23808, then at this point the decryption is complete, we will see the decrypted bootloader:


This program is already much more similar to the typical case mentioned above. The registers IXand DEloads the value $4000( 16384) is something else, and transfers control to the subroutine ROM address $055A(it's a few bytes lower than the standard entry point LD-BYTES). It seems that this approach implements some kind of copy protection, because the standard procedure does not load this file and some copyists do not understand it.

Program entry point

It remains to figure out how the program is called after loading. Instead of the usual CALL LD-BYTESand JPis used here LD SP, XXXXand JP LD-BYTES. The first (usual) option works as follows:

  1. CALLpushes the current value of the software counter ( PC) onto the stack .
  2. Control is passed to the called routine.
  3. When you return from the subroutine ( RET), the value is removed from the stack and the transition to the calling program occurs.

Why is it done differently here? The fact is that Pac-Man is compatible with the ZX Spectrum 16K and occupies absolutely all the RAM (see file size above). Thus, when loading, the program overwrites itself both the loader and the stack, wherever they are. If we wanted to switch from ROM to the bootloader using the stack and then call the downloaded program through JP, at the time the download was completed, JPthere would be no memory address at the time of the download .

Instead, the stack pointer moves to the memory area where, after loading, the address of the entry point to the program appears, and the processor, not noticing the spoofing, removes it from the stack by the new pointer and goes to the specified address.

The complete disassembly result can be viewed in the project repository on the github.


As a result of studying the bootloader, we found out the following:

  1. An headerless file with a length of 16384 bytes is downloaded at 16384 (in the screen area, which is generally obvious during the download process).
  2. At the end of the download, the stack pointer is located at the address $5D7Cwhere the control is transferred.

In the following parts I will talk about how to prepare files for writing to disk and write a monoblock file loader in assembler.

Related links:

  1. Profile "TRUB Spectrumist . "
  2. Reverse engineering ZX Spectrum (Z80) games .
  3. Adaptação de jogos de fita para Beta 48 .

Also popular now: