A vector is the one thing about a program that nothing else can tell
you. A pinned vector has its number in the source that pinned it, but a
vector the assembler numbered has that number nowhere at all - not in
the source, not in the binary in any form a reader can find. Until now
there was no way to learn that a vector became number 64.
It also cost two hops to follow by hand. The name in "SWI osPrintString"
is not the name of the routine that implements it, so finding the code
meant searching for the vector, reading the handler's name off the
Vector Segment, and searching again. A vector row now names the handler
and gives the line the two were tied together on.
Both numbers, at the user's asking, because neither can be worked out
from the other without knowing which table the vector is in: the Number
is what a program writes and the machine dispatches on, the Address is
where the handler's address is stored, base plus twice the number. The
slot is computed with the same expression the loader is given, so what
the table says and what gets written there cannot drift apart. A vector
a program only declares is listed too - that is how a program says which
vectors it calls, and how two programs can be checked against each other
for agreeing about a number.
A device has no name of its own, being named by the port it is plugged
into, so it is listed under its handler.
The first field is now Kind rather than Memory, because Vector and
Device are not memories. Sorted Program, Data, Vector, Device.
docs.sh checks the six fields against the manual and against real dumps
of two programs - Keys, a loadable program with all four kinds, and
cosmos, a boot image whose segments both start at zero. It now also
checks that a row's name really appears on the line the row names, which
is what catches the string-newline bug fixed in ca6c8ca coming back.
Verified with break.sh four ways: wrong slot arithmetic, vectors
dropped, a field renamed in the manual, and that bug reintroduced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
927 lines
43 KiB
C
927 lines
43 KiB
C
// secondPass.c
|
|
// Functions for the 'second pass' of the SplitBit Assembler.
|
|
// The goal here is to resolve the addresses of labels.
|
|
// We'll want to abort if the program comes out to greater than the maximum memory for SplitBit.
|
|
// We'll also want to abort if there's a label used with no definition.
|
|
// Written by Anachronaut
|
|
// 10/25/2024
|
|
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <ctype.h>
|
|
#include <stdint.h>
|
|
#include "secondPass.h"
|
|
#include "Assm-util.h"
|
|
#include "assembly.h"
|
|
#include "sbex.h"
|
|
|
|
int debugSecondPass = 0;
|
|
|
|
Label labelArray[MAX_LABELS];
|
|
int labelCount = 0;
|
|
|
|
// Beside the labels rather than beside the code that fills it, because the symbol file
|
|
// below reads both tables and this one would otherwise not be declared yet.
|
|
VectorEntry vectorArray[MAX_VECTORS];
|
|
int vectorArrayCount = 0;
|
|
|
|
// ---- Where everything ended up ----
|
|
//
|
|
// Every label with the memory and address it was given and the place it was written, one
|
|
// per line, fields separated by tabs. The assembler knows all of this and nothing else
|
|
// does: a program on the disk is bytes, and the machine's own monitor can disassemble it
|
|
// but has no idea what any of it is called or where it came from.
|
|
//
|
|
// WHAT IT IS FOR is two questions. The first is where a program spends its time - counting
|
|
// which addresses get called says a great deal and names nothing, so the answer arrives as
|
|
// a list of numbers and somebody has to work out by hand which routine each one is inside.
|
|
// The second is where a name lives, which matters more the bigger the program gets: CosmOS
|
|
// and its libraries define over a thousand names across a dozen files, and finding the one
|
|
// definition of a routine means grepping for it and reading past every place it is called.
|
|
// The file and line answer that outright.
|
|
//
|
|
// TABS, so that the file is a table every ordinary tool already understands - cut -f3, awk
|
|
// -F'\t', sort -k1,2 - and so that a name never has to be quoted. No header line, for the
|
|
// same reason: nothing that reads it should have to know to skip one.
|
|
//
|
|
// THE KIND IS THE FIRST FIELD BECAUSE THIS IS A HARVARD MACHINE. Program and Data are
|
|
// separate address spaces, so 0x3000 is two different places and an address alone does not
|
|
// say which. That is invisible in a loadable program, where the two segments are based
|
|
// somewhere apart, and immediate in a boot image, where both start at zero and every
|
|
// address in the file appears twice.
|
|
//
|
|
// VECTORS ARE IN HERE TOO, and they are the part nothing else can tell you. A pinned vector
|
|
// has its number written in the source that pins it, but an automatic one is handed a number
|
|
// by this assembler and that number appears nowhere at all - not in the source, not in the
|
|
// binary in any form a reader can find. A vector also costs two hops to follow by hand: the
|
|
// name in "SWI osFileRead" is not the name of the routine that implements it, so finding the
|
|
// code means grepping for the vector, reading the handler's name off the Vector Segment, and
|
|
// grepping again. A vector row gives its number, the handler's name, and the line the two
|
|
// were tied together on.
|
|
//
|
|
// A VECTOR CARRIES BOTH ITS NUMBERS, which is why there is a sixth field. Its number is what
|
|
// a program writes and what the machine dispatches on; its slot is where the handler's
|
|
// address is stored in Program Memory, base + number * 2, which is what the loader writes
|
|
// and what a memory dump shows. Neither can be worked out from the other without knowing
|
|
// which table it is in, so the file says both.
|
|
//
|
|
// Sorted by kind and then address rather than by name, because the question asked of it is
|
|
// always "what is at this address"; sorting by address alone would interleave separate
|
|
// address spaces and make the first column flicker between them. The table is small enough
|
|
// that sorting it is free.
|
|
|
|
// The four kinds, in the order they are listed. Program and Data are the two memories, then
|
|
// the two vector tables.
|
|
#define ROW_PROGRAM 0
|
|
#define ROW_DATA 1
|
|
#define ROW_VECTOR 2
|
|
#define ROW_DEVICE 3
|
|
|
|
typedef struct {
|
|
int kind;
|
|
uint16_t address; // Where it lives: a label's address, or a vector's slot.
|
|
const char *name;
|
|
const char *fileName;
|
|
int lineNumber;
|
|
int number; // The vector or port number, or -1 for a label, which has none.
|
|
} SymbolRow;
|
|
|
|
static int byPlace(const void *left, const void *right) {
|
|
const SymbolRow *a = left, *b = right;
|
|
if (a->kind != b->kind) {
|
|
return a->kind < b->kind ? -1 : 1;
|
|
}
|
|
if (a->address != b->address) {
|
|
return a->address < b->address ? -1 : 1;
|
|
}
|
|
return strcmp(a->name, b->name);
|
|
}
|
|
|
|
void writeSymbolFile(const char *path) {
|
|
FILE *file = fopen(path, "w");
|
|
if (!file) {
|
|
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, path);
|
|
exit(1);
|
|
}
|
|
int rowCount = labelCount + vectorArrayCount;
|
|
SymbolRow *rows = malloc((size_t)rowCount * sizeof(SymbolRow) + 1);
|
|
if (!rows) {
|
|
fprintf(stderr, RED "Error: Out of memory writing the symbol file.\n" RESET);
|
|
fclose(file);
|
|
exit(1);
|
|
}
|
|
int n = 0;
|
|
for (int i = 0; i < labelCount; i++) {
|
|
// A label is put in one of the two segments by populateLabelTable and there is no
|
|
// third, but the kind is taken from the type rather than assumed, so a label that
|
|
// somehow arrived as neither is not quietly filed under Data.
|
|
rows[n].kind = labelArray[i].type == PROGRAM ? ROW_PROGRAM : ROW_DATA;
|
|
rows[n].address = labelArray[i].address;
|
|
rows[n].name = labelArray[i].label;
|
|
rows[n].fileName = labelArray[i].fileName;
|
|
rows[n].lineNumber = labelArray[i].lineNumber;
|
|
rows[n].number = -1;
|
|
n++;
|
|
}
|
|
for (int i = 0; i < vectorArrayCount; i++) {
|
|
int hardware = vectorArray[i].base == HARDWARE_VECTOR_BASE;
|
|
rows[n].kind = hardware ? ROW_DEVICE : ROW_VECTOR;
|
|
// The same arithmetic the loader is given, so that what this says a vector's slot is
|
|
// and what actually gets written there cannot drift apart.
|
|
rows[n].address = vectorArray[i].base
|
|
+ (uint16_t)vectorArray[i].index * VECTOR_ENTRY_BYTES;
|
|
// A device has no name of its own - it is named by the port it is plugged into - so
|
|
// it is listed under its handler, which is the only name it has.
|
|
rows[n].name = vectorArray[i].name ? vectorArray[i].name
|
|
: vectorArray[i].handlerName ? vectorArray[i].handlerName
|
|
: "?";
|
|
rows[n].fileName = vectorArray[i].fileName;
|
|
rows[n].lineNumber = vectorArray[i].lineNumber;
|
|
rows[n].number = vectorArray[i].index;
|
|
n++;
|
|
}
|
|
qsort(rows, (size_t)n, sizeof(SymbolRow), byPlace);
|
|
static const char *kindName[4] = { "Program", "Data", "Vector", "Device" };
|
|
for (int i = 0; i < n; i++) {
|
|
char number[12]; // Wide enough for any int, which is more than a vector needs.
|
|
if (rows[i].number < 0) {
|
|
// A LABEL HAS NO NUMBER, and the field says so rather than being left empty:
|
|
// a run of tabs with nothing between them is the one thing a reader of this
|
|
// file, human or otherwise, can miscount.
|
|
snprintf(number, sizeof(number), "-");
|
|
} else {
|
|
snprintf(number, sizeof(number), "%d", rows[i].number);
|
|
}
|
|
fprintf(file, "%s\t%04X\t%s\t%s\t%d\t%s\n",
|
|
kindName[rows[i].kind], rows[i].address, rows[i].name,
|
|
rows[i].fileName ? rows[i].fileName : "?",
|
|
rows[i].lineNumber, number);
|
|
}
|
|
free(rows);
|
|
fclose(file);
|
|
}
|
|
|
|
void freeLabelList() {
|
|
for (int i = 0; i < labelCount; i++) {
|
|
if (labelArray[i].label) {
|
|
free(labelArray[i].label);
|
|
}
|
|
}
|
|
labelCount = 0;
|
|
}
|
|
|
|
void addLabel(char *labelName, uint16_t address, int type, const char *fileName, int lineNumber) {
|
|
if (labelCount < MAX_LABELS) {
|
|
// Duplicate labelName and remove the trailing colon, if present
|
|
char *cleanedLabel = strdup(labelName);
|
|
int len = strlen(cleanedLabel);
|
|
if (cleanedLabel[len - 1] == ':') {
|
|
cleanedLabel[len - 1] = '\0'; // Remove the colon
|
|
}
|
|
|
|
// A NAME THAT IS ALREADY AN INSTRUCTION IS REFUSED, and the error is here rather
|
|
// than at the branch that could not find it. Mnemonics are matched with the token
|
|
// uppercased, so a label called "wait" and the instruction WAIT are the same word
|
|
// - and when WAIT was added, Keys.asm had been using that label for a year. What
|
|
// it reported was "Branch without label" at the BRQ, thirty lines from the cause
|
|
// and naming nothing that had changed.
|
|
//
|
|
// This will happen again. Every instruction added takes a word out of the space
|
|
// of label names, so the check belongs where the name is claimed.
|
|
{
|
|
char upper[8];
|
|
int n = 0;
|
|
for (; cleanedLabel[n] != '\0' && n < (int)sizeof(upper) - 1; n++) {
|
|
upper[n] = (char)toupper((unsigned char)cleanedLabel[n]);
|
|
}
|
|
upper[n] = '\0';
|
|
// Only a name short enough to BE a mnemonic can collide with one, and the
|
|
// longest is four characters. A longer name is truncated by the loop above
|
|
// and would not match anything, which is the right answer.
|
|
if (cleanedLabel[n] == '\0' && getOpcode(upper) != NOT_AN_OPCODE) {
|
|
fprintf(stderr, RED "Error: \"%s\" is an instruction, so it cannot also be"
|
|
" a label.\n" RESET, cleanedLabel);
|
|
printf("File: %s at line %d.\n", fileName, lineNumber);
|
|
free(cleanedLabel);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
// A name may only be defined once. Without this check a reference quietly
|
|
// resolves to whichever definition came first, so a typo or a name that two
|
|
// libraries both happen to use is very hard to track down.
|
|
for (int i = 0; i < labelCount; i++) {
|
|
if (strcmp(labelArray[i].label, cleanedLabel) == 0) {
|
|
fprintf(stderr, RED "Error: Label \"%s\" is defined more than once.\n" RESET, cleanedLabel);
|
|
printf("File: %s at line %d.\n", fileName, lineNumber);
|
|
free(cleanedLabel);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
labelArray[labelCount].label = cleanedLabel;
|
|
labelArray[labelCount].address = address;
|
|
labelArray[labelCount].type = type;
|
|
labelArray[labelCount].fileName = fileName;
|
|
labelArray[labelCount].lineNumber = lineNumber;
|
|
if (debugSecondPass) printf("Added label %s with address %04X\n", labelName, labelArray[labelCount].address);
|
|
labelCount++;
|
|
} else {
|
|
fprintf(stderr, RED "Error: Too many labels. This program and everything it\n"
|
|
" includes may define %d between them, and \"%s\" is one too many.\n" RESET,
|
|
MAX_LABELS, labelName);
|
|
printf("File: %s at line %d.\n", fileName, lineNumber);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
void populateLabelTable(intermediateElement *intermediateArray, int arraySize) {
|
|
// 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,
|
|
// so it can only be worked out here, walking the tokens in order. It has to be
|
|
// settled before the running count moves past it, or every label after it lands
|
|
// in the wrong place.
|
|
if (intermediateArray[i].type == ALIGNMENT) {
|
|
int cursor = (intermediateArray[i].destination == PROGRAM) ? programCount : dataCount;
|
|
int alignment = intermediateArray[i].address;
|
|
intermediateArray[i].byteLength = (alignment - (cursor % alignment)) % alignment;
|
|
}
|
|
if (intermediateArray[i].type == LABEL_DEFINITION && intermediateArray[i].destination != VECTORS) {
|
|
if (intermediateArray[i].destination == PROGRAM) {
|
|
addLabel(intermediateArray[i].token, (uint16_t)programCount, PROGRAM,
|
|
intermediateArray[i].fileName, intermediateArray[i].lineNumber);
|
|
} else {
|
|
addLabel(intermediateArray[i].token, (uint16_t)dataCount, DATA,
|
|
intermediateArray[i].fileName, intermediateArray[i].lineNumber);
|
|
}
|
|
}
|
|
if (intermediateArray[i].destination == PROGRAM) {
|
|
programCount += intermediateArray[i].byteLength;
|
|
} else if (intermediateArray[i].destination == DATA) {
|
|
dataCount += intermediateArray[i].byteLength;
|
|
}
|
|
if (debugSecondPass) printf("Token: %s with byte length %d to destination %d of type %d\n", intermediateArray[i].token ,intermediateArray[i].byteLength, intermediateArray[i].destination, intermediateArray[i].type);
|
|
|
|
}
|
|
if (programCount > PROGRAM_SEGMENT_LIMIT ) {
|
|
fprintf(stderr, RED "Error: Program is too long to fit in Program Memory.\n"
|
|
" The Program Segment may not run past 0x%04X, where the vector table begins.\n" RESET,
|
|
PROGRAM_SEGMENT_LIMIT - 1);
|
|
exit(1);
|
|
}
|
|
if (dataCount > 0xFFFF ) {
|
|
fprintf(stderr, RED "Error: Data is too long to fit in Data Memory.\n" RESET);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
// ---- The Vector Segment ----
|
|
|
|
int findLabelAddress(const char *labelName);
|
|
|
|
int vectorCount() {
|
|
return vectorArrayCount;
|
|
}
|
|
|
|
void freeVectorList() {
|
|
for (int i = 0; i < vectorArrayCount; i++) {
|
|
if (vectorArray[i].name) {
|
|
free(vectorArray[i].name);
|
|
}
|
|
if (vectorArray[i].handlerName) {
|
|
free(vectorArray[i].handlerName);
|
|
}
|
|
}
|
|
vectorArrayCount = 0;
|
|
}
|
|
|
|
// The words the Vector Segment understands. These are spelled without regard to case,
|
|
// the way mnemonics are, because they are part of the language rather than names the
|
|
// programmer chose.
|
|
static int sameWord(const char *a, const char *b) {
|
|
while (*a && *b) {
|
|
if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) {
|
|
return 0;
|
|
}
|
|
a++;
|
|
b++;
|
|
}
|
|
return *a == *b;
|
|
}
|
|
|
|
// The vectors that already mean something. Everything else a program names is numbered
|
|
// for it, starting above the range held back for faults.
|
|
static const struct {
|
|
const char *name;
|
|
uint8_t index;
|
|
} reservedVectors[] = {
|
|
{ "Boot", VECTOR_BOOT },
|
|
{ "SoftReset", VECTOR_SOFT_RESET },
|
|
{ "BadOpcode", VECTOR_INVALID_OPCODE },
|
|
{ "GuardViolation", VECTOR_GUARD_VIOLATION },
|
|
{ "BankFault", VECTOR_BANK_FAULT },
|
|
{ "NoHandler", VECTOR_NO_HANDLER },
|
|
{ "NoDevice", VECTOR_NO_DEVICE },
|
|
};
|
|
static const int reservedVectorCount = (int)(sizeof(reservedVectors) / sizeof(reservedVectors[0]));
|
|
|
|
static void vectorError(const char *message, intermediateElement *element) {
|
|
fprintf(stderr, RED "Error: %s\n" RESET, message);
|
|
printf("File: %s at line %d.\n", element->fileName, element->lineNumber);
|
|
printf("Token: %s\n", element->token);
|
|
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++) {
|
|
if (intermediateArray[i].destination == VECTORS && intermediateArray[i].type != KEYWORD) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// 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,
|
|
const char *handlerName) {
|
|
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
|
|
&& !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);
|
|
}
|
|
if (name && vectorArray[i].name && strcmp(vectorArray[i].name, name) == 0) {
|
|
fprintf(stderr, RED "Error: Vector \"%s\" is named more than once.\n" RESET, name);
|
|
printf("File: %s at line %d.\n", element->fileName, element->lineNumber);
|
|
exit(1);
|
|
}
|
|
}
|
|
vectorArray[vectorArrayCount].name = name ? strdup(name) : NULL;
|
|
vectorArray[vectorArrayCount].index = index;
|
|
vectorArray[vectorArrayCount].base = base;
|
|
vectorArray[vectorArrayCount].handler = handler;
|
|
vectorArray[vectorArrayCount].declaredOnly = declaredOnly;
|
|
vectorArray[vectorArrayCount].handlerName = handlerName ? strdup(handlerName) : NULL;
|
|
vectorArray[vectorArrayCount].fileName = element->fileName;
|
|
vectorArray[vectorArrayCount].lineNumber = element->lineNumber;
|
|
vectorArrayCount++;
|
|
}
|
|
|
|
// Resolves the handler named by the token at the given index.
|
|
static uint16_t resolveHandler(intermediateElement *intermediateArray, int at, const char *what) {
|
|
if (at < 0) {
|
|
fprintf(stderr, RED "Error: %s is not followed by a handler to go to.\n" RESET, what);
|
|
exit(1);
|
|
}
|
|
if (intermediateArray[at].type != LABEL) {
|
|
vectorError("A vector's handler has to be named by a label.", &intermediateArray[at]);
|
|
}
|
|
int address = findLabelAddress(intermediateArray[at].token);
|
|
if (address == -1) {
|
|
vectorError("That handler does not exist.", &intermediateArray[at]);
|
|
}
|
|
return (uint16_t)address;
|
|
}
|
|
|
|
// Is anything already using this software vector number?
|
|
static int vectorNumberTaken(uint8_t index) {
|
|
for (int i = 0; i < vectorArrayCount; i++) {
|
|
if (vectorArray[i].base == SOFTWARE_VECTOR_BASE && vectorArray[i].index == index) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void populateVectorTable(intermediateElement *intermediateArray, int arraySize) {
|
|
// Software vectors a program names for itself and does not pin are numbered in the
|
|
// order they are written, from the automatic range. A programmer never types one of
|
|
// these, so there is no way to land on a reserved vector or on somebody else's
|
|
// agreed number by accident.
|
|
int nextFreeVector = VECTOR_FIRST_AUTO;
|
|
|
|
int i = nextVectorToken(intermediateArray, arraySize, 0);
|
|
while (i >= 0) {
|
|
char *token = intermediateArray[i].token;
|
|
|
|
if (sameWord(token, "Device")) {
|
|
// A device is named by the port it is plugged into, because that is what
|
|
// decides which vector it arrives through. There is nothing to allocate.
|
|
int portToken = nextVectorToken(intermediateArray, arraySize, i + 1);
|
|
if (portToken < 0 || intermediateArray[portToken].type != VALUE) {
|
|
vectorError("Device has to say which port, as a number.", &intermediateArray[i]);
|
|
}
|
|
int handlerToken = nextVectorToken(intermediateArray, arraySize, portToken + 1);
|
|
uint16_t handler = resolveHandler(intermediateArray, handlerToken, "Device");
|
|
addVector(NULL, intermediateArray[portToken].byteValue, HARDWARE_VECTOR_BASE,
|
|
handler, 0, &intermediateArray[i],
|
|
intermediateArray[handlerToken].token);
|
|
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
|
continue;
|
|
}
|
|
|
|
// A number on the same line, between the name and any handler, PINS the vector.
|
|
// Nothing else can appear there: a label may not begin with a digit, so a value in
|
|
// this position is a number and can be nothing else.
|
|
int after = nextVectorToken(intermediateArray, arraySize, i + 1);
|
|
int pinned = -1;
|
|
if (sameLine(intermediateArray, i, after) && intermediateArray[after].type == VALUE) {
|
|
pinned = intermediateArray[after].byteValue;
|
|
after = nextVectorToken(intermediateArray, arraySize, after + 1);
|
|
}
|
|
int handlerToken = after;
|
|
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.
|
|
//
|
|
// A number here has to be the one it was declared with. Saying it again is
|
|
// allowed, because an implementer repeating what it is implementing is
|
|
// harmless; saying a different one means the two sides disagree about which
|
|
// vector this is, which is the whole thing pinning exists to prevent.
|
|
if (pinned >= 0 && pinned != vectorArray[already].index) {
|
|
vectorError("That vector was already given a different number.",
|
|
&intermediateArray[i]);
|
|
}
|
|
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;
|
|
// The declaration said what it is called; this says where it was implemented,
|
|
// which is the more useful of the two places to be sent.
|
|
free(vectorArray[already].handlerName);
|
|
vectorArray[already].handlerName = strdup(intermediateArray[handlerToken].token);
|
|
vectorArray[already].fileName = intermediateArray[i].fileName;
|
|
vectorArray[already].lineNumber = intermediateArray[i].lineNumber;
|
|
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
|
continue;
|
|
}
|
|
|
|
uint8_t index;
|
|
int reserved = 0;
|
|
for (int r = 0; r < reservedVectorCount; r++) {
|
|
if (sameWord(token, reservedVectors[r].name)) {
|
|
index = reservedVectors[r].index;
|
|
reserved = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (reserved && pinned >= 0) {
|
|
// Boot, SoftReset and BadOpcode are where the machine looks, not where a
|
|
// program says to look, so their numbers are not anybody's to choose.
|
|
vectorError("That vector's number is fixed by the machine and cannot be given.",
|
|
&intermediateArray[i]);
|
|
}
|
|
if (!reserved && pinned >= 0) {
|
|
if (pinned < VECTOR_FIRST_PINNED || pinned >= VECTOR_FIRST_AUTO) {
|
|
fprintf(stderr, RED "Error: %d is not a vector number that can be given.\n"
|
|
" Numbers below %d belong to the machine, and %d and up are\n"
|
|
" handed out by the assembler. A vector two programs have to\n"
|
|
" agree about takes one from %d to %d.\n" RESET,
|
|
pinned, VECTOR_FIRST_PINNED, VECTOR_FIRST_AUTO,
|
|
VECTOR_FIRST_PINNED, VECTOR_FIRST_AUTO - 1);
|
|
printf("File: %s at line %d.\n",
|
|
intermediateArray[i].fileName, intermediateArray[i].lineNumber);
|
|
exit(1);
|
|
}
|
|
if (vectorNumberTaken((uint8_t)pinned)) {
|
|
vectorError("Another vector already has that number.", &intermediateArray[i]);
|
|
}
|
|
index = (uint8_t)pinned;
|
|
} else if (!reserved) {
|
|
if (nextFreeVector > 255) {
|
|
vectorError("There are no software vectors left to give this one.", &intermediateArray[i]);
|
|
}
|
|
index = (uint8_t)nextFreeVector;
|
|
nextFreeVector++;
|
|
}
|
|
|
|
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], NULL);
|
|
i = handlerToken;
|
|
continue;
|
|
}
|
|
uint16_t handler = resolveHandler(intermediateArray, handlerToken, token);
|
|
addVector(token, index, SOFTWARE_VECTOR_BASE, handler, 0, &intermediateArray[i],
|
|
intermediateArray[handlerToken].token);
|
|
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
|
|
}
|
|
}
|
|
|
|
void fillInVectorReferences(intermediateElement *intermediateArray, int arraySize) {
|
|
for (int i = 0; i < arraySize; i++) {
|
|
if (intermediateArray[i].type != VECTOR_REFERENCE) {
|
|
continue;
|
|
}
|
|
int found = 0;
|
|
for (int v = 0; v < vectorArrayCount; v++) {
|
|
if (vectorArray[v].name && strcmp(vectorArray[v].name, intermediateArray[i].token) == 0) {
|
|
if (vectorArray[v].base != SOFTWARE_VECTOR_BASE) {
|
|
vectorError("SWI can only reach a software vector.", &intermediateArray[i]);
|
|
}
|
|
intermediateArray[i].byteValue = vectorArray[v].index;
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
fprintf(stderr, RED "Error: \"%s\" is not a vector.\n Names used with SWI have to be given a handler in a #Vectors section.\n" RESET,
|
|
intermediateArray[i].token);
|
|
printf("File: %s at line %d.\n", intermediateArray[i].fileName, intermediateArray[i].lineNumber);
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
int findLabelAddress(const char *labelName) {
|
|
for (int i = 0; i < labelCount; i++) {
|
|
if (strcmp(labelArray[i].label, labelName) == 0) {
|
|
return labelArray[i].address;
|
|
}
|
|
}
|
|
return -1; // Label not found
|
|
}
|
|
|
|
void fillInLabelAddresses(intermediateElement *intermediateArray, int arraySize) {
|
|
for (int i = 0; i < arraySize; i++) {
|
|
// The Vector Segment is resolved separately. Most of what it holds is not a
|
|
// label at all: the words that name a vector are the segment's own, and looking
|
|
// them up here would report them as undefined.
|
|
if (intermediateArray[i].destination == VECTORS) {
|
|
continue;
|
|
}
|
|
if (intermediateArray[i].type == LABEL) {
|
|
// Look up the label in the label table
|
|
int address = findLabelAddress(intermediateArray[i].token);
|
|
if (address == -1) {
|
|
fprintf(stderr, RED "Error: Undefined label \"%s\".\n" RESET, intermediateArray[i].token);
|
|
printf("File: %s at line %d.\n", intermediateArray[i].fileName, intermediateArray[i].lineNumber);
|
|
exit(1);
|
|
}
|
|
// Assign the found address to the element
|
|
intermediateArray[i].address = address;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reports the type of the token following index i, or UNKNOWN if there isn't one.
|
|
// The operand checks go through this so that an instruction sitting at the very end of
|
|
// a program is reported as a missing operand instead of reading off the end of the array.
|
|
static int nextTokenType(intermediateElement *intermediateArray, int arraySize, int i) {
|
|
if (i + 1 >= arraySize) {
|
|
return UNKNOWN;
|
|
}
|
|
return intermediateArray[i + 1].type;
|
|
}
|
|
|
|
// Every instruction that reads operand bytes out of Program Memory needs those bytes to
|
|
// actually be there. If they aren't, the following instruction gets eaten as an operand
|
|
// and everything after it shifts, so these all have to be hard errors.
|
|
static void checkOperands(intermediateElement *intermediateArray, int arraySize, int i) {
|
|
uint8_t opcode = intermediateArray[i].byteValue;
|
|
int nextType = nextTokenType(intermediateArray, arraySize, i);
|
|
const char *problem = NULL;
|
|
|
|
// Listed rather than matched on the high nibble, because not every instruction in
|
|
// these two blocks takes an address: RET has none, and BRD gets its destination from
|
|
// a Data Pointer instead of from the program.
|
|
if (opcode == 0x60 || opcode == 0x61 || opcode == 0x62 ||
|
|
opcode == 0x63 || opcode == 0x64 || opcode == 0x71 ||
|
|
opcode == 0x66 || opcode == 0x67 || opcode == 0x68 || opcode == 0x69) {
|
|
// Branches and CALL take a two byte address, which only a label can supply.
|
|
if (nextType != LABEL) problem = "Branch without label.";
|
|
} else if ((opcode & 0xF0) == 0xD0 || (opcode & 0xF0) == 0xE0) {
|
|
// The instruction is either an input or output and must be followed by a value.
|
|
if (nextType != VALUE) problem = "I/O without destination port.";
|
|
} else if (opcode == 0x72) {
|
|
// SWI names a vector, either by the name it was given in the Vector Segment or,
|
|
// rarely, as a literal number. Without one it swallows whatever follows it and
|
|
// every address after that shifts.
|
|
if (nextType != VECTOR_REFERENCE && nextType != VALUE) problem = "SWI without a vector to go to.";
|
|
} else if (opcode == 0x26 || opcode == 0x27) {
|
|
// INIA and INIB must be followed by the literal value to load.
|
|
if (nextType != VALUE) problem = "Immediate load without a value to load.";
|
|
} else if (opcode == 0x48 || opcode == 0x49) {
|
|
// DPUP and DPDN must be followed by the literal offset to apply.
|
|
if (nextType != VALUE) problem = "Data Pointer offset without an offset value.";
|
|
} else if (opcode == 0x47) {
|
|
// SETD takes a two byte address, as either a label or a pair of literal bytes.
|
|
if (nextType == VALUE) {
|
|
if (nextTokenType(intermediateArray, arraySize, i + 1) != VALUE) {
|
|
problem = "SETD given one literal byte, but an address is two bytes.";
|
|
}
|
|
} else if (nextType != LABEL) {
|
|
problem = "SETD without an address.";
|
|
}
|
|
}
|
|
|
|
if (problem) {
|
|
fprintf(stderr, RED "Error: %s\n" RESET, problem);
|
|
printf("File: %s at line %d.\n", intermediateArray[i].fileName, intermediateArray[i].lineNumber);
|
|
printf("Token: %s\n", intermediateArray[i].token);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize, uint8_t *Program, int *programCount, uint8_t *Data, int *dataCount) {
|
|
for (int i = 0; i < arraySize; i++) {
|
|
if (intermediateArray[i].destination == PROGRAM) {
|
|
switch (intermediateArray[i].type) {
|
|
case INSTRUCTION:
|
|
// Add instruction byte to Program buffer.
|
|
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.
|
|
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);
|
|
break;
|
|
case VALUE:
|
|
// Add literal value to Program buffer.
|
|
Program[(*programCount)++] = intermediateArray[i].byteValue;
|
|
break;
|
|
case VECTOR_REFERENCE:
|
|
// A vector is a number rather than a place, so this is one byte
|
|
// where a label would be two.
|
|
Program[(*programCount)++] = intermediateArray[i].byteValue;
|
|
break;
|
|
case PADDING:
|
|
case ALIGNMENT:
|
|
for (int z = 0; z < intermediateArray[i].byteLength; z++) {
|
|
Program[(*programCount)++] = 0x00;
|
|
}
|
|
break;
|
|
case LABEL:
|
|
// Split 16-bit label address into high and low bytes.
|
|
Program[(*programCount)++] = (intermediateArray[i].address >> 8) & 0xFF; // High byte
|
|
Program[(*programCount)++] = intermediateArray[i].address & 0xFF; // Low byte
|
|
break;
|
|
}
|
|
} else if (intermediateArray[i].destination == DATA) {
|
|
switch (intermediateArray[i].type) {
|
|
case VALUE:
|
|
// Add literal value to Data buffer.
|
|
Data[(*dataCount)++] = intermediateArray[i].byteValue;
|
|
break;
|
|
case STRING:
|
|
// Copy string literal to Data buffer, including null terminator.
|
|
for (int j = 0; intermediateArray[i].token[j] != '\0'; j++) {
|
|
Data[(*dataCount)++] = intermediateArray[i].token[j];
|
|
}
|
|
Data[(*dataCount)++] = '\0'; // Add null terminator to Data buffer
|
|
break;
|
|
case LABEL:
|
|
// 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;
|
|
case PADDING:
|
|
case ALIGNMENT:
|
|
for (int z = 0; z < intermediateArray[i].byteLength; z++) {
|
|
Data[(*dataCount)++] = 0x00;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
// Boot says where a program begins, which is what the entry field holds, so in a
|
|
// loadable program that line fills it in. It is NOT installed as vector 0: that entry
|
|
// is where the whole machine starts, and a program being loaded into a running system
|
|
// has no business saying anything about that.
|
|
//
|
|
// Without a Boot line the entry is the first byte of the code, which is where a
|
|
// program begins if it does not say otherwise.
|
|
uint16_t entry = codeBase;
|
|
int installed = 0;
|
|
for (int i = 0; i < vectorArrayCount; i++) {
|
|
if (vectorArray[i].declaredOnly) {
|
|
continue;
|
|
}
|
|
if (vectorArray[i].base == SOFTWARE_VECTOR_BASE
|
|
&& vectorArray[i].index == VECTOR_BOOT) {
|
|
entry = vectorArray[i].handler;
|
|
continue;
|
|
}
|
|
installed++;
|
|
}
|
|
if (installed > 255) {
|
|
fprintf(stderr, RED "Error: A loadable program may bring at most 255 vectors.\n" RESET);
|
|
exit(1);
|
|
}
|
|
|
|
uint8_t header[SBEX_HEADER_BYTES];
|
|
memset(header, 0, sizeof(header));
|
|
memcpy(header, SBEX_MAGIC, SBEX_MAGIC_BYTES);
|
|
// A program that brings vectors needs something of its loader that a version one
|
|
// loader does not know how to give, so it says so, and an older one refuses it rather
|
|
// than running it without them.
|
|
header[SBEX_VERSION_AT] = installed > 0 ? SBEX_VERSION_VECTORS : SBEX_VERSION;
|
|
header[SBEX_VECTORS_AT] = (uint8_t)installed;
|
|
header[SBEX_CODE_AT] = (uint8_t)(codeBase >> 8);
|
|
header[SBEX_CODE_AT + 1] = (uint8_t)(codeBase & 0xFF);
|
|
header[SBEX_ENTRY_AT] = (uint8_t)(entry >> 8);
|
|
header[SBEX_ENTRY_AT + 1] = (uint8_t)(entry & 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);
|
|
|
|
// The vectors, last, so that everything before them sits where a version one loader
|
|
// already expects to find it.
|
|
for (int i = 0; i < vectorArrayCount; i++) {
|
|
if (vectorArray[i].declaredOnly) {
|
|
continue;
|
|
}
|
|
if (vectorArray[i].base == SOFTWARE_VECTOR_BASE
|
|
&& vectorArray[i].index == VECTOR_BOOT) {
|
|
continue;
|
|
}
|
|
uint16_t slot = vectorArray[i].base + (uint16_t)vectorArray[i].index * VECTOR_ENTRY_BYTES;
|
|
fputc((slot >> 8) & 0xFF, outputFile);
|
|
fputc(slot & 0xFF, outputFile);
|
|
fputc((vectorArray[i].handler >> 8) & 0xFF, outputFile);
|
|
fputc(vectorArray[i].handler & 0xFF, 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" RESET,
|
|
codeLength, codeBase, dataLength, dataBase);
|
|
if (entry != codeBase) {
|
|
printf(GREEN " Starts at 0x%04X.\n" RESET, entry);
|
|
}
|
|
if (installed > 0) {
|
|
printf(GREEN " Vectors: %d. Version 2, so a loader that cannot install them will say so.\n" RESET,
|
|
installed);
|
|
}
|
|
printf(GREEN " Total size: %d bytes.\n" RESET,
|
|
SBEX_HEADER_BYTES + codeLength + dataLength + installed * SBEX_VECTOR_ENTRY_BYTES);
|
|
}
|
|
|
|
void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCount, uint8_t *Data, int dataCount) {
|
|
FILE *outputFile = fopen(outputFileName, "wb");
|
|
if (!outputFile) {
|
|
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, outputFileName);
|
|
exit(1);
|
|
}
|
|
|
|
// Write the file header: the magic, the format version, and the features this
|
|
// boot image 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), SEGMENT_MARKER_LENGTH, outputFile);
|
|
|
|
// Write the program segment length as a 2-byte value (big-endian)
|
|
uint16_t programSize = programCount;
|
|
fputc((programSize >> 8) & 0xFF, outputFile); // High byte
|
|
fputc(programSize & 0xFF, outputFile); // Low byte
|
|
|
|
// Write the Program buffer to the file
|
|
if (fwrite(Program, sizeof(uint8_t), programCount, outputFile) != (size_t)programCount) {
|
|
fprintf(stderr, RED "Error: Failed to write Program data to file \"%s\".\n" RESET, outputFileName);
|
|
fclose(outputFile);
|
|
exit(1);
|
|
}
|
|
|
|
// Write the "DAT" header for the data segment
|
|
fwrite("DAT", sizeof(char), SEGMENT_MARKER_LENGTH, outputFile);
|
|
|
|
// Write the data segment length as a 2-byte value (big-endian)
|
|
uint16_t dataSize = dataCount;
|
|
fputc((dataSize >> 8) & 0xFF, outputFile); // High byte
|
|
fputc(dataSize & 0xFF, outputFile); // Low byte
|
|
|
|
// Write the Data buffer to the file
|
|
if (fwrite(Data, sizeof(uint8_t), dataCount, outputFile) != (size_t)dataCount) {
|
|
fprintf(stderr, RED "Error: Failed to write Data data to file \"%s\".\n" RESET, outputFileName);
|
|
fclose(outputFile);
|
|
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 an image 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 (installed > 0) {
|
|
fwrite("VEC", sizeof(char), SEGMENT_MARKER_LENGTH, outputFile);
|
|
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);
|
|
fputc((vectorArray[i].handler >> 8) & 0xFF, outputFile);
|
|
fputc(vectorArray[i].handler & 0xFF, outputFile);
|
|
}
|
|
}
|
|
|
|
fclose(outputFile);
|
|
printf("Successfully wrote SplitBit boot image to \"%s\".\n", outputFileName);
|
|
printf(GREEN " Program Segment size: %d bytes.\n Data Segment size: %d bytes.\n" RESET, programCount, dataCount);
|
|
if (installed > 0) {
|
|
printf(GREEN " Vectors: %d.\n" RESET, installed);
|
|
}
|
|
printf(GREEN " Total size: %d bytes.\n" RESET,
|
|
(programCount + dataCount + SPLITBIT_HEADER_BYTES
|
|
+ (installed > 0 ? SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES + vectorBytes : 0)));
|
|
}
|