shithub: rgbds

Download patch

ref: 828b2adcdfee526ea270b986002c3020765c5a88
parent: 1c2965467d266a8eddc7eb4e69c415148a6bebe9
author: ISSOtm <eldredhabert0@gmail.com>
date: Sun Jul 10 06:50:33 EDT 2022

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.

--- a/Makefile
+++ b/Makefile
@@ -91,6 +91,7 @@
 	src/link/output.o \
 	src/link/patch.o \
 	src/link/script.o \
+	src/link/sdas_obj.o \
 	src/link/section.o \
 	src/link/symbol.o \
 	src/extern/getopt.o \
--- a/include/link/script.h
+++ b/include/link/script.h
@@ -11,11 +11,13 @@
 #define RGBDS_LINK_SCRIPT_H
 
 #include <stdint.h>
+#include "linkdefs.h"
 
 extern FILE * linkerScript;
 
 struct SectionPlacement {
 	struct Section *section;
+        enum SectionType type;
 	uint16_t org;
 	uint32_t bank;
 };
--- /dev/null
+++ b/include/link/sdas_obj.h
@@ -1,0 +1,19 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/* Assigning all sections a place */
+#ifndef RGBDS_LINK_SDAS_OBJ_H
+#define RGBDS_LINK_SDAS_OBJ_H
+
+#include <stdio.h>
+
+struct FileStackNode;
+
+void sdobj_ReadFile(struct FileStackNode const *fileName, FILE *file);
+
+#endif /* RGBDS_LINK_SDAS_OBJ_H */
--- a/include/link/section.h
+++ b/include/link/section.h
@@ -48,6 +48,8 @@
 	enum SectionType type;
 	enum SectionModifier modifier;
 	bool isAddressFixed;
+	// This `struct`'s address in ROM.
+	// Importantly for fragments, this does not include `offset`!
 	uint16_t org;
 	bool isBankFixed;
 	uint32_t bank;
@@ -60,7 +62,7 @@
 	/* Extra info computed during linking */
 	struct Symbol **fileSymbols;
 	uint32_t nbSymbols;
-	struct Symbol const **symbols;
+	struct Symbol **symbols;
 	struct Section *nextu; /* The next "component" of this unionized sect */
 };
 
--- a/include/link/symbol.h
+++ b/include/link/symbol.h
@@ -27,6 +27,7 @@
 	int32_t lineNo;
 	int32_t sectionID;
 	union {
+		// Both types must be identical
 		int32_t offset;
 		int32_t value;
 	};
--- a/include/linkdefs.h
+++ b/include/linkdefs.h
@@ -9,6 +9,7 @@
 #ifndef RGBDS_LINKDEFS_H
 #define RGBDS_LINKDEFS_H
 
+#include <assert.h>
 #include <stdbool.h>
 #include <stdint.h>
 
@@ -74,6 +75,8 @@
 	SECTTYPE_SRAM,
 	SECTTYPE_OAM,
 
+	// In RGBLINK, this is used for "indeterminate" sections; this is primarily for SDCC
+	// areas, which do not carry any section type info and must be told from the linker script
 	SECTTYPE_INVALID
 };
 
@@ -93,6 +96,7 @@
  */
 static inline bool sect_HasData(enum SectionType type)
 {
+	assert(type != SECTTYPE_INVALID);
 	return type == SECTTYPE_ROM0 || type == SECTTYPE_ROMX;
 }
 
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -81,6 +81,7 @@
     "link/output.c"
     "link/patch.c"
     "link/script.c"
+    "link/sdas_obj.c"
     "link/section.c"
     "link/symbol.c"
     "hashmap.c"
--- a/src/link/main.c
+++ b/src/link/main.c
@@ -29,6 +29,7 @@
 #include "extern/getopt.h"
 
 #include "error.h"
+#include "linkdefs.h"
 #include "platform.h"
 #include "version.h"
 
@@ -350,6 +351,12 @@
 	}
 }
 
