Skip to content

Published 8 November 202628 min

Ten POKEs That Turn the ZX Spectrum's ROM Against Itself

Press play to watch here. The video loads from YouTube only then.

What happens when one POKE changes a byte the ZX Spectrum's ROM trusts?

Ten single-byte changes climb from shifted letters and misdirected output to ROM data being read as text, then run as code, with every result typed, shown and explained on a 48K Spectrum.

See HAL come back as IBM, BREAK restart the machine, printer text sliced across the screen, CAT replaced by the report table, Statement lost give the processor orders, and the ROM's own font rewrite line 30.

Watch on YouTube

Sources

Every fact in the film is tied to a quotation from a source. These are the 38 published ones, most cited first.

  1. ZX Spectrum BASIC manual, chapter 25: the system variablesworldofspectrum.org · cited 52 times
  2. World of Spectrum - Documentation - ZX Spectrum manualworldofspectrum.org · cited 31 times
  3. SkoolKit's annotated 48K ROM disassembly: 1391skoolkid.github.io · cited 15 times
  4. SkoolKit's annotated 48K ROM disassembly: 0D6Bskoolkid.github.io · cited 10 times
  5. SkoolKit's annotated 48K ROM disassembly: 09F4skoolkid.github.io · cited 9 times
  6. Z80 instruction set table (ClrHome)clrhome.org · cited 9 times
  7. SkoolKit's annotated 48K ROM disassembly: 3D00skoolkid.github.io · cited 8 times
  8. SkoolKit's annotated 48K ROM disassembly: 0B24skoolkid.github.io · cited 7 times
  9. World of Spectrum - Documentation - ZX Spectrum manualworldofspectrum.org · cited 7 times
  10. SkoolKit's annotated 48K ROM disassembly: 2951skoolkid.github.io · cited 7 times
  11. Spectrum ROM: Routine at 24FBskoolkid.github.io · cited 6 times
  12. SkoolKit's annotated 48K ROM disassembly: 155Dskoolkid.github.io · cited 5 times
Show all 38 sources
  1. HAL 9000 (Wikipedia)en.wikipedia.org · cited 5 times
  2. Spectrum ROM: Text at 0095skoolkid.github.io · cited 4 times
  3. World of Spectrum - Documentation - ZX Spectrum manualworldofspectrum.org · cited 4 times
  4. SkoolKit's annotated 48K ROM disassembly: 12A2skoolkid.github.io · cited 4 times
  5. SkoolKit's annotated 48K ROM disassembly: 0E44skoolkid.github.io · cited 4 times
  6. Spectrum ROM: Data at 0205skoolkid.github.io · cited 4 times
  7. SkoolKit's annotated 48K ROM disassembly: 2BF1skoolkid.github.io · cited 4 times
  8. Spectrum ROM: Routine at 15EFskoolkid.github.io · cited 4 times
  9. SkoolKit's annotated 48K ROM disassembly: 0038skoolkid.github.io · cited 4 times
  10. SkoolKit's annotated 48K ROM disassembly: 0053skoolkid.github.io · cited 3 times
  11. Bugs in the ROM: some of the Spectrum's undocumented features (Nonowt compilation)nonowt.com · cited 3 times
  12. Spectrum ROM: Data at 15AFskoolkid.github.io · cited 3 times
  13. SkoolKit's annotated 48K ROM disassembly: 28B2skoolkid.github.io · cited 3 times
  14. Spectrum ROM: Routine at 361Fskoolkid.github.io · cited 3 times
  15. SkoolKit's annotated 48K ROM disassembly: 386Eskoolkid.github.io · cited 3 times
  16. SkoolKit's annotated 48K ROM disassembly: 1539skoolkid.github.io · cited 2 times
  17. SkoolKit's annotated 48K ROM disassembly: 1BB3skoolkid.github.io · cited 2 times
  18. ZX Spectrum errors (redcode/ZXSpectrum wiki)github-wiki-see.page · cited 2 times
  19. Spectrum ROM: Memory mapskoolkid.github.io · cited 2 times
  20. SkoolKit's annotated 48K ROM disassembly: 16B0skoolkid.github.io · cited 2 times
  21. Spectrum ROM: Data at 0A11skoolkid.github.io · cited 2 times
  22. SkoolKit's annotated 48K ROM disassembly: 0C0Askoolkid.github.io · cited 2 times
  23. World of Spectrum - Documentation - ZX Spectrum manualworldofspectrum.org · cited once
  24. The Nonowt eZine X Magazinenonowt.com · cited once
  25. SkoolKit's annotated 48K ROM disassembly: 27BDskoolkid.github.io · cited once
  26. World of Spectrum - Documentationworldofspectrum.org · cited once

Our own research: 11 records of the experiments, measurements and readings we made for this film, cited 387 times.

Transcript

The narration as it is spoken in the film, chapter by chapter. Pick a time to play the film from there.

Read the full transcript

One changed byte

0:12Ask for HAL. The Spectrum prints IBM. That's number ten.

0:21At number one, it changes our program. How do single-byte POKEs make its unchanged ROM do that?

10. HAL becomes IBM

0:32The Spectrum draws text using values it keeps in memory. This POKE changes one of them. Watch the word in quotes.

0:58Why IBM, rather than rubbish?

1:05Every letter's been drawn with the next letter's shape. H uses I, A uses B, and L uses M. The word hasn't changed. The shapes used to draw it have. A POKE puts a number into one location in RAM, the memory the machine can write to. Each location holds one byte, from zero to two hundred and fifty-five. The ROM is the Spectrum's built-in program and tables. A POKE can't change it, but the ROM can read the values in RAM. Instead, the ROM keeps working notes in RAM, called system variables. The Spectrum manual says, "some of them can be usefully poked". Each of these ten POKEs changes one byte. We're following how far the unchanged ROM will go when it trusts that byte.

2:02One of those working notes is CHARS, the base address the ROM uses to find character shapes. An address is a number saying where in memory to look. Let's follow H. Every character has a character code, its number in the character set. The ROM font stores H's shape as eight bytes, one for each row of eight dots. To find those bytes, the ROM multiplies H's code by eight and adds CHARS. That lands on the eight rows used to draw H. The font's first shape is the space character. The space has code thirty-two, and thirty-two times eight is two hundred and fifty-six. So CHARS sits that far before the font, letting the same calculation land on its first shape. The CHARS address itself takes two bytes, low first and high second. This POKE changes the low byte from zero to eight. That moves the base eight bytes on, so the ROM finds I's shape when it draws H, and likewise for every other character.

3:16And look at the report line underneath, where the Spectrum says how the program ended. Its digits, spaces and punctuation have shifted too. The ROM uses the same font to draw all of them. HAL is the computer in Two Thousand and One: A Space Odyssey. Arthur C. Clarke wrote that someone spotted the IBM shift about once a week. He and Stanley Kubrick denied it was a dig at IBM.

9. BREAK means start

3:47This POKE changes where the ROM goes when BREAK stops the program. The program has two lines. The second keeps running until we press BREAK.

4:19That's a restart. The program's gone. BREAK was meant to stop the loop, not start the machine again.

4:31The Z80, the Spectrum's processor, keeps return addresses on the machine stack, its way back from what it's doing. A system variable called ERR SP tells the ROM where on that stack to find its return after an error. Keep your eye on ERR SP's pointer. During this run, it starts on the machine stack near the top of RAM. An address's low byte selects a position within a block of two hundred and fifty-six locations. Its high byte chooses the block, so changing only that byte moves the pointer in steps of two hundred and fifty-six. Here it changes from two hundred and fifty-five to a hundred, and ERR SP jumps down into empty memory. BREAK raises report L, the Spectrum's message that BREAK has stopped a program. The ROM goes to the place ERR SP names, reads two zero bytes and takes them as the address to return to. So it returns to address zero. That's where the Z80 starts at power-on. The start-up code clears the BASIC program, which is why the copyright line comes back and our loop doesn't. But that same copyright line can appear without a restart.

8. Copyright, line ten

5:53The value we're changing tells the Spectrum which final report to print. This time, there isn't even a PRINT.

6:15There's the copyright line again, but now it has a report letter and a line number. Nothing's restarted. The program's still there. ERR NR holds one less than the report number. Before the program runs, the ROM sets it to two hundred and fifty-five, meaning no error. When a program runs out, the ROM doesn't raise a new report. It adds one to ERR NR and prints that report. Because one byte wraps after two hundred and fifty-five, the normal value becomes zero, OK. Here, the last thing our program does is put twenty-eight into ERR NR. Twenty-eight plus one gives twenty-nine. Report numbers from ten upwards are shown as letters, so twenty-nine appears as T. The ROM's report messages sit one after another in a table. After Tape loading error comes the comma and space used after every report. Then comes the copyright message, the very same bytes the start-up screen prints. T isn't an error the machine has detected. It's a message we've made the report printer reach, complete with the line number.

7. The printer on the television

7:34The last POKE made the ROM choose a message we didn't expect. This one sends printer output to the wrong place. LPRINT is the command for sending text to the printer, so watch where its letters end up.

8:10Those are slices of HELLO SPECTRUM. Why are the slices a whole text line apart? A Spectrum e-zine called Nonowt says the manual called this byte unused. It's actually the high byte of PR CC, the printer position. The ROM reads both bytes as one address. At the bottom of RAM is screen memory: the picture's dots first, then the colours, one byte for each character square. Above those is the printer buffer, where LPRINT builds the rows of dots it's going to send. Our changed high byte moves that destination onto the beginning of screen memory. Each row of dots in the printer buffer begins thirty-two bytes after the last. A row on the Spectrum's screen is also thirty-two characters wide. But the screen isn't stored in reading order: its first two hundred and fifty-six bytes hold the top row of dots from each of the first eight text lines.

9:17So the next row of our letter lands at the top of the next text line. All eight rows are there, spread down the screen.

6. Two letters, all these stripes

9:32The last result showed us where the screen keeps its dots. Now we're changing how many lines the ROM reserves for commands and reports. This time it's two letters, HI. Watch the whole screen.

10:03It still says OK. The stripes come from a colour value that the ROM has written where the picture's dots belong. DF SZ is the size of the lower part of the screen, where you type commands and read reports. It normally allows two lines. We're telling it there's only one. After clearing that lower area, the ROM works backwards through its colour bytes. It subtracts one from a copy of DF SZ before the colour loop, so one becomes zero. This counter is one byte: if it counts down below zero, it wraps to two hundred and fifty-five. The loop therefore runs through two hundred and fifty-five lines. Across those two hundred and fifty-five lines, the loop writes ATTR P, the permanent current colour. It works backwards through the colours and into the bitmap, the memory holding the dots. That colour value is fifty-six. Read the same byte as dots and it's zero zero one one one zero zero zero. The ones become ink, the zeroes paper. That's the stripe.

5. Zero doesn't mean nothing

11:21One came back with OK. Now we're changing the same screen-size byte from one to zero. What would you expect it to clear?

11:49It doesn't come back. The clearing routine is still running. At one, the damage happened in the colour-writing loop after the clear. At zero, the clear itself goes wrong. The ROM hands the line-clearing routine a count of zero. A one-byte counter that counts below zero wraps to two hundred and fifty-five. So when it starts at zero, it takes two hundred and fifty-six steps to reach zero again. That carries the clearing through the colours and beyond the screen, into the system variables, the program and the machine stack. It wipes the copies of its own counters saved on that stack. So it can't finish its work. A quarter of a million instructions later, just seventeen bytes of RAM are non-zero.

12:44That wipe came from one value in a much larger field. The sweep covered two hundred and three bytes in the system variables and the channel table, which holds routes for input and output. For each byte, every value except its starting value was tried. That's fifty-one thousand, seven hundred and sixty-five POKEs. At least nine thousand and six changed something the test program could detect. Five is as destructive as this countdown gets. I'm ranking what follows by how strangely the ROM uses its own bytes, not how much it destroys.

4. HI knows the keyboard

13:26Temporary values need somewhere to go. Here, we're changing where the Spectrum keeps them. The program still asks it to print HI. Watch the first four characters that appear.

13:55Zero, one, Q, A. Then BASIC keywords. Why would two letters print the keyboard? This POKE changes WORKSP, an address that tells the ROM where its temporary work space begins. Before each statement, the ROM puts the calculator stack there. That's its scratch pad for values it's currently working with. Here, one changed high byte puts that scratch pad inside the ROM. A string is text. The ROM passes a string around using a five-byte description, which tells it where the text starts and how long it is. PRINT needs that description to know what to print.

14:45It tries to store HI's description in that scratch pad. But the address is now in read-only ROM, so the write changes nothing. The five bytes already there are instructions at the end of the ROM's routine for FN, a user-defined BASIC function. Instructions are stored as bytes like any other data, so PRINT reads those instruction bytes as HI's description.

15:13Read as a string description, those bytes give it a starting address in the keyboard tables and a length of more than sixty-three thousand characters. The address and length each have a low byte first, so these pairs tell PRINT where to look and how far to go. That starting point holds the last four entries of the ordinary keyboard table: zero, one, Q and A. Immediately after them is the extended-mode table. Most of its entries are keyword codes, single bytes that print whole BASIC words. PRINT expands the keyword codes into READ, BIN, LPRINT, and on through the table. A colour control is a byte that tells PRINT to change colour instead of showing a character. When PRINT reaches one here, the run stops with Invalid colour.

3. CAT has a complaint

16:12The address we're changing is one the ROM normally uses only while evaluating a function. The string this time is CAT. It's stored in b dollar, then printed.

16:51CAT's been replaced by the ROM's report messages, starting at Out of memory and running on to the copyright line. But their endings aren't quite the messages we know. This POKE changes DEFADD. The Spectrum manual marks it with an N, which it defines as, "Poking the variable will have no lasting effect". That isn't the same as no effect. A user-defined function works with values called arguments. DEFADD says where those arguments are while the function is being evaluated. Otherwise, it's zero. Our POKE makes its high byte non-zero. During this program's string lookup, that's enough for the ROM to act as though it's inside a function.

17:44So instead of finding our stored CAT, the ROM searches for b dollar among what it thinks are function arguments. But DEFADD now points into ROM code. After sixty-nine passes, the name test reads one instruction byte as the letter b. The next test asks whether the following byte marks a number. It doesn't, so the ROM treats the match as a string argument. As at number four, a string argument is represented by five bytes giving its start and length. Here the ROM borrows five instruction bytes from the routine that adds a typed BASIC line. Read as a description, they start the string at Out of memory and give it a length of nearly twenty-five thousand characters. The CAT stored in the program is never read. Those strange endings come from the way the table marks the end of a message. In the stored table, a hundred and twenty-eight has already been added to each message's last letter. We're printing the raw bytes as a string, so those marked last letters now have keyword codes. Out of memory ends in RANDOMIZE, and Statement lost ends in POKE.

18:59We've just printed Statement lost. The next POKE makes the processor run part of that message.

2. The message gives orders

19:10PRINT follows an output route to the screen. This POKE changes it. The program still asks for HI. Watch what appears below the original line.

19:38That's a second line ten, containing RUN, the command we've just typed. So how did output turn into input? PRINT doesn't draw the character itself. It hands the character's code to an output routine. The channel table holds the address of that routine, with different routes for different destinations. This table sits just beyond the system variables. The screen route is channel S. Our POKE changes just the high byte of its output address. Instead of going to the printing routine, the processor goes to the last t of Statement in Statement lost. We've been reading the ROM's instructions as text. Now we're sending its text to the processor as code. The Z80 doesn't know these bytes were meant to be a message. The t is an instruction that writes a byte. The space after it is a conditional jump, and the l supplies the jump distance. In this run, that jump lands inside the routine that adds a typed BASIC line. It's the same routine b dollar borrowed its five bytes from.

20:51Normally, that routine first records the new line number. We've jumped past that step, so the current line number stays at ten. The input line still contains the RUN command we typed to start the program. The routine stores that command with the current number, which is why the listing gains a second line ten containing RUN.

1. The font edits line thirty

21:21STR$ uses a different output route while turning a number into text. We're changing that one. Keep an eye on line thirty.

21:54What should those two PRINTs give us?

22:03A alone. And Nonsense in BASIC, in line thirty. Something has changed that line, which we haven't edited. The ROM prints the digits into its temporary work space through channel R. Number two changed the screen channel's output address. This changes R's, pointing it into the ROM's unused area, just before the font. When the ROM tries to output the digit five, it jumps there instead. That unused space is filled with one repeated byte: two hundred and fifty-five. Read as an instruction, each byte calls the routine the Spectrum normally uses for an interrupt. The call saves the address of the following byte as its way back. The routine advances the clock and checks the keyboard. Then it returns to that saved address, so execution moves to the next two hundred and fifty-five.

23:01From our new output address, there are one thousand, one hundred and fifty-one of those calls before execution reaches the font. Now it's running the character shapes we saw at number ten. Eight rows of dots per character, read as bytes of a program. The shapes before the digit two run first, but none of them changes the BASIC program.

23:30The digit two is where the program first gets changed. A, B, C and D are registers, small named places inside the processor that hold values. On the board, addresses and byte values are written in hexadecimal, where counting continues past nine with letters. That's why one more than 5B is 5C. The processor reaches the digit two with A at zero, D at 5D and C at zero. Those values come from the route we've just followed: earlier font shapes set A to zero and D to 5D, while C was already zero. Its first row does nothing. The second raises A from zero to one. The third copies D into B, so B and C together now make the address 5D00. In this typed program, 5D00 holds line thirty's closing quote. The fourth row, byte zero two, writes A to the address held by B and C. So the shape of the digit two writes the value one, not the digit one, over the quote. We've now found what changed line thirty.

24:50Near the end of the two, another row loads the printer position's high byte into A. That's the supposedly unused byte from number seven, still holding its normal value here. That value is 5B. The digit three increments A from 5B to 5C. It copies D into B again, then increments C from zero to one. That moves the destination from 5D00 to 5D01, line thirty's end-of-line byte. Its write stores 5C there. That's the code for a backslash. These are the only two writes into the BASIC program. The processor still has to get back to the call that sent it here. It runs through the rest of the font, the screen memory, the colours and the printer buffer, then into the system variables. There it reaches the byte C0 in the current-channel pointer. Read as code, C0 is a conditional return. In this run it uses the saved way back to return to the ROM's printing routine, as though the five had been printed. The processor is back, but line thirty has already changed.

26:10But no digit was added to the string. String dollar five comes back empty, so the first PRINT gives us A alone. Then BASIC reaches line thirty. Its closing quote and its line ending have been overwritten, so the ROM reports Nonsense in BASIC. Let's look at what's stored now.

26:37The question mark is how LIST shows the byte value one inside a string. That's the two's write. The backslash is the three's write, where the line used to end.

26:51The ROM never changed. One address it trusted sent it through its own font, and the font rewrote the program.

27:02If you like getting right inside these machines, subscribe to Retro Computer TV.

Credits

Every image, clip and piece of music in the film, with its licence and where it came from: the film's credits.

More on ZX Spectrum