diff --git a/Programs/Boot/slotTest.asm b/Programs/Boot/slotTest.asm new file mode 100644 index 0000000..d3edf76 --- /dev/null +++ b/Programs/Boot/slotTest.asm @@ -0,0 +1,33 @@ +; slotTest.asm +; Something small to put in a boot slot, so that the chain can be proved end to end. +; +; NO DATA SEGMENT, and that is not tidiness - it is the shape of what stage one can do. +; Stage one reads raw blocks into Program Memory and jumps to the first byte. It places no +; data, because it has no way to know where a payload's data ends and its code begins +; without knowing a format, and knowing a format is the thing ROM must do as little of as +; possible. So a payload either carries no initialised data or arranges its own. +; +; Which is why this prints with INIA and OUTA rather than from a string. The real stage two +; needs a Data Segment, and how it gets one is an open question written up beside this. +; +; Written by Anachronaut + +#Program + #Base 0xC000 + +here: + INIA 0x62 ; "b" + OUTA 0x00 + INIA 0x6F ; "o" + OUTA 0x00 + INIA 0x6F ; splitlint[redundant-assignment]: a second "o", spelled out like the rest + OUTA 0x00 + INIA 0x74 ; "t" + OUTA 0x00 + INIA 0x65 ; "e" + OUTA 0x00 + INIA 0x64 ; "d" + OUTA 0x00 + INIA 0x0A + OUTA 0x00 + HALT diff --git a/Programs/Boot/stage1.asm b/Programs/Boot/stage1.asm new file mode 100644 index 0000000..4f3d12c --- /dev/null +++ b/Programs/Boot/stage1.asm @@ -0,0 +1,263 @@ +; stage1.asm +; The first thing the machine runs. Reads the boot area off the disk and jumps into it. +; +; THIS IS THE PART THAT ONE DAY CANNOT BE CHANGED. It is written to go in ROM, so +; everything it knows has to be a thing that is true forever: which port the disk is on, +; that a SplitBit disk begins with its own name, and where two numbers sit in the block +; that name is in. It does not know what a file is, what a directory is, or that SBFS has +; versions. All of that lives in the boot area, on the disk, where it can be replaced. +; +; The test to apply to any line added here is the only test that matters for ROM: am I +; certain this is right forever? A loader that could find /System/cosmos.bin by name would +; be friendlier and would freeze the filesystem format in silicon. +; +; It is an ordinary boot image for now, so it can be run and tested with everything that +; already exists. Nothing about it changes when it moves into ROM except who puts it in +; memory. +; +; Written by Anachronaut + +#Program + +; ---- What is known forever ---- +; +; Disk: 0x20 block high, 0x21 block low, 0x22 command, 0x23 status +; Controller: 0xE0 source bank, 0xE1/0xE2 source, 0xE3 dest bank, 0xE4/0xE5 dest, +; 0xE6/0xE7 count, 0xE8 command +; Banks: 0 Program Memory, 1 Data Memory, 3 the disk's buffer once registered + +start: + ; The disk's buffer becomes bank 3. Memory a device brings is reachable only through the + ; controller, so this is what makes the block readable at all. + INIA 0d3 + OUTA 0xE3 + INIA 0x20 + OUTA 0xE2 + INIA 0x03 + OUTA 0xE8 + + ; Block 0, the superblock. + RSTA + SETD.0 BlockHigh + STA.0 + SETD.0 BlockLow + STA.0 + RCAL readBlock + BNQ bootFailed + + ; "SBFS", or there is nothing here to boot from. Four bytes, compared where they landed. + SETD.2 ScratchAt + LDD.0.2 + SETD.2 DiskMagic + INIA 0d4 + SETD.1 Counter + STA.1 +magicLoop: + LDA.0 + LDB.2 + XOR + BNQ bootFailed + INCD.0 + INCD.2 + SETD.1 Counter + LDA.1 + DECA + STA.1 + BNA magicLoop + + ; Two numbers, at fixed offsets in the block whose name has just been checked: how many + ; blocks a boot slot holds, and which of the two slots to start from. + SETD.2 ScratchAt + LDD.0.2 + DPUP.0 0d14 + LDA.0 + BNA bootFailed ; A high byte means a slot larger than this will ever read. + INCD.0 + LDA.0 + BRA bootFailed ; No boot area at all, so this disk cannot be started. + SETD.1 SlotBlocks + STA.1 + + SETD.2 ScratchAt + LDD.0.2 + DPUP.0 0d16 + LDA.0 + + ; The first block of the live slot. Slot 0 begins at block 1 and slot 1 begins a whole + ; slot later, so the only arithmetic is an addition and there is nothing to multiply. + BRA slotZero + SETD.0 SlotBlocks + LDA.0 + INCA + BRI slotFound +slotZero: + INIA 0d1 +slotFound: + SETD.0 BlockLow + STA.0 + RSTA + SETD.0 BlockHigh + STA.0 + + ; And where it goes. Stage two lives above everything the system will occupy, so that + ; loading the system does not walk over the loader while it is still running. + SETD.0 StageHigh + INIA 0xC0 + STA.0 + +readLoop: + RCAL readBlock + BNQ bootFailed + + ; The block, out of the disk's buffer and into Program Memory where it will be run. + INIA 0d3 + OUTA 0xE0 + RSTA + OUTA 0xE1 + OUTA 0xE2 + RSTA ; splitlint[redundant-assignment]: a bank number, not the address above + OUTA 0xE3 ; DestBank: Program Memory. + SETD.0 StageHigh + LDA.0 + OUTA 0xE4 + RSTA + OUTA 0xE5 + INIA 0x01 + OUTA 0xE6 + RSTA + OUTA 0xE7 ; A whole block. + INIA 0x01 + OUTA 0xE8 + + ; On to the next block, and the next page of Program Memory to put it in. + SETD.0 StageHigh + LDA.0 + INCA + STA.0 + SETD.0 BlockLow + LDA.0 + INCA + STA.0 + BNA blockStepped + SETD.0 BlockHigh + LDA.0 + INCA + STA.0 +blockStepped: + + SETD.0 SlotBlocks + LDA.0 + DECA + STA.0 + BNA readLoop + + ; Into it. Nothing is checked about what was read, because there is nothing here that + ; could check it: what a valid stage two looks like is stage two's business, and a ROM + ; that knew would be a ROM that could be wrong about it later. + SETD.0 StageStart + LDD.1.0 + BRD.1 + +; ---- Reading the block the two block registers name ---- +; +; RCAL rather than CALL because what it hands back is Q, and an ordinary call would put +; back the registers this leaves its answer in. +readBlock: + SETD.0 BlockHigh + LDA.0 + OUTA 0x20 + SETD.0 BlockLow + LDA.0 + OUTA 0x21 + INIA 0x01 + OUTA 0x22 ; Read. + +waitDisk: + INA 0x23 + INIB 0x01 + AND + BRQ readDone ; The busy bit is down, so there is nothing to wait for. + WAIT + BRI waitDisk +readDone: + ; The error bit, which is the whole of what can go wrong down here. + INIB 0x02 + AND + BNQ readBad + + ; And into Data Memory, where the CPU can look at it. + INIA 0d3 + OUTA 0xE0 + RSTA + OUTA 0xE1 + OUTA 0xE2 + INIA 0d1 + OUTA 0xE3 ; DestBank: Data Memory. + INIA 0x80 + OUTA 0xE4 + RSTA + OUTA 0xE5 + INIA 0x01 + OUTA 0xE6 + RSTA + OUTA 0xE7 + INIA 0x01 + OUTA 0xE8 + + RSTA + RSTB + CCF + ADD ; Q is zero: the block is in Scratch. + RRET +readBad: + RSTA + INIB 0d1 + CCF + ADD + RRET + +; ---- When there is nothing to start ---- +; +; One character and a stop. A ROM has no room for an explanation and nowhere to put one: +; the console is the only thing it can be sure of, and even that only in the sense that +; writing to a port nobody is listening to costs nothing. +bootFailed: + INIA 0x3F ; "?" + OUTA 0x00 + INIA 0x0A + OUTA 0x00 + HALT + +#Data + +DiskMagic: +"SBFS" + +BlockHigh: + 0x00 +BlockLow: + 0x00 +SlotBlocks: + 0x00 +Counter: + 0x00 + +; Where stage two is being written, a page at a time, and where it begins. The high byte is +; stepped as the blocks go by; the low byte is always zero, because a block is a page. +StageHigh: + 0xC0 +StageStart: + 0xC0 0x00 + +; Where the superblock, and then each block of the boot area, lands on its way past. +; +; AN ADDRESS RATHER THAN STORAGE. Reserving it, or aligning to it, would put thirty two +; kilobytes of zeroes into a file that is going to be a ROM - which is what the assembler's +; own scratch map exists to avoid, for the same reason. Two bytes here say where; nothing +; carries what. +ScratchAt: + 0x80 0x00 + +#Vectors + + Boot start diff --git a/README.md b/README.md index 376bc40..ef772dc 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,8 @@ increment, decrement, addition, and subtraction when their inputs are known. | Command | What it does | | --- | --- | +| `boot [slot]` | Write a file into a boot slot, padding the rest of it with zeroes. Slot 0 unless told otherwise. | +| `bootslot ` | Choose which slot the machine starts from. One byte, on its own, so writing a slot and committing to it stay separate decisions. | | `format [blocks] [dirblocks] [bootblocks]` | Lay down a fresh filesystem. 512 blocks and 8 of directory by default, which is 128K and room for 64 entries. A fourth number reserves a boot area of two slots that size. | | `list [path]` | Show the whole disk, or one directory of it. | | `put [path]` | Put a host file onto it. Without a path it uses the file's own name, which is often longer than the 22 characters a name may be. | @@ -273,6 +275,14 @@ moving one byte in the superblock turns that into a machine that boots what it h `bootBlocks` and `directoryStart` describe the same fact from two sides, so a disk where they disagree is refused rather than guessed at. +`Programs/Boot/stage1.asm` is what starts a machine from one, and is written to end up in +ROM: **330 bytes**, 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. It does not know what a file is, what a directory is, or that SBFS +has versions - all of that lives in the boot area, on the disk, where it can be replaced. +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. + 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/Source/DiskTool/SplitDisk.c b/Source/DiskTool/SplitDisk.c index 84a4465..0972ac9 100644 --- a/Source/DiskTool/SplitDisk.c +++ b/Source/DiskTool/SplitDisk.c @@ -670,6 +670,118 @@ static int commandList(const char *path, const char *within) { return 0; } +// ---- Writing a boot slot ---- +// +// Raw blocks, outside the filesystem, with no entry and no name. That is what makes this +// different from put: there is nothing to rename, so the safety comes from writing the +// slot that is NOT live and moving one byte afterwards. +// +// The one byte is moved by a separate command on purpose. Writing a slot and choosing it +// are different decisions - a slot can be written now and chosen after it has been looked +// at - and putting them in one command would make every write a commitment. +static int commandBoot(const char *path, const char *hostFile, long slot) { + FILE *image = openImage(path, "r+b"); + if (image == NULL) { + return 1; + } + Superblock super; + if (readSuperblock(image, &super)) { + fclose(image); + return 1; + } + if (super.bootBlocks == 0) { + fprintf(stderr, "Error: That disk has no boot area. Format it with one.\n"); + fclose(image); + return 1; + } + if (slot < 0 || slot >= SBFS_BOOT_SLOTS) { + fprintf(stderr, "Error: There are %d boot slots, numbered 0 and %d.\n", + SBFS_BOOT_SLOTS, SBFS_BOOT_SLOTS - 1); + fclose(image); + return 1; + } + FILE *source = fopen(hostFile, "rb"); + if (source == NULL) { + fprintf(stderr, "Error: Couldn't open \"%s\".\n", hostFile); + fclose(image); + return 1; + } + if (fseek(source, 0, SEEK_END) != 0) { + fprintf(stderr, "Error: Couldn't measure \"%s\".\n", hostFile); + fclose(source); fclose(image); + return 1; + } + long size = ftell(source); + rewind(source); + long blocks = (size + SBFS_BLOCK_BYTES - 1) / SBFS_BLOCK_BYTES; + if (blocks > super.bootBlocks) { + fprintf(stderr, "Error: \"%s\" is %ld bytes, which is %ld blocks, and a slot" + " holds %u.\n", hostFile, size, blocks, super.bootBlocks); + fclose(source); fclose(image); + return 1; + } + + // THE WHOLE SLOT IS WRITTEN, not just the part the file fills. A slot holding the tail + // of whatever was there before is a slot whose contents depend on its history, and the + // first stage reads all of it without knowing where the file stopped. + uint16_t first = (uint16_t)(SBFS_FIRST_BOOT_BLOCK + + (uint32_t)slot * super.bootBlocks); + uint8_t block[SBFS_BLOCK_BYTES]; + for (uint16_t i = 0; i < super.bootBlocks; i++) { + memset(block, 0, sizeof(block)); + size_t got = fread(block, 1, SBFS_BLOCK_BYTES, source); + if (got == 0 && ferror(source)) { + fprintf(stderr, "Error: Couldn't read \"%s\".\n", hostFile); + fclose(source); fclose(image); + return 1; + } + if (writeBlock(image, (uint16_t)(first + i), block)) { + fclose(source); fclose(image); + return 1; + } + } + fclose(source); + fclose(image); + printf("Wrote %s into boot slot %ld: %ld bytes in %u blocks from block %u.%s\n", + hostFile, slot, size, super.bootBlocks, first, + slot == super.bootSlot ? "" : " It is not the live slot."); + return 0; +} + +// Choosing which slot the machine starts from. One byte, written on its own, so that the +// change from one system to another is a single block write that either happened or did +// not. +static int commandBootSlot(const char *path, long slot) { + FILE *image = openImage(path, "r+b"); + if (image == NULL) { + return 1; + } + Superblock super; + if (readSuperblock(image, &super)) { + fclose(image); + return 1; + } + if (super.bootBlocks == 0) { + fprintf(stderr, "Error: That disk has no boot area.\n"); + fclose(image); + return 1; + } + if (slot < 0 || slot >= SBFS_BOOT_SLOTS) { + fprintf(stderr, "Error: There are %d boot slots, numbered 0 and %d.\n", + SBFS_BOOT_SLOTS, SBFS_BOOT_SLOTS - 1); + fclose(image); + return 1; + } + super.bootSlot = (uint8_t)slot; + if (writeSuperblock(image, &super)) { + fclose(image); + return 1; + } + fclose(image); + printf("The machine now starts from boot slot %ld.\n", slot); + return 0; +} + static int commandPut(const char *path, const char *hostFile, const char *asName) { FILE *source = fopen(hostFile, "rb"); if (source == NULL) { @@ -1064,6 +1176,20 @@ int main(int argc, char *argv[]) { } return commandRemoveDirectory(path, argv[3]); } + if (strcmp(command, "boot") == 0) { + if (argc < 4) { + fprintf(stderr, "Error: boot needs a file to write into a slot.\n"); + return 1; + } + return commandBoot(path, argv[3], (argc > 4) ? strtol(argv[4], NULL, 0) : 0); + } + if (strcmp(command, "bootslot") == 0) { + if (argc < 4) { + fprintf(stderr, "Error: bootslot needs the slot to start from.\n"); + return 1; + } + return commandBootSlot(path, strtol(argv[3], NULL, 0)); + } if (strcmp(command, "put") == 0) { if (argc < 4) { fprintf(stderr, "Error: put needs a file to put on.\n"); diff --git a/Tests/disk.sh b/Tests/disk.sh index f6df954..bc11dcd 100755 --- a/Tests/disk.sh +++ b/Tests/disk.sh @@ -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." diff --git a/Tests/expected/bootChain.out b/Tests/expected/bootChain.out new file mode 100644 index 0000000..6e744be --- /dev/null +++ b/Tests/expected/bootChain.out @@ -0,0 +1,3 @@ +booted +Execution halted. +[exit 0] diff --git a/Tests/expected/bootChainAlt.out b/Tests/expected/bootChainAlt.out new file mode 100644 index 0000000..4831c0a --- /dev/null +++ b/Tests/expected/bootChainAlt.out @@ -0,0 +1,3 @@ +ooohed +Execution halted. +[exit 0] diff --git a/Tests/expected/bootNoArea.out b/Tests/expected/bootNoArea.out new file mode 100644 index 0000000..a73845b --- /dev/null +++ b/Tests/expected/bootNoArea.out @@ -0,0 +1,3 @@ +? +Execution halted. +[exit 0] diff --git a/Tests/lint-baseline.txt b/Tests/lint-baseline.txt index b2c362d..3042a1b 100644 --- a/Tests/lint-baseline.txt +++ b/Tests/lint-baseline.txt @@ -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 diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index 4bca435..4d5f7f2 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -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 diff --git a/Tests/manifest b/Tests/manifest index 75320e2..0ae5409 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -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.