+_Noreturn void reportErrors(void) {
+	fprintf(stderr, "Linking failed with %" PRIu32 " error%s\n",
+		nbErrors, nbErrors == 1 ? "" : "s");
+	exit(1);
+}
+
 int main(int argc, char *argv[])
 {
 	int optionChar;
@@ -457,7 +464,15 @@
 		while ((placement = script_NextSection())) {
 			struct Section *section = placement->section;
 
+			assert(section->offset == 0);
 			/* Check if this doesn't conflict with what the code says */
+			if (section->type == SECTTYPE_INVALID) {
+				for (struct Section *sect = section; sect; sect = sect->nextu)
+					sect->type = placement->type; // SDCC "unknown" sections
+			} else if (section->type != placement->type) {
+				error(NULL, 0, "Linker script contradicts \"%s\"'s type",
+				      section->name);
+			}
 			if (section->isBankFixed && placement->bank != section->bank)
 				error(NULL, 0, "Linker script contradicts \"%s\"'s bank placement",
 				      section->name);
@@ -479,11 +494,17 @@
 		fclose(linkerScript);
 
 		script_Cleanup();
+
+		// If the linker script produced any errors, some sections may be in an invalid state
+		if (nbErrors != 0)
+			reportErrors();
 	}
 
 
 	/* then process them, */
 	obj_DoSanityChecks();
+	if (nbErrors != 0)
+		reportErrors();
 	assign_AssignSections();
 	obj_CheckAssertions();
 	assign_Cleanup();
@@ -490,11 +511,8 @@
 
 	/* and finally output the result. */
 	patch_ApplyPatches();
-	if (nbErrors) {
-		fprintf(stderr, "Linking failed with %" PRIu32 " error%s\n",
-			nbErrors, nbErrors == 1 ? "" : "s");
-		exit(1);
-	}
+	if (nbErrors != 0)
+		reportErrors();
 	out_WriteFiles();
 
 	/* Do cleanup before quitting, though. */
--- a/src/link/object.c
+++ b/src/link/object.c
@@ -18,6 +18,7 @@
 #include "link/main.h"
 #include "link/object.h"
 #include "link/patch.h"
+#include "link/sdas_obj.h"
 #include "link/section.h"
 #include "link/symbol.h"
 
@@ -408,7 +409,7 @@
  * @param symbol The symbol to link
  * @param section The section to link
  */
-static void linkSymToSect(struct Symbol const *symbol, struct Section *section)
+static void linkSymToSect(struct Symbol *symbol, struct Section *section)
 {
 	uint32_t a = 0, b = section->nbSymbols;
 
@@ -421,7 +422,7 @@
 			a = c + 1;
 	}
 
-	struct Symbol const *tmp = symbol;
+	struct Symbol *tmp = symbol;
 
 	for (uint32_t i = a; i <= section->nbSymbols; i++) {
 		symbol = tmp;
@@ -466,6 +467,39 @@
 	if (!file)
 		err("Could not open file %s", fileName);
 
+	// First, check if the object is a RGBDS object or a SDCC one. If the first byte is 'R',
+	// we'll assume it's a RGBDS object file, and otherwise, that it's a SDCC object file.
+	int c = getc(file);
+
+	ungetc(c, file); // Guaranteed to work
+	switch (c) {
+	case EOF:
+		fatal(NULL, 0, "File \"%s\" is empty!", fileName);
+
+	case 'R':
+		break;
+
+	default: // This is (probably) a SDCC object file, defer the rest of detection to it
+		// Since SDCC does not provide line info, everything will be reported as coming from the
+		// object file. It's better than nothing.
+		nodes[fileID].nbNodes = 1;
+		nodes[fileID].nodes = malloc(sizeof(nodes[fileID].nodes[0]) * nodes[fileID].nbNodes);
+		if (!nodes[fileID].nodes)
+			err("Failed to get memory for %s's nodes", fileName);
+		struct FileStackNode *where = &nodes[fileID].nodes[0];
+
+		if (!where)
+			fatal(NULL, 0, "Failed to alloc fstack node for \"%s\": %s", fileName, strerror(errno));
+		where->parent = NULL;
+		where->type = NODE_FILE;
+		where->name = strdup(fileName);
+		if (!where->name)
+			fatal(NULL, 0, "Failed to duplicate \"%s\"'s name: %s", fileName, strerror(errno));
+
+		sdobj_ReadFile(where, file);
+		return;
+	}
+
 	/* Begin by reading the magic bytes and version number */
 	unsigned versionNumber;
 	int matchedElems = fscanf(file, RGBDS_OBJECT_VERSION_STRING,
@@ -508,8 +542,7 @@
 		readFileStackNode(file, nodes[fileID].nodes, i, fileName);
 
 	/* This file's symbols, kept to link sections to them */
-	struct Symbol **fileSymbols =
-		malloc(sizeof(*fileSymbols) * nbSymbols + 1);
+	struct Symbol **fileSymbols = malloc(sizeof(*fileSymbols) * nbSymbols + 1);
 
 	if (!fileSymbols)
 		err("Failed to get memory for %s's symbols", fileName);
--- a/src/link/output.c
+++ b/src/link/output.c
@@ -9,6 +9,7 @@
 #include <assert.h>
 #include <inttypes.h>
 #include <stdint.h>
+#include <stdio.h>
 #include <stdlib.h>
 
 #include "link/output.h"
@@ -199,6 +200,7 @@
 	while (bankSections) {
 		struct Section const *section = bankSections->section;
 
+		assert(section->offset == 0);
 		/* Output padding up to the next SECTION */
 		while (offset + baseOffset < section->org) {
 			putc(overlayFile ? getc(overlayFile) : padValue,
@@ -381,6 +383,7 @@
 
 		used += sect->size;
 
+		assert(sect->offset == 0);
 		if (sect->size != 0)
 			fprintf(mapFile, "  SECTION: $%04" PRIx16 "-$%04x ($%04" PRIx16
 				" byte%s) [\"%s\"]\n",
@@ -394,6 +397,7 @@
 		uint16_t org = sect->org;
 
 		while (sect) {
+			fprintf(mapFile, "    ; New %s\n", sect->modifier == SECTION_FRAGMENT ? "fragment": "union");
 			for (size_t i = 0; i < sect->nbSymbols; i++)
 				fprintf(mapFile, "           $%04" PRIx32 " = %s\n",
 					sect->symbols[i]->offset + org,
--- a/src/link/patch.c
+++ b/src/link/patch.c
@@ -354,6 +354,7 @@
 				;
 
 			sect = sect_GetSection(name);
+			assert(sect->offset == 0);
 
 			if (!sect) {
 				error(patch->src, patch->lineNo,
@@ -497,9 +498,6 @@
  */
 static void applyFilePatches(struct Section *section, struct Section *dataSection)
 {
-	if (!sect_HasData(section->type))
-		return;
-
 	verbosePrint("Patching section \"%s\"...\n", section->name);
 	for (uint32_t patchID = 0; patchID < section->nbPatches; patchID++) {
 		struct Patch *patch = &section->patches[patchID];
@@ -512,8 +510,7 @@
 		if (patch->type == PATCHTYPE_JR) {
 			// Offset is relative to the byte *after* the operand
 			// PC as operand to `jr` is lower than reference PC by 2
-			uint16_t address = patch->pcSection->org
-							+ patch->pcOffset + 2;
+			uint16_t address = patch->pcSection->org + patch->pcOffset + 2;
 			int16_t jumpOffset = value - address;
 
 			if (!isError && (jumpOffset < -128 || jumpOffset > 127))
@@ -555,6 +552,9 @@
  */
 static void applyPatches(struct Section *section, void *arg)
 {
+	if (!sect_HasData(section->type))
+		return;
+
 	(void)arg;
 	struct Section *dataSection = section;
 
--- a/src/link/script.c
+++ b/src/link/script.c
@@ -365,8 +365,7 @@
 
 struct SectionPlacement *script_NextSection(void)
 {
-	static struct SectionPlacement section;
-	static enum SectionType type;
+	static struct SectionPlacement placement;
 	static uint32_t bank;
 	static uint32_t bankID;
 
@@ -380,7 +379,7 @@
 				curaddr[i][b] = startaddr[i];
 		}
 
-		type = SECTTYPE_INVALID;
+		placement.type = SECTTYPE_INVALID;
 
 		parserState = PARSER_LINESTART;
 	}
@@ -392,15 +391,15 @@
 		bool hasArg;
 		uint32_t arg;
 
-		if (type != SECTTYPE_INVALID) {
-			if (curaddr[type][bankID] > endaddr(type) + 1)
+		if (placement.type != SECTTYPE_INVALID) {
+			if (curaddr[placement.type][bankID] > endaddr(placement.type) + 1)
 				errx("%s(%" PRIu32 "): Sections would extend past the end of %s ($%04" PRIx16 " > $%04" PRIx16 ")",
-				     linkerScriptName, lineNo, typeNames[type],
-				     curaddr[type][bankID], endaddr(type));
-			if (curaddr[type][bankID] < startaddr[type])
+				     linkerScriptName, lineNo, typeNames[placement.type],
+				     curaddr[placement.type][bankID], endaddr(placement.type));
+			if (curaddr[placement.type][bankID] < startaddr[placement.type])
 				errx("%s(%" PRIu32 "): PC underflowed ($%04" PRIx16 " < $%04" PRIx16 ")",
 				     linkerScriptName, lineNo,
-				     curaddr[type][bankID], startaddr[type]);
+				     curaddr[placement.type][bankID], startaddr[placement.type]);
 		}
 
 		switch (parserState) {
@@ -431,21 +430,21 @@
 			case TOKEN_STRING:
 				parserState = PARSER_LINEEND;
 
-				if (type == SECTTYPE_INVALID)
+				if (placement.type == SECTTYPE_INVALID)
 					errx("%s(%" PRIu32 "): Didn't specify a location before the section",
 					     linkerScriptName, lineNo);
 
-				section.section =
+				placement.section =
 					sect_GetSection(token->attr.string);
-				if (!section.section)
+				if (!placement.section)
 					errx("%s(%" PRIu32 "): Unknown section \"%s\"",
 					     linkerScriptName, lineNo,
 					     token->attr.string);
-				section.org = curaddr[type][bankID];
-				section.bank = bank;
+				placement.org = curaddr[placement.type][bankID];
+				placement.bank = bank;
 
-				curaddr[type][bankID] += section.section->size;
-				return &section;
+				curaddr[placement.type][bankID] += placement.section->size;
+				return &placement;
 
 			case TOKEN_COMMAND:
 			case TOKEN_BANK:
@@ -466,7 +465,7 @@
 				arg = hasArg ? token->attr.number : 0;
 
 				if (tokType == TOKEN_COMMAND) {
-					if (type == SECTTYPE_INVALID)
+					if (placement.type == SECTTYPE_INVALID)
 						errx("%s(%" PRIu32 "): Didn't specify a location before the command",
 						     linkerScriptName, lineNo);
 					if (!hasArg)
@@ -473,28 +472,28 @@
 						errx("%s(%" PRIu32 "): Command specified without an argument",
 						     linkerScriptName, lineNo);
 
-					processCommand(attr.command, arg, &curaddr[type][bankID]);
+					processCommand(attr.command, arg, &curaddr[placement.type][bankID]);
 				} else { /* TOKEN_BANK */
-					type = attr.secttype;
+					placement.type = attr.secttype;
 					/*
 					 * If there's only one bank,
 					 * specifying the number is optional.
 					 */
-					if (!hasArg && nbbanks(type) != 1)
+					if (!hasArg && nbbanks(placement.type) != 1)
 						errx("%s(%" PRIu32 "): Didn't specify a bank number",
 						     linkerScriptName, lineNo);
 					else if (!hasArg)
-						arg = bankranges[type][0];
-					else if (arg < bankranges[type][0])
+						arg = bankranges[placement.type][0];
+					else if (arg < bankranges[placement.type][0])
 						errx("%s(%" PRIu32 "): specified bank number is too low (%" PRIu32 " < %" PRIu32 ")",
 						     linkerScriptName, lineNo,
-						     arg, bankranges[type][0]);
-					else if (arg > bankranges[type][1])
+						     arg, bankranges[placement.type][0]);
+					else if (arg > bankranges[placement.type][1])
 						errx("%s(%" PRIu32 "): specified bank number is too high (%" PRIu32 " > %" PRIu32 ")",
 						     linkerScriptName, lineNo,
-						     arg, bankranges[type][1]);
+						     arg, bankranges[placement.type][1]);
 					bank = arg;
-					bankID = arg - bankranges[type][0];
+					bankID = arg - bankranges[placement.type][0];
 				}
 
 				/* If we read a token we shouldn't have... */
--- /dev/null
+++ b/src/link/sdas_obj.c
@@ -1,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);
+}
--- a/src/link/section.c
+++ b/src/link/section.c
@@ -6,6 +6,7 @@
  * SPDX-License-Identifier: MIT
  */
 
+#include <assert.h>
 #include <inttypes.h>
 #include <stdbool.h>
 #include <stdlib.h>
@@ -153,18 +154,33 @@
 
 	case SECTION_FRAGMENT:
 		checkFragmentCompat(target, other);
+		// Append `other` to `target`
+		// Note that the order in which fragments are stored in the `nextu` list does not
+		// really matter, only that offsets are properly computed
+		other->offset = target->size;
 		target->size += other->size;
-		other->offset = target->size - other->size;
-		if (sect_HasData(target->type)) {
-			/* Ensure we're not allocating 0 bytes */
-			target->data = realloc(target->data,
-					       sizeof(*target->data) * target->size + 1);
-			if (!target->data)
-				errx("Failed to concatenate \"%s\"'s fragments", target->name);
-			memcpy(target->data + target->size - other->size, other->data, other->size);
+		// Normally we'd check that `sect_HasData`, but SDCC areas may be `_INVALID` here
+		// Note that if either fragment has data (= a non-NULL `data` pointer), then it's
+		// assumed that both fragments "have data", and thus should either have a non-NULL
+		// `data` pointer, or a size of 0.
+		if (other->data) {
+			if (target->data) {
+				/* Ensure we're not allocating 0 bytes */
+				target->data = realloc(target->data,
+						       sizeof(*target->data) * target->size + 1);
+				if (!target->data)
+					errx("Failed to concatenate \"%s\"'s fragments", target->name);
+				memcpy(&target->data[other->offset], other->data, other->size);
+			} else {
+				assert(target->size == other->size); // It has been increased just above
+				target->data = other->data;
+				other->data = NULL; // Prevent a double free()
+			}
 			/* Adjust patches' PC offsets */
 			for (uint32_t patchID = 0; patchID < other->nbPatches; patchID++)
 				other->patches[patchID].pcOffset += other->offset;
+		} else if (target->data) {
+			assert(other->size == 0);
 		}
 		break;
 
@@ -208,26 +224,20 @@
 	hash_EmptyMap(sections);
 }
 
-static bool sanityChecksFailed;
-
 static void doSanityChecks(struct Section *section, void *ptr)
 {
 	(void)ptr;
-#define fail(...) do { \
-	warnx(__VA_ARGS__); \
-	sanityChecksFailed = true; \
-} while (0)
 
 	/* Sanity check the section's type */
 
 	if (section->type < 0 || section->type >= SECTTYPE_INVALID) {
-		fail("Section \"%s\" has an invalid type.", section->name);
+		error(NULL, 0, "Section \"%s\" has an invalid type", section->name);
 		return;
 	}
 
 	if (is32kMode && section->type == SECTTYPE_ROMX) {
 		if (section->isBankFixed && section->bank != 1)
-			fail("%s: ROMX sections must be in bank 1 (if any) with option -t",
+			error(NULL, 0, "%s: ROMX sections must be in bank 1 (if any) with option -t",
 			     section->name);
 		else
 			section->type = SECTTYPE_ROM0;
@@ -234,13 +244,13 @@
 	}
 	if (isWRA0Mode && section->type == SECTTYPE_WRAMX) {
 		if (section->isBankFixed && section->bank != 1)
-			fail("%s: WRAMX sections must be in bank 1 with options -w or -d",
+			error(NULL, 0, "%s: WRAMX sections must be in bank 1 with options -w or -d",
 			     section->name);
 		else
 			section->type = SECTTYPE_WRAMX;
 	}
 	if (isDmgMode && section->type == SECTTYPE_VRAM && section->bank == 1)
-		fail("%s: VRAM bank 1 can't be used with option -d",
+		error(NULL, 0, "%s: VRAM bank 1 can't be used with option -d",
 		     section->name);
 
 	/*
@@ -252,13 +262,13 @@
 
 	/* Too large an alignment may not be satisfiable */
 	if (section->isAlignFixed && (section->alignMask & startaddr[section->type]))
-		fail("%s: %s sections cannot be aligned to $%04x bytes",
+		error(NULL, 0, "%s: %s sections cannot be aligned to $%04x bytes",
 		     section->name, typeNames[section->type], section->alignMask + 1);
 
 	uint32_t minbank = bankranges[section->type][0], maxbank = bankranges[section->type][1];
 
 	if (section->isBankFixed && section->bank < minbank && section->bank > maxbank)
-		fail(minbank == maxbank
+		error(NULL, 0, minbank == maxbank
 			? "Cannot place section \"%s\" in bank %" PRIu32 ", it must be %" PRIu32
 			: "Cannot place section \"%s\" in bank %" PRIu32 ", it must be between %" PRIu32 " and %" PRIu32,
 		     section->name, section->bank, minbank, maxbank);
@@ -265,7 +275,7 @@
 
 	/* Check if section has a chance to be placed */
 	if (section->size > maxsize[section->type])
-		fail("Section \"%s\" is bigger than the max size for that type: %#" PRIx16 " > %#" PRIx16,
+		error(NULL, 0, "Section \"%s\" is bigger than the max size for that type: %#" PRIx16 " > %#" PRIx16,
 		     section->name, section->size, maxsize[section->type]);
 
 	/* Translate loose constraints to strong ones when they're equivalent */
@@ -279,7 +289,7 @@
 		/* It doesn't make sense to have both org and alignment set */
 		if (section->isAlignFixed) {
 			if ((section->org & section->alignMask) != section->alignOfs)
-				fail("Section \"%s\"'s fixed address doesn't match its alignment",
+				error(NULL, 0, "Section \"%s\"'s fixed address doesn't match its alignment",
 				     section->name);
 			section->isAlignFixed = false;
 		}
@@ -287,12 +297,12 @@
 		/* Ensure the target address is valid */
 		if (section->org < startaddr[section->type]
 		 || section->org > endaddr(section->type))
-			fail("Section \"%s\"'s fixed address %#" PRIx16 " is outside of range [%#"
+			error(NULL, 0, "Section \"%s\"'s fixed address %#" PRIx16 " is outside of range [%#"
 			     PRIx16 "; %#" PRIx16 "]", section->name, section->org,
 			     startaddr[section->type], endaddr(section->type));
 
 		if (section->org + section->size > endaddr(section->type) + 1)
-			fail("Section \"%s\"'s end address %#x is greater than last address %#x",
+			error(NULL, 0, "Section \"%s\"'s end address %#x is greater than last address %#x",
 			     section->name, section->org + section->size,
 			     endaddr(section->type) + 1);
 	}
@@ -303,6 +313,4 @@
 void sect_DoSanityChecks(void)
 {
 	sect_ForEach(doSanityChecks, NULL);
-	if (sanityChecksFailed)
-		errx("Sanity checks failed");
 }
--- a/test/link/rom0-tiny-no-t.out
+++ b/test/link/rom0-tiny-no-t.out
@@ -1,2 +1,2 @@
-warning: Section "rom" is bigger than the max size for that type: 0x8000 > 0x4000
-error: Sanity checks failed
+error: Section "rom" is bigger than the max size for that type: 0x8000 > 0x4000
+Linking failed with 1 error
--- a/test/link/vram-fixed-dmg-mode-d.out
+++ b/test/link/vram-fixed-dmg-mode-d.out
@@ -1,2 +1,2 @@
-warning: v1: VRAM bank 1 can't be used with option -d
-error: Sanity checks failed
+error: v1: VRAM bank 1 can't be used with option -d
+Linking failed with 1 error