A disassembly of MasterDOS 2.3 + MasterBASIC 1.7 for the SAM Coupé, built as
assembler source that can be proved correct: assembling it reproduces the
original file byte for byte.
dumps/MasterBasicMasterDos.bin is a SAM Coupé CODE file of 32640 bytes: a
nine-byte header followed by the 32631 bytes it declares.
The sector chain has already been followed. A disc sector holds 510 bytes of payload and two link bytes naming the next one, and those links are not in this file — 64 × 510 is exactly 32640, where 64 × 512 would be 32768. So it is the payloads concatenated, nothing more.
The header is part of the loaded image rather than something to skip: it sits at
offset 0 of the first half, so BOOT begins at &4009 and &4000–&4008 is
the header itself.
It is two 16320-byte halves that end up in different RAM pages, and that is the one thing to understand before reading any of it:
file 0–16319 |
MasterDOS 2.3, with MasterBASIC's patches spliced in |
file 16320–32639 |
MasterBASIC 1.7 |
Both halves are assembled to run at &4000–&7FBF, and each is written to
see the other at &8000–&BFBF. Which is which depends on the paging at the
time: with the DOS at &4000 the extension is at &8000, and when the extension
takes over the two swap. That is why the DOS's message pointer at &4210 holds
&9200 while the extension calls &BD79 — each is reaching into the other page.
So it is two address spaces, not one, and it is disassembled as two files. A
reference to &8000–&BFBF is resolved against the other half and written with
its label under a DOS_ or MB_ prefix.
Read listings/clean/. Two files, one per half, and they are the reading copy: every
routine headed with what it does and why, no number left as a bare constant
where a name would say more, and written for someone who knows Z80 but not this
machine. Conclusions only.
listings/disasm/ is the same code with the working notes left in — where a name came
from, what an earlier reading got wrong, which claims are still open. Read it
when you want the argument rather than the answer, or when you are checking
something and need to know how firmly it is held. Both hold only what can be
shown: names carried from sources that assemble to the same bytes, tables read
from the ROM, and descriptions from the MasterBASIC manual. Where something
could not be established, they say so rather than guessing.
listings/speculate/ is a reading, not a record. It is the same two listings with a
derived register contract on every routine and a machine-composed guess at what
each one is for, every guess marked with a leading ?. It is useful for finding
your way around 2,118 routines; it is not evidence. Its
README sets out where it is wrong and why.
postinstall-syspage.asm is a third kind again: a reconstruction.
MasterBASIC builds seven blocks into the ROM's own system page at boot
and points CMDV, EDITV, RST8V and five more vectors at them, so the code
the ROM actually calls lives at &45A2, &46CC, &484D and &4BA0 and
appears in the two listings only at the addresses it was stored at -- and
one of the blocks is not stored here at all, being assembled partly out
of the ROM's own PUT. tools/syspage.py builds that page and
disassembles it where it really runs, writing it beside the listings it
belongs with: listings/clean/postinstall-syspage.asm to read, and
listings/disasm/postinstall-syspage.asm with the working notes. It cannot be
verified by assembling -- there is no original to compare it with -- and
it says so at the top of itself.
listings/clean/, listings/disasm/ and listings/speculate/ all assemble to the original bytes —
everything added is a comment — so all six files are checked on every build.
Each tree has a base.asm that INCLUDEs both halves, and that is what is
assembled: the two halves are ORGed at &4000, where they run, and DUMPed
to a page each so they do not overlap in the output. It is one assembly and
still two answers — each half is compared with its own half of the image, so a
mismatch says which one it is in. base.asm also holds the equates both halves
need, said once, and the equates that bridge between the halves, written as
references rather than as numbers so that the assembler checks them.
python -m pip install pyz80
tools/build.shExit status is 0 only if all six listings come back byte-identical:
listings/disasm/masterdos.asm: BYTE-IDENTICAL
listings/disasm/masterbasic.asm: BYTE-IDENTICAL
listings/clean/masterdos.asm: BYTE-IDENTICAL
listings/clean/masterbasic.asm: BYTE-IDENTICAL
listings/speculate/masterdos.asm: BYTE-IDENTICAL
listings/speculate/masterbasic.asm: BYTE-IDENTICAL
The build assembles the annotated MasterDOS source and the SAM ROM first, for their symbol tables and BASIC token tables, then disassembles the image against them, rebuilds the system page, then reassembles everything it wrote and compares. It prints the byte census and the description count as it goes, so the figures below are measured on every run rather than remembered. Nothing is left to be checked by eye.
The two reference trees are submodules:
git submodule update --initdumps/ |
the image being disassembled, and the hardware captures it is checked against |
listings/clean/ |
the reading copy — start here; also the ROM's system page as MasterBASIC leaves it, and the base.asm that assembles both halves |
listings/disasm/ |
the same code with the working notes left in |
listings/speculate/ |
the same again, with a machine's reading of every routine |
notes/ |
hand-written names and descriptions, fed into both |
tools/ |
the disassembler and the passes that annotate it |
docs/ |
how it works, the idiom guide, how it is built, the manual |
docs/original/ |
the MasterBASIC manual as scanned, and its errata |
ref/masterdos/ |
annotated MasterDOS 2.3 source (submodule) |
ref/samrom/ |
SAM Coupé ROM 3.0 source (submodule) |
diskimages/ |
the original disk images |
design/ |
what this project set out to do |
docs/how-it-works.md is the narrative: what MasterBASIC does, how it gets the ROM to call it, how it finds the ROM in the first place, and what is still unexplained. Read that before the listings.
docs/idioms.md collects the tricks that recur across the
image, the SAM ROM and MasterDOS -- the paging conventions, inline call
parameters, the &21 skip, bit merges, self-modifying operands, and how to
tell that a block of code is not running where it is stored -- each with the
code and a line-by-line reading. Worth having open beside the listings.
docs/evidence-wanted.md is the short list of things this project cannot settle by reading -- what to capture on a real machine or an emulator, and what each would decide.
docs/disc.md follows one read and one write all the way down, from the BASIC statement to the bytes moving through the WD1772 -- the layer the other documents leave to the DOS.
docs/disassembly.md is the other long form: how the listings are built, what each pass contributes, and what is still open.
docs/bugs.md is the defects found in MasterDOS itself — three confirmed and one suspected so far. The listings cannot be corrected, since they assemble to the original image byte for byte, so a defect is written down there and explained where it sits.
docs/nmi-hook.md is a how-to rather than a description: what to do with the snapshot button's user vector, how to install one from BASIC, what your routine is entered with, and the two ways back into the machine.
docs/masterbasic-keywords.md and docs/masterbasic-tokens.md are the reference pair: the 28 keywords MasterBASIC adds, and the mechanism by which a DOS gets a word into a tokeniser that lives in ROM.
docs/tokens.md widens that to every token the machine
knows -- the ROM's, MasterDOS's and MasterBASIC's in one table, generated
from KEYWTAB and MBKEYS by tools/tokentab.py rather than typed.
docs/sam-basic-grammar.txt is the same list in
machine-readable form with the syntax of each keyword beside it, enough to
write a tokeniser, a lister, a syntax highlighter or a completion list
against.
notes/*.txt is the way in for anything you work out yourself. Adding a name
costs one line and no code:
MB &5934 SERINIT
Set up the SCC2691 for LPRINT MODE 2.
AFTER CHECK_WRITE_STATUS : read the controller status through the patched port
RENAME ULA BORDER
EQU STKEND : end of the calculator stack
DOS &4220-&42BC data DVAR
DOS &4835 value DISKCTL_0_BASE
Eight kinds of entry, by address or by label name. Hand-written entries beat anything the tools worked out, and a disagreement is reported rather than resolved silently — as is a name that matches nothing, or one that matches an address in both pages. A typo is reported and skipped; it cannot break the build. The rules are at the top of tools/notes.py, with examples in notes/example.txt.
Every one of the 32640 bytes is accounted for:
| bytes | |
|---|---|
| Code | 28376 (86.9%) |
| Variables and other data | 2636 |
| Inline call parameters | 893 |
| Message and keyword text | 696 |
RST &08 codes |
29 |
| Pointer tables | 10 |
| Unclassified | 0 |
No label lands inside an instruction. 365 of the 2421 labelled addresses carry a description -- some written by hand after reading the code, the rest the annotated MasterDOS author's, carried across where the two instruction streams still agree. That is a minority of the 2,118 routines the disassembler segments, and the remainder are named but not explained. The build prints these counts, so they are measured rather than remembered.
MasterDOS and MasterBASIC were written by Andrew J. A. Wright — the author of the
SAM Coupé ROM itself, which is why the DOS calls internal ROM addresses under the
ROM's own label names. The manual in docs/original/ was scanned by Steve
Parry-Thomas; the annotated MasterDOS and ROM sources are separate projects,
included here as submodules.