ref: b04e71ed34cbbd9cb15f79cd20ba787438e6bcfb
dir: /src/link/script.c/
/*
* This file is part of RGBDS.
*
* Copyright (c) 2019, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#include <ctype.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "link/main.h"
#include "link/script.h"
#include "link/section.h"
#include "extern/err.h"
FILE *linkerScript;
char *includeFileName;
static uint32_t lineNo;
static struct {
FILE *file;
uint32_t lineNo;
char *name;
} *fileStack;
static uint32_t fileStackSize;
static uint32_t fileStackIndex;
static void pushFile(char *newFileName)
{
if (fileStackIndex == UINT32_MAX)
errx(1, "%s(%" PRIu32 "): INCLUDE recursion limit reached",
linkerScriptName, lineNo);
if (fileStackIndex == fileStackSize) {
if (!fileStackSize) /* Init file stack */
fileStackSize = 4;
fileStackSize *= 2;
fileStack = realloc(fileStack, sizeof(*fileStack) * fileStackSize);
if (!fileStack)
err(1, "%s(%" PRIu32 "): Internal INCLUDE error",
linkerScriptName, lineNo);
}
fileStack[fileStackIndex].file = linkerScript;
fileStack[fileStackIndex].lineNo = lineNo;
fileStack[fileStackIndex].name = linkerScriptName;
fileStackIndex++;
linkerScript = fopen(newFileName, "r");
if (!linkerScript)
err(1, "%s(%" PRIu32 "): Could not open \"%s\"",
linkerScriptName, lineNo, newFileName);
lineNo = 1;
linkerScriptName = newFileName;
}
static bool popFile(void)
{
if (!fileStackIndex)
return false;
free(linkerScriptName);
fileStackIndex--;
linkerScript = fileStack[fileStackIndex].file;
lineNo = fileStack[fileStackIndex].lineNo;
linkerScriptName = fileStack[fileStackIndex].name;
return true;
}
static bool isWhiteSpace(int c)
{
return c == ' ' || c == '\t';
}
static bool isNewline(int c)
{
return c == '\r' || c == '\n';
}
/**
* Try parsing a number, in base 16 if it begins with a dollar,
* in base 10 otherwise
* @param str The number to parse
* @param number A pointer where the number will be written to
* @return True if parsing was successful, false otherwise
*/
static bool tryParseNumber(char const *str, uint32_t *number)
{
static char const digits[] = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F'
};
uint8_t base = 10;
if (*str == '$') {
str++;
base = 16;
}
/* An empty string is not a number */
if (!*str)
return false;
*number = 0;
do {
char chr = toupper(*str++);
uint8_t digit = 0;
while (digit < base) {
if (chr == digits[digit])
break;
digit++;
}
if (digit == base)
return false;
*number = *number * base + digit;
} while (*str);
return true;
}
enum LinkerScriptTokenType {
TOKEN_NEWLINE,
TOKEN_COMMAND,
TOKEN_BANK,
TOKEN_INCLUDE,
TOKEN_NUMBER,
TOKEN_STRING,
TOKEN_EOF,
TOKEN_INVALID
};
char const *tokenTypes[] = {
[TOKEN_NEWLINE] = "newline",
[TOKEN_COMMAND] = "command",
[TOKEN_BANK] = "bank command",
[TOKEN_NUMBER] = "number",
[TOKEN_STRING] = "string",
[TOKEN_EOF] = "end of file"
};
enum LinkerScriptCommand {
COMMAND_ORG,
COMMAND_ALIGN,
COMMAND_INVALID
};
struct LinkerScriptToken {
enum LinkerScriptTokenType type;
union LinkerScriptTokenAttr {
enum LinkerScriptCommand command;
enum SectionType secttype;
uint32_t number;
char *string;
} attr;
};
static char const * const commands[] = {
[COMMAND_ORG] = "ORG",
[COMMAND_ALIGN] = "ALIGN"
};
static int nextChar(void)
{
int curchar = getc(linkerScript);
if (curchar == EOF && ferror(linkerScript))
err(1, "%s(%" PRIu32 "): Unexpected error in %s",
linkerScriptName, lineNo, __func__);
return curchar;
}
static struct LinkerScriptToken *nextToken(void)
{
static struct LinkerScriptToken token;
int curchar;
/* If the token has a string, make sure to avoid leaking it */
if (token.type == TOKEN_STRING)
free(token.attr.string);
/* Skip initial whitespace... */
do
curchar = nextChar();
while (isWhiteSpace(curchar));
/* If this is a comment, skip to the end of the line */
if (curchar == ';') {
do {
curchar = nextChar();
} while (!isNewline(curchar) && curchar != EOF);
}
if (curchar == EOF) {
token.type = TOKEN_EOF;
} else if (isNewline(curchar)) {
/* If we have a newline char, this is a newline token */
token.type = TOKEN_NEWLINE;
if (curchar == '\r') {
/* Handle CRLF */
curchar = nextChar();
if (curchar != '\n')
ungetc(curchar, linkerScript);
}
} else if (curchar == '"') {
/* If we have a string start, this is a string */
token.type = TOKEN_STRING;
token.attr.string = NULL; /* Force initial alloc */
size_t size = 0;
size_t capacity = 16; /* Half of the default capacity */
do {
curchar = nextChar();
if (curchar == EOF || isNewline(curchar)) {
errx(1, "%s(%" PRIu32 "): Unterminated string",
linkerScriptName, lineNo);
} else if (curchar == '"') {
/* Quotes force a string termination */
curchar = '\0';
} else if (curchar == '\\') {
/* Backslashes are escape sequences */
curchar = nextChar();
if (curchar == EOF || isNewline(curchar))
errx(1, "%s(%" PRIu32 "): Unterminated string",
linkerScriptName, lineNo);
else if (curchar == 'n')
curchar = '\n';
else if (curchar == 'r')
curchar = '\r';
else if (curchar == 't')
curchar = '\t';
else if (curchar != '\\' && curchar != '"')
errx(1, "%s(%" PRIu32 "): Illegal character escape",
linkerScriptName, lineNo);
}
if (size >= capacity || token.attr.string == NULL) {
capacity *= 2;
token.attr.string = realloc(token.attr.string, capacity);
if (!token.attr.string)
err(1, "%s: Failed to allocate memory for string",
__func__);
}
token.attr.string[size++] = curchar;
} while (curchar);
} else {
/* This is either a number, command or bank, that is: a word */
char *str = NULL;
size_t size = 0;
size_t capacity = 8; /* Half of the default capacity */
for (;;) {
if (size >= capacity || str == NULL) {
capacity *= 2;
str = realloc(str, capacity);
if (!str)
err(1, "%s: Failed to allocate memory for token",
__func__);
}
str[size] = toupper(curchar);
size++;
if (!curchar)
break;
curchar = nextChar();
/* Whitespace, a newline or a comment end the token */
if (isWhiteSpace(curchar) || isNewline(curchar) || curchar == ';') {
ungetc(curchar, linkerScript);
curchar = '\0';
}
}
token.type = TOKEN_INVALID;
/* Try to match a command */
for (enum LinkerScriptCommand i = 0; i < COMMAND_INVALID; i++) {
if (!strcmp(commands[i], str)) {
token.type = TOKEN_COMMAND;
token.attr.command = i;
break;
}
}
if (token.type == TOKEN_INVALID) {
/* Try to match a bank specifier */
for (enum SectionType type = 0; type < SECTTYPE_INVALID; type++) {
if (!strcmp(typeNames[type], str)) {
token.type = TOKEN_BANK;
token.attr.secttype = type;
break;
}
}
}
if (token.type == TOKEN_INVALID) {
/* Try to match an include token */
if (!strcmp("INCLUDE", str))
token.type = TOKEN_INCLUDE;
}
if (token.type == TOKEN_INVALID) {
/* None of the strings matched, do we have a number? */
if (tryParseNumber(str, &token.attr.number))
token.type = TOKEN_NUMBER;
else
errx(1, "%s(%" PRIu32 "): Unknown token \"%s\"",
linkerScriptName, lineNo, str);
}
free(str);
}
return &token;
}
static void processCommand(enum LinkerScriptCommand command, uint16_t arg, uint16_t *pc)
{
switch (command) {
case COMMAND_INVALID:
unreachable_();
case COMMAND_ORG:
break;
case COMMAND_ALIGN:
if (arg >= 16)
arg = 0;
else
arg = (*pc + (1 << arg) - 1) & ~((1 << arg) - 1);
}
if (arg < *pc)
errx(1, "%s(%" PRIu32 "): `%s` cannot be used to go backwards (currently at $%x)",
linkerScriptName, lineNo, commands[command], *pc);
*pc = arg;
}
enum LinkerScriptParserState {
PARSER_FIRSTTIME,
PARSER_LINESTART,
PARSER_INCLUDE, /* After an INCLUDE token */
PARSER_LINEEND
};
/* Part of internal state, but has data that needs to be freed */
static uint16_t *curaddr[SECTTYPE_INVALID];
/* Put as global to ensure it's initialized only once */
static enum LinkerScriptParserState parserState = PARSER_FIRSTTIME;
struct SectionPlacement *script_NextSection(void)
{
static struct SectionPlacement section;
static enum SectionType type;
static uint32_t bank;
static uint32_t bankID;
if (parserState == PARSER_FIRSTTIME) {
lineNo = 1;
/* Init PC for all banks */
for (enum SectionType i = 0; i < SECTTYPE_INVALID; i++) {
curaddr[i] = malloc(sizeof(*curaddr[i]) * nbbanks(i));
for (uint32_t b = 0; b < nbbanks(i); b++)
curaddr[i][b] = startaddr[i];
}
type = SECTTYPE_INVALID;
parserState = PARSER_LINESTART;
}
for (;;) {
struct LinkerScriptToken *token = nextToken();
enum LinkerScriptTokenType tokType;
union LinkerScriptTokenAttr attr;
bool hasArg;
uint32_t arg;
if (type != SECTTYPE_INVALID) {
if (curaddr[type][bankID] > endaddr(type) + 1)
errx(1, "%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])
errx(1, "%s(%" PRIu32 "): PC underflowed ($%04" PRIx16 " < $%04" PRIx16 ")",
linkerScriptName, lineNo,
curaddr[type][bankID], startaddr[type]);
}
switch (parserState) {
case PARSER_FIRSTTIME:
unreachable_();
case PARSER_LINESTART:
switch (token->type) {
case TOKEN_INVALID:
unreachable_();
case TOKEN_EOF:
if (!popFile())
return NULL;
parserState = PARSER_LINEEND;
break;
case TOKEN_NUMBER:
errx(1, "%s(%" PRIu32 "): stray number \"%" PRIu32 "\"",
linkerScriptName, lineNo,
token->attr.number);
case TOKEN_NEWLINE:
lineNo++;
break;
/* A stray string is a section name */
case TOKEN_STRING:
parserState = PARSER_LINEEND;
if (type == SECTTYPE_INVALID)
errx(1, "%s(%" PRIu32 "): Didn't specify a location before the section",
linkerScriptName, lineNo);
section.section =
sect_GetSection(token->attr.string);
if (!section.section)
errx(1, "%s(%" PRIu32 "): Unknown section \"%s\"",
linkerScriptName, lineNo,
token->attr.string);
section.org = curaddr[type][bankID];
section.bank = bank;
curaddr[type][bankID] += section.section->size;
return §ion;
case TOKEN_COMMAND:
case TOKEN_BANK:
tokType = token->type;
attr = token->attr;
token = nextToken();
hasArg = token->type == TOKEN_NUMBER;
/*
* Leaving `arg` uninitialized when `!hasArg`
* causes GCC to warn about its use as an
* argument to `processCommand`. This cannot
* happen because `hasArg` has to be true, but
* silence the warning anyways.
* I dislike doing this because it could swallow
* actual errors, but I don't have a choice.
*/
arg = hasArg ? token->attr.number : 0;
if (tokType == TOKEN_COMMAND) {
if (type == SECTTYPE_INVALID)
errx(1, "%s(%" PRIu32 "): Didn't specify a location before the command",
linkerScriptName, lineNo);
if (!hasArg)
errx(1, "%s(%" PRIu32 "): Command specified without an argument",
linkerScriptName, lineNo);
processCommand(attr.command, arg, &curaddr[type][bankID]);
} else { /* TOKEN_BANK */
type = attr.secttype;
/*
* If there's only one bank,
* specifying the number is optional.
*/
if (!hasArg && nbbanks(type) != 1)
errx(1, "%s(%" PRIu32 "): Didn't specify a bank number",
linkerScriptName, lineNo);
else if (!hasArg)
arg = bankranges[type][0];
else if (arg < bankranges[type][0])
errx(1, "%s(%" PRIu32 "): specified bank number is too low (%" PRIu32 " < %" PRIu32 ")",
linkerScriptName, lineNo,
arg, bankranges[type][0]);
else if (arg > bankranges[type][1])
errx(1, "%s(%" PRIu32 "): specified bank number is too high (%" PRIu32 " > %" PRIu32 ")",
linkerScriptName, lineNo,
arg, bankranges[type][1]);
bank = arg;
bankID = arg - bankranges[type][0];
}
/* If we read a token we shouldn't have... */
if (token->type != TOKEN_NUMBER)
goto lineend;
break;
case TOKEN_INCLUDE:
parserState = PARSER_INCLUDE;
break;
}
break;
case PARSER_INCLUDE:
if (token->type != TOKEN_STRING)
errx(1, "%s(%" PRIu32 "): Expected a file name after INCLUDE",
linkerScriptName, lineNo);
/* Switch to that file */
pushFile(token->attr.string);
/* The file stack took ownership of the string */
token->attr.string = NULL;
parserState = PARSER_LINESTART;
break;
case PARSER_LINEEND:
lineend:
lineNo++;
parserState = PARSER_LINESTART;
if (token->type == TOKEN_EOF) {
if (!popFile())
return NULL;
parserState = PARSER_LINEEND;
} else if (token->type != TOKEN_NEWLINE)
errx(1, "%s(%" PRIu32 "): Unexpected %s at the end of the line",
linkerScriptName, lineNo,
tokenTypes[token->type]);
break;
}
}
}
void script_Cleanup(void)
{
for (enum SectionType type = 0; type < SECTTYPE_INVALID; type++)
free(curaddr[type]);
}