Reverse engineering ZX Spectrum (Z80) games
The home computer was born in the early 1980’s and in England this came in the shape of the ZX Spectrum. Along with it came the birth of the British video game industry. Although I had a ZX81 the year before, it was the ZX Spectrum I received as an xmas gift in 1983 that got me hooked on games, in particular the classic Manic Miner; a simple, addictive, and deceptively difficult platformer.
A year and a half ago I found the Skoolkit disassembly toolkit and one of the examples was the disassembled source code for Manic Miner. As I’d recently started learning to program in C, I thought it’d be a great learning experience to port this classic Z80 game to the C language. After some months I had a working version of Manic Miner written in C and using SDL2.
Porting Manic Miner was a lot of fun and it’d given me a greater understanding of how these early assembly games were written. It had also piqued my interest for how these old Z80 games are disassembled so I decided to give it a go myself.
There are many speccy games to choose from such as Chuckie Egg or the classic Knight Lore, but for this article I decided to take a look at Jetpac developed by Ultimate Play the Game and released in 1983.
Note: if you’ve never encountered Z80 assembly code before then I strongly recommend you do some study first - the internet is your friend here. I learned a lot while developing my Manic Miner port.
Reverse Engineering Z80 machine code
For this we’re going to use the Skoolkit disassembly toolkit. Although there are other disassembly tools available, Skoolkit is by far the best suited for ZX Spectrum games.
You’ll want to make good use of the Skoolkit documentation for installation and usage instructions, which can be found on skoolkit.ca.
Although it’s possible to start decompiling directly form a tape image (.tzx
) it’s actually more useful to use a Z80 snapshot, and the best way to get that is via an emulator. I’m running macOS so I’m a limited in choice, but whatever platform you’re on, the FUSE emulator is one of the more popular.
After loading the tape image into FUSE you will be presented with the games’ main menu.
At this point you should export a Z80 snapshot file as jetpac.z80
.
1. Creating a Map file
You could begin immediately by running this Z80 snapshot through Skoolkit, however, you’ll save yourself a great deal of time if you first create a code execution map (also known as a profile or trace) - plus you’ll have some fun playing the game!
At the core of the Skoolkit disassembly process is the Control File and if you have a good execution map Skoolkit will produce an even more accurate control file.
In FUSE, start the profiler by selecting the Machine > Profiler > Start
menu item.
The trick to producing a good map file is to play the game from back-to-front and top-to-bottom. You want to interact with as muc of the game as possible: collect all items, shoot all enemies, crash into all things. Win if you’re able!
Your aim for this process is to exercise every aspect of the game code. Once done, stop the profiler and save the map as jetpac.map
.
My first try at disassembly was without a code execution map, but thankfully I decided to start again using the map. Not only did it find all the code/data blocks I’d discovered manually, it also found additional code blocks that I’d marked as data. Needless to say, I would’ve saved myself a heck of a lot of time if I’d used the map in the first place – lesson learned!
The better the map file, the better the control file.
2. Creating a Skoolkit Control File
A control file contains a list of start addresses of code and data blocks. This information can be used by sna2skool.py
to organise a skool file into corresponding code and data blocks. – excerpt from skoolkit.ca
With our code execution map ready, we can begin by generating a new .ctl
file. From the terminal, execute the following command inside the folder where your .z80
and .map
files are located:
$ sna2skool.py -M jetpac.map -g jetpac.ctl jetpac.z80 > jetpac.skool
This will produce a jetpac.ctl
file and a jetpac.skool
file.
Eventually the control file will become redundant, with all work done directly on the skool file, but for now we’ll concentrate our efforts exclusively here.
Here’s how the jetpac.ctl
file looks at this stage:
b $4000
b $5B00
c $5B80
b $5B8E
b $5C00
c $5CB0
b $5CB1
b $5CC0
b $5CCB
; ... etc., etc., etc.
Each line starts with a control directive, followed by an address value. If you don’t know the significance of this first address I recommend you do some study on the ZX Spectrum’s memory layout.
As we can see, the first address is $4000
(16384
in decimal), the start of the Spectrum’s display file, which at the time that we created the Z80 snapshot, was the main menu.
It’s possible to tell Skoolkit to output all addresses as either DECIMAL
or as HEXADECIMAL
values. The Skoolkit examples use decimal, but often you’ll find examples of assembly code using hex. Working with assembly instructions in hex is definitely worth the effort to become comfortable in them. I highly recommend it.
3. Refine the Control File….rinse and repeat
The process of disassembling a game with Skoolkit consists or two core steps:
- analysing the
skool
file - annotate the
ctl
file
You will analyse the skool file, trying to find game text, sprites, data blocks and code blocks, then annotate the control file with your new found understanding. You then generate a new skool file and iterate until the disassembly is complete.
When we generated the ctl
file we also generated a skool
file. From now on we need only re-generate the skool file. For this we need a slightly different command so that we don’t overwrite our edited control file. Let’s do that now:
$ sna2skool.py -c jetpac.ctl -H jetpac.z80 > jetpac.skool
Now, when you update the control file, you can run the above command to generate a new skool file.
Here’s a slice of our control file after adding some annotations:
$608A label=level_init
$608A,3 Black Border
$608D,3 Clear the screen
$6090,3 Reset the screen colours
$6093,3 Display score labels
$6096,3 Attribute file address
$6099,3 #REGb=loop counter, #REGc=Cyan/Yellow
$609C,1 Reset bytes in the attribute file
$609E,2 Loop back
$60A0,3 Update display with player 1 score
$60A3,3 Update display with player 2 score
$60A6,3 Update display with high score
; ...
How to Analyse a skool file?
There’s no magic sauce to understanding what the different parts of the assembly code do. You just need time and patience. However there are some techniques to aid in this process.
Skoolkit’s introduction provides some useful advice, which I strongly recommend you read. I’ll list the 6 core steps here:
- Skim the code blocks for any code whose purpose is familiar or obvious, such as drawing something on the screen, or producing a sound effect.
- Document that code (and any related data) as far as possible.
- Find another code block that calls the code block just documented, and figure out when, why and how it uses it.
- Document that code (and any related data) as far as possible.
- If there’s anything left to document, return to step 3.
- Done!
The Skoolkit docs say you should make your edits directly to the skool
file. Richard has a lot more experience than I at this, but from my experience I’d say continue working with the ctl
file. I find this much more intuitive and faster. I can play around with blocks (is it data, is it code?) more freely when working directly on the control file, and much less text twiddling.
Side note: if you use the SublimeText 3 editor then you might like my SkoolkitZ80 Syntax highlighting package to make viewing your
.skool
files a little more pleasant.
The first step above states, “Skim the code blocks for any code whose purpose is familiar or obvious”. At this point you might be saying to yourself, ‘I’ve never disassembled machine code before so there is neither familiar nor obvious code!’.
What do we do?
Here’s a few things I’ve learned while reverse engineering the Jetpac game.
Extracting game Text
This one is generally quite easy as Skoolkit does a good job of displaying all hard-coded text strings. For sure there’ll be false-positives, but you can figure these out pretty quickly and annotate the control file appropriately.
Many games however don’t use hard-coded strings, they have their own custom font or text graphics. To find these you’ll need to extract sprites from bytes.
Extracting game Sprites
This is not so straight forward as you’ll need an external tool for extracting sprites and graphics.
There are various tools that can help to show what sprites exist in a bunch of bytes but none of the ones I found for macOS were that friendly; difficult to use/navigate, and the sprites were displayed at their original size, which on my 13-inch laptop made distinguishing sprites from random noise difficult indeed.
In the end I wrote a simple Ruby script to dump the whole jetpac.z80
bytes to a file as pseudo pixels, which I then opened in a text editor to see what sprites I could find (see below for an example). Rather rough-n-ready but it worked well enough.
Understanding Sprites
Here’s a Jetpac sprite as displayed on the game status bar at the top of the screen:
; Icon representing the player "lives"
b$70A4 DEFB $18,$24,$3C,$7E,$5A,$3C,$3C,$66
Address b$70A4
is an 8x8
sprite. Each decimal number expresses a row of 8
pixels, which means we need to convert each decimal number to a binary format.
$18
expressed as binary becomes: 00011000
. With 1’s and 0’s representing pixels and spaces. Therefore this whole sprite in binary would be:
$18 : 00011000
$24 : 00100100
$3C : 00111100
$7E : 01111110
$5A : 01011010
$3C : 00111100
$3C : 00111100
$66 : 01100110
And converted to something that resembles a pixelated spaceman:
██
█ █
████
██████
█ ██ █
████
████
██ ██
Comparing game states
It’s also possible to discover some variables and other data by comparing two different game snapshots. I’ve only tried this once so far, and I imagine it can get out of hand quite easily, but it may be worth considering.
For this experiment I tried to keep the scope of the two snapshots very close. I slowed down the FUSE emulation and when the game reached the point just before picking up a fuel pod I made snapshot. Then after collecting the fuel, I made a second snapshot.
From these snapshots I generated two separate .skool
files, which I then diff
‘d to see all the places that looked like they could be fuel related variables.
I think as an early stage experiment it can be useful, although it’s probably of limited use.
Final Words
Reverse engineering old Z80 games is an interesting challenge. You not only become familiar with assembly code, but also the techniques for how these games had to be written for machines with limited CPU power and limited memory - 3.5 Mhz
CPU and 48 KB
RAM in the case of the ZX Spectrum. Pretty good for 1982!
Eventually you will reach the point during the disassembly where editing the skool
directly will be more useful than ping-ponging with the control file.
I hope you’ve found this short introduction to reverse engineering ZX Spectrum games useful. If you have any questions about the process, please leave a comment below and I will try to answer.
UPDATE 2018-12-22: After some time I switched to using hexadecimal numbers. That change is now reflected in the examples given.
UPDATE 2021-02-27: I continued working on the disassembly and a few months later finished the bulk of the work and in 2020 released the skool files. You can download my annotated Jetpac disassembly on Github.com.