Make RGBLINK able to link SDCC object files

This requires a LOT of tricky code, mostly due to the format itself being,
er, not the most straightforward.
Everything is converted to existing RGBLINK concepts (sections, patches,
etc.), so the core code is essentially unchanged.
(A couple of genuine RGBLINK bugs were uncovered along the way, so some of
the core code *is* changed, notably regarding `SECTION FRAGMENT`s.)

All of this code was clean-roomed, so SDCC's GPLv2 license does not apply.
This commit is contained in:
ISSOtm
2022-07-10 10:50:33 +02:00
committed by Eldred Habert
parent 1c2965467d
commit 828b2adcdf
16 changed files with 922 additions and 71 deletions

759
src/link/sdas_obj.c Normal file
View File

@@ -0,0 +1,759 @@
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "linkdefs.h"
#include "platform.h"
#include "link/assign.h"
#include "link/main.h"
#include "link/sdas_obj.h"
#include "link/section.h"
#include "link/symbol.h"
enum NumberType {
HEX = 16, // X
DEC = 10, // D
OCT = 8, // Q
};
static void consumeLF(struct FileStackNode const *where, uint32_t lineNo, FILE *file) {
if (getc(file) != '\n')
fatal(where, lineNo, "Bad line ending (CR without LF)");
}
static char const *delim = " \f\n\r\t\v"; // Whitespace according to the C and POSIX locales
static int nextLine(char **restrict lineBuf, size_t *restrict bufLen, uint32_t *restrict lineNo, struct FileStackNode const *where, FILE *file) {
retry:
++*lineNo;
int firstChar = getc(file);
switch (firstChar) {
case EOF:
return EOF;
case ';':
// Discard comment line
// TODO: if `;!FILE [...]` on the first line (`lineNo`), return it
do {
firstChar = getc(file);
} while (firstChar != EOF && firstChar != '\r' && firstChar != '\n');
// fallthrough
case '\r':
if (firstChar == '\r' && getc(file) != '\n')
consumeLF(where, *lineNo, file);
// fallthrough
case '\n':
goto retry;
}
size_t i = 0;
for (;;) {
if (i >= *bufLen) {
assert(*bufLen != 0);
*bufLen *= 2;
*lineBuf = realloc(*lineBuf, *bufLen);
if (!*lineBuf)
fatal(where, *lineNo, "Failed to realloc: %s", strerror(errno));
}
int c = getc(file);
switch (c) {
case '\r':
consumeLF(where, *lineNo, file);
// fallthrough
case '\n':
case EOF:
(*lineBuf)[i] = '\0'; // Terminate the string (space was ensured above)
return firstChar;
}
(*lineBuf)[i] = c;
++i;
}
}
static uint32_t readNumber(char const *restrict str, char const **endptr, enum NumberType base) {
uint32_t res = 0;
for (;;) {
static char const *digits = "0123456789ABCDEF";
char const *ptr = strchr(digits, toupper(*str));
if (!ptr || ptr - digits >= base) {
*endptr = str;
return res;
}
++str;
res = res * base + (ptr - digits);
}
}
static uint32_t parseNumber(struct FileStackNode const *where, uint32_t lineNo, char const *restrict str, enum NumberType base) {
if (str[0] == '\0')
fatal(where, lineNo, "Expected number, got empty string");
char const *endptr;
uint32_t res = readNumber(str, &endptr, base);
if (*endptr != '\0')
fatal(where, lineNo, "Expected number, got \"%s\"", str);
return res;
}
static uint8_t parseByte(struct FileStackNode const *where, uint32_t lineNo, char const *restrict str, enum NumberType base) {
uint32_t num = parseNumber(where, lineNo, str, base);
if (num > UINT8_MAX)
fatal(where, lineNo, "\"%s\" is not a byte", str);
return num;
}
enum AreaFlags {
AREA_TYPE = 2, // 0: Concatenate, 1: overlay
AREA_ISABS, // 0: Relative (???) address, 1: absolute address
AREA_PAGING, // Unsupported
AREA_ALL_FLAGS = 1 << AREA_TYPE | 1 << AREA_ISABS | 1 << AREA_PAGING,
};
enum RelocFlags {
RELOC_SIZE, // 0: 16-bit, 1: 8-bit
RELOC_ISSYM, // 0: Area, 1: Symbol
RELOC_ISPCREL, // 0: Normal, 1: PC-relative
RELOC_EXPR16, // Only for 8-bit size; 0: 8-bit expr, 1: 16-bit expr
RELOC_SIGNED, // 0: signed, 1: unsigned
RELOC_ZPAGE, // Unsupported
RELOC_NPAGE, // Unsupported
RELOC_WHICHBYTE, // 8-bit size with 16-bit expr only; 0: LOW(), 1: HIGH()
RELOC_EXPR24, // Only for 8-bit size; 0: follow RELOC_EXPR16, 1: 24-bit expr
RELOC_BANKBYTE, // 8-bit size with 24-bit expr only; 0: follow RELOC_WHICHBYTE, 1: BANK()
RELOC_ALL_FLAGS = 1 << RELOC_SIZE | 1 << RELOC_ISSYM | 1 << RELOC_ISPCREL | 1 << RELOC_EXPR16
| 1 << RELOC_SIGNED | 1 << RELOC_ZPAGE | 1 << RELOC_NPAGE | 1 << RELOC_WHICHBYTE
| 1 << RELOC_EXPR24 | 1 << RELOC_BANKBYTE,
};
void sdobj_ReadFile(struct FileStackNode const *where, FILE *file) {
size_t bufLen = 256;
char *line = malloc(bufLen);
char const *token;
#define getToken(ptr, ...) do { \
token = strtok((ptr), delim); \
if (!token) \
fatal(where, lineNo, __VA_ARGS__); \
} while (0)
#define expectEol(...) do { \
token = strtok(NULL, delim); \
if (token) \
fatal(where, lineNo, __VA_ARGS__); \
} while (0)
#define expectToken(expected, lineType) do { \
getToken(NULL, "'%c' line is too short", (lineType)); \
if (strcasecmp(token, (expected)) != 0) \
fatal(where, lineNo, "Malformed '%c' line: expected \"%s\", got \"%s\"", (lineType), (expected), token); \
} while (0)
if (!line)
fatal(where, 0, "Failed to alloc a line buffer: %s", strerror(errno));
uint32_t lineNo = 0;
int lineType = nextLine(&line, &bufLen, &lineNo, where, file);
enum NumberType numberType;
// The first letter (thus, the line type) identifies the integer type
switch (lineType) {
case EOF:
fatal(where, lineNo, "SDCC object only contains comments and empty lines");
case 'X':
numberType = HEX;
break;
case 'D':
numberType = DEC;
break;
case 'Q':
numberType = OCT;
break;
default:
fatal(where, lineNo, "This does not look like a SDCC object file (unknown integer format '%c')", lineType);
}
switch (line[0]) {
case 'L':
break;
case 'H':
fatal(where, lineNo, "Big-endian SDCC object files are not supported");
default:
fatal(where, lineNo, "Unknown endianness type '%c'", line[0]);
}
#define ADDR_SIZE 3
if (line[1] != '0' + ADDR_SIZE)
fatal(where, lineNo, "Unknown or unsupported address size '%c'", line[1]);
if (line[2] != '\0')
warning(where, lineNo, "Ignoring unknown characters (\"%s\") in first line", &line[2]);
// Header line
lineType = nextLine(&line, &bufLen, &lineNo, where, file);
if (lineType != 'H')
fatal(where, lineNo, "Expected header line, got '%c' line", lineType);
// Expected format: "A areas S global symbols"
getToken(line, "Empty 'H' line");
uint32_t expectedNbAreas = parseNumber(where, lineNo, token, numberType);
expectToken("areas", 'H');
getToken(NULL, "'H' line is too short");
uint32_t expectedNbSymbols = parseNumber(where, lineNo, token, numberType);
expectToken("global", 'H');
expectToken("symbols", 'H');
expectEol("'H' line is too long");
// Now, let's parse the rest of the lines as they come!
struct {
struct Section *section;
uint16_t writeIndex;
} *fileSections = NULL;
struct Symbol **fileSymbols = malloc(sizeof(*fileSymbols) * expectedNbSymbols);
size_t nbSections = 0, nbSymbols = 0;
if (!fileSymbols)
fatal(where, lineNo, "Failed to alloc file symbols table: %s", strerror(errno));
size_t nbBytes = 0; // How many bytes are in `data`, including the ADDR_SIZE "header" bytes
size_t dataCapacity = 16 + ADDR_SIZE; // SDCC object files usually contain 16 bytes per T line
uint8_t *data = malloc(sizeof(*data) * dataCapacity);
if (!data)
fatal(where, lineNo, "Failed to alloc data buffer: %s", strerror(errno));
for (;;) {
lineType = nextLine(&line, &bufLen, &lineNo, where, file);
if (lineType == EOF)
break;
switch (lineType) {
uint32_t tmp;
case 'M': // Module name
case 'O': // Assembler flags
// Ignored
break;
case 'A':
if (nbSections == expectedNbAreas)
warning(where, lineNo, "Got more 'A' lines than the expected %" PRIu32, expectedNbAreas);
fileSections = realloc(fileSections, sizeof(*fileSections) * (nbSections + 1));
if (!fileSections)
fatal(where, lineNo, "Failed to realloc file areas: %s", strerror(errno));
fileSections[nbSections].writeIndex = 0;
#define curSection (fileSections[nbSections].section)
curSection = malloc(sizeof(*curSection));
if (!curSection)
fatal(where, lineNo, "Failed to alloc new area: %s", strerror(errno));
getToken(line, "'A' line is too short");
assert(strlen(token) != 0); // This should be impossible, tokens are non-empty
curSection->name = strdup(token); // We need a pointer that will live longer
if (!curSection->name)
fatal(where, lineNo, "Failed to alloc new area's name: %s", strerror(errno));
// The following is required for fragment offsets to be reliably predicted
for (size_t i = 0; i < nbSections; ++i) {
if (!strcmp(token, fileSections[i].section->name))
fatal(where, lineNo, "Area \"%s\" already defined earlier", token);
}
expectToken("size", 'A');
getToken(NULL, "'A' line is too short");
tmp = parseNumber(where, lineNo, token, numberType);
if (tmp > UINT16_MAX)
fatal(where, lineNo, "Area \"%s\" is larger than the GB address space!?", curSection->name);
curSection->size = tmp;
expectToken("flags", 'A');
getToken(NULL, "'A' line is too short");
tmp = parseNumber(where, lineNo, token, numberType);
if (tmp & (1 << AREA_PAGING))
fatal(where, lineNo, "Internal error: paging is not supported");
curSection->isAddressFixed = tmp & (1 << AREA_ISABS);
curSection->isBankFixed = curSection->isAddressFixed;
curSection->modifier = curSection->isAddressFixed || (tmp & (1 << AREA_TYPE))
? SECTION_NORMAL : SECTION_FRAGMENT;
expectToken("addr", 'A');
getToken(NULL, "'A' line is too short");
tmp = parseNumber(where, lineNo, token, numberType);
curSection->org = tmp; // Truncation keeps the address portion only
curSection->bank = tmp >> 16;
expectEol("'A' line is too long");
// Init the rest of the members
curSection->offset = 0;
if (curSection->isAddressFixed) {
uint8_t high = curSection->org >> 8;
if (high < 0x40) {
curSection->type = SECTTYPE_ROM0;
} else if (high < 0x80) {
curSection->type = SECTTYPE_ROMX;
} else if (high < 0xA0) {
curSection->type = SECTTYPE_VRAM;
} else if (high < 0xC0) {
curSection->type = SECTTYPE_SRAM;
} else if (high < 0xD0) {
curSection->type = SECTTYPE_WRAM0;
} else if (high < 0xE0) {
curSection->type = SECTTYPE_WRAMX;
} else if (high < 0xFE) {
fatal(where, lineNo, "Areas in echo RAM are not supported");
} else if (high < 0xFF) {
curSection->type = SECTTYPE_OAM;
} else {
curSection->type = SECTTYPE_HRAM;
}
} else {
curSection->type = SECTTYPE_INVALID; // This means "indeterminate"
}
curSection->isAlignFixed = false; // No such concept!
// The array will be allocated if the section does contain data
curSection->data = NULL;
curSection->nbPatches = 0;
curSection->patches = NULL; // Same as `data`
curSection->fileSymbols = fileSymbols; // IDs are instead per-section
curSection->nbSymbols = 0;
curSection->symbols = NULL; // Will be allocated on demand as well
curSection->nextu = NULL;
#undef curSection
++nbSections;
break;
case 'S':
if (nbSymbols == expectedNbSymbols)
warning(where, lineNo, "Got more 'S' lines than the expected %" PRIu32, expectedNbSymbols);
// `realloc` is dangerous, as sections contain a pointer to `fileSymbols`.
// We can try to be nice, but if the pointer moves, it's game over!
if (nbSymbols >= expectedNbSymbols) {
struct Symbol **newFileSymbols = realloc(fileSymbols, sizeof(*fileSymbols) * (nbSymbols + 1));
if (!newFileSymbols)
fatal(where, lineNo, "Failed to alloc extra symbols: %s", strerror(errno));
if (newFileSymbols != fileSymbols)
fatal(where, lineNo, "Couldn't handle extra 'S' lines (pointer moved)");
// No need to assign, obviously
}
#define symbol (fileSymbols[nbSymbols])
symbol = malloc(sizeof(*symbol));
if (!symbol)
fatal(where, lineNo, "Failed to alloc symbol: %s", strerror(errno));
// Init other members
symbol->objFileName = where->name;
symbol->src = where;
symbol->lineNo = lineNo;
// No need to set the `sectionID`, since we can directly set the pointer
symbol->section = fileSections ? fileSections[nbSections - 1].section : NULL;
getToken(line, "'S' line is too short");
symbol->name = strdup(token);
if (!symbol->name)
fatal(where, lineNo, "Failed to alloc symbol name: %s", strerror(errno));
getToken(NULL, "'S' line is too short");
// It might be an `offset`, but both types are the same so type punning is fine
symbol->value = parseNumber(where, lineNo, &token[3], numberType);
if (symbol->section && symbol->section->isAddressFixed) {
assert(symbol->offset >= symbol->section->org);
symbol->offset -= symbol->section->org;
assert(symbol->offset <= symbol->section->size);
}
// Expected format: /[DR]ef[0-9A-F]+/i
if (token[0] == 'R' || token[0] == 'r') {
symbol->type = SYMTYPE_IMPORT;
// TODO: hard error if the rest is not zero
} else if (token[0] != 'D' && token[0] != 'd') {
fatal(where, lineNo, "'S' line is neither \"Def\" nor \"Ref\"");
} else {
// All symbols are exported
symbol->type = SYMTYPE_EXPORT;
struct Symbol const *other = sym_GetSymbol(symbol->name);
if (other) {
// The same symbol can only be defined twice if neither
// definition is in a floating section
if ((other->section && !other->section->isAddressFixed)
|| (symbol->section && !symbol->section->isAddressFixed)) {
sym_AddSymbol(symbol); // This will error out
} else if (other->value != symbol->value) {
error(where, lineNo,
"Definition of \"%s\" conflicts with definition in %s (%" PRId32 " != %" PRId32 ")",
symbol->name, other->objFileName, symbol->value, other->value);
}
} else {
// Add a new definition
sym_AddSymbol(symbol);
}
// It's fine to keep modifying the symbol after `AddSymbol`, only
// the name must not be modified
}
if (strncasecmp(&token[1], "ef", 2) != 0)
fatal(where, lineNo, "'S' line is neither \"Def\" nor \"Ref\"");
if (nbSections != 0) {
struct Section *section = fileSections[nbSections - 1].section;
++section->nbSymbols;
section->symbols = realloc(section->symbols, sizeof(section->symbols[0]) * section->nbSymbols);
if (!section->symbols)
fatal(where, lineNo, "Failed to realloc \"%s\"'s symbol list: %s", section->name, strerror(errno));
section->symbols[section->nbSymbols - 1] = symbol;
}
#undef symbol
expectEol("'S' line is too long");
++nbSymbols;
break;
case 'T':
// Now, time to parse the data!
if (nbBytes != 0)
warning(where, lineNo, "Previous 'T' line had no 'R' line (ignored)");
nbBytes = 0;
for (token = strtok(line, delim); token; token = strtok(NULL, delim)) {
if (dataCapacity == nbBytes) {
dataCapacity *= 2;
data = realloc(data, sizeof(*data) * dataCapacity);
if (!data)
fatal(where, lineNo, "Failed to realloc data buffer: %s", strerror(errno));
}
data[nbBytes] = parseByte(where, lineNo, token, numberType);
++nbBytes;
}
if (nbBytes < ADDR_SIZE)
fatal(where, lineNo, "'T' line is too short");
// Importantly, now we know that `nbBytes != 0`, which means "pending data"
break;
case 'R': // Supposed to directly follow `T`
if (nbBytes == 0) {
warning(where, lineNo, "'R' line with no 'T' line, ignoring");
break;
}
// First two bytes are ignored
getToken(line, "'R' line is too short");
getToken(NULL, "'R' line is too short");
uint16_t areaIdx;
getToken(NULL, "'R' line is too short");
areaIdx = parseByte(where, lineNo, token, numberType);
getToken(NULL, "'R' line is too short");
areaIdx |= (uint16_t)parseByte(where, lineNo, token, numberType) << 8;
if (areaIdx >= nbSections)
fatal(where, lineNo, "'R' line references area #%" PRIu16 ", but there are only %zu (so far)", areaIdx, nbSections);
assert(fileSections); // There should be at least one, from the above check
struct Section *section = fileSections[areaIdx].section;
uint16_t *writeIndex = &fileSections[areaIdx].writeIndex;
uint8_t writtenOfs = ADDR_SIZE; // Bytes before this have been written to ->data
uint16_t addr = data[0] | data[1] << 8;
if (section->isAddressFixed) {
if (addr < section->org)
fatal(where, lineNo, "'T' line reports address $%04" PRIx16 " in \"%s\", which starts at $%04" PRIx16, addr, section->name, section->org);
addr -= section->org;
}
// Lines are emitted that violate this check but contain no "payload";
// ignore those. "Empty" lines shouldn't trigger allocation, either.
if (nbBytes != ADDR_SIZE) {
if (addr != *writeIndex)
fatal(where, lineNo, "'T' lines which don't append to their section are not supported (%" PRIu16 " != %" PRIu16 ")", addr, *writeIndex);
if (!section->data) {
assert(section->size != 0);
section->data = malloc(section->size);
if (!section->data)
fatal(where, lineNo, "Failed to alloc data for \"%s\": %s", section->name, strerror(errno));
}
}
// Processing relocations is made difficult by SDLD's honestly quite bonkers
// handling of the thing.
// The way they work is that 16-bit relocs are, simply enough, writing a
// 16-bit value over a 16-bit "gap". Nothing weird here.
// 8-bit relocs, however, do not write an 8-bit value over an 8-bit gap!
// They write an 8-bit value over a 16-bit gap... and either of the two
// bytes is *discarded*. The "24-bit" flag extends this behavior to three
// bytes instead of two, but the idea's the same.
// Additionally, the "offset" is relative to *before* bytes from previous
// relocs are removed, so this needs to be accounted for as well.
// This all can be "translated" to RGBDS parlance by generating the
// appropriate RPN expression (depending on flags), plus an addition for the
// bytes being patched over.
while ((token = strtok(NULL, delim)) != NULL) {
uint16_t flags = parseByte(where, lineNo, token, numberType);
if ((flags & 0xF0) == 0xF0) {
getToken(NULL, "Incomplete relocation");
flags = (flags & 0x0F) | (uint16_t)parseByte(where, lineNo, token, numberType) << 4;
}
getToken(NULL, "Incomplete relocation");
uint8_t offset = parseByte(where, lineNo, token, numberType);
if (offset < ADDR_SIZE)
fatal(where, lineNo, "Relocation index cannot point to header (%" PRIu16 " < %u)", offset, ADDR_SIZE);
if (offset >= nbBytes)
fatal(where, lineNo, "Relocation index is out of bounds (%" PRIu16 " >= %zu)", offset, nbBytes);
getToken(NULL, "Incomplete relocation");
uint16_t idx = parseByte(where, lineNo, token, numberType);
getToken(NULL, "Incomplete relocation");
idx |= (uint16_t)parseByte(where, lineNo, token, numberType);
// Loudly fail on unknown flags
if (flags & (1 << RELOC_ZPAGE | 1 << RELOC_NPAGE))
fatal(where, lineNo, "Paging flags are not supported");
if (flags & ~RELOC_ALL_FLAGS)
warning(where, lineNo, "Unknown reloc flags 0x%x", flags & ~RELOC_ALL_FLAGS);
// Turn this into a Patch
section->patches = realloc(section->patches, sizeof(section->patches[0]) * (section->nbPatches + 1));
if (!section->patches)
fatal(where, lineNo, "Failed to alloc extra patch for \"%s\"", section->name);
struct Patch *patch = &section->patches[section->nbPatches];
patch->lineNo = lineNo;
patch->src = where;
patch->offset = offset - writtenOfs + *writeIndex;
if (section->nbPatches != 0 && section->patches[section->nbPatches - 1].offset >= patch->offset)
fatal(where, lineNo, "Relocs not sorted by offset are not supported (%" PRIu32 " >= %" PRIu32 ")", section->patches[section->nbPatches - 1].offset, patch->offset);
patch->pcSection = section; // No need to fill `pcSectionID`, then
patch->pcOffset = patch->offset - 1; // For `jr`s
patch->type = flags & 1 << RELOC_SIZE ? PATCHTYPE_BYTE : PATCHTYPE_WORD;
uint8_t nbBaseBytes = patch->type == PATCHTYPE_BYTE ? ADDR_SIZE : 2;
uint32_t baseValue = 0;
assert(offset < nbBytes);
if (nbBytes - offset < nbBaseBytes)
fatal(where, lineNo, "Reloc would patch out of bounds (%" PRIu8 " > %zu)", nbBaseBytes, nbBytes - offset);
for (uint8_t i = 0; i < nbBaseBytes; ++i)
baseValue = baseValue | data[offset + i] << (8 * i);
// Extra size that must be reserved for additional operators
#define RPN_EXTRA_SIZE (5 + 1 + 5 + 1 + 5 + 1) // >> 8 & $FF, then + <baseValue>
#define allocPatch(size) do { \
patch->rpnSize = (size); \
patch->rpnExpression = malloc(patch->rpnSize + RPN_EXTRA_SIZE); \
if (!patch->rpnExpression) \
fatal(where, lineNo, "Failed to alloc RPN expression: %s", strerror(errno)); \
} while (0)
// Bit 4 specifies signedness, but I don't think that matters?
// Generate a RPN expression from the info and flags
if (flags & 1 << RELOC_ISSYM) {
if (idx >= nbSymbols)
fatal(where, lineNo, "Reloc refers to symbol #%" PRIu16 " out of %zu", idx, nbSymbols);
struct Symbol const *sym = fileSymbols[idx];
// SDCC has a bunch of "magic symbols" that start with a
// letter and an underscore. These are not compatibility
// hacks, this is how SDLD actually works.
if (sym->name[0] == 'b' && sym->name[1] == '_') {
// Look for the symbol being referenced, and use its index instead
for (idx = 0; idx < nbSymbols; ++idx) {
if (strcmp(&sym->name[1], fileSymbols[idx]->name) == 0)
break;
}
if (idx == nbSymbols)
fatal(where, lineNo, "\"%s\" is missing a reference to \"%s\"", sym->name, &sym->name[1]);
allocPatch(5);
patch->rpnExpression[0] = RPN_BANK_SYM;
patch->rpnExpression[1] = idx;
patch->rpnExpression[2] = idx >> 8;
patch->rpnExpression[3] = idx >> 16;
patch->rpnExpression[4] = idx >> 24;
} else if (sym->name[0] == 'l' && sym->name[1] == '_') {
allocPatch(1 + strlen(&sym->name[2]) + 1);
patch->rpnExpression[0] = RPN_SIZEOF_SECT;
strcpy((char *)&patch->rpnExpression[1], &sym->name[2]);
} else if (sym->name[0] == 's' && sym->name[1] == '_') {
allocPatch(1 + strlen(&sym->name[2]) + 1);
patch->rpnExpression[0] = RPN_STARTOF_SECT;
strcpy((char *)&patch->rpnExpression[1], &sym->name[2]);
} else {
allocPatch(5);
patch->rpnExpression[0] = RPN_SYM;
patch->rpnExpression[1] = idx;
patch->rpnExpression[2] = idx >> 8;
patch->rpnExpression[3] = idx >> 16;
patch->rpnExpression[4] = idx >> 24;
}
} else {
if (idx >= nbSections)
fatal(where, lineNo, "Reloc refers to area #%" PRIu16 " out of %zu", idx, nbSections);
// It gets funky. If the area is absolute, *actually*, we
// must not add its base address, as the assembler will
// already have added it in `baseValue`.
// We counteract this by subtracting the section's base
// address from `baseValue`, undoing what the assembler did;
// this allows the relocation to still be correct, even if
// the section gets moved for any reason.
if (fileSections[idx].section->isAddressFixed)
baseValue -= fileSections[idx].section->org;
char const *name = fileSections[idx].section->name;
struct Section const *other = sect_GetSection(name);
// Unlike with `s_<AREA>`, referencing an area in this way
// wants the beginning of this fragment, so we must add the
// fragment's (putative) offset to account for this.
// The fragment offset prediction is guaranteed since each
// section can only have one fragment per SDLD object file,
// so this fragment will be appended to the existing section
// *if any*, and thus its offset will be the section's
// current size.
if (other)
baseValue += other->size;
allocPatch(1 + strlen(name) + 1);
patch->rpnSize = 1 + strlen(name) + 1;
patch->rpnExpression = malloc(patch->rpnSize + RPN_EXTRA_SIZE);
if (!patch->rpnExpression)
fatal(where, lineNo, "Failed to alloc RPN expression: %s", strerror(errno));
patch->rpnExpression[0] = RPN_STARTOF_SECT;
// The cast is fine, it's just different signedness
strcpy((char *)&patch->rpnExpression[1], name);
}
#undef allocPatch
patch->rpnExpression[patch->rpnSize] = RPN_CONST;
patch->rpnExpression[patch->rpnSize + 1] = baseValue;
patch->rpnExpression[patch->rpnSize + 2] = baseValue >> 8;
patch->rpnExpression[patch->rpnSize + 3] = baseValue >> 16;
patch->rpnExpression[patch->rpnSize + 4] = baseValue >> 24;
patch->rpnExpression[patch->rpnSize + 5] = RPN_ADD;
patch->rpnSize += 5 + 1;
if (patch->type == PATCHTYPE_BYTE) {
// Despite the flag's name, as soon as it is set, 3 bytes
// are present, so we must skip two of them
if (flags & 1 << RELOC_EXPR16) {
if (*writeIndex + (offset - writtenOfs) > section->size)
fatal(where, lineNo, "'T' line writes past \"%s\"'s end (%u > %" PRIu16 ")", section->name, *writeIndex + (offset - writtenOfs), section->size);
// Copy all bytes up to those (plus the byte that we'll overwrite)
memcpy(&section->data[*writeIndex], &data[writtenOfs], offset - writtenOfs + 1);
*writeIndex += offset - writtenOfs + 1;
writtenOfs = offset + 3; // Skip all three `baseValue` bytes, though
}
// Append the necessary operations...
if (flags & 1 << RELOC_ISPCREL) {
// The result must *not* be truncated for those!
patch->type = PATCHTYPE_JR;
// TODO: check the other flags?
} else if (flags & 1 << RELOC_EXPR24 && flags & 1 << RELOC_BANKBYTE) {
patch->rpnExpression[patch->rpnSize] = RPN_CONST;
patch->rpnExpression[patch->rpnSize + 1] = 16;
patch->rpnExpression[patch->rpnSize + 2] = 16 >> 8;
patch->rpnExpression[patch->rpnSize + 3] = 16 >> 16;
patch->rpnExpression[patch->rpnSize + 4] = 16 >> 24;
patch->rpnExpression[patch->rpnSize + 5] = flags & 1 << RELOC_SIGNED ? RPN_SHR : RPN_USHR;
patch->rpnSize += 5 + 1;
} else {
if (flags & 1 << RELOC_EXPR16 && flags & 1 << RELOC_WHICHBYTE) {
patch->rpnExpression[patch->rpnSize] = RPN_CONST;
patch->rpnExpression[patch->rpnSize + 1] = 8;
patch->rpnExpression[patch->rpnSize + 2] = 8 >> 8;
patch->rpnExpression[patch->rpnSize + 3] = 8 >> 16;
patch->rpnExpression[patch->rpnSize + 4] = 8 >> 24;
patch->rpnExpression[patch->rpnSize + 5] = flags & 1 << RELOC_SIGNED ? RPN_SHR : RPN_USHR;
patch->rpnSize += 5 + 1;
}
patch->rpnExpression[patch->rpnSize] = RPN_CONST;
patch->rpnExpression[patch->rpnSize + 1] = 0xFF;
patch->rpnExpression[patch->rpnSize + 2] = 0xFF >> 8;
patch->rpnExpression[patch->rpnSize + 3] = 0xFF >> 16;
patch->rpnExpression[patch->rpnSize + 4] = 0xFF >> 24;
patch->rpnExpression[patch->rpnSize + 5] = RPN_AND;
patch->rpnSize += 5 + 1;
}
} else if (flags & 1 << RELOC_ISPCREL) {
assert(patch->type == PATCHTYPE_WORD);
fatal(where, lineNo, "16-bit PC-relative relocations are not supported");
} else if (flags & (1 << RELOC_EXPR16 | 1 << RELOC_EXPR24)) {
fatal(where, lineNo, "Flags 0x%x are not supported for 16-bit relocs", flags & (1 << RELOC_EXPR16 | 1 << RELOC_EXPR24));
}
++section->nbPatches;
}
// If there is some data left to append, do so
if (writtenOfs != nbBytes) {
assert(nbBytes > writtenOfs);
if (*writeIndex + (nbBytes - writtenOfs) > section->size)
fatal(where, lineNo, "'T' line writes past \"%s\"'s end (%zu > %" PRIu16 ")", section->name, *writeIndex + (nbBytes - writtenOfs), section->size);
memcpy(&section->data[*writeIndex], &data[writtenOfs], nbBytes - writtenOfs);
*writeIndex += nbBytes - writtenOfs;
}
nbBytes = 0; // Do not allow two R lines to refer to the same T line
break;
case 'P':
default:
warning(where, lineNo, "Unknown/unsupported line type '%c', ignoring", lineType);
break;
}
}
if (nbBytes != 0)
warning(where, lineNo, "Last 'T' line had no 'R' line (ignored)");
if (nbSections < expectedNbAreas)
warning(where, lineNo, "Expected %" PRIu32 " 'A' lines, got only %zu", expectedNbAreas, nbSections);
if (nbSymbols < expectedNbSymbols)
warning(where, lineNo, "Expected %" PRIu32 " 'S' lines, got only %zu", expectedNbSymbols, nbSymbols);
nbSectionsToAssign += nbSections;
for (size_t i = 0; i < nbSections; ++i) {
struct Section *section = fileSections[i].section;
// RAM sections can have a size, but don't get any data (they shouldn't have any)
if (fileSections[i].writeIndex != section->size && fileSections[i].writeIndex != 0)
fatal(where, lineNo, "\"%s\" was not fully written (%" PRIu16 " < %" PRIu16 ")", section->name, fileSections[i].writeIndex, section->size);
// This must be done last, so that `->data` is not NULL anymore
sect_AddSection(section);
if (section->modifier == SECTION_FRAGMENT) {
// Add the fragment's offset to all of its symbols
for (uint32_t j = 0; j < section->nbSymbols; ++j)
section->symbols[j]->offset += section->offset;
}
}
#undef expectEol
#undef expectToken
#undef getToken
free(fileSections);
free(data);
fclose(file);
}