A 16 byte intro for the ZX Spectrum released at Lovebyte 2025.
This intro was an experiment to see what could be done with self modifying code in a tiny intro. Self modifying code is a quite common technique on the Z80 and I’m using it all the time to speed up code. In most cases I use it to modify immediate values, basically storing variables directly in the code.
In this intro however, I wanted to write some kind of loop that manipulated a value with one or more instructions and wrote the value to the screen. What would happen if one of these instructions was changed over time?
A tiny way to change an instruction is to let hl
point to the
instruction and then use inc (hl)
or dec (hl)
to change it. The
opcode range from 0x78 to 0xbf turns out to contain instruction that
only modify the accumulator and/or the flags. These instructions are
ld a, X
, add X
, adc X
, sub X
, sbc X
, and X
, xor X
, or
X
and cp X
. X
is b
, c
, d
, e
, h
, l
, (hl)
and a
.
Here is a breakdown of the source code. See here for the full source.
ORG 0xbf14
This is the start address of the intro. The significance of this will be explained below.
di
The intro will write to the memory from 0x4000 to 0x7fff and since some of this memory is used by the ROM interrupt handler, I figured it was a good idea to disable interrupts. This is probably not needed but I somehow left this in.
db 0x21
0x21 is the opcode for ld hl, immediate
. The next two bytes contains
the immediate value to load (in little endian). To save bytes, these
two bytes are reused as instructions in the loop.
loop:
rla
cp a
This is the start of the loop in the intro. The opcodes for these two
bytes are 0x17 0xbf. At startup, the ld hl, immediate
above will
load 0xbf17 into hl instead of executing these two bytes. Since the
program is loaded at 0xbf14, 0xbf17 is the address of the cp a
instruction.
For the rest of the program, these instructions modifies the accumulator
and the carry flag. The cp a
instruction is the instruction
that will be modified.
The accumulator and the carry flag are not modified by any other instructions.
inc d
res 7, d
ld (de), a
inc e
This writes the a
register to memory address in de
. res 7, d
makes sure that de
stays within 0x0000 to 0x7fff. inc d
and inc
e
increments the high and low bytes of de
.
djnz loop
This will run the inner loop 256 times (except for the first iteration
where b
is 0xbf).
inc e
jr nz, loop
After the inner loop, e
is incremented and then the code jumps back
to run the inner loop as long as e
is not zero. e
is modified in
the inner loop as well, but since that loop runs 256 times the end
result this loop also will run 256 times.
dec (hl)
jp (hl)
After 256*256 iterations of the inner loops, the cp a
(0xbf)
instruction is decremented by dec (hl)
, ending up as cp (hl)
(0xbe). The code then jumps to the modified instruction. The rla
is
skipped, but this is not important.
The intro runs until the modified instruction changes to 0x77, or ld
(hl), a
. This will overwrite the instruction again with the current
value of a
which is 0x01. This value is the opcode for ld bc,
immediate
, a 3 byte instruction. The inc d
instruction and the
prefix of the res 7, d
instruction will therefore never be executed
and the inner loop now looks like this:
loop:
rla
ld bc, 0xcb14
cp d
ld (de), a
inc e
djnz loop
This code will run forever since b
never reaches zero. de
points
to the rom so the screen will not change.
When writing this intro I tried several different ways to modify a
and different ways of iterating through memory. In the end it was this
version that ended up looking “best”.
I also tried incrementing instead of decrementing the
instruction. This basically reverses the patterns shown but will crash
at the end since instruction 0xc0 is ret nz
.