Block device peripheral and SBFS file system implemented.

This commit is contained in:
Anachronaut
2026-08-16 14:03:29 -04:00
parent 04dfcd707b
commit eff6902bcf
36 changed files with 3251 additions and 31 deletions
+555
View File
@@ -0,0 +1,555 @@
// SplitDisk.c
// Makes and edits SplitBit disk images from the host.
//
// Until SplitBit can write its own filesystem there has to be some way to get a program
// onto a disk, and this is it. It is not a shortcut around the machine: it speaks exactly
// the format SplitBit will speak, so an image this makes is one the machine can read and
// an image the machine writes is one this can read back.
//
// Written by Anachronaut
#include "sbfs.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Numbers on a SplitBit disk are most significant byte first, the same as everywhere
// else on the machine.
static uint16_t readWord(const uint8_t *at) {
return (uint16_t)((at[0] << 8) | at[1]);
}
static void writeWord(uint8_t *at, uint16_t value) {
at[0] = (uint8_t)(value >> 8);
at[1] = (uint8_t)(value & 0xFF);
}
static FILE *openImage(const char *path, const char *mode) {
FILE *image = fopen(path, mode);
if (image == NULL) {
fprintf(stderr, "Error: Couldn't open the disk image \"%s\".\n", path);
}
return image;
}
static int readBlock(FILE *image, uint16_t block, uint8_t *into) {
if (fseek(image, (long)block * SBFS_BLOCK_BYTES, SEEK_SET) != 0
|| fread(into, 1, SBFS_BLOCK_BYTES, image) != SBFS_BLOCK_BYTES) {
fprintf(stderr, "Error: Couldn't read block %u.\n", block);
return 1;
}
return 0;
}
static int writeBlock(FILE *image, uint16_t block, const uint8_t *from) {
if (fseek(image, (long)block * SBFS_BLOCK_BYTES, SEEK_SET) != 0
|| fwrite(from, 1, SBFS_BLOCK_BYTES, image) != SBFS_BLOCK_BYTES) {
fprintf(stderr, "Error: Couldn't write block %u.\n", block);
return 1;
}
return 0;
}
typedef struct {
uint16_t diskBlocks;
uint16_t directoryStart;
uint16_t directoryBlocks;
uint16_t freeBlocks;
} Superblock;
// Reads block 0 and checks it really is one of ours. Without the magic a blank image and
// a formatted one with no files would be the same thing.
static int readSuperblock(FILE *image, Superblock *super) {
uint8_t block[SBFS_BLOCK_BYTES];
if (readBlock(image, 0, block)) {
return 1;
}
if (memcmp(block, SBFS_MAGIC, SBFS_MAGIC_BYTES) != 0) {
fprintf(stderr, "Error: That is not a SplitBit disk. Format it first.\n");
return 1;
}
if (block[SBFS_SUPER_VERSION] != SBFS_VERSION) {
fprintf(stderr, "Error: That disk is version %u, and this understands version %u.\n",
block[SBFS_SUPER_VERSION], SBFS_VERSION);
return 1;
}
super->diskBlocks = readWord(block + SBFS_SUPER_DISK);
super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART);
super->directoryBlocks = readWord(block + SBFS_SUPER_DIRBLOCKS);
super->freeBlocks = readWord(block + SBFS_SUPER_FREE);
return 0;
}
static int writeSuperblock(FILE *image, const Superblock *super) {
uint8_t block[SBFS_BLOCK_BYTES];
memset(block, 0, sizeof(block));
memcpy(block, SBFS_MAGIC, SBFS_MAGIC_BYTES);
block[SBFS_SUPER_VERSION] = SBFS_VERSION;
writeWord(block + SBFS_SUPER_DISK, super->diskBlocks);
writeWord(block + SBFS_SUPER_DIRSTART, super->directoryStart);
writeWord(block + SBFS_SUPER_DIRBLOCKS, super->directoryBlocks);
writeWord(block + SBFS_SUPER_FREE, super->freeBlocks);
return writeBlock(image, 0, block);
}
// The whole directory, held in memory while a command works on it. It is small enough
// that reading it once and writing it back once is simpler than picking at blocks, and it
// is what the machine will do too once it has the Data Memory to spare.
typedef struct {
uint8_t *bytes;
int entries;
} Directory;
static int readDirectory(FILE *image, const Superblock *super, Directory *directory) {
directory->entries = super->directoryBlocks * SBFS_ENTRIES_PER_BLOCK;
directory->bytes = malloc((size_t)super->directoryBlocks * SBFS_BLOCK_BYTES);
if (directory->bytes == NULL) {
fprintf(stderr, "Error: Out of memory reading the directory.\n");
return 1;
}
for (uint16_t i = 0; i < super->directoryBlocks; i++) {
if (readBlock(image, (uint16_t)(super->directoryStart + i),
directory->bytes + (size_t)i * SBFS_BLOCK_BYTES)) {
free(directory->bytes);
directory->bytes = NULL;
return 1;
}
}
return 0;
}
static int writeDirectory(FILE *image, const Superblock *super, const Directory *directory) {
for (uint16_t i = 0; i < super->directoryBlocks; i++) {
if (writeBlock(image, (uint16_t)(super->directoryStart + i),
directory->bytes + (size_t)i * SBFS_BLOCK_BYTES)) {
return 1;
}
}
return 0;
}
static uint8_t *entryAt(const Directory *directory, int index) {
return directory->bytes + (size_t)index * SBFS_ENTRY_BYTES;
}
static int entryInUse(const uint8_t *entry) {
return (entry[SBFS_ENTRY_FLAGS] & SBFS_FLAG_IN_USE) != 0;
}
static uint32_t entrySize(const uint8_t *entry) {
return (uint32_t)readWord(entry + SBFS_ENTRY_BLOCKS) * SBFS_BLOCK_BYTES
+ entry[SBFS_ENTRY_TAIL];
}
static uint16_t entryBlocksUsed(const uint8_t *entry) {
return (uint16_t)SBFS_BLOCKS_USED(readWord(entry + SBFS_ENTRY_BLOCKS),
entry[SBFS_ENTRY_TAIL]);
}
// Names are compared as written, the way labels are, and are padded rather than
// terminated, so a name that fills the field has no terminator to find.
static void entryName(const uint8_t *entry, char *into) {
memcpy(into, entry + SBFS_ENTRY_NAME, SBFS_NAME_BYTES);
into[SBFS_NAME_BYTES] = '\0';
}
static int findByName(const Directory *directory, const char *name) {
char held[SBFS_NAME_BYTES + 1];
for (int i = 0; i < directory->entries; i++) {
const uint8_t *entry = entryAt(directory, i);
if (!entryInUse(entry)) {
continue;
}
entryName(entry, held);
if (strcmp(held, name) == 0) {
return i;
}
}
return -1;
}
// Where the first free run of the wanted length begins, or -1 if there is not one.
//
// There is no allocation bitmap, and that is the design rather than an omission: with
// files laid down contiguously, every block is either inside some entry's range or it is
// not, so the directory already is the allocation map. A bitmap would be a second copy of
// a fact that is already written down, and a second copy is a thing that can disagree.
static long findFreeRun(const Directory *directory, const Superblock *super, uint16_t wanted) {
if (wanted == 0) {
// An empty file occupies nothing, so it has no start to speak of. Block 0 is the
// superblock and can never hold file data, which makes it the honest way to say
// "nowhere" without inventing a place that belongs to somebody else.
(void)directory;
return 0;
}
uint32_t firstData = (uint32_t)super->directoryStart + super->directoryBlocks;
for (uint32_t candidate = firstData; candidate + wanted <= super->diskBlocks; candidate++) {
uint32_t clash = 0;
for (int i = 0; i < directory->entries && !clash; i++) {
const uint8_t *entry = entryAt(directory, i);
if (!entryInUse(entry)) {
continue;
}
uint32_t start = readWord(entry + SBFS_ENTRY_START);
uint32_t used = entryBlocksUsed(entry);
if (used == 0) {
continue;
}
if (candidate < start + used && start < candidate + wanted) {
// Overlaps this file, so start looking again past the end of it.
clash = start + used;
}
}
if (clash) {
candidate = clash - 1; // The loop's increment takes it to clash.
continue;
}
return (long)candidate;
}
return -1;
}
static uint16_t countFree(const Directory *directory, const Superblock *super) {
uint32_t used = 0;
for (int i = 0; i < directory->entries; i++) {
const uint8_t *entry = entryAt(directory, i);
if (entryInUse(entry)) {
used += entryBlocksUsed(entry);
}
}
// Block 0 and everything up to the end of the directory is not available.
uint32_t overhead = (uint32_t)super->directoryStart + super->directoryBlocks;
return (uint16_t)(super->diskBlocks - overhead - used);
}
// ---- Commands ----
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks) {
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);
return 1;
}
// Quietly, because a disk that is not there yet is the ordinary case for format and
// not something to complain about on the way past.
FILE *image = fopen(path, "r+b");
if (image == NULL) {
image = openImage(path, "w+b");
if (image == NULL) {
return 1;
}
}
uint8_t empty[SBFS_BLOCK_BYTES];
memset(empty, 0, sizeof(empty));
for (uint16_t i = 0; i < blocks; i++) {
if (writeBlock(image, i, empty)) {
fclose(image);
return 1;
}
}
Superblock super;
super.diskBlocks = blocks;
super.directoryStart = SBFS_FIRST_DIRECTORY_BLOCK;
super.directoryBlocks = directoryBlocks;
super.freeBlocks = (uint16_t)(blocks - 1 - directoryBlocks);
if (writeSuperblock(image, &super)) {
fclose(image);
return 1;
}
fclose(image);
printf("Formatted %s: %u blocks, %u of directory, %u free.\n",
path, blocks, directoryBlocks, super.freeBlocks);
return 0;
}
static int commandList(const char *path) {
FILE *image = openImage(path, "rb");
if (image == NULL) {
return 1;
}
Superblock super;
Directory directory;
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
fclose(image);
return 1;
}
printf("%s: %u blocks, %u of directory, %u entries.\n",
path, super.diskBlocks, super.directoryBlocks, directory.entries);
printf("%-22s %8s %7s %7s\n", "NAME", "BYTES", "START", "BLOCKS");
char name[SBFS_NAME_BYTES + 1];
int shown = 0;
for (int i = 0; i < directory.entries; i++) {
const uint8_t *entry = entryAt(&directory, i);
if (!entryInUse(entry)) {
continue;
}
entryName(entry, name);
printf("%-22s %8u %7u %7u\n", name, entrySize(entry),
readWord(entry + SBFS_ENTRY_START), entryBlocksUsed(entry));
shown++;
}
// The count in the superblock is a cache, so say what the directory actually adds up
// to as well. If the two ever disagree, the directory is the one to believe.
uint16_t counted = countFree(&directory, &super);
printf("%d file%s, %u blocks free", shown, shown == 1 ? "" : "s", counted);
if (counted != super.freeBlocks) {
printf(" (the superblock says %u, which is stale)", super.freeBlocks);
}
printf(".\n");
free(directory.bytes);
fclose(image);
return 0;
}
static int commandPut(const char *path, const char *hostFile, const char *asName) {
FILE *source = fopen(hostFile, "rb");
if (source == NULL) {
fprintf(stderr, "Error: Couldn't open \"%s\".\n", hostFile);
return 1;
}
fseek(source, 0, SEEK_END);
long size = ftell(source);
rewind(source);
if (size < 0) {
fprintf(stderr, "Error: Couldn't measure \"%s\".\n", hostFile);
fclose(source);
return 1;
}
if (strlen(asName) > SBFS_NAME_BYTES) {
fprintf(stderr, "Error: \"%s\" is %zu characters, and a name may be %d.\n"
" Give a shorter one as the last argument.\n",
asName, strlen(asName), SBFS_NAME_BYTES);
fclose(source);
return 1;
}
FILE *image = openImage(path, "r+b");
if (image == NULL) {
fclose(source);
return 1;
}
Superblock super;
Directory directory;
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
fclose(source);
fclose(image);
return 1;
}
if (findByName(&directory, asName) >= 0) {
fprintf(stderr, "Error: \"%s\" is already on the disk. Delete it first.\n", asName);
goto failed;
}
int slot = -1;
for (int i = 0; i < directory.entries && slot < 0; i++) {
if (!entryInUse(entryAt(&directory, i))) {
slot = i;
}
}
if (slot < 0) {
fprintf(stderr, "Error: The directory is full: %d entries, all taken.\n", directory.entries);
goto failed;
}
uint16_t whole = (uint16_t)SBFS_WHOLE_BLOCKS(size);
uint8_t tail = (uint8_t)SBFS_TAIL_BYTES(size);
uint16_t needed = (uint16_t)SBFS_BLOCKS_USED(whole, tail);
long start = findFreeRun(&directory, &super, needed);
if (start < 0) {
fprintf(stderr, "Error: No run of %u free blocks. There may be room on the disk"
" without there being room in one piece.\n", needed);
goto failed;
}
uint8_t block[SBFS_BLOCK_BYTES];
for (uint16_t i = 0; i < needed; i++) {
memset(block, 0, sizeof(block));
size_t got = fread(block, 1, SBFS_BLOCK_BYTES, source);
if (got == 0 && i < needed) {
fprintf(stderr, "Error: \"%s\" ended sooner than its size said.\n", hostFile);
goto failed;
}
if (writeBlock(image, (uint16_t)(start + i), block)) {
goto failed;
}
}
uint8_t *entry = entryAt(&directory, slot);
memset(entry, 0, SBFS_ENTRY_BYTES);
entry[SBFS_ENTRY_FLAGS] = SBFS_FLAG_IN_USE;
writeWord(entry + SBFS_ENTRY_START, (uint16_t)start);
writeWord(entry + SBFS_ENTRY_BLOCKS, whole);
entry[SBFS_ENTRY_TAIL] = tail;
memcpy(entry + SBFS_ENTRY_NAME, asName, strlen(asName));
super.freeBlocks = countFree(&directory, &super);
if (writeDirectory(image, &super, &directory) || writeSuperblock(image, &super)) {
goto failed;
}
printf("Put %s on as \"%s\": %ld bytes at block %ld.\n", hostFile, asName, size, start);
free(directory.bytes);
fclose(source);
fclose(image);
return 0;
failed:
free(directory.bytes);
fclose(source);
fclose(image);
return 1;
}
static int commandGet(const char *path, const char *name, const char *hostFile) {
FILE *image = openImage(path, "rb");
if (image == NULL) {
return 1;
}
Superblock super;
Directory directory;
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
fclose(image);
return 1;
}
int slot = findByName(&directory, name);
if (slot < 0) {
fprintf(stderr, "Error: There is no \"%s\" on that disk.\n", name);
free(directory.bytes);
fclose(image);
return 1;
}
const uint8_t *entry = entryAt(&directory, slot);
uint32_t size = entrySize(entry);
uint16_t start = readWord(entry + SBFS_ENTRY_START);
uint16_t used = entryBlocksUsed(entry);
FILE *out = fopen(hostFile, "wb");
if (out == NULL) {
fprintf(stderr, "Error: Couldn't write \"%s\".\n", hostFile);
free(directory.bytes);
fclose(image);
return 1;
}
uint8_t block[SBFS_BLOCK_BYTES];
uint32_t left = size;
for (uint16_t i = 0; i < used; i++) {
if (readBlock(image, (uint16_t)(start + i), block)) {
fclose(out);
free(directory.bytes);
fclose(image);
return 1;
}
uint32_t take = (left < SBFS_BLOCK_BYTES) ? left : SBFS_BLOCK_BYTES;
fwrite(block, 1, take, out);
left -= take;
}
fclose(out);
printf("Got \"%s\" off as %s: %u bytes.\n", name, hostFile, size);
free(directory.bytes);
fclose(image);
return 0;
}
static int commandDelete(const char *path, const char *name) {
FILE *image = openImage(path, "r+b");
if (image == NULL) {
return 1;
}
Superblock super;
Directory directory;
if (readSuperblock(image, &super) || readDirectory(image, &super, &directory)) {
fclose(image);
return 1;
}
int slot = findByName(&directory, name);
if (slot < 0) {
fprintf(stderr, "Error: There is no \"%s\" on that disk.\n", name);
free(directory.bytes);
fclose(image);
return 1;
}
// A deleted entry and a never used one are the same thing: the in use bit goes down
// and its blocks are free again. The blocks themselves are left as they were, which
// is worth knowing if anything is ever meant to be private.
memset(entryAt(&directory, slot), 0, SBFS_ENTRY_BYTES);
super.freeBlocks = countFree(&directory, &super);
int failed = writeDirectory(image, &super, &directory) || writeSuperblock(image, &super);
if (!failed) {
printf("Deleted \"%s\". %u blocks free.\n", name, super.freeBlocks);
}
free(directory.bytes);
fclose(image);
return failed;
}
static void printUsage(const char *program) {
printf("Usage: %s <command> <image> [arguments]\n", program);
printf("\n");
printf("Commands:\n");
printf(" format <image> [blocks] [dirblocks] Lay down a fresh filesystem.\n");
printf(" list <image> Show what is on the disk.\n");
printf(" put <image> <file> [name] Put a host file onto it.\n");
printf(" get <image> <name> [file] Take one off it.\n");
printf(" delete <image> <name> Remove one.\n");
printf("\n");
printf("Blocks are %d bytes. A name may be %d characters. Without one, put uses the\n",
SBFS_BLOCK_BYTES, SBFS_NAME_BYTES);
printf("file's own name, which is often too long, and it will say so.\n");
}
// The part of a path after the last separator, so that put can default to a file's own
// name rather than the whole path it was found at.
static const char *baseName(const char *path) {
const char *slash = strrchr(path, '/');
return slash ? slash + 1 : path;
}
int main(int argc, char *argv[]) {
if (argc < 2 || strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) {
printUsage(argv[0]);
return argc < 2 ? 1 : 0;
}
const char *command = argv[1];
if (argc < 3) {
fprintf(stderr, "Error: %s needs a disk image.\n", command);
return 1;
}
const char *path = argv[2];
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;
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 (strcmp(command, "list") == 0) {
return commandList(path);
}
if (strcmp(command, "put") == 0) {
if (argc < 4) {
fprintf(stderr, "Error: put needs a file to put on.\n");
return 1;
}
return commandPut(path, argv[3], (argc > 4) ? argv[4] : baseName(argv[3]));
}
if (strcmp(command, "get") == 0) {
if (argc < 4) {
fprintf(stderr, "Error: get needs the name of a file on the disk.\n");
return 1;
}
return commandGet(path, argv[3], (argc > 4) ? argv[4] : argv[3]);
}
if (strcmp(command, "delete") == 0) {
if (argc < 4) {
fprintf(stderr, "Error: delete needs the name of a file on the disk.\n");
return 1;
}
return commandDelete(path, argv[3]);
}
fprintf(stderr, "Error: There is no \"%s\" command.\n", command);
printUsage(argv[0]);
return 1;
}
+46
View File
@@ -0,0 +1,46 @@
// sbex.h
// The SplitBit loadable program format, version one.
//
// A program that is not the one the machine booted from has to say where it wants to
// live, because nothing relocates it. This is a header saying that, in front of the
// bytes themselves. It is the same idea as the load address on the front of a C64 .PRG,
// with room for the machine to ask a few more questions later.
//
// Two things read this: whatever builds one on the host, and the loader running on
// SplitBit. As with the filesystem, nothing is shared between them but the specification.
//
// All multi byte numbers are most significant byte first.
//
// 0 4 "SBEX"
// 4 1 Version
// 5 1 Reserved
// 6 2 Where the code goes in Program Memory
// 8 2 Where to start running, an address in Program Memory
// 10 2 How many bytes of code there are
// 12 2 Where the data goes in Data Memory
// 14 2 How many bytes of data there are
// 16 The code, then the data
//
// Sixteen bytes, so the code begins at a round offset and finding it is one step rather
// than an arithmetic. Nothing here relocates anything: the addresses are where the
// program was built to live, and putting it anywhere else would leave every branch and
// every SETD inside it pointing at the wrong place.
//
// Written by Anachronaut
#ifndef SBEX_H
#define SBEX_H
#define SBEX_MAGIC "SBEX"
#define SBEX_MAGIC_BYTES 4
#define SBEX_VERSION 1
#define SBEX_HEADER_BYTES 16
#define SBEX_VERSION_AT 4
#define SBEX_CODE_AT 6
#define SBEX_ENTRY_AT 8
#define SBEX_CODE_LEN_AT 10
#define SBEX_DATA_AT 12
#define SBEX_DATA_LEN_AT 14
#endif // SBEX_H
+79
View File
@@ -0,0 +1,79 @@
// sbfs.h
// The SplitBit Filesystem, version one.
//
// This is the host side's copy of the format. The other implementation is SplitBit
// assembly running on the machine itself, so nothing can be shared between them except
// the specification: the two have to be kept honest by a document rather than by a
// header. Everything here follows that document exactly, and anything that changes here
// has to change there in the same breath.
//
// All multi byte numbers are most significant byte first, the same as every other number
// SplitBit stores: addresses, the SPBT binary header, and the vector table.
//
// Written by Anachronaut
#ifndef SBFS_H
#define SBFS_H
#include <stdint.h>
#define SBFS_MAGIC "SBFS"
#define SBFS_MAGIC_BYTES 4
#define SBFS_VERSION 1
#define SBFS_BLOCK_BYTES 256
// ---- Block 0, the superblock ----
//
// 0 4 "SBFS"
// 4 1 Version
// 5 1 Reserved
// 6 2 Blocks on the disk
// 8 2 First directory block
// 10 2 Blocks the directory occupies
// 12 2 Free blocks, a cache rather than the authority
// 14 Reserved to the end of the block
#define SBFS_SUPER_VERSION 4
#define SBFS_SUPER_DISK 6
#define SBFS_SUPER_DIRSTART 8
#define SBFS_SUPER_DIRBLOCKS 10
#define SBFS_SUPER_FREE 12
// ---- Directory entries ----
//
// 0 1 Flags
// 1 2 First block of the file's data
// 3 2 Whole blocks the file occupies
// 5 1 Bytes in the trailing part block, or zero if there is not one
// 6 22 Name, padded with zeroes
// 28 4 Reserved
//
// Thirty two divides two hundred and fifty six, so an entry never straddles a block and
// reading one never means handling a split.
#define SBFS_ENTRY_BYTES 32
#define SBFS_ENTRIES_PER_BLOCK (SBFS_BLOCK_BYTES / SBFS_ENTRY_BYTES)
#define SBFS_ENTRY_FLAGS 0
#define SBFS_ENTRY_START 1
#define SBFS_ENTRY_BLOCKS 3
#define SBFS_ENTRY_TAIL 5
#define SBFS_ENTRY_NAME 6
#define SBFS_NAME_BYTES 22
#define SBFS_FLAG_IN_USE 0x01
// The directory begins at block 1 and is this many blocks unless told otherwise, which
// is sixty four files. The superblock carries the real number, so this is only what a
// freshly formatted disk gets.
#define SBFS_FIRST_DIRECTORY_BLOCK 1
#define SBFS_DEFAULT_DIRECTORY_BLOCKS 8
// A file of n bytes occupies n / 256 whole blocks and, if anything is left over, one more
// for the tail. Zero means zero in both, so an empty file occupies nothing at all.
#define SBFS_WHOLE_BLOCKS(bytes) ((bytes) / SBFS_BLOCK_BYTES)
#define SBFS_TAIL_BYTES(bytes) ((bytes) % SBFS_BLOCK_BYTES)
#define SBFS_BLOCKS_USED(blocks, tail) ((blocks) + ((tail) ? 1 : 0))
#endif // SBFS_H
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Wraps an assembled SplitBit binary into a loadable program.
The assembler emits a boot image: a Program Segment that loads at zero and a Data Segment
that does the same. A program meant to be loaded somewhere else has to say where it goes,
which is what the SBEX header in front of it is for.
A program says where it lives by reserving the front of each segment, so the addresses
given here have to match the reserves in its source. Nothing checks that for you, and
nothing relocates anything if you get it wrong.
"""
import struct
import sys
def segments(raw):
at = 4 + 1 + 4 # magic, version, feature flags
assert raw[:4] == b"SPBT", "not a SplitBit binary"
assert raw[at:at + 3] == b"PRG"
plen = struct.unpack(">H", raw[at + 3:at + 5])[0]
program = raw[at + 5:at + 5 + plen]
at = at + 5 + plen
assert raw[at:at + 3] == b"DAT"
dlen = struct.unpack(">H", raw[at + 3:at + 5])[0]
return program, raw[at + 5:at + 5 + dlen]
def main():
if len(sys.argv) != 6:
sys.exit("usage: wrap.py <binary> <output> <code address> <data address> <entry>")
binary, output = sys.argv[1], sys.argv[2]
codeAt, dataAt, entry = (int(a, 0) for a in sys.argv[3:6])
program, data = segments(open(binary, "rb").read())
# Everything below the address a segment is placed at is the padding the reserve put
# there, and is not part of the program.
code = program[codeAt:]
values = data[dataAt:]
header = bytearray(16)
header[0:4] = b"SBEX"
header[4] = 1
struct.pack_into(">H", header, 6, codeAt)
struct.pack_into(">H", header, 8, entry)
struct.pack_into(">H", header, 10, len(code))
struct.pack_into(">H", header, 12, dataAt)
struct.pack_into(">H", header, 14, len(values))
with open(output, "wb") as out:
out.write(header)
out.write(code)
out.write(values)
print("%s: %d bytes of code at 0x%04X, %d of data at 0x%04X, entry 0x%04X"
% (output, len(code), codeAt, len(values), dataAt, entry))
main()