B1: a boot area on the disk, reserved by arithmetic that was already there

The first rung of booting from disk. A boot area is blocks between the
superblock and the directory that the filesystem never allocates and never
sees, and NOTHING WAS ADDED TO RESERVE THEM: both implementations work out
the first usable block as directoryStart + directoryBlocks, and
directoryStart has always been a field rather than a constant. Formatting
with the directory moved up reserves everything below it. Neither allocator
changed, on either side.

Two new superblock fields in bytes that were reserved: bootBlocks at 14,
per slot, and bootSlot at 16. A disk made before this has zero in both,
which reads as "no boot area" - true, and the same shape as the version two
parent field, where the value an older disk already held was the right
answer without conversion.

TWO SLOTS, ALWAYS. A boot slot is raw blocks with no entry to rename, so
the write-a-temporary-and-rename ordering that protects every file cannot
protect it, and a machine interrupted while updating its only slot would
not boot at all - the one failure on this disk with no way back. Writing
the slot that is not live and then moving one byte makes that a machine
that boots what it had before.

bootBlocks and directoryStart say the same thing from two sides, so a disk
where they disagree is refused rather than guessed at, as is one naming a
slot that does not exist.

Checked where it matters: the HOST formats a disk with a boot area and the
MACHINE fills it, then the reserved blocks are compared against zero. The
machine's allocator is the one that had no idea any of this was happening,
which is what makes that the check worth having. Six host checks besides,
including both halves of the superblock disagreeing.
This commit is contained in:
Anachronaut
2026-08-26 22:58:45 -04:00
parent 0a2965bc63
commit 612bd1b97c
5 changed files with 162 additions and 9 deletions
+54 -7
View File
@@ -56,6 +56,8 @@ typedef struct {
uint16_t directoryStart;
uint16_t directoryBlocks;
uint16_t freeBlocks;
uint16_t bootBlocks; // Per slot. Zero on a disk that cannot be booted.
uint8_t bootSlot; // Which of the two is live.
} Superblock;
// Reads block 0 and checks it really is one of ours. Without the magic a blank image and
@@ -93,6 +95,24 @@ static int readSuperblock(FILE *image, Superblock *super) {
super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART);
super->directoryBlocks = directoryBlocks;
super->freeBlocks = readWord(block + SBFS_SUPER_FREE);
super->bootBlocks = readWord(block + SBFS_SUPER_BOOTBLOCKS);
super->bootSlot = block[SBFS_SUPER_BOOTSLOT];
// The boot area and the directory's position describe the same fact from two sides,
// so they have to agree or one of them is wrong and there is no way to tell which.
uint32_t expected = SBFS_FIRST_BOOT_BLOCK
+ (uint32_t)super->bootBlocks * SBFS_BOOT_SLOTS;
if (super->directoryStart != expected) {
fprintf(stderr, "Error: That disk says %u blocks of boot area and puts its"
" directory at %u, which should then be %u.\n",
super->bootBlocks, super->directoryStart, expected);
return 1;
}
if (super->bootSlot >= SBFS_BOOT_SLOTS) {
fprintf(stderr, "Error: That disk names boot slot %u, and there are %u.\n",
super->bootSlot, SBFS_BOOT_SLOTS);
return 1;
}
return 0;
}
@@ -108,6 +128,8 @@ static int writeSuperblock(FILE *image, const Superblock *super) {
writeWord(block + SBFS_SUPER_DIRSTART, super->directoryStart);
writeWord(block + SBFS_SUPER_DIRBLOCKS, super->directoryBlocks);
writeWord(block + SBFS_SUPER_FREE, super->freeBlocks);
writeWord(block + SBFS_SUPER_BOOTBLOCKS, super->bootBlocks);
block[SBFS_SUPER_BOOTSLOT] = super->bootSlot;
return writeBlock(image, 0, block);
}
@@ -406,7 +428,8 @@ static uint16_t countFree(const Directory *directory, const Superblock *super) {
// ---- Commands ----
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks) {
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks,
uint16_t bootBlocks) {
if (directoryBlocks > SBFS_MAX_DIRECTORY_BLOCKS) {
fprintf(stderr, "Error: %u directory blocks is %u entries, and entry 65535 has no"
" parent number - adding one wraps to zero, which is the root."
@@ -415,9 +438,12 @@ static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBl
SBFS_MAX_DIRECTORY_BLOCKS, SBFS_MAX_ENTRIES);
return 1;
}
if (blocks <= 1u + directoryBlocks) {
fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock and a"
" directory of %u.\n", blocks, directoryBlocks);
uint32_t overhead = 1u + (uint32_t)bootBlocks * SBFS_BOOT_SLOTS + directoryBlocks;
if (blocks <= overhead) {
fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock, %u of"
" boot area and a directory of %u.\n",
blocks, (unsigned)((uint32_t)bootBlocks * SBFS_BOOT_SLOTS),
directoryBlocks);
return 1;
}
// Quietly, because a disk that is not there yet is the ordinary case for format and
@@ -445,14 +471,26 @@ static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBl
// what raises it, because mkdir is what makes the difference true.
super.version = SBFS_VERSION_FLAT;
super.diskBlocks = blocks;
super.directoryStart = SBFS_FIRST_DIRECTORY_BLOCK;
// THE DIRECTORY MOVES UP BY THE BOOT AREA, and that is the whole mechanism. Both
// implementations already work out the first usable block as directoryStart plus
// directoryBlocks, so everything below the directory is reserved by arithmetic that
// was there before any of this, and no allocator changed.
super.directoryStart = (uint16_t)(SBFS_FIRST_BOOT_BLOCK
+ (uint32_t)bootBlocks * SBFS_BOOT_SLOTS);
super.directoryBlocks = directoryBlocks;
super.freeBlocks = (uint16_t)(blocks - 1 - directoryBlocks);
super.freeBlocks = (uint16_t)(blocks - overhead);
super.bootBlocks = bootBlocks;
super.bootSlot = 0;
if (writeSuperblock(image, &super)) {
fclose(image);
return 1;
}
fclose(image);
if (bootBlocks) {
printf("Formatted %s: %u blocks, two boot slots of %u, %u of directory, %u free.\n",
path, blocks, bootBlocks, directoryBlocks, super.freeBlocks);
return 0;
}
printf("Formatted %s: %u blocks, %u of directory, %u free.\n",
path, blocks, directoryBlocks, super.freeBlocks);
return 0;
@@ -993,12 +1031,21 @@ int main(int argc, char *argv[]) {
if (strcmp(command, "format") == 0) {
long blocks = (argc > 3) ? strtol(argv[3], NULL, 0) : 512;
long directoryBlocks = (argc > 4) ? strtol(argv[4], NULL, 0) : SBFS_DEFAULT_DIRECTORY_BLOCKS;
// Blocks in EACH boot slot, and there are two of them. Left off, a disk gets no
// boot area at all, which is what every disk made before this had.
long bootBlocks = (argc > 5) ? strtol(argv[5], NULL, 0) : 0;
if (blocks < 2 || blocks > 0xFFFF || directoryBlocks < 1 || directoryBlocks > 0xFFFF) {
fprintf(stderr, "Error: A disk is between 2 and 65535 blocks, with at least"
" one of directory.\n");
return 1;
}
return commandFormat(path, (uint16_t)blocks, (uint16_t)directoryBlocks);
if (bootBlocks < 0 || bootBlocks * SBFS_BOOT_SLOTS > 0xFFFE) {
fprintf(stderr, "Error: A boot slot is between 0 and %d blocks, and there are"
" two of them.\n", 0xFFFE / SBFS_BOOT_SLOTS);
return 1;
}
return commandFormat(path, (uint16_t)blocks, (uint16_t)directoryBlocks,
(uint16_t)bootBlocks);
}
if (strcmp(command, "list") == 0) {
return commandList(path, (argc > 3) ? argv[3] : NULL);