In this series of articles, I demonstrate how easy it is to discover or re-engineer ZX Spectrum games with SpectNetIDE. I have chosen Pac-man as an example.
Creating the Initial Project
To prepare the initial project, follow these steps.
- Start the Visual Studio IDE (with SpectNetIDE already installed)
- Create a new ZX Spectrum Code Discovery project. In the New Project dialog name it
PacManDiscovery
and click OK. Select the ZX Spectrum 48K PAL - Normal Speed model. - Download the
Pac-Man.tzx
file into a temporary folder from here. Right-click theTapeFiles
folder in Solution Explorer, and choose the Add | Existing file item. Navigate to the previously downloaded file, and add it to the folder. - Right-click
Pac-Man.tzx
and select the Set as default tape file command. - Click the
Welcome.tzx
file and press the Delete key to remove this file from the project.
Start the ZX Spectrum emulator, and issue a LOAD ""
command. The command will load and start the Pac-Man game.
Note: These tutorials explain you the basics of starting the emulator and fast loading tape files.
Now, you are ready to start discovering the internals of the game.
Discovering the Pac-Man.tzx File
SpectNetIDE allows you to look into the structure of .TZX and .TAP files. Double click the Pac-Man.tzx
node in Solution Explorer to look into its details:
This dialog shows the Tape File Explorer window with the structure of Pac-Man.tzx
. The left pane shows the data blocks of the file; the right pane displays the contents of the selected block. As the figure indicates, Pac-Man contains three Standard Speed data blocks. The first is the program header:
The second block is the BASIC program that autostarts Pac-Man. Its code immediately calls into a machine code that starts at a mysterious address:
Note: You do not see the BASIC program instantly when you select the block. Move the mouse over the Data label, and click it to turn to the BASIC code view.
So what is the result of the PEEK 23635 + 256 * PEEK 23636 + 91
operation? The 23635 address stores the PROG
system variable in two bytes, which retrieves the start address of the BASIC program in the memory. So, this expression shows that the machine code starts from the 92nd byte of the BASIC code that loads into the memory.
Obtaining the Loader Code
How can we obtain the loader code? SpectNetIDE makes it easy.
By examining the .TZX file the first data block tells that the autoloader code (including the BASIC) is 153 bytes long. With these steps, we can get the machine code between offset 91 and 152:
- Stop the virtual machine.
- Open the Z80 Disassembly window and set a breakpoint at #05e2 with the
SB 05E2
command. - Start the virtual machine with the Start Debugging (F5) command.
- Type the
LOAD ""
command to start loading Pac-Man. - The machine will pause at #05E2 when the first data block is loaded, you can see the current breakpoint in the Z80 Disassembly window.
- While either the ZX Spectrum Emulator or the Z80 Disassembly window has the focus, press F5, and wait for the next pause. Now, the autoload code is already the memory.
- Open the ZX Spectrum Memory window, and use the
G 5C53
command to navigate to thePROG
system variable:
As the figure shows, the value of the PROG
variable is #5CCB. We know that the loader code can be found between offset 91 and 152, so it is between #5D26 and #5D63.
Let’s disassemble that code with these steps:
- Execute the
RD
command in the Z80 Disassembly view to re-disassembly the code freshly loaded in the RAM. - Use the
MD 5D26 5D64
command to tell the disassembler to display the range (note, the end address is exclusive) as Z80 instructions, and not as.defb
pragmas. - With the
G 5D26
command, navigate to the beginning of the code.
You can see the loader code in the disassembly view:
We can create a .z80asm
file from the disassembly and add it to the project for future examination. Issue the X 5D26 5D64
command in the disassembly view. The command pops up the Export Disassembly window:
Change the default file name to AutoLoad.z80asm
and click Export. SpectNetIDE adds the disassembly export to the project:
Now, you can click AutoLoad.z80asm
to re-engineer its contents.
.org #5D26
; External symbols
di
im 1
ld d,yh
ld e,yl
ld b,#25
ex de,hl
ld de,#0019
add hl,de
ld e,(hl)
inc hl
ld d,(hl)
ld xh,d
ld xl,e
ld a,(ix+#7F)
ld hl,#0035
add hl,de
push hl
L5D43:
xor (hl)
ld (hl),a
inc hl
djnz L5D43
and (hl)
ret nz
ld (hl),a
L5D4B:
xor (ix+#7F)
ld (ix+#7F),a
inc ix
djnz L5D4B
add ix,de
ex (sp),hl
scf
jp (ix)
in a,(#FE)
rra
and #20
ld c,a
cp a
jp (ix)
I analyzed this code; it was easy to decipher. I used the SpectNetIDE debugger to execute the code step-by-step. The comments tell you how it works:
.org #5D26
; External symbols
di ; Disable the interrupt
im 1 ; Use interrupt mode 1
ld d,yh ; DE := IY
ld e,yl ; DE point to the ERR_NR system variable (#5C3A)
ld b,#25 ; B := #25 (37), later we'll use it as a counter
ex de,hl ; HL = IY
ld de,#0019
add hl,de ; HL points to the PROG variable (#5C53)
ld e,(hl)
inc hl
ld d,(hl) ; DE points to the start of the BASIC program (#5CCB)
ld xh,d
ld xl,e ; IX points to the start of the BASIC program (#5CCB)
ld a,(ix+#7F) ; Get the byte from offset #7F (from #5D4A). A=#77
ld hl,#0035
add hl,de ; Modify HL so that it points to #5D00
push hl ; Push #5D00 to the stack (RET NZ later will jump here)
`loop:
xor (hl) ; This loop "decrypts" the code between #5d00-#5d24
ld (hl),a ; it XORs each byte with #77, so actually rewrites
inc hl ; the code
djnz `loop
and (hl) ; As the contents (HL) [at this point HL=#5D25] is #F6
ret nz ; This RET NZ jumps to #5D00
DECRYPT_CODE
.defb #77 ; Decryption code
This program contains a small encrypted routine that loads the last block from tape and starts it. The code above uses a decryption code (#77) to XOR this 37 bytes long code, that spans from #5D00 to #5D24, right before this loader section. After decrypting the loader code, it jumps to #5D00 and initiates loading the actual game, including the screen. You can see that the bytes after the #5D4B are not used at all.
Here is the decrypted loader. I obtained it with the same techniques (the SB
, MD
, RS
, and X
disassembly commands) as the previous code part. Before allowing the loader to run, I added a breakpoint to #5D00 and then exported the disassembly of the #5D00-#5D25 memory section.
.org #5D00
ld xh,#40
ld xl,#00 ; IX=#4000, load start address
ld d,xh
ld e,xl ; DE=#4000
ld hl,#5DA6
ld (iy+#03),l
ld (iy+#04),h ; Sets the ERR_SP system variable to #5DA6.
; If any error (e.g. Load error) occurs, the execution
; returns to that address
ld sp,#5D7C ; SP points to the new stack
; The code will return to the address at #5D7C
; after the last datablock is loaded.
ex de,hl
ld xl,e
ld a,xl
ex de,hl ; DE (#4000) contains the length of the data to load
ld xl,#00 ; IX (#4000) points to the start address
and a ; A=#A6, it will be used as the header's type value
ccf ; sets the carry flag to indicate LOAD operation
ex af,af' ; Saves A and the carry flag to AF'
jp L055A ; Jumps into the LD_BYTES subroutine
; That routine loads #4000 bytes from address #4000
; and overwrites this code.
; When load completes, the code returns to the address
; stored at #5D7C
Determining the Start Address
The last piece of the puzzle is to guess out the entry address of the game. As the second part of the loader shows, the Stack Pointer is set to #5D7C before the last block is loaded. It means that the loader method will “return” to the address that is stored at #5D7C.
With the tape explorer, we can get this address. The ZX Spectrum tape block contains a header byte, then the data block, and a checksum byte. The load address of the data block is #4000, so the #1D7D and #1D7E offsets store the address we’re looking, as the first data byte is the header type.
And here it is: #7394
As a last check, I set a breakpoint to that address before allowing the final code block to load. The debugger stopped, and I could follow how it started initializing the game.
Stay tuned, the story continues…