From c312853f8effc78b8a90fc8e0047aa700010de4e Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Thu, 27 Aug 2026 14:13:23 -0400 Subject: [PATCH] The machine starts itself stage two CosmOS > saved it read it back, 22 bytes: Stage one hands over to stage two out of a boot slot; stage two mounts the filesystem, finds /System/cosmos.bin, takes the image apart and places its code, its data and its vector table, and jumps to the entry point the vectors named. Nothing placed memory for it. What it loads is an ORDINARY BOOT IMAGE, the same SPBT file the emulator has always been handed. That was the user's call and it is the whole trick: a second stage that loads the machine's normal image format is not a boot-specific mechanism, so bare metal SplitBit stops being a special case. A program wanting no operating system under it is just an image, written under CosmOS like any other, and startable because it is a file. Three things in it worth knowing: - THE ENTRY POINT IS CAUGHT ON ITS WAY PAST. Program Memory cannot be read back, so the boot vector cannot be looked up after being installed; the vector loop notices the one addressed at 0xFC00 and keeps it. - A missing "VEC" is not a fault. An image written before vectors existed simply ends after its data, and then the entry point is zero, which is what every such image has always relied on. - Feature flags that are set mean an image asking for a machine this may not be, and the honest answer to a request that cannot be understood is to refuse rather than to run it anyway. The test records that the system WORKS afterwards rather than that it started. A loaded program running is what says the vector table arrived, because a program reaches the system through SWI and nothing else; the file written and the directory entered say the filesystem and the console came up with it. A second disk has a boot slot and nothing to start, and says so rather than jumping somewhere. --- Programs/Boot/stage2.asm | 459 ++++++++++++++++++++++++++++ README.md | 17 ++ Tests/expected/selfBoot.out | 14 + Tests/expected/selfBootNoSystem.out | 4 + Tests/input/selfBoot.in | 5 + Tests/lint-baseline.txt | 1 + Tests/makedisks.sh | 26 ++ Tests/manifest | 15 + 8 files changed, 541 insertions(+) create mode 100644 Programs/Boot/stage2.asm create mode 100644 Tests/expected/selfBoot.out create mode 100644 Tests/expected/selfBootNoSystem.out create mode 100644 Tests/input/selfBoot.in diff --git a/Programs/Boot/stage2.asm b/Programs/Boot/stage2.asm new file mode 100644 index 0000000..1240c5b --- /dev/null +++ b/Programs/Boot/stage2.asm @@ -0,0 +1,459 @@ +; stage2.asm +; The second stage. Finds the system on the disk and starts it. +; +; Stage one knows nothing about filesystems and never will, because it is going into a ROM +; and a ROM that knew SBFS would freeze the format in silicon. This is where that knowledge +; lives instead: on the disk, in a boot slot, replaceable by writing blocks. +; +; ---- What it loads ---- +; +; An ordinary SplitBit boot image, the same SPBT file the emulator has always been handed. +; That is the whole trick and it was the user's: a second stage that loads the machine's +; NORMAL image format is not a boot-specific mechanism at all. Bare metal SplitBit stops +; being a special case - a program that wants no operating system is just an image, written +; under CosmOS like any other, and startable because it is a file. +; +; ---- How it gets its own data ---- +; +; Stage one places Program Memory and nothing else. A loadable image is written into the +; slot as code followed by data, so this program's Data Segment is sitting in Program Memory +; just past its own code when it starts, and the first thing it does is blit it down. See +; slotData.asm, which proves the mechanism on its own. +; +; Written by Anachronaut + +#Program + + ; Above everything the system will occupy, so that loading the system does not walk over + ; the loader while it is still running. + #Base 0xC000 + +start: + ; ---- The Data Segment, fetched from just past the code ---- + ; + ; A ROUND FOUR KILOBYTES rather than the exact size. The blit needs a length, the + ; assembler will not work out the difference between two labels, and a hand kept number + ; is a number that goes wrong quietly - slotData.asm padded to 257 and copied 256, and + ; the byte that never arrived happened to be padding. Copying more than there is costs + ; nothing: the source runs into slot padding, and the destination is nobody's memory. + RSTA + OUTA 0xE0 ; SourceBank: Program Memory, where stage one put everything. + SETD.0 codeEnd + PSHD.0 + POPA + POPB + OUTB 0xE1 + OUTA 0xE2 + INIA 0d1 + OUTA 0xE3 ; DestBank: Data Memory. + SETD.0 StageDataBase + PSHD.0 + POPA + POPB + OUTB 0xE4 + OUTA 0xE5 + INIA 0x10 + OUTA 0xE6 + RSTA + OUTA 0xE7 ; Four kilobytes. + INIA 0x01 + OUTA 0xE8 + + ; ---- Now there is data, so there can be words ---- + SETD.0 StageName + RCAL say + + CALL sbfsMount + BNQ noFilesystem + + SETD.0 SystemName + CALL sbfsFind + BNQ noSystem + + ; ---- The image, read whole into somewhere nothing else is using ---- + SETD.2 StageAt + LDD.1.2 + CALL sbfsRead + BNQ unreadable + + ; "SPBT", or this is a file rather than something to start. + SETD.2 StageAt + LDD.0.2 + SETD.2 BootMagic + INIA 0d4 + RCAL matchTag + BNQ notAnImage + + ; The format version, and then four bytes of feature flags. A flag set is an image + ; asking for something this machine may not have, and the honest answer to a request + ; that cannot be understood is to refuse rather than to run it anyway. + LDA.0 + INIB 0d1 + XOR + BNQ wrongVersion + INCD.0 + INIA 0d4 + RCAL allZero + BNQ wantsMore + + ; "PRG", its length, and then the code itself straight into Program Memory. + SETD.2 ProgTag + INIA 0d3 + RCAL matchTag + BNQ notAnImage + RCAL takeCount + RSTA + RCAL placeSegment + + ; "DAT", the same again into Data Memory. + SETD.2 DataTag + INIA 0d3 + RCAL matchTag + BNQ notAnImage + RCAL takeCount + INIA 0d1 + RCAL placeSegment + + ; ---- "VEC", which an image written before vectors existed simply does not have ---- + ; + ; So running out of file here is the ordinary case rather than a fault: what follows is + ; the end of a perfectly good image, and the entry point is then the address zero. + SETD.2 VecTag + INIA 0d3 + RCAL matchTag + BNQ noVectors + RCAL takeCount + +vectorLoop: + SETD.2 CountHigh + LDA.2 + INCD.2 + LDB.2 + OR + BRQ vectorsDone + + ; Four bytes: where in Program Memory the vector sits, then what to put there. The + ; controller writes it, because nothing else can write Program Memory. + LDA.0 + SETD.2 VecWhere + STA.2 + INCD.0 + LDA.0 + INCD.2 + STA.2 + INCD.0 + + RSTA + OUTA 0xE3 ; DestBank: Program Memory. + SETD.2 VecWhere + LDA.2 + OUTA 0xE4 + INCD.2 + LDA.2 + OUTA 0xE5 + LDA.0 + OUTA 0xE9 ; A byte straight in, rather than a blit of one. + INCD.0 + LDA.0 + OUTA 0xE9 + INCD.0 + + ; THE BOOT VECTOR IS THE ONE WORTH KEEPING. Program Memory cannot be read back, so the + ; address to start at has to be noticed on its way past rather than looked up after. + SETD.2 VecWhere + LDA.2 + INIB 0xFC + XOR + BNQ vectorStepped + INCD.2 + LDA.2 + BNA vectorStepped + SETD.2 EntryHigh + DECD.0 + DECD.0 + LDA.0 + STA.2 + INCD.0 + LDA.0 + INCD.2 + STA.2 + INCD.0 + +vectorStepped: + SETD.2 CountLow + LDA.2 + INIB 0d4 + CCF + SUB + MVQA + STA.2 + BNC vectorLoop + SETD.2 CountHigh + LDA.2 + DECA + STA.2 + BRI vectorLoop + +noVectors: +vectorsDone: + ; And into it. Everything above has been arranging memory; this is the only instruction + ; that hands the machine over. + SETD.0 EntryHigh + LDD.1.0 + BRD.1 + +unreadable: + SETD.0 UnreadableText + RCAL say + HALT +notAnImage: + SETD.0 NotImageText + RCAL say + HALT +wrongVersion: + SETD.0 VersionText + RCAL say + HALT +wantsMore: + SETD.0 FeatureText + RCAL say + HALT + +noFilesystem: + SETD.0 NoDiskText + RCAL say + HALT + +noSystem: + SETD.0 NoSystemText + RCAL say + SETD.0 SystemName + RCAL say + RCAL newLine + HALT + +; ---- Comparing a marker and stepping past it ---- +; +; DP0 is in the image, DP2 names what it should say, A is how many bytes. DP0 is left past +; them either way, which is what lets a caller try one marker and carry on. +matchTag: + SETD.1 TagLeft + STA.1 +tagLoop: + SETD.1 TagLeft + LDA.1 + BRA tagSame + DECA + STA.1 + LDA.0 + LDB.2 + XOR + BNQ tagDiffers + INCD.0 + INCD.2 + BRI tagLoop +tagSame: + RSTA + RSTB + CCF + ADD + RRET +tagDiffers: + ; Past the rest of it regardless, so that what DP0 points at does not depend on which + ; byte happened to differ. + SETD.1 TagLeft + LDA.1 + BRA tagDiffersDone + DECA + STA.1 + INCD.0 + BRI tagDiffers +tagDiffersDone: + RSTA + INIB 0d1 + CCF + ADD + RRET + +; A bytes at DP0, all of which have to be zero. Used for the feature flags, where anything +; set is the image asking for something this machine may not be able to give it. +allZero: + SETD.1 TagLeft + STA.1 +zeroLoop: + SETD.1 TagLeft + LDA.1 + BRA zeroAllClear + DECA + STA.1 + LDA.0 + INCD.0 + BNA zeroNotClear + BRI zeroLoop +zeroAllClear: + RSTA + RSTB + CCF + ADD + RRET +zeroNotClear: + RSTA + INIB 0d1 + CCF + ADD + RRET + +; Two bytes at DP0, most significant first, into the count that placeSegment and the vector +; loop both work through. DP0 is left on the byte after them. +takeCount: + LDA.0 + SETD.1 CountHigh + STA.1 + INCD.0 + LDA.0 + SETD.1 CountLow + STA.1 + INCD.0 + RRET + +; ---- A segment, out of the staged image and into the memory it belongs in ---- +; +; A is the bank: 0 for Program Memory, 1 for Data. Both segments go to address zero, which +; is what a boot image means - it is the format for something that owns the machine. +; +; DP0 is left past the segment, ready for whatever marker comes next. +placeSegment: + SETD.1 WantBank + STA.1 + + INIA 0d1 + OUTA 0xE0 ; SourceBank: Data Memory, where the image was staged. + PSHD.0 + POPA + POPB + OUTB 0xE1 + OUTA 0xE2 + SETD.1 WantBank + LDA.1 + OUTA 0xE3 + RSTA + OUTA 0xE4 + OUTA 0xE5 ; To the bottom of it. + SETD.1 CountHigh + LDA.1 + OUTA 0xE6 + INCD.1 + LDA.1 + OUTA 0xE7 + INIA 0x01 + OUTA 0xE8 + + ; And past it. DPUW steps a pointer by A and B together, which is exactly the shape the + ; two halves of a length are already in. + SETD.1 CountHigh + LDA.1 + INCD.1 + LDB.1 + DPUW.0 + RRET + +; ---- Saying something ---- +; +; Its own rather than console.asm's, because everything this includes has to fit in a boot +; slot and the console library brings a great deal this will never use. DP0 names a string. +say: + LDA.0 + BRA sayDone + OUTA 0x00 + INCD.0 + BRI say +sayDone: + RRET + +newLine: + INIA 0x0A + OUTA 0x00 + RRET + +#Data + + ; Out of the way of everything: the system's data goes to 0x0000, and whatever image is + ; being loaded is staged below this. + #Base 0xE000 + +; The first thing in the segment, so that its address is where the blit above puts the +; whole of it. +StageDataBase: + +StageName: +"stage two +" +FoundText: +"found " +NoDiskText: +"no filesystem +" +NoSystemText: +"no " + +; What to start. A name for now; the plan is for this to be read out of a file so that any +; number of systems can sit on one disk and the choice is an ordinary safe write. +SystemName: +"/System/cosmos.bin" + +UnreadableText: +"could not read it +" +NotImageText: +"not a boot image +" +VersionText: +"an image this cannot start +" +FeatureText: +"the image wants more machine +" + +BootMagic: +"SPBT" +ProgTag: +"PRG" +DataTag: +"DAT" +VecTag: +"VEC" + +TagLeft: + 0x00 +WantBank: + 0x00 +CountHigh: + 0x00 +CountLow: + 0x00 +VecWhere: + 0x00 0x00 + +; Where to start once memory is arranged. Zero unless the image says otherwise, which is +; what a boot image with no vector table means and what every program written before the +; table existed relies on. +EntryHigh: + 0x00 +EntryLow: + 0x00 + +; Where the image is put while it is being taken apart. Below this program's own data and +; above everything the system will occupy. An address rather than storage: reserving it +; would put its zeroes in the boot slot. +StageAt: + 0x20 0x00 + +#Include sbfs.asm + +; ---- The end of everything ---- +; +; Back to the Program Segment so that this label lands past the included code as well as +; past the code above. It is where the Data Segment's image begins once stage one has put +; the whole slot into Program Memory. +#Program +codeEnd: diff --git a/README.md b/README.md index ef772dc..913ed33 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,23 @@ has versions - all of that lives in the boot area, on the disk, where it can be It reads the live slot into Program Memory, jumps to the first byte, and prints one character and stops if there is nothing to start. +`Programs/Boot/stage2.asm` is what sits in the slot. It mounts the filesystem, finds the +system by name, and takes the image apart: code into Program Memory, data into Data +Memory, and each vector written through the controller, which is the only thing that can +write Program Memory at all. The entry point is noticed on the boot vector's way past, +because Program Memory cannot be read back to look it up afterwards. + +**What it loads is an ordinary boot image** - the same SPBT file the emulator has always +been handed directly. That is the whole trick: a second stage that loads the machine's +normal image format is not a boot-specific mechanism, so **bare metal SplitBit stops being +a special case.** A program that wants no operating system under it is just an image, +written under CosmOS like any other, and startable because it is a file. + +Stage two arranges its own Data Segment. Stage one places Program Memory and nothing else, +since knowing where a payload's data ended would mean knowing a format, so the image is +written into the slot as code followed by data and stage two's first act is to blit its +data down from just past its own code. + SplitDisk speaks the same on disk format SplitBit does, so an image it makes is one the machine can read, and one the machine writes is one it can read back. It is a convenience rather than a necessity: SplitBit writes its own filesystem, and now assembles its own programs, so a disk can be filled without leaving the machine. Files are laid down contiguously, so a disk can have free blocks without having them in one piece. When that happens `put` says so rather than putting part of a file on. diff --git a/Tests/expected/selfBoot.out b/Tests/expected/selfBoot.out new file mode 100644 index 0000000..d52025d --- /dev/null +++ b/Tests/expected/selfBoot.out @@ -0,0 +1,14 @@ +stage two +CosmOS +> saved it +read it back, 22 bytes: +a file kept by asking +renamed it +deleted it +and it is gone +finished +> made +> /Notes> 0 files +/Notes> halted +Execution halted. +[exit 0] diff --git a/Tests/expected/selfBootNoSystem.out b/Tests/expected/selfBootNoSystem.out new file mode 100644 index 0000000..9a4dcab --- /dev/null +++ b/Tests/expected/selfBootNoSystem.out @@ -0,0 +1,4 @@ +stage two +no /System/cosmos.bin +Execution halted. +[exit 0] diff --git a/Tests/input/selfBoot.in b/Tests/input/selfBoot.in new file mode 100644 index 0000000..6b14388 --- /dev/null +++ b/Tests/input/selfBoot.in @@ -0,0 +1,5 @@ +Files +mkdir Notes +cd Notes +dir +exit diff --git a/Tests/lint-baseline.txt b/Tests/lint-baseline.txt index c7d6a01..6e955e5 100644 --- a/Tests/lint-baseline.txt +++ b/Tests/lint-baseline.txt @@ -1,5 +1,6 @@ Programs/Boot/slotData.asm redundant-setd 1 Programs/Boot/stage1.asm redundant-setd 2 +Programs/Boot/stage2.asm redundant-setd 1 Programs/CosmOS/Apps/Copy.asm redundant-setd 2 Programs/CosmOS/Apps/Edit.asm redundant-setd 3 Programs/CosmOS/Apps/Wander.asm redundant-setd 1 diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index 7e5bf43..249be0d 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -386,3 +386,29 @@ cp "$DISKS/chain.img" "$DISKS/chainAlt.img" "$ROOT/Assembler" "$ROOT/Programs/Boot/slotData.asm" -o "$WORK/slotData.sbx" >/dev/null tail -c +17 "$WORK/slotData.sbx" > "$WORK/slotData.raw" "$TOOL" boot "$DISKS/chainData.img" "$WORK/slotData.raw" 0 >/dev/null + +# ---- A disk the machine can start itself from ---- +# +# Stage one in the emulator's hands, stage two in a boot slot, and the system as an +# ordinary file. Nothing here is a boot-specific format: /System/cosmos.bin is the same +# SPBT image the emulator has always been handed directly, which is what makes a program +# that wants no operating system startable the same way. +"$TOOL" format "$DISKS/selfboot.img" 512 4 32 >/dev/null +"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ + "$ROOT/Programs/Boot/stage2.asm" -o "$WORK/stage2.sbx" >/dev/null +tail -c +17 "$WORK/stage2.sbx" > "$WORK/stage2.raw" +"$TOOL" boot "$DISKS/selfboot.img" "$WORK/stage2.raw" 0 >/dev/null + +"$ROOT/Assembler" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \ + "$ROOT/Programs/CosmOS/Source/cosmos.asm" -o "$WORK/bootCosmos.bin" >/dev/null +"$TOOL" mkdir "$DISKS/selfboot.img" /System >/dev/null +"$TOOL" put "$DISKS/selfboot.img" "$WORK/bootCosmos.bin" /System/cosmos.bin >/dev/null +"$TOOL" mkdir "$DISKS/selfboot.img" /Apps >/dev/null +"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ + "$ROOT/Programs/CosmOS/Apps/Files.asm" -o "$WORK/bootFiles.sbx" >/dev/null +"$TOOL" put "$DISKS/selfboot.img" "$WORK/bootFiles.sbx" /Apps/Files.sbx >/dev/null + +# And one with no system on it, so that a second stage which cannot find what to start +# says so rather than jumping somewhere. +"$TOOL" format "$DISKS/nosystem.img" 512 4 32 >/dev/null +"$TOOL" boot "$DISKS/nosystem.img" "$WORK/stage2.raw" 0 >/dev/null diff --git a/Tests/manifest b/Tests/manifest index 64176f6..350fede 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -374,6 +374,21 @@ bootNoArea | Boot/stage1.asm | run | - # variables and the string that sbfs.asm needs. bootData | Boot/slotData.asm | assemble | - | - bootDataRuns | Boot/stage1.asm | run | - | - | disks/chainData.img + +# ---- The machine starting itself ---- +# +# The whole chain: the emulator hands over stage one, stage one reads the live boot slot +# into Program Memory and jumps, stage two mounts the filesystem, finds /System/cosmos.bin, +# takes the image apart and places its code, its data and its vector table, and jumps to +# the entry point the vectors named. +# +# WHAT IS RECORDED IS THAT THE SYSTEM WORKS AFTERWARDS, not that it started. A loaded +# program running is what says the vector table arrived, because a program reaches the +# system through SWI and nothing else; the file written and the directory entered say the +# filesystem and the console came up with it. +selfBoot | Boot/stage1.asm | run | selfBoot.in | 90000000 | disks/selfboot.img +# And a disk with a boot slot but nothing to start, which says so rather than jumping. +selfBootNoSystem | Boot/stage1.asm | run | - | 90000000 | disks/nosystem.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.