SpectNet IDE

Visual Studio 2017/2019 integrated ZX Spectrum IDE for the Community

Pac-Man Discovery » Decrypting the loader

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.

  1. Start the Visual Studio IDE (with SpectNetIDE already installed)
  2. 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.
  3. Download the Pac-Man.tzx file into a temporary folder from here. Right-click the TapeFiles folder in Solution Explorer, and choose the Add | Existing file item. Navigate to the previously downloaded file, and add it to the folder.
  4. Right-click Pac-Man.tzx and select the Set as default tape file command.
  5. Click the Welcome.tzx file and press the Delete key to remove this file from the project.

PacMan

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.

PacMan

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:

PacMan

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:

PacMan

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:

PacMan

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:

  1. Stop the virtual machine.
  2. Open the Z80 Disassembly window and set a breakpoint at #05e2 with the SB 05E2 command.
  3. Start the virtual machine with the Start Debugging (F5) command.
  4. Type the LOAD "" command to start loading Pac-Man.
  5. The machine will pause at #05E2 when the first data block is loaded, you can see the current breakpoint in the Z80 Disassembly window.
  6. 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.
  7. Open the ZX Spectrum Memory window, and use the G 5C53 command to navigate to the PROG system variable:

PacMan

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:

  1. Execute the RD command in the Z80 Disassembly view to re-disassembly the code freshly loaded in the RAM.
  2. 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.
  3. With the G 5D26 command, navigate to the beginning of the code.

You can see the loader code in the disassembly view:

PacMan

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:

PacMan

Change the default file name to AutoLoad.z80asm and click Export. SpectNetIDE adds the disassembly export to the project:

PacMan

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

PacMan

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…