Long standing assembler bugs fixed, new path system. Make compatibility update.

This commit is contained in:
Anachronaut
2026-08-14 16:53:28 -04:00
parent 94b3a7af28
commit 638b68b25c
32 changed files with 1002 additions and 273 deletions
+88 -6
View File
@@ -8,6 +8,8 @@
#include <stdint.h>
#include <string.h>
#include <libgen.h>
#include <unistd.h>
#include <getopt.h>
#include "Assm-util.h"
#include "firstPass.h"
#include "secondPass.h"
@@ -82,11 +84,83 @@ void assemblerCleanup(intermediateElement *intermediateArray, int arraySize, cha
free(outputFileName);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
void printUsage(const char *programName) {
printf("Usage: %s [OPTIONS] <sourcefile>\n", programName);
printf("\n");
printf("Options:\n");
printf(" -o <file> Write the binary to this path instead of alongside the source.\n");
printf(" -I <dir> Look in this directory for included files. May be given more than once.\n");
printf(" -M <file> Write the source files this binary depends on, as a make rule.\n");
printf(" -h, --help Display this help message.\n");
}
// Writes a make rule naming every source file that went into the binary, so that a
// build system knows to reassemble when any of them changes. The empty rules after it
// are so that deleting a library does not leave make with a prerequisite it cannot
// build; without them the build stops instead of just reassembling.
void writeDependencyFile(const char *dependencyPath, const char *outputPath) {
FILE *file = fopen(dependencyPath, "w");
if (!file) {
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, dependencyPath);
exit(1);
}
fprintf(file, "%s:", outputPath);
for (int i = 0; i < sourceFileCount(); i++) {
fprintf(file, " %s", sourceFile(i));
}
fprintf(file, "\n\n");
// The source itself is index 0, and always exists, so it needs no empty rule.
for (int i = 1; i < sourceFileCount(); i++) {
fprintf(file, "%s:\n", sourceFile(i));
}
fclose(file);
}
int main(int argc, char *argv[]) {
static struct option long_options[] = {
{"output", required_argument, 0, 'o'},
{"include", required_argument, 0, 'I'},
{"depend", required_argument, 0, 'M'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0 }
};
char *outputFileName = NULL;
char *dependencyFileName = NULL;
int option_index = 0;
int opt;
while ((opt = getopt_long(argc, argv, "o:I:M:h", long_options, &option_index)) != -1) {
switch (opt) {
case 'o':
outputFileName = strdup(optarg);
break;
case 'I':
addIncludeDirectory(optarg);
break;
case 'M':
dependencyFileName = strdup(optarg);
break;
case 'h':
printUsage(argv[0]);
return 0;
default:
printUsage(argv[0]);
return 1;
}
}
if (optind >= argc) {
fprintf(stderr, RED "Error: No source file specified.\n" RESET);
printUsage(argv[0]);
return 1;
}
char *fileName = argv[optind];
optind++;
if (optind < argc) {
fprintf(stderr, RED "Error: Unexpected argument: %s\n" RESET, argv[optind]);
return 1;
}
// Allocate initial space for the intermediate array.
size_t arraySize = 1024;
intermediateElement *intermediateArray = malloc(arraySize * sizeof(intermediateElement));
@@ -95,14 +169,22 @@ int main(int argc, char *argv[]) {
exit(1);
}
char *fileName = argv[1];
// The file named on the command line is the first source file, and the include
// list owns the copy that everything else points at.
char *source = recordSourceFile(strdup(fileName));
int index = 0;
loadFile(&intermediateArray, fileName, &index, &arraySize);
loadFile(&intermediateArray, source, &index, &arraySize);
populateLabelTable(intermediateArray, index);
fillInLabelAddresses(intermediateArray, index);
populateOutputBuffers(intermediateArray, index, Program, &programLength, Data, &dataLength);
char *outputFileName = createOutputFileName(fileName);
if (!outputFileName) {
outputFileName = createOutputFileName(fileName);
}
writeOutputFile(outputFileName, Program, programLength, Data, dataLength);
if (dependencyFileName) {
writeDependencyFile(dependencyFileName, outputFileName);
free(dependencyFileName);
}
assemblerCleanup(intermediateArray, index, outputFileName);
return 0;
}
+42 -28
View File
@@ -51,13 +51,21 @@ int checkIfInstruction(intermediateElement *currentElement) {
strcpy(token, currentElement->token);
toUppercase(token);
// An instruction that works through a Data Pointer may name which one by
// hanging a selector off the mnemonic, as in LDA.2. Split that off before
// looking the mnemonic up.
char *selector = strchr(token, '.');
if (selector) {
*selector = '\0';
selector++;
// An instruction that works through a Data Pointer names which one by hanging a
// selector off the mnemonic, as in LDA.2. LDD and STD move a pointer through a
// pointer, so they take two, as in LDD.1.0. Split those off before looking the
// mnemonic up.
char *selector[MAX_DATA_POINTER_OPERANDS] = { NULL };
int selectorsGiven = 0;
char *dot = strchr(token, '.');
while (dot) {
*dot = '\0';
dot++;
if (selectorsGiven < MAX_DATA_POINTER_OPERANDS) {
selector[selectorsGiven] = dot;
}
selectorsGiven++;
dot = strchr(dot, '.');
}
uint8_t opcode = getOpcode(token);
@@ -66,32 +74,38 @@ int checkIfInstruction(intermediateElement *currentElement) {
return 0;
}
currentElement->type = INSTRUCTION;
currentElement->byteValue = opcode;
currentElement->byteLength = 1;
currentElement->dataPointer = 0;
if (instructionTakesDataPointer(opcode)) {
// The selector is emitted whether or not it was written, so these are
// always two bytes. Leaving it off is the same as writing 0.
currentElement->byteLength = 2;
if (selector) {
char *end;
long value = strtol(selector, &end, 10);
if (*selector == '\0' || *end != '\0' || value < 0 || value >= DATA_POINTERS) {
fprintf(stderr, RED "Error: \"%s\" does not name a Data Pointer.\n Selectors run from 0 to %d.\n" RESET, currentElement->token, DATA_POINTERS - 1);
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
exit(1);
}
currentElement->dataPointer = (uint8_t)value;
int selectorsWanted = dataPointerOperands(opcode);
if (selectorsGiven > selectorsWanted) {
if (selectorsWanted == 0) {
fprintf(stderr, RED "Error: %s does not work through a Data Pointer, so it cannot take a selector.\n" RESET, token);
} else {
fprintf(stderr, RED "Error: %s takes %d Data Pointer selector(s), but \"%s\" gives %d.\n" RESET, token, selectorsWanted, currentElement->token, selectorsGiven);
}
} else if (selector) {
fprintf(stderr, RED "Error: %s does not work through a Data Pointer, so it cannot take a selector.\n" RESET, token);
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
exit(1);
}
if (debug) printf("Token: %s is an instruction using Data Pointer %d.\n", currentElement->token, currentElement->dataPointer);
currentElement->type = INSTRUCTION;
currentElement->byteValue = opcode;
// The selectors are emitted whether or not they were written, so the length is
// fixed by the instruction. Leaving one off is the same as writing 0.
currentElement->byteLength = 1 + selectorsWanted;
for (int i = 0; i < selectorsWanted; i++) {
currentElement->dataPointer[i] = 0;
if (i < selectorsGiven && selector[i]) {
char *end;
long value = strtol(selector[i], &end, 10);
if (*selector[i] == '\0' || *end != '\0' || value < 0 || value >= DATA_POINTERS) {
fprintf(stderr, RED "Error: \"%s\" does not name a Data Pointer.\n Selectors run from 0 to %d.\n" RESET, currentElement->token, DATA_POINTERS - 1);
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
exit(1);
}
currentElement->dataPointer[i] = (uint8_t)value;
}
}
if (debug) printf("Token: %s is an instruction with %d selector(s).\n", currentElement->token, selectorsWanted);
return 1;
}
+2 -1
View File
@@ -11,6 +11,7 @@
#include <string.h>
#include <ctype.h>
#include "Assm-util.h"
#include "assembly.h"
#define MAX_INCLUDES 128
@@ -48,7 +49,7 @@ typedef struct {
char* fileName;
int lineNumber;
uint8_t byteValue;
uint8_t dataPointer; // Which Data Pointer this instruction works through, if it works through one.
uint8_t dataPointer[MAX_DATA_POINTER_OPERANDS]; // Which Data Pointers this instruction works through, if any.
int byteLength;
uint16_t address;
int type; // "KEYWORD", "INSTRUCTION" , "LABEL" , "VALUE", "STRING"
+9 -3
View File
@@ -59,6 +59,8 @@ Instruction instruction_set[] = {
{0x47, "SETD"},
{0x48, "DPUP"},
{0x49, "DPDN"},
{0x4A, "LDD"},
{0x4B, "STD"},
// Output Operations:
{0xD0, "OUTQ"},
{0xD1, "OUTA"},
@@ -82,10 +84,14 @@ const char* getMnemonic(uint8_t opcode) {
return "---";
}
int instructionTakesDataPointer(uint8_t opcode) {
// These instructions all work through a Data Pointer, and so are followed by a
// byte naming which one. Everything else is a single byte opcode as before.
int dataPointerOperands(uint8_t opcode) {
// How many Data Pointer selector bytes follow this opcode. Most instructions
// have none. The ones that work through a pointer have one naming which pointer.
// LDD and STD move a pointer through a pointer, so they name two.
switch (opcode) {
case 0x4A: // LDD
case 0x4B: // STD
return 2;
case 0x33: // PSHD
case 0x36: // POPD
case 0x40: // INCD
+43 -1
View File
@@ -8,10 +8,52 @@
#ifndef ASSEMBLY_H
#define ASSEMBLY_H
// ---- The SplitBit binary format ----
//
// A binary starts with a file header, then the Program Segment, then the Data
// Segment. All multi byte numbers are stored most significant byte first.
//
// Offset Size Field
// 0 4 "SPBT", so a file that is not a SplitBit binary is spotted at once
// 4 1 Format version
// 5 4 Required feature flags
// 9 3 "PRG"
// 12 2 Program Segment length
// 14 N Program Segment
// .. 3 "DAT"
// .. 2 Data Segment length
// .. M Data Segment
//
// The feature flags are how a binary says it needs something the base machine does
// not provide, so that an emulator which cannot provide it refuses to run the binary
// rather than quietly doing the wrong thing. No features are defined yet; the field
// is here so that adding one later does not need another format version.
#define SPLITBIT_MAGIC "SPBT"
#define SPLITBIT_MAGIC_LENGTH 4
#define SPLITBIT_FORMAT_VERSION 1
#define SPLITBIT_FLAGS_LENGTH 4
#define SEGMENT_MARKER_LENGTH 3
#define SEGMENT_LENGTH_BYTES 2
// Everything the format costs a file, on top of the two segments themselves.
#define SPLITBIT_HEADER_BYTES (SPLITBIT_MAGIC_LENGTH + 1 + SPLITBIT_FLAGS_LENGTH \
+ 2 * (SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES))
// Features this build of the emulator can provide. A binary asking for anything
// outside this set is refused.
#define SPLITBIT_FEATURES_SUPPORTED 0x00000000u
// Features the assembler currently needs to ask for. Nothing, so far.
#define SPLITBIT_FEATURES_REQUIRED 0x00000000u
const char* getMnemonic(uint8_t opcode);
uint8_t getOpcode(char* mnemonic);
int instructionTakesDataPointer(uint8_t opcode);
// How many Data Pointer selector bytes follow the given opcode. Never more than two.
#define MAX_DATA_POINTER_OPERANDS 2
int dataPointerOperands(uint8_t opcode);
#endif // CPU_H
+138 -24
View File
@@ -7,46 +7,153 @@
#include <stdlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <libgen.h>
#include <limits.h>
#include "firstPass.h"
#include "Assm-util.h"
#include "assembly.h"
char *includeList[MAX_INCLUDES];
// Each source file that has gone into this assembly. The path is kept as it was
// written, because that is what reads well in an error message or a dependency
// rule. The canonical form is kept alongside it so that the same file reached by
// two different routes is recognised as one file and included only once.
typedef struct {
char *path;
char *canonical;
} SourceFile;
SourceFile includeList[MAX_INCLUDES];
int includeCount = 0;
char *includeDirectories[MAX_INCLUDES];
int includeDirectoryCount = 0;
void freeIncludeList() {
for (int i = 0; i < includeCount; i++) {
if (includeList[i]) {
free(includeList[i]);
}
free(includeList[i].path);
free(includeList[i].canonical);
}
includeCount = 0;
for (int i = 0; i < includeDirectoryCount; i++) {
free(includeDirectories[i]);
}
includeDirectoryCount = 0;
}
int isFileIncluded(const char *fileName) {
void addIncludeDirectory(const char *directory) {
if (includeDirectoryCount >= MAX_INCLUDES) {
fprintf(stderr, RED "Error: Too many include directories.\n" RESET);
exit(1);
}
includeDirectories[includeDirectoryCount] = strdup(directory);
includeDirectoryCount++;
}
int sourceFileCount() {
return includeCount;
}
const char *sourceFile(int index) {
return includeList[index].path;
}
char *recordSourceFile(char *path) {
// realpath resolves symlinks and things like "..", so that Libraries/print.asm
// and ../Libraries/print.asm are known to be the same file.
char *canonical = realpath(path, NULL);
if (!canonical) {
canonical = strdup(path);
}
for (int i = 0; i < includeCount; i++) {
if (strcmp(includeList[i], fileName) == 0) {
return 1;
if (strcmp(includeList[i].canonical, canonical) == 0) {
// Already assembled once. Including it again is not an error, it just
// does nothing, which is what lets two libraries share a dependency.
free(canonical);
free(path);
return NULL;
}
}
if (includeCount >= MAX_INCLUDES) {
fprintf(stderr, RED "Error: Too many included files.\n" RESET);
exit(1);
}
includeList[includeCount].path = path;
includeList[includeCount].canonical = canonical;
includeCount++;
return path;
}
// Joins a directory and a file name into a newly allocated path.
static char *joinPath(const char *directory, const char *name) {
if (!directory || directory[0] == '\0' || strcmp(directory, ".") == 0) {
return strdup(name);
}
size_t length = strlen(directory) + 1 + strlen(name) + 1;
char *joined = malloc(length);
if (!joined) {
fprintf(stderr, RED "Error: Memory allocation failed.\n" RESET);
exit(1);
}
snprintf(joined, length, "%s/%s", directory, name);
return joined;
}
static int fileExists(const char *path) {
FILE *file = fopen(path, "r");
if (file) {
fclose(file);
return 1;
}
return 0;
}
void addIncludedFile(const char *fileName) {
if (includeCount < MAX_INCLUDES) {
includeList[includeCount] = strdup(fileName);
includeCount++;
} else {
fprintf(stderr, RED "Error: Too many included files.\n" RESET);
exit(1);
// Works out where an #Include actually points. An absolute path is taken as given.
// Otherwise the file is looked for beside the file that asked for it, so that a
// library including its own siblings needs no help, and then along the include
// directories in the order they were given. Returns NULL if it is nowhere.
static char *resolveInclude(const char *includingFile, const char *requested) {
if (requested[0] == '/') {
return fileExists(requested) ? strdup(requested) : NULL;
}
char *copy = strdup(includingFile);
char *candidate = joinPath(dirname(copy), requested);
free(copy);
if (fileExists(candidate)) {
return candidate;
}
free(candidate);
for (int i = 0; i < includeDirectoryCount; i++) {
candidate = joinPath(includeDirectories[i], requested);
if (fileExists(candidate)) {
return candidate;
}
free(candidate);
}
return NULL;
}
// Says where the assembler looked, so that a missing include is easy to fix.
static void reportMissingInclude(const char *includingFile, const char *requested, int lineNumber) {
fprintf(stderr, RED "Error: Couldn't find included file \"%s\".\n" RESET, requested);
printf(" File: %s at line %d.\n", includingFile, lineNumber);
printf(" Looked for it:\n");
char *copy = strdup(includingFile);
printf(" beside %s\n", includingFile);
free(copy);
for (int i = 0; i < includeDirectoryCount; i++) {
printf(" in %s\n", includeDirectories[i]);
}
if (includeDirectoryCount == 0) {
printf(" and nowhere else, because no include directories were given with -I.\n");
}
}
int loadFile(intermediateElement **intermediateArray, char *fileName, int *intermediateIndex, size_t *arraySize){
// Initial setup.
addIncludedFile(fileName);
// 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;
int lineNumber = 1; // Line numbers start at 1.
// Open the file.
@@ -73,19 +180,26 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
int testValue = checkIfKeyword(&(*intermediateArray)[*intermediateIndex]);
if(testValue > 0) {
switch (testValue){
case KEYWORD_INCLUDE:
case KEYWORD_INCLUDE: {
// We need to load another file and process it before continuing.
status = NOWHERE;
// Get the filename and load up the file.
// Get the filename and work out where it actually is.
(*intermediateIndex)++;
readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber);
char *includeFile = (*intermediateArray)[*intermediateIndex].token;
if (isFileIncluded(includeFile)) {
fprintf(stderr, RED "Error: File %s is included more than once.\n" RESET, includeFile);
printf(" File: %s at line %d.\n", fileName, lineNumber);
char *requested = (*intermediateArray)[*intermediateIndex].token;
char *resolved = resolveInclude(fileName, requested);
if (!resolved) {
reportMissingInclude(fileName, requested, lineNumber);
exit(1);
}
loadFile(intermediateArray, includeFile, intermediateIndex, arraySize);
// recordSourceFile takes the path, and hands back NULL if this file
// has already been assembled. Including it twice is harmless, which
// is what lets two libraries depend on a third.
char *owned = recordSourceFile(resolved);
if (owned) {
loadFile(intermediateArray, owned, intermediateIndex, arraySize);
}
}
break;
case KEYWORD_PROGRAM:
// Set the state PROGRAM so we mark additional tokens for inclusion into Program Memory.
+12
View File
@@ -14,6 +14,18 @@
void freeIncludeList();
// Adds a directory to search when an #Include is not found beside the file that
// asked for it. Directories are searched in the order they were added.
void addIncludeDirectory(const char *directory);
// Records a source file and returns the copy the assembler will keep, which is what
// element fileNames point at. Returns NULL if this file has already been included.
char *recordSourceFile(char *path);
// Every source file that went into this assembly, for writing make dependencies.
int sourceFileCount();
const char *sourceFile(int index);
int loadFile(intermediateElement **intermediateArray, char *fileName, int *intermediateIndex, size_t *arraySize);
#endif // FIRSTPASS_H
+21 -11
View File
@@ -162,8 +162,8 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
Program[(*programCount)++] = intermediateArray[i].byteValue;
// Instructions that work through a Data Pointer carry a selector
// byte naming which one, whether or not the programmer wrote it.
if (instructionTakesDataPointer(intermediateArray[i].byteValue)) {
Program[(*programCount)++] = intermediateArray[i].dataPointer;
for (int d = 0; d < dataPointerOperands(intermediateArray[i].byteValue); d++) {
Program[(*programCount)++] = intermediateArray[i].dataPointer[d];
}
// Make sure any operand bytes this instruction expects are present.
checkOperands(intermediateArray, arraySize, i);
@@ -192,12 +192,12 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
Data[(*dataCount)++] = '\0'; // Add null terminator to Data buffer
break;
case LABEL:
// The label table reserved two bytes for this, but there's nothing here
// that knows how to emit them, so every later Data label would be shifted
// out of place. Refuse it rather than assemble something that looks fine.
fprintf(stderr, RED "Error: Label \"%s\" used as a value in the Data Segment.\n Label references are only supported in the Program Segment.\n" RESET, intermediateArray[i].token);
printf("File: %s at line %d.\n", intermediateArray[i].fileName, intermediateArray[i].lineNumber);
exit(1);
// A label named in the Data Segment puts its address there, which is
// how a program lays down a table of addresses for LDD to walk.
// Two bytes, most significant first, the same order addresses are
// stored in everywhere else.
Data[(*dataCount)++] = (intermediateArray[i].address >> 8) & 0xFF; // High byte
Data[(*dataCount)++] = intermediateArray[i].address & 0xFF; // Low byte
break;
}
}
@@ -211,8 +211,18 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
exit(1);
}
// Write the file header: the magic, the format version, and the features this
// binary needs from the machine. An emulator that cannot provide one of those
// features refuses the file rather than running it and going quietly wrong.
fwrite(SPLITBIT_MAGIC, sizeof(char), SPLITBIT_MAGIC_LENGTH, outputFile);
fputc(SPLITBIT_FORMAT_VERSION, outputFile);
uint32_t required = SPLITBIT_FEATURES_REQUIRED;
for (int i = SPLITBIT_FLAGS_LENGTH - 1; i >= 0; i--) {
fputc((required >> (i * 8)) & 0xFF, outputFile); // Most significant byte first.
}
// Write the "PRG" header for the program segment
fwrite("PRG", sizeof(char), 3, outputFile);
fwrite("PRG", sizeof(char), SEGMENT_MARKER_LENGTH, outputFile);
// Write the program segment length as a 2-byte value (big-endian)
uint16_t programSize = programCount;
@@ -227,7 +237,7 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
}
// Write the "DAT" header for the data segment
fwrite("DAT", sizeof(char), 3, outputFile);
fwrite("DAT", sizeof(char), SEGMENT_MARKER_LENGTH, outputFile);
// Write the data segment length as a 2-byte value (big-endian)
uint16_t dataSize = dataCount;
@@ -243,5 +253,5 @@ 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 Total size: %d bytes.\n" RESET, programCount, dataCount, (programCount+dataCount+10));
printf(GREEN " Program Segment size: %d bytes.\n Data Segment size: %d bytes.\n Total size: %d bytes.\n" RESET, programCount, dataCount, (programCount + dataCount + SPLITBIT_HEADER_BYTES));
}
+77 -57
View File
@@ -4,47 +4,80 @@
// 10/16/2024
#include "bootstrap.h"
#include "../Assembler/assembly.h" // For the binary format, which both tools share.
#include <stdio.h>
#include <string.h>
int byte = 0;
const uint8_t HEADER_LENGTH = 3; // Number of bytes for the header.
const uint8_t LENGTH_SIZE = 2; // Number of bytes for the segment length.
uint32_t readLength(FILE *file) {
uint16_t length = 0;
for (int i = 0; i < LENGTH_SIZE; i++) {
// Reads a number of the given width, most significant byte first.
// Returns 1 if the file runs out before the number does.
static uint8_t readNumber(FILE *file, int width, const char *what, uint32_t *result) {
uint32_t value = 0;
for (int i = 0; i < width; i++) {
int byte = fgetc(file);
if (byte == EOF) {
fprintf(stderr, "Error: Unexpected end of file while reading a segment length.\n");
return UINT32_MAX;
fprintf(stderr, "Error: Unexpected end of file while reading %s.\n", what);
return 1;
}
length = (length << 8) | (uint8_t)byte;
value = (value << 8) | (uint8_t)byte;
}
return length;
*result = value;
return 0;
}
uint32_t readHeader(FILE *file, const char *expectedHeader) {
char header[HEADER_LENGTH];
for (int i = 0; i < HEADER_LENGTH; i++) {
// Reads a fixed length marker and checks it against the one expected. The caller's
// buffer must have room for length + 1 characters; it always comes back with a null
// on the end, so it is safe to print in an error message either way.
static uint8_t readMarker(FILE *file, const char *expected, int length, char *found) {
memset(found, 0, length + 1);
for (int i = 0; i < length; i++) {
int byte = fgetc(file);
if (byte == EOF) {
fprintf(stderr, "Error: Unexpected end of file while reading a header.\n");
return UINT32_MAX;
return 1;
}
header[i] = (uint8_t)byte;
found[i] = (char)byte;
}
if (strncmp(header, expectedHeader, 3)) {
fprintf(stderr, "Error: Bad header.\n");
printf("Header: %s\nExpected Header: %s\n", header, expectedHeader);
return UINT32_MAX;
return strncmp(found, expected, length) != 0;
}
// Reads the file header: the magic, the format version, and the features the binary
// says it needs from the machine.
static uint8_t readFileHeader(FILE *file) {
char magic[SPLITBIT_MAGIC_LENGTH + 1];
if (readMarker(file, SPLITBIT_MAGIC, SPLITBIT_MAGIC_LENGTH, magic)) {
fprintf(stderr, "Error: This is not a SplitBit binary.\n");
if (strncmp(magic, "PRG", 3) == 0) {
fprintf(stderr, " It looks like a binary from before the format carried a version.\n Reassemble it and try again.\n");
} else {
fprintf(stderr, " Expected the file to begin with \"%s\", found \"%s\".\n", SPLITBIT_MAGIC, magic);
}
return 1;
}
uint32_t version;
if (readNumber(file, 1, "the format version", &version)) {
return 1;
}
if (version != SPLITBIT_FORMAT_VERSION) {
fprintf(stderr, "Error: This binary is in format version %u, and this emulator reads version %u.\n", version, SPLITBIT_FORMAT_VERSION);
return 1;
}
uint32_t required;
if (readNumber(file, SPLITBIT_FLAGS_LENGTH, "the feature flags", &required)) {
return 1;
}
uint32_t missing = required & ~(uint32_t)SPLITBIT_FEATURES_SUPPORTED;
if (missing) {
fprintf(stderr, "Error: This binary was built for a machine this emulator cannot provide.\n");
fprintf(stderr, " It asks for feature bits 0x%08X, which are not implemented here.\n", missing);
return 1;
}
return 0;
}
uint8_t loadSegment(FILE *file, uint8_t *Memory, uint16_t length) {
for (int16_t i = 0; i < length; i++) {
byte = fgetc(file);
static uint8_t loadSegment(FILE *file, uint8_t *Memory, uint32_t length) {
for (uint32_t i = 0; i < length; i++) {
int byte = fgetc(file);
if (byte == EOF) {
fprintf(stderr, "Error: Unexpected end of file while reading a segment.\n");
return 1;
@@ -54,44 +87,31 @@ uint8_t loadSegment(FILE *file, uint8_t *Memory, uint16_t length) {
return 0;
}
// Reads one segment: its marker, its length, and then its contents.
static uint8_t readSegment(FILE *file, const char *marker, uint8_t *Memory) {
char found[SEGMENT_MARKER_LENGTH + 1];
if (readMarker(file, marker, SEGMENT_MARKER_LENGTH, found)) {
fprintf(stderr, "Error: Expected a \"%s\" segment here, found \"%s\".\n", marker, found);
return 1;
}
uint32_t length;
if (readNumber(file, SEGMENT_LENGTH_BYTES, "a segment length", &length)) {
return 1;
}
return loadSegment(file, Memory, length);
}
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
FILE *file = fopen(path, "rb");
if (file == NULL) {
fprintf(stderr, "Error: Couldn't open file: %s\n", path);
return 1;
}
// Read the first three bytes and check if they're the PRG header.
if (readHeader(file, "PRG") == UINT32_MAX) {
fclose(file);
return 1;
}
// Now we need to get the length of the Program Section.
uint32_t length = readLength(file);
if (length == UINT32_MAX) {
fclose(file);
return 1;
}
// Now we load the Program Segment.
if (loadSegment(file, Program, (uint16_t)length)) {
fclose(file);
return 1;
}
// Okay, Program is loaded, now do the same thing but for Data.
// Check the header.
if (readHeader(file, "DAT") == UINT32_MAX) {
return 1;
}
// Get the legnth of the Data Section.
length = readLength(file);
if (length == UINT32_MAX){
fclose(file);
return 1;
}
// Now load the Data Section.
if (loadSegment(file, Data, (uint16_t)length)) {
fclose(file);
return 1;
}
// The Program Segment must come first, then the Data Segment. Short circuiting
// here means there is one exit, and so only one place that has to close the file.
uint8_t failed = readFileHeader(file)
|| readSegment(file, "PRG", Program)
|| readSegment(file, "DAT", Data);
fclose(file);
return 0;
return failed;
}
+22
View File
@@ -368,6 +368,28 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
*target -= cpu->Program[cpu->ProgramCounter];
}
break;
case 0x4A: {
// LDD - Load the first Data Pointer from the two bytes of Data Memory
// addressed by the second. Byte order matches everywhere else an address
// is stored: most significant first, then least significant.
// The source is taken by value before anything is written, so LDD.0.0
// follows the pointer in DP0 rather than tripping over itself.
uint16_t *destination = selectDataPointer(cpu);
uint16_t source = *selectDataPointer(cpu);
*destination = (uint16_t)cpu->Data[source] << 8;
*destination |= (uint16_t)cpu->Data[(uint16_t)(source + 1)];
}
break;
case 0x4B: {
// STD - Store the first Data Pointer into the two bytes of Data Memory
// addressed by the second. The cast on the second address keeps it inside
// Data Memory when the pointer sits at the very top of it.
uint16_t value = *selectDataPointer(cpu);
uint16_t address = *selectDataPointer(cpu);
cpu->Data[address] = (value >> 8) & 0xFF;
cpu->Data[(uint16_t)(address + 1)] = value & 0xFF;
}
break;
//
// Dx - Output Operations:
//