Rung 2: the machine starts itself off a disk

Stage one exists and works. It is 330 bytes of program and everything it
knows is a thing that will be true forever: which port the disk is on, that
a SplitBit disk begins with its own name, and where two numbers sit in that
first block. Not what a file is, not what a directory is, not that SBFS has
versions. It reads the live boot slot into Program Memory, jumps to the
first byte, and prints one character and halts if there is nothing there.

It is an ordinary boot image for now, so the whole chain runs on machinery
that already exists and the emulator has not been touched. Nothing about it
changes when it moves into ROM except who puts it in memory.

SplitDisk gained "boot" to write a slot and "bootslot" to choose one, kept
apart on purpose: writing a slot and starting from it are different
decisions, and joining them would make every write a commitment. A slot is
always written WHOLE, because one still holding the tail of what was there
before is one whose contents depend on its history, and stage one reads all
of it without knowing where the file stopped.

Three recorded tests, and the pair is the point: two disks differing only
in which slot the superblock names, with payloads that say different
things. One prints "booted" and the other does not, so this is a test of
CHOOSING a slot rather than a test that some bytes were read. The third
boots a disk with no boot area and gets the one character a ROM has room
for. Eight more host checks, including that a slot is padded whole.

Two things worth recording. The first draft used #Align to put the scratch
buffer at 0x8000 and produced a 33K file - thirty two kilobytes of zeroes
in something meant to be a ROM. It is an address, not storage, which is
exactly what the assembler's own scratch map exists to say.

And SplitLint caught the second in code written an hour after the baseline
that catches it. In the blit set-up, RSTA writes a source address of zero
and then RSTA writes a bank number of zero - two unrelated quantities that
are equal by accident, in the most safety critical file in the repository.
It is marked with a reason rather than removed.
This commit is contained in:
Anachronaut
2026-08-26 23:31:04 -04:00
parent 612bd1b97c
commit d07b23f90b
11 changed files with 509 additions and 0 deletions
+28
View File
@@ -192,6 +192,34 @@ cp boot.img badslot.img
bootField badslot.img 16 07 # Names slot 7, and there are two.
refuses "nor a slot that does not exist" "$TOOL" list badslot.img
# ---- Writing a boot slot, and choosing between them ----
#
# Two commands rather than one, deliberately: writing a slot and starting from it are
# different decisions, and joining them would make every write a commitment.
printf 'not really a bootloader' > stage.bin
check "write a boot slot" "$TOOL" boot boot.img stage.bin 0
check "and the other one" "$TOOL" boot boot.img stage.bin 1
check "choose which one starts" "$TOOL" bootslot boot.img 1
refuses "no third slot to write" "$TOOL" boot boot.img stage.bin 2
refuses "nor a third to choose" "$TOOL" bootslot boot.img 2
refuses "no boot slot without an area" "$TOOL" boot plain.img stage.bin 0
# A slot holds what it holds. Something too big for one is refused rather than cut off,
# because half a bootloader is the failure with no way back.
head -c 9000 /dev/zero > toobig.bin # A slot on boot.img is 32 blocks, so 8192.
refuses "nor more than a slot holds" "$TOOL" boot boot.img toobig.bin 0
# THE WHOLE SLOT IS WRITTEN, not just the part the file fills. A slot still holding the
# tail of whatever was there before is one whose contents depend on its history.
printf 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' > long.bin
"$TOOL" boot boot.img long.bin 0 >/dev/null
"$TOOL" boot boot.img stage.bin 0 >/dev/null
check "and it is written whole" python3 -c "
import sys
d = open('boot.img','rb').read()
slot = d[256:256 + 32 * 256]
sys.exit(1 if b'aaaa' in slot else 0)"
echo
if [ "$FAIL" -eq 0 ]; then
echo "All $PASS disk tool checks passed."
+3
View File
@@ -0,0 +1,3 @@
booted
Execution halted.
[exit 0]
+3
View File
@@ -0,0 +1,3 @@
ooohed
Execution halted.
[exit 0]
+3
View File
@@ -0,0 +1,3 @@
?
Execution halted.
[exit 0]
+1
View File
@@ -1,3 +1,4 @@
Programs/Boot/stage1.asm redundant-setd 2
Programs/CosmOS/Apps/Copy.asm redundant-setd 2
Programs/CosmOS/Apps/Edit.asm redundant-setd 3
Programs/CosmOS/Apps/Wander.asm redundant-setd 1
+24
View File
@@ -353,3 +353,27 @@ done
# so the superblock is written by hand.
"$TOOL" format "$DISKS/bigdir.img" 64 2 >/dev/null
printf '\x20\x00' | dd of="$DISKS/bigdir.img" bs=1 seek=10 conv=notrunc status=none
# A disk that can be started: stage one reads the live boot slot into Program Memory and
# jumps into it, and each slot holds a different payload so that choosing between them is
# visible rather than assumed.
#
# The payload is written RAW. A loadable program carries a sixteen byte header saying where
# its pieces go, and stage one does not read headers - it reads blocks, which is the whole
# point of keeping it small enough to put in a ROM.
"$TOOL" format "$DISKS/chain.img" 256 4 4 >/dev/null
"$ROOT/Assembler" "$ROOT/Programs/Boot/slotTest.asm" -o "$WORK/slotTest.sbx" >/dev/null
tail -c +17 "$WORK/slotTest.sbx" > "$WORK/slotTest.raw"
"$TOOL" boot "$DISKS/chain.img" "$WORK/slotTest.raw" 0 >/dev/null
# The other slot says something else, so a test that reads "booted" is reading slot zero
# rather than reading whatever happens to be in Program Memory.
sed 's/INIA 0x62 ; "b"/INIA 0x6F ; "o"/; s/INIA 0x74 ; "t"/INIA 0x68 ; "h"/' \
"$ROOT/Programs/Boot/slotTest.asm" > "$WORK/otherSlot.asm"
"$ROOT/Assembler" "$WORK/otherSlot.asm" -o "$WORK/otherSlot.sbx" >/dev/null
tail -c +17 "$WORK/otherSlot.sbx" > "$WORK/otherSlot.raw"
"$TOOL" boot "$DISKS/chain.img" "$WORK/otherSlot.raw" 1 >/dev/null
# And the same disk with the other slot chosen, so both are exercised.
cp "$DISKS/chain.img" "$DISKS/chainAlt.img"
"$TOOL" bootslot "$DISKS/chainAlt.img" 1 >/dev/null
+15
View File
@@ -353,6 +353,21 @@ cosmosDeep | CosmOS/Source/cosmos.asm | run | cosmosDee
# nothing, so the same create works again and again and fills the root with entries of one
# name. Refused at mount, which is the only place it can be refused once and for all.
cosmosBigDir | CosmOS/Source/cosmos.asm | run | cosmosBigDir.in | - | disks/bigdir.img
# ---- Starting the machine off a disk ----
#
# Stage one is what will one day be in ROM. It knows the disk's ports, that a SplitBit disk
# begins with its own name, and where two numbers sit in that first block - and nothing
# else. It reads the live boot slot into Program Memory and jumps to the first byte.
#
# Two disks, differing only in which slot the superblock names, and the payloads say
# different things. That is what makes this a test of CHOOSING a slot rather than a test
# that some bytes were read: one prints "booted" and the other does not.
bootChain | Boot/stage1.asm | run | - | - | disks/chain.img
bootChainAlt | Boot/stage1.asm | run | - | - | disks/chainAlt.img
# A disk with no boot area cannot be started, and says so in the one character a ROM has
# room to say anything in.
bootNoArea | Boot/stage1.asm | run | - | - | disks/sbfs.img
# Reading a disk that has directories on it. The machine can walk a path at this point but
# cannot make a directory, so the disk is built by the host tool and read here - which is
# the two implementations checking each other rather than either checking itself.