fugløy

Fugløy is best on a desktop.

TRS-80 Model I and III · Z80 · 1977–1983

Inside Fugløy

Fugløy takes a forty-year-old TRS-80 program apart into readable Z80 assembly, then puts it back together and proves that not one byte changed.

This page walks through how it does that, one idea at a time. Every byte here is colored by what it turned out to be.

Z80 code BASIC Data and text Headers and pointers
TRS-80 VIDEO DISPLAY64 × 16 CHARACTERS

Why it's new

A disassembler that knows its machine

First and foremost, Fugløy is a Z80 disassembler built for one machine: the TRS-80 Model I and Model III. Around that core it adds what the machine's software needs, and it proves every result.

What each kind of tool is built for

Fugløy's point is doing all of this together, for one machine, and proving the result.

The machine

64K is the whole world

A Z80 can address 65,536 bytes, and on a TRS-80 everything lives in that one range: the BASIC ROM, the keyboard, the screen, and the program you loaded.

Fugløy models memory the same way: one flat array of 65,536 bytes, and a second array that marks which of them a file actually loaded.

Memory map · Model I
$0000$4000$8000$C000$FFFF

The file

A program is a box of records

A .CMD file is not a picture of memory. It is a list of records, each a type byte, a length byte, and a payload: "load these bytes here", "this program is called", "start running there".

Tap a record to read it. This one is six bytes of real Z80 that put the letter A in the top-left corner of the screen.

Go deeper: the length byte that wraps

In a load record the length also counts the two address bytes, and it wraps around. A length of 8 means six data bytes, but a length of 2 means 256 of them, 1 means 255, and 0 means 254.

Real files bend the other rules too: a transfer record with a length of zero, damaged records, padding after the end. Fugløy reads each one by its content, never by the file's extension, and every quirk has its own test.

DEMO/CMD · 20 bytes

BASIC on disk

BASIC, squeezed into bytes

Level II BASIC never stores the word PRINT. It stores one byte, B2. All 124 keywords, from END at 80 to the apostrophe at FB, are crunched that way as you type.

Each stored line is a pointer to the next line, the line number, the crunched body, and a 00. Type a line and watch it shrink.

Go deeper: the odd ones

ELSE is stored with a colon in front of it, and a remark apostrophe becomes three bytes, :REM'. A question mark typed for PRINT becomes the PRINT token. Text inside quotes, after REM, and in DATA is copied as typed, so a keyword in a string stays letters.

Voyage of the Valkyrie · Leo Christopherson · 1980

Machine code hiding in plain sight

One file, one load, and you have a BASIC program and a pile of fast machine code. Line 1 says GOTO248. Everything it jumps over is not BASIC at all.

valkyri1.bas · all 257 lines
BASIC linemachine codeline 250, which plants the jump
1BASIC line: GOTO248
182lines of Z80, 2 to 245
74BASIC lines, 248 to 4800

Point at a square to see its line.

Line 2, first 8 bytes, three ways

Telling them apart

Is it BASIC? Ask the grammar

Counting bytes cannot tell code from BASIC, because the commonest Z80 instructions are BASIC keywords too:

So Fugløy reads each line the way BASIC would. Every statement must have the shape its keyword demands, and every byte must be one BASIC could have stored. One wrong byte and the line is code.

Tap a card for the verdict
Why each Valkyrie code line failed · 2,366 lines across 13 files

About six code lines per file are clean enough to pass the byte check. Only the statement shapes catch those, and they catch every one.

Finding the way in

Following a planted pointer

The code needs an entry point, and BASIC builds one at run time. Line 250 reads where the program sits in memory, adds 14, splits the result into two bytes, and pokes a JP instruction into memory.

Fugløy does not run BASIC. It evaluates just enough of it with symbols in place of numbers, so "wherever the program happens to load, plus 14" comes out as a real address for each file.

Line 250, step by step

Code or data

Follow the flow, not the bytes

The simple way to disassemble is to decode every byte in order, a linear sweep. It turns text into nonsense instructions, because the Z80 happily decodes the letter H as LD C,B.

Fugløy starts where the program starts and follows every jump and call instead. Bytes nothing jumps to stay data, and terminated runs of text become quoted strings.

12 bytes at $7000

        
mnemonicregisternumberlabelstringcommentdirective

Asylum · Med Systems Software · 1981

Games that move themselves

Many games load high in memory, out of the disk system's way, then copy themselves down to where they really run. A disassembler that reads the file sees only the copier.

asylum16.cmd: 123 records copied down, then JP $43AE
In the file · $8000–$BFEF
When it runs · $4000–$7FFF
$4000$5000$6000$7000$8000

Fugløy's answer is a relocate hint: these file bytes execute over there. It analyzes them at the address they run at, then writes them back where they load, inside zmac's .PHASE, so labels take the running addresses and the bytes still land in their original place.

For Asylum the 123 hints are generated from the loader's own record stream. With them, 61% of the game decodes as code at its running address.


      

The five-instruction mover

Most of the other games use a simpler copier, like this one from Sea Dragon (1982), which its author released into the public domain in 1995. Fugløy spots it, works out what moves where, and proposes the exact hint lines, without applying anything on its own.


      
Code found before and after the proposal
before: the copier aloneafter: the game too

The guarantee

Prove it: put it back together

A listing can look right and still be wrong. So every test ends the same way: assemble Fugløy's output with a real assembler and compare the result with the original, byte for byte.

What the proof catches

        

1,786Z80 encodings, each reassembled to its own bytes
83real programs that round-trip exactly
2,149automated tests, all passing
0bytes allowed to differ

Your notes

Notes that survive a move

What you learn about a program goes into a hint file: plain text, one fact per line, made to be kept in git. It names routines, marks data, declares tables, adds entry points, and says what moves where.

Every line is pinned to a block and an offset, and a block is recognized by its content: its length plus a fingerprint of its first 64 bytes. Asylum 1b, 1c, and 1d are the same 20,297 bytes loaded at $6300, $A000, and $5200, and one hint file fits all three.

game.hints

        
Go deeper: moving a BASIC program

A tokenized BASIC file records the address it was saved from, inside every line pointer. --rebase 7000 rewrites the pointers so the program reads as if saved at $7000, and flags any machine code that still points at the old addresses as stale.

For BASIC the fingerprint reads each pointer as an offset from the start, so hints written before a rebase still match after it.

Phase 3 · Names

From numbers to names

A TRS-80 program talks to the machine by address: $0049 waits for a key, $3C00 is the screen, port $FF is the cassette. Fugløy knows those addresses for the Model I and III, and Model 4's DOS calls, from eight tables, and the listing says what they are.

Nothing here is guessed. Every entry in every table cites the manual and the page it came from, and the workbench opens your own copy of that page if you have added one. Where two manuals name one address differently, as TRSDOS and LDOS do, you choose which DOS's names to use.

These five rows are from Eliminator, one of the four public-domain games that ship with Fugløy.

Five rows of Eliminator

        

How it was built

Small steps, each one proven

Fugløy grew in phases, and every phase ended with something that ran and a test suite that said so.

Phase 1 · v0.1.0

Decoder and loaders

The Z80's instruction tables are generated from the bit patterns of each opcode, never typed by hand, and checked against a reference written independently: 1,792 encodings, every one compared.

  • .CMD and .BAS loaders, one test per quirk
  • A BASIC detokenizer and a linear-sweep listing
Phase 2A

Follow the flow, prove the round trip

Recursive descent, a JSON description of the program, and the zmac emitter, with a harness that reassembles every output and compares images.

Phase 2B

Names and notes

Labels for every jump, call, and data reference, and the hint file with content-based block identity.

Phase 2C

Hidden code and moving code

The BASIC grammar, the planted-pointer evaluator, strings, table and loader proposals, rebasing, and relocation with .PHASE; Asylum's generated hints and a ROM smoke test.

Phase 3

Names for the ROM, hardware, and DOS

Eight tables of named addresses, 634 entries, each citing the manual and page it came from. A listing says CALL $KBWAIT where it used to say CALL 0049H, and a comment says what the routine does.

Emulator · phases 0–1

A Z80 that passes the exercisers

A headless core, with no screen and no clock of its own, that runs ZEXDOC and ZEXALL to the end and agrees with the disassembler's tables on every opcode's timing.

Phase 4

The workbench

This site: the listing in your browser with names, notes, jumps, exports, and your own manuals opening at the cited page. Nothing leaves your machine.

Part 2 · planned, not built

From listing to a game that runs anywhere

The disassembler comes first. The listing it produces is also the first step of a longer plan: recompile a game into readable TypeScript and ship it as one file that plays in any modern browser, with no original binary or ROM image inside.

The pipeline

How it would work

The game's own logic becomes TypeScript functions, named by Fugløy's labels, with no Z80 instructions decoded while it runs. A small runtime plays the machine around it: video memory at $3C00 drawn on a canvas, the keyboard matrix at $3800, the ROM routines the game calls, cassette-port sound, and the 30 Hz heartbeat.

Every frame is checked against the emulator. The same keystrokes, at the same cycle, go to the original and to the translation, and their screens are compared; the first difference names the block that is wrong.

The plan targets the browser: one HTML file, or an ES module with a <trs80-game> element. Because the output is plain TypeScript, a Node build that draws the screen in a terminal would be a natural extension, but it is not part of the plan yet.

Bring your own
  • Fugløy ships no ROMs, and only four programs: the public-domain games Sea Dragon, Rear Guard, Eliminator, and Armored Patrol, so anyone can try it on real games. You supply the TRS-80 ROM and any other .CMD and .BAS files you want to work on.
  • Everything happens on your own machine: the listing, the hint files, and any game built from them stay with you.
  • Some games are free to share. Wayne Westmoreland released the TRS-80 games he wrote with Terry Gilman into the public domain in 1995: Sea Dragon, Armored Patrol and its original Tank Zone, Eliminator, Rear Guard, Zaxxon, and a Donkey Kong that was never sold. They make natural first candidates, and four of them already ship with Fugløy.
When

After the workbench ships as v1 and the plan's kill criteria have been reviewed. The emulator comes first, because it is the oracle every translation is checked against.

What's next

From bytes to names, and beyond

Fugløy is MIT-licensed: github.com/cschweda/fugloy.

Fugløy ships no ROMs, and four public-domain games by Wayne Westmoreland and Terry Gilman, released in 1995: Sea Dragon, whose copy loop appears above, Rear Guard, Eliminator, and Armored Patrol. For everything else, you bring your own. The other games named here are the ones it was tested on, and this page shows only a few of their bytes.