CosmOS pre-alpha and launchable application versions of old programs.

This commit is contained in:
Anachronaut
2026-08-17 15:31:49 -04:00
parent eff6902bcf
commit 91c9d49d1b
66 changed files with 5612 additions and 160 deletions
+47
View File
@@ -119,6 +119,45 @@ void writeDependencyFile(const char *dependencyPath, const char *outputPath) {
fclose(file);
}
// A program that bases one segment and not the other is a mistake the assembler is the
// last place to catch. Nothing relocates, so the unbased half keeps the addresses it was
// given, which are addresses from zero up, and the loader puts it there: on top of
// whatever the system keeps at the bottom of memory. It does not fail at load time and it
// does not fail at the jump. It fails later, somewhere else, as corruption.
//
// Only a segment with something in it can land on anything, so an empty one says nothing.
// A base of zero that was actually asked for is left alone, which is how a program says
// it meant it.
void checkSegmentBases(const char *fileName) {
if (!programIsLoadable()) {
return; // A boot image. Both segments begin at zero because that is where they go.
}
const char *segmentName[3];
segmentName[PROGRAM] = "Program";
segmentName[DATA] = "Data";
int segmentEnd[3];
segmentEnd[PROGRAM] = programLength;
segmentEnd[DATA] = dataLength;
const int segments[2] = { PROGRAM, DATA };
for (int i = 0; i < 2; i++) {
int mine = segments[i], other = segments[1 - i];
int content = segmentEnd[mine] - segmentBase(mine);
if (segmentBaseWasGiven(mine) || content <= 0 || !segmentBaseWasGiven(other)) {
continue;
}
fprintf(stderr, RED "Error: The %s Segment is based at 0x%04X, but the %s Segment\n"
" has %d byte%s at 0x0000 and was never given a #Base.\n"
" Half a program loaded at zero lands on whatever is already there.\n" RESET,
segmentName[other], segmentBase(other), segmentName[mine],
content, content == 1 ? "" : "s");
printf(" File: %s\n", fileName);
printf(" Say \"#Base 0x0000\" in the %s Segment if that is what you meant.\n", segmentName[mine]);
exit(1);
}
}
int main(int argc, char *argv[]) {
static struct option long_options[] = {
{"output", required_argument, 0, 'o'},
@@ -185,7 +224,15 @@ int main(int argc, char *argv[]) {
// the buffers are filled, because SWI needs the number its vector was given.
populateVectorTable(intermediateArray, index);
fillInVectorReferences(intermediateArray, index);
// The buffers are filled from wherever each segment is based, so that a byte's place
// in the buffer is the address it will have. For a boot image both bases are zero and
// this changes nothing.
programLength = segmentBase(PROGRAM);
dataLength = segmentBase(DATA);
populateOutputBuffers(intermediateArray, index, Program, &programLength, Data, &dataLength);
// After the buffers, because how much a segment actually holds is not known until it
// has been filled, and an empty segment is not a mistake.
checkSegmentBases(fileName);
if (!outputFileName) {
outputFileName = createOutputFileName(fileName);
}
+50 -4
View File
@@ -13,6 +13,30 @@
int debug = 0;
static uint16_t segmentBases[3];
static int basesGiven = 0;
// Per segment, because a base of zero that was asked for and a base of zero that was
// never mentioned are different things, and only the second one is likely a mistake.
static int baseGiven[3];
void setSegmentBase(int segment, uint16_t base) {
segmentBases[segment] = base;
baseGiven[segment] = 1;
basesGiven = 1;
}
uint16_t segmentBase(int segment) {
return segmentBases[segment];
}
int segmentBaseWasGiven(int segment) {
return baseGiven[segment];
}
int programIsLoadable(void) {
return basesGiven;
}
void toUppercase(char *str) {
for (int i = 0; str[i]; i++) {
str[i] = toupper(str[i]);
@@ -37,6 +61,10 @@ int checkIfKeyword(intermediateElement *currentElement) {
// the number that follows. The file that needs the boundary is then the
// file that asks for it, rather than relying on whatever came before.
return KEYWORD_ALIGN;
} else if (strcmp(currentElement->token, "#Base") == 0) {
// Says where this segment is loaded, which makes the program a loadable one
// rather than a boot image.
return KEYWORD_BASE;
} else if (strcmp(currentElement->token, "#Reserve") == 0) {
// Puts down the number of zero bytes that follows, so that a label can
// stand for a region rather than just its first byte.
@@ -172,7 +200,9 @@ int checkIfLiteralValue(intermediateElement *currentElement) {
return 1;
}
uint16_t readCount(intermediateElement *currentElement, const char *what) {
// Shared by readCount and readAddress, which differ only in whether zero is an answer.
// A count of nothing is a typo; an address of zero is the bottom of memory.
static uint16_t readNumber(intermediateElement *currentElement, const char *what, long least) {
const char *token = currentElement->token;
int base;
const char *baseName;
@@ -202,14 +232,22 @@ uint16_t readCount(intermediateElement *currentElement, const char *what) {
}
}
long value = strtol(digits, NULL, base);
if (value < 1 || value > 0xFFFF) {
fprintf(stderr, RED "Error: %s was given \"%s\". It has to be at least 1 and no more than 0xFFFF.\n" RESET, what, token);
if (value < least || value > 0xFFFF) {
fprintf(stderr, RED "Error: %s was given \"%s\". It has to be at least %ld and no more than 0xFFFF.\n" RESET, what, token, least);
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
exit(1);
}
return (uint16_t)value;
}
uint16_t readCount(intermediateElement *currentElement, const char *what) {
return readNumber(currentElement, what, 1);
}
uint16_t readAddress(intermediateElement *currentElement, const char *what) {
return readNumber(currentElement, what, 0);
}
int checkIfLabel(intermediateElement *currentElement) {
char *token = currentElement->token;
int length = strlen(token);
@@ -261,7 +299,15 @@ int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber)
if (i < (int)(sizeof(buffer) - 1)) {
buffer[i++] = c;
} else {
fprintf(stderr, "Error: String literal too long.\n");
// Say the limit and where it was met. A string long enough to reach this
// is usually several lines of help text, and "too long" on its own leaves
// somebody counting characters to find out by how much.
fprintf(stderr, RED "Error: String literal longer than %d characters.\n"
" Every string carries its own zero byte, so two written in a row"
" are two strings\n rather than one long one. Give each its own"
" label and print them one after another.\n" RESET,
(int)(sizeof(buffer) - 1));
printf(" File: %s at line %d.\n", currentElement->fileName, *lineNumber);
exit(1);
}
}
+28
View File
@@ -40,6 +40,7 @@
#define KEYWORD_VECTORS 4
#define KEYWORD_ALIGN 5
#define KEYWORD_RESERVE 6
#define KEYWORD_BASE 7
// Destination values.
#define NOWHERE 0
@@ -86,4 +87,31 @@ int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber);
// ever emitted as a byte, so there is no reason to hold them to a byte's range.
uint16_t readCount(intermediateElement *currentElement, const char *what);
// The same, but zero is allowed. #Base takes one of these: a segment deliberately based
// at the bottom of memory is a thing a program is entitled to say, and saying it out loud
// is how it is told apart from a segment nobody based at all.
uint16_t readAddress(intermediateElement *currentElement, const char *what);
// ---- Where a segment is based ----
//
// A program that says nothing about this is a boot image: both its segments begin at
// zero, and the machine loads them there. A program that gives either segment a base is
// meant to be loaded somewhere else, so it is written out as a loadable program instead,
// with its addresses in front of it and none of the space below them in the file.
//
// Nothing relocates anything, so the base a program is assembled for has to be the one it
// is loaded at.
void setSegmentBase(int segment, uint16_t base);
uint16_t segmentBase(int segment);
// Whether this particular segment was given one. A segment left at zero because nobody
// said otherwise cannot be told from one deliberately based at zero by its value alone,
// and the difference is what the mismatch check below is about.
int segmentBaseWasGiven(int segment);
// Whether either segment was given one, which is what decides the kind of file written.
int programIsLoadable(void);
#endif
+2
View File
@@ -76,6 +76,7 @@ Instruction instruction_set[] = {
{0x4A, "LDD"},
{0x4B, "STD"},
{0x4C, "MVSD"},
{0x4D, "MVDS"},
// Output Operations:
{0xD0, "OUTQ"},
{0xD1, "OUTA"},
@@ -121,6 +122,7 @@ int dataPointerOperands(uint8_t opcode) {
case 0x48: // DPUP
case 0x49: // DPDN
case 0x4C: // MVSD
case 0x4D: // MVDS
return 1;
default:
return 0;
+40 -2
View File
@@ -155,6 +155,9 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
// fileName is a path already resolved and recorded by the caller, and the copy
// the include list owns, so element fileNames can safely point at it.
int status = NOWHERE;
// A base has to come before anything else in its segment, so this remembers whether
// that segment has had anything put in it yet.
static int segmentUsed[3] = {0, 0, 0};
int lineNumber = 1; // Line numbers start at 1.
// Open the file.
FILE *file = fopen(fileName, "r");
@@ -229,6 +232,34 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
// Set the state to DATA so we mark additional tokens for inclusion into Data Memory.
status = DATA;
break;
case KEYWORD_BASE: {
if (status != PROGRAM && status != DATA) {
fprintf(stderr, RED "Error: #Base outside the Program or Data Segment.\n There is no segment for it to be the base of.\n" RESET);
printf(" File: %s at line %d.\n", fileName, lineNumber);
exit(1);
}
if (segmentUsed[status]) {
fprintf(stderr, RED "Error: #Base after something is already in the segment.\n"
" A base says where the whole segment begins, so it has to come first.\n" RESET);
printf(" File: %s at line %d.\n", fileName, lineNumber);
exit(1);
}
(*intermediateIndex)++;
if (!readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
fprintf(stderr, RED "Error: #Base without an address.\n" RESET);
exit(1);
}
(*intermediateArray)[*intermediateIndex].fileName = fileName;
(*intermediateArray)[*intermediateIndex].lineNumber = lineNumber;
setSegmentBase(status, readAddress(&(*intermediateArray)[*intermediateIndex], "#Base"));
(*intermediateArray)[*intermediateIndex].type = KEYWORD;
(*intermediateArray)[*intermediateIndex].byteLength = 0;
(*intermediateArray)[*intermediateIndex].destination = NOWHERE;
(*intermediateArray)[*intermediateIndex - 1].destination = NOWHERE;
(*intermediateArray)[*intermediateIndex - 1].byteLength = 0;
(*intermediateIndex)++;
continue;
}
case KEYWORD_ALIGN:
case KEYWORD_RESERVE: {
// Both take a count, and both only make sense somewhere that has a
@@ -288,8 +319,11 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
printf(" File: %s at line %d.\n", fileName, lineNumber);
exit(1);
}
// Next, check if it's a literal value.
} else if (checkIfLiteralValue(&(*intermediateArray)[*intermediateIndex])) {
// Next, check if it's a literal value. A string is never one, however it begins:
// the quotes are gone by now, so a string starting with a zero looks exactly like
// a malformed literal and used to be rejected as one.
} else if ((*intermediateArray)[*intermediateIndex].type != STRING
&& checkIfLiteralValue(&(*intermediateArray)[*intermediateIndex])) {
// We should check to make sure we have a destination for it.
if (status == NOWHERE) {
fprintf(stderr, RED "Error: Attempting to write a value to nowhere!\n Did you forget to use the #Program or #Data keyword?\n" RESET);
@@ -320,6 +354,10 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
}
}
(*intermediateArray)[*intermediateIndex].destination = status;
if ((status == PROGRAM || status == DATA)
&& (*intermediateArray)[*intermediateIndex].type != KEYWORD) {
segmentUsed[status] = 1;
}
(*intermediateIndex)++;
}
return 0;
+129 -12
View File
@@ -14,6 +14,7 @@
#include "secondPass.h"
#include "Assm-util.h"
#include "assembly.h"
#include "sbex.h"
int debugSecondPass = 0;
@@ -62,8 +63,11 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName,
}
void populateLabelTable(intermediateElement *intermediateArray, int arraySize) {
int programCount = 0;
int dataCount = 0;
// Counting starts at the base, so a label in a program built to live somewhere else
// already holds the address it will have once it is there. Nothing relocates
// anything, which is exactly why this has to be right at assembly time.
int programCount = segmentBase(PROGRAM);
int dataCount = segmentBase(DATA);
// Loop through the array, if there's a label definition, add it to the label list.
for (int i = 0; i < arraySize ; i++) {
// How many zeroes an #Align comes to depends on where the cursor has reached,
@@ -159,6 +163,16 @@ static void vectorError(const char *message, intermediateElement *element) {
exit(1);
}
// Whether the token at b is written on the same line as the one at a, and so belongs to
// the same entry. A line is what tells a name with a handler apart from a name on its own.
static int sameLine(intermediateElement *intermediateArray, int a, int b) {
if (a < 0 || b < 0) {
return 0;
}
return intermediateArray[a].lineNumber == intermediateArray[b].lineNumber
&& intermediateArray[a].fileName == intermediateArray[b].fileName;
}
// The next token belonging to the Vector Segment, or -1 if the segment has run out.
static int nextVectorToken(intermediateElement *intermediateArray, int arraySize, int from) {
for (int i = from; i < arraySize; i++) {
@@ -169,12 +183,28 @@ static int nextVectorToken(intermediateElement *intermediateArray, int arraySize
return -1;
}
static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler, intermediateElement *element) {
// A declared vector has a name and a number but no handler, so nothing goes into the
// table for it. It exists so that a program can name a service it calls without claiming
// to implement it, which is what lets one file be included by both sides.
// Where a vector of this name already is, or -1. A name can be met twice: once where it
// is declared and once where somebody supplies its handler.
static int findVector(const char *name) {
for (int i = 0; i < vectorArrayCount; i++) {
if (vectorArray[i].name && strcmp(vectorArray[i].name, name) == 0) {
return i;
}
}
return -1;
}
static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler,
int declaredOnly, intermediateElement *element) {
if (vectorArrayCount >= MAX_VECTORS) {
vectorError("Too many vectors defined.", element);
}
for (int i = 0; i < vectorArrayCount; i++) {
if (vectorArray[i].index == index && vectorArray[i].base == base) {
if (vectorArray[i].index == index && vectorArray[i].base == base
&& !vectorArray[i].declaredOnly && !declaredOnly) {
fprintf(stderr, RED "Error: That vector already has a handler.\n" RESET);
printf("File: %s at line %d.\n", element->fileName, element->lineNumber);
exit(1);
@@ -189,6 +219,7 @@ static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler
vectorArray[vectorArrayCount].index = index;
vectorArray[vectorArrayCount].base = base;
vectorArray[vectorArrayCount].handler = handler;
vectorArray[vectorArrayCount].declaredOnly = declaredOnly;
vectorArrayCount++;
}
@@ -228,7 +259,26 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
int handlerToken = nextVectorToken(intermediateArray, arraySize, portToken + 1);
uint16_t handler = resolveHandler(intermediateArray, handlerToken, "Device");
addVector(NULL, intermediateArray[portToken].byteValue, HARDWARE_VECTOR_BASE,
handler, &intermediateArray[i]);
handler, 0, &intermediateArray[i]);
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
continue;
}
int handlerToken = nextVectorToken(intermediateArray, arraySize, i + 1);
int hasHandler = sameLine(intermediateArray, i, handlerToken);
int already = findVector(token);
if (already >= 0) {
// Met before. A handler now is somebody implementing what was declared
// earlier, which is how one shared file can serve both sides.
if (!hasHandler) {
vectorError("That vector is declared more than once.", &intermediateArray[i]);
}
if (!vectorArray[already].declaredOnly) {
vectorError("That vector already has a handler.", &intermediateArray[i]);
}
vectorArray[already].handler = resolveHandler(intermediateArray, handlerToken, token);
vectorArray[already].declaredOnly = 0;
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
continue;
}
@@ -250,9 +300,15 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
nextFreeVector++;
}
int handlerToken = nextVectorToken(intermediateArray, arraySize, i + 1);
if (!hasHandler) {
// Nothing follows it on the line, so this says what the vector is called and
// what number it has, and leaves implementing it to somebody else.
addVector(token, index, SOFTWARE_VECTOR_BASE, 0, 1, &intermediateArray[i]);
i = handlerToken;
continue;
}
uint16_t handler = resolveHandler(intermediateArray, handlerToken, token);
addVector(token, index, SOFTWARE_VECTOR_BASE, handler, &intermediateArray[i]);
addVector(token, index, SOFTWARE_VECTOR_BASE, handler, 0, &intermediateArray[i]);
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
}
}
@@ -440,6 +496,48 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
}
}
// A loadable program: sixteen bytes saying where it belongs, then the code and the data.
// The space below each base is not written out, because nothing needs to carry it: the
// header says where the bytes go and the loader puts them there.
static void writeLoadable(const char *outputFileName, uint8_t *Program, int programCount,
uint8_t *Data, int dataCount) {
uint16_t codeBase = segmentBase(PROGRAM);
uint16_t dataBase = segmentBase(DATA);
int codeLength = programCount - codeBase;
int dataLength = dataCount - dataBase;
if (codeLength < 0) codeLength = 0;
if (dataLength < 0) dataLength = 0;
FILE *outputFile = fopen(outputFileName, "wb");
if (!outputFile) {
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, outputFileName);
exit(1);
}
uint8_t header[SBEX_HEADER_BYTES];
memset(header, 0, sizeof(header));
memcpy(header, SBEX_MAGIC, SBEX_MAGIC_BYTES);
header[SBEX_VERSION_AT] = SBEX_VERSION;
header[SBEX_CODE_AT] = (uint8_t)(codeBase >> 8);
header[SBEX_CODE_AT + 1] = (uint8_t)(codeBase & 0xFF);
// Where it starts is where it begins. A program that wants otherwise puts a branch
// at its first instruction, which costs three bytes and needs no format for it.
header[SBEX_ENTRY_AT] = (uint8_t)(codeBase >> 8);
header[SBEX_ENTRY_AT + 1] = (uint8_t)(codeBase & 0xFF);
header[SBEX_CODE_LEN_AT] = (uint8_t)(codeLength >> 8);
header[SBEX_CODE_LEN_AT + 1] = (uint8_t)(codeLength & 0xFF);
header[SBEX_DATA_AT] = (uint8_t)(dataBase >> 8);
header[SBEX_DATA_AT + 1] = (uint8_t)(dataBase & 0xFF);
header[SBEX_DATA_LEN_AT] = (uint8_t)(dataLength >> 8);
header[SBEX_DATA_LEN_AT + 1] = (uint8_t)(dataLength & 0xFF);
fwrite(header, 1, sizeof(header), outputFile);
fwrite(Program + codeBase, 1, (size_t)codeLength, outputFile);
fwrite(Data + dataBase, 1, (size_t)dataLength, outputFile);
fclose(outputFile);
printf("Successfully wrote SplitBit loadable program to \"%s\".\n", outputFileName);
printf(GREEN " Code: %d bytes at 0x%04X.\n Data: %d bytes at 0x%04X.\n Total size: %d bytes.\n" RESET,
codeLength, codeBase, dataLength, dataBase, SBEX_HEADER_BYTES + codeLength + dataLength);
}
void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCount, uint8_t *Data, int dataCount) {
FILE *outputFile = fopen(outputFileName, "wb");
if (!outputFile) {
@@ -487,16 +585,35 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
exit(1);
}
// A program with a base is one meant to be loaded, so it is written out with its
// addresses in front of it and nothing below them. A boot image carries the padding
// because the machine loads it at zero; a loadable one would only be carrying space
// it does not use.
if (programIsLoadable()) {
fclose(outputFile);
writeLoadable(outputFileName, Program, programCount, Data, dataCount);
return;
}
// The Vector Segment, only if the program named any. Leaving it out entirely is
// what lets a binary written before vectors existed still load: the reader treats
// the end of the file as an empty table rather than a missing one.
int installed = 0;
for (int i = 0; i < vectorArrayCount; i++) {
if (!vectorArray[i].declaredOnly) {
installed++;
}
}
int vectorBytes = 0;
if (vectorArrayCount > 0) {
if (installed > 0) {
fwrite("VEC", sizeof(char), SEGMENT_MARKER_LENGTH, outputFile);
vectorBytes = vectorArrayCount * VECTOR_ENTRY_FILE_BYTES;
vectorBytes = installed * VECTOR_ENTRY_FILE_BYTES;
fputc((vectorBytes >> 8) & 0xFF, outputFile);
fputc(vectorBytes & 0xFF, outputFile);
for (int i = 0; i < vectorArrayCount; i++) {
if (vectorArray[i].declaredOnly) {
continue;
}
uint16_t slot = vectorArray[i].base + (uint16_t)vectorArray[i].index * VECTOR_ENTRY_BYTES;
fputc((slot >> 8) & 0xFF, outputFile);
fputc(slot & 0xFF, outputFile);
@@ -508,10 +625,10 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
fclose(outputFile);
printf("Successfully wrote SplitBit binary to \"%s\".\n", outputFileName);
printf(GREEN " Program Segment size: %d bytes.\n Data Segment size: %d bytes.\n" RESET, programCount, dataCount);
if (vectorArrayCount > 0) {
printf(GREEN " Vectors: %d.\n" RESET, vectorArrayCount);
if (installed > 0) {
printf(GREEN " Vectors: %d.\n" RESET, installed);
}
printf(GREEN " Total size: %d bytes.\n" RESET,
(programCount + dataCount + SPLITBIT_HEADER_BYTES
+ (vectorArrayCount > 0 ? SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES + vectorBytes : 0)));
+ (installed > 0 ? SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES + vectorBytes : 0)));
}
+1
View File
@@ -26,6 +26,7 @@ typedef struct {
uint8_t index; // Which vector in its table.
uint16_t base; // Which table: software or hardware.
uint16_t handler; // Where the handler ended up.
int declaredOnly; // Named and numbered, with nobody implementing it here.
} VectorEntry;
void freeLabelList();
-46
View File
@@ -1,46 +0,0 @@
// 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
-58
View File
@@ -1,58 +0,0 @@
#!/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()
+17
View File
@@ -592,6 +592,23 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
*target = cpu->StackPointer;
}
break;
case 0x4D: {
// MVDS - Copy the selected Data Pointer into the Stack Pointer.
//
// This one is dangerous and is meant to be used rarely. Moving the Stack
// under a running program abandons every return address on it, so a RET
// after this goes wherever the new Stack happens to say.
//
// It exists because a system that runs other programs has no other way to
// get its Stack back. A program that gives up part way through leaves
// whatever it pushed behind, and the interrupt frame that carried the
// request to stop is on there too. Without this the Stack only ever grows
// downward, one abandoned program at a time, and a shell cannot outlive
// many of them.
uint16_t *source = selectDataPointer(cpu);
cpu->StackPointer = *source;
}
break;
//
// Dx - Output Operations:
//
+4 -1
View File
@@ -111,7 +111,10 @@ int main (int argc, char *argv[]) {
if (options.debug) {
// Wait before advancing, not after, so that a keypress is what moves the
// machine on rather than something that happens once it already has.
getchar();
// Through the console rather than getchar, so that everything reading standard
// input reads it the same way and the console's pushback stays the only place
// a byte can be sitting.
consoleReadByte();
}
int cycles;
if (options.debug) {
+179 -3
View File
@@ -7,7 +7,167 @@
#include "../Assembler/assembly.h" // For the fault vector numbers.
#include "controller.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <termios.h>
#include <unistd.h>
#include <poll.h>
// ---- The console ----
//
// The console owns its own reading rather than going through getchar. stdio keeps a
// buffer, and the status port asks the operating system what is waiting; those two
// disagree the moment stdio has read ahead, and the status port would then swear nothing
// was there while a read returned instantly. One byte of pushback here is enough, because
// nothing needs to look further ahead than the byte it is about to take.
static int consoleKeyMode = 0;
static int consoleEnded = 0;
static int consolePushback = -1; // A byte already taken from the host, or -1.
static struct termios consoleSavedTerminal;
static int consoleTerminalSaved = 0;
void consoleRestore(void) {
if (consoleTerminalSaved) {
tcsetattr(STDIN_FILENO, TCSANOW, &consoleSavedTerminal);
consoleTerminalSaved = 0;
}
consoleKeyMode = 0;
}
// Restores the terminal and then dies the way it would have died anyway, so that the
// shell sees the signal it was expecting rather than a machine that exited quietly.
static void consoleSignalHandler(int signalNumber) {
consoleRestore();
signal(signalNumber, SIG_DFL);
raise(signalNumber);
}
static void consoleSetMode(uint8_t mode) {
int wantKeys = (mode & CONSOLE_MODE_KEY) != 0;
if (wantKeys == consoleKeyMode) {
return;
}
if (!wantKeys) {
consoleRestore();
return;
}
// Nothing to configure when input is not a terminal, but the mode is still recorded:
// a program asking the status port what mode it is in should be told what it asked
// for, whether or not there was a terminal to carry it out on.
consoleKeyMode = 1;
if (!isatty(STDIN_FILENO)) {
return;
}
if (!consoleTerminalSaved) {
if (tcgetattr(STDIN_FILENO, &consoleSavedTerminal) != 0) {
return;
}
consoleTerminalSaved = 1;
// Registered on the first use rather than at startup, so a run that never asks
// for key mode installs nothing at all.
atexit(consoleRestore);
signal(SIGINT, consoleSignalHandler);
signal(SIGTERM, consoleSignalHandler);
}
struct termios raw = consoleSavedTerminal;
raw.c_lflag &= (tcflag_t)~(ICANON | ECHO);
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &raw);
}
// Everything already written is put where it can be seen before the machine asks the host
// anything. Standard output is line buffered on a terminal, so a prompt with no newline
// after it - "> " is exactly that, and exactly why this matters - would sit in the buffer
// while the machine waited for an answer to a question nobody had been shown.
//
// getchar used to do this by accident, because reading through stdio flushes the line
// buffered streams first. Reading with read() does not, so what was a side effect of the
// old way is done deliberately here.
static void consoleShowWhatIsWritten(void) {
fflush(stdout);
}
uint8_t consoleReadByte(void) {
if (consolePushback >= 0) {
uint8_t byte = (uint8_t)consolePushback;
consolePushback = -1;
return byte;
}
consoleShowWhatIsWritten();
unsigned char byte;
for (;;) {
ssize_t got = read(STDIN_FILENO, &byte, 1);
if (got == 1) {
return byte;
}
if (got == 0) {
// End of input. Still 0xFF, which is what getchar's EOF became when this was
// the only answer available, so nothing written against the old behaviour
// changes. The ENDED bit is the new way to know it was not a real byte.
consoleEnded = 1;
return 0xFF;
}
if (errno != EINTR) {
consoleEnded = 1;
return 0xFF;
}
// Interrupted before anything arrived, so ask again.
}
}
// Asking the host whether anything is waiting, and TAKING IT IF THERE IS. The byte goes
// into the pushback and the next read of the data port hands it over, so nothing is lost
// and no program can tell that it was fetched early.
//
// Fetching it early is what makes the answer worth having. The operating system will say a
// pipe is readable when what is waiting is the end of it, so asking without reading can
// only report that SOMETHING is there. Reading settles which: a byte, or the end. Without
// this, ENDED could not go up until a program had already read the 0xFF that stands for
// it, and every program would have to swallow one imaginary byte to find out there were
// none.
static void consoleFetch(void) {
if (consolePushback >= 0 || consoleEnded) {
return;
}
// Flushed here too. A program that draws something and then polls rather than reads is
// just as entitled to have the drawing appear, and it never reaches the read that
// would otherwise have flushed for it.
consoleShowWhatIsWritten();
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
if (poll(&waiting, 1, 0) <= 0 || (waiting.revents & (POLLIN | POLLHUP)) == 0) {
return;
}
unsigned char byte;
ssize_t got = read(STDIN_FILENO, &byte, 1);
if (got == 1) {
consolePushback = byte;
} else if (got == 0) {
consoleEnded = 1;
}
// A read that failed for any other reason is left alone: the next attempt asks again,
// and an interrupted poll is not news.
}
static uint8_t consoleStatus(void) {
uint8_t status = consoleKeyMode ? CONSOLE_STATUS_KEYMODE : 0;
consoleFetch();
if (consoleEnded) {
// READY IS NOT SET HERE, although a read would answer immediately. The bit means
// "there is a byte to be had", and at the end of input there is not; what a read
// returns then is 0xFF standing in for nothing. A program looping while READY
// stops on its own at the end, which is the behaviour worth having, and one that
// wants to know why asks ENDED.
return status | CONSOLE_STATUS_ENDED;
}
if (consolePushback >= 0) {
status |= CONSOLE_STATUS_READY;
}
return status;
}
// One bit per port, so a device can ask for attention without anything having to poll
// it. Eight ports to the byte, low bit first.
@@ -233,6 +393,11 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
if (port >= CONTROLLER_PORT_BASE && port <= CONTROLLER_PORT_TOP) {
return &controllerRecord;
}
if (port > PORT_CONSOLE && port <= PORT_CONSOLE_TOP) {
// The status and control ports are the same device as the data port, which is the
// one in the table and the one that would raise a line if the console ever did.
return deviceOnPort(PORT_CONSOLE);
}
if (port > PORT_DISK && port <= PORT_DISK_TOP) {
// The base port is in the table proper, since that is the one that owns the
// memory and raises the line. The rest of the block reports the same device.
@@ -269,12 +434,17 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
}
// This function sends the DataByte to the appropriate place based on the Port Address.
switch(Address) {
case PORT_CONSOLE:
case CONSOLE_DATA:
// If data is sent here, it should be written to STDOUT.
// For now, I'll implement this so it simply writes each byte out as it comes in.
// Later, I'll want to use a buffer for this for performance, probably.
putchar(DataByte);
break;
case CONSOLE_CONTROL: consoleSetMode(DataByte); break;
case CONSOLE_STATUS:
// Read only. A device saying how it is does not take instructions through the
// same hole, so a write here is ignored rather than meaning something.
break;
case DISK_BLOCK_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break;
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
case DISK_COMMAND: diskCommand(DataByte); break;
@@ -319,9 +489,15 @@ uint8_t InputHandler(uint8_t Address) {
return controllerRead(Address);
}
switch(Address) {
case PORT_CONSOLE:
case CONSOLE_DATA:
// If data is sent here, it should be read from STDIN.
return getchar();
return consoleReadByte();
break;
case CONSOLE_STATUS: return consoleStatus();
case CONSOLE_CONTROL:
// Write only. Reading it gives zero rather than the mode, because the mode is
// a bit in the status port and one fact wants one place to live.
return 0;
break;
case DISK_BLOCK_HIGH: return (uint8_t)(diskBlock >> 8);
case DISK_BLOCK_LOW: return (uint8_t)(diskBlock & 0xFF);
+57 -1
View File
@@ -14,7 +14,15 @@
// Which port a device answers on is a property of the machine rather than of any
// program, so the numbers live here and everything else refers to them by name.
#define PORT_CONSOLE 0x00
// The console answers on three ports. The data port is the machine's oldest promise and
// does not change: writing sends a byte, reading takes one and waits for it. The other two
// are additions, so a program written before they existed cannot notice them.
#define PORT_CONSOLE 0x00
#define PORT_CONSOLE_TOP 0x02
#define CONSOLE_DATA 0x00
#define CONSOLE_STATUS 0x01
#define CONSOLE_CONTROL 0x02
#define PORT_TEST 0x10
#define PORT_REFUSE 0x11
#define PORT_MEMORY 0x12
@@ -30,6 +38,54 @@
#define DISK_STATUS 0x23
#define PORT_REGISTRY 0xFF
// ---- The console ----
//
// Two modes, chosen by the program through the control port. The console starts in LINE
// mode, which is what the machine has always done: the terminal holds what is typed until
// Return, and does the echoing and the backspacing on the way. Reading the data port waits
// for a whole line to be finished somewhere else and then hands it over a byte at a time.
//
// KEY mode turns that off. Keys arrive as they are pressed, and nothing echoes them, so a
// program that wants them seen has to send them back out itself. That is not a choice this
// machine is making; it is what asking the terminal to stop holding a line means, and the
// editing goes away with it. A program that wants keys is expected to want that.
//
// READING THE DATA PORT WAITS IN BOTH MODES. The status port is how a program declines to
// wait, and keeping that in one place means the data port means one thing everywhere. A
// read that sometimes blocked and sometimes did not, depending on state set somewhere
// else, is the kind of thing that works until it does not.
//
// KEY MODE ONLY REACHES THE TERMINAL when there is one. With input coming from a pipe
// there is nothing to put into another mode, and the status port answers by asking the
// operating system whether anything is waiting, which is true of a pipe with bytes in it.
#define CONSOLE_MODE_LINE 0x00
#define CONSOLE_MODE_KEY 0x01
// Set when there is a byte to be had. NOT set at the end of input, although a read would
// answer at once there: what it answers is 0xFF standing in for nothing, and calling that
// ready would make a loop that reads while READY spin on imaginary bytes forever. A loop
// like that now stops when the input does, which is what anybody writing one intends.
#define CONSOLE_STATUS_READY 0x01
// Set once input has run out for good. The data port still answers 0xFF, which is what it
// always did and what every program written before this expects, but 0xFF is also an
// ordinary byte and this bit is the only thing that can tell the difference.
#define CONSOLE_STATUS_ENDED 0x02
// Which mode the console is in, so that a program can put it back the way it found it
// rather than assuming it knows.
#define CONSOLE_STATUS_KEYMODE 0x04
// Puts the terminal back the way it was found. Registered with atexit and called from the
// signal handlers, because a machine that stops in key mode and does not undo it leaves
// the shell that started it unusable, which is a far worse failure than anything the
// program was doing.
void consoleRestore(void);
// One byte from the console, waiting if it has to. Everything that reads standard input
// goes through here: the emulator owns one byte of pushback, and stdio holding a buffer
// of its own behind that would make the status port lie about what is waiting.
uint8_t consoleReadByte(void);
// ---- Device classes ----
//
// What kind of thing is plugged into a port. Class 0 is not a device: reading an