diff --git a/examples/count.asm b/examples/count.asm new file mode 100644 index 0000000..b6823a5 --- /dev/null +++ b/examples/count.asm @@ -0,0 +1,18 @@ +; count.asm - prints "0123456789" then a newline and halts. +; Exercises ADC, CMP, branches and a loop. + + .export start + +start: + lda #'0' +loop: + sta $F001 + clc + adc #$01 + cmp #$3A ; '9' + 1 + bne loop + lda #$0A ; newline + sta $F001 + lda #$00 + sta $F003 + rts diff --git a/examples/hello.asm b/examples/hello.asm new file mode 100644 index 0000000..94a3a1f --- /dev/null +++ b/examples/hello.asm @@ -0,0 +1,24 @@ +; hello.asm - single-object demo. +; Prints "Hello, World!" through the memory-mapped terminal at $F001, +; then writes to the halt register at $F003. + + .export start + .org $0600 + + jmp start + +msg: + .asciiz "Hello, World!" + +start: + ldx #0 +loop: + lda msg,x + beq done + sta $F001 + inx + jmp loop +done: + lda #$00 + sta $F003 + rts diff --git a/examples/main.asm b/examples/main.asm new file mode 100644 index 0000000..10e8fbd --- /dev/null +++ b/examples/main.asm @@ -0,0 +1,23 @@ +; main.asm - cross-object demo, part 2. +; Imports `putc` from putc.wo and prints "Hello, 6502!". + + .export start + .import putc + + jmp start + +msg: + .asciiz "Hello, 6502!" + +start: + ldx #0 +loop: + lda msg,x + beq done + jsr putc + inx + jmp loop +done: + lda #$00 + sta $F003 + rts diff --git a/examples/putc.asm b/examples/putc.asm new file mode 100644 index 0000000..40261f4 --- /dev/null +++ b/examples/putc.asm @@ -0,0 +1,9 @@ +; putc.asm - cross-object demo, part 1. +; Exports `putc`: write the byte in A to the terminal and return. +; This object is relocatable; the linker places it at the load address. + + .export putc + +putc: + sta $F001 + rts diff --git a/src/assembler/assembler.d b/src/assembler/assembler.d new file mode 100644 index 0000000..3f1b610 --- /dev/null +++ b/src/assembler/assembler.d @@ -0,0 +1,1616 @@ +module assembler.assembler; + +import std.array : split; +import std.ascii : isHexDigit; +import std.conv : to; +import std.file : readText; +import std.format : format; +import std.string : strip, toLower; +import wcore.objfmt; +alias Object = wcore.objfmt.Object; + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/// Assemble `source` into a wcore.objfmt.Object. `filename` is used only for +/// error messages. Errors are thrown as Exception with "file:line: message". +Object assembleSource(string source, string filename = "") +{ + auto a = Asm(filename); + return a.assemble(source); +} + +/// Read and assemble a source file. +Object assembleFile(string path) +{ + string src; + try + src = readText(path); + catch (Exception e) + throw new Exception(format("%s: %s", path, e.msg)); + return assembleSource(src, path); +} + +// --------------------------------------------------------------------------- +// Internal types +// --------------------------------------------------------------------------- + +private enum AddrMode : ubyte +{ + imp, + acc, + imm, + zp, + zpx, + zpy, + abs, + absx, + absy, + ind, + izx, + izy, + rel, +} + +private enum OpForm : ubyte +{ + imp, + acc, + imm, + direct, + izx, + izy, + ind, +} + +private enum ubyte SEG_CODE = 0x07; // alloc|exec|write +private enum ubyte SEG_DATA = 0x05; // alloc|write +private enum ubyte SEG_BSS = 0x0D; // alloc|write|bss + +private struct Expr +{ + bool isNum; + int num; + string sym; + int offset; + bool low; + bool high; +} + +private struct DataItem +{ + bool isStr; + ubyte[] bytes; + Expr expr; +} + +private struct Line +{ + int lineNo; + string text; + string label; + bool labelOnly; + bool isEquate; + string eqName; + Expr expr; // equate value / .org value / instruction operand + bool isDirective; + string op; // lowercased mnemonic or directive (with leading dot) + OpForm form; + bool indexX; + bool indexY; + Expr[] exprs; // .word list, .res [count, fill] + DataItem[] items; // .byte list + ubyte[] strBytes; // .ascii / .asciiz + bool hasFill; + Expr exprFill; + string[] names; // .export / .import / .zp + string entrySym; // .entry + AddrMode mode; + bool modeSet; +} + +private struct SymEntry +{ + string name; + int index = -1; + bool imported; + bool defined; + bool global; + bool zeropage; + bool valueKnown; + ushort value; + int seg = -1; +} + +private struct SegState +{ + string name; + ubyte flags; + ushort origin; + size_t len; + ubyte[] data; +} + +// --------------------------------------------------------------------------- +// Opcode table +// --------------------------------------------------------------------------- + +private struct OpDef +{ + string m; + AddrMode a; + ubyte c; +} + +private __gshared ubyte[AddrMode][string] gOpTable; + +private void add(string m, AddrMode a, ubyte c) +{ + gOpTable[m][a] = c; +} + +static this() +{ + // ADC + add("adc", AddrMode.imm, 0x69); add("adc", AddrMode.zp, 0x65); + add("adc", AddrMode.zpx, 0x75); add("adc", AddrMode.abs, 0x6D); + add("adc", AddrMode.absx, 0x7D); add("adc", AddrMode.absy, 0x79); + add("adc", AddrMode.izx, 0x61); add("adc", AddrMode.izy, 0x71); + // AND + add("and", AddrMode.imm, 0x29); add("and", AddrMode.zp, 0x25); + add("and", AddrMode.zpx, 0x35); add("and", AddrMode.abs, 0x2D); + add("and", AddrMode.absx, 0x3D); add("and", AddrMode.absy, 0x39); + add("and", AddrMode.izx, 0x21); add("and", AddrMode.izy, 0x31); + // ASL + add("asl", AddrMode.acc, 0x0A); add("asl", AddrMode.zp, 0x06); + add("asl", AddrMode.zpx, 0x16); add("asl", AddrMode.abs, 0x0E); + add("asl", AddrMode.absx, 0x1E); + // Branches + add("bcc", AddrMode.rel, 0x90); add("bcs", AddrMode.rel, 0xB0); + add("beq", AddrMode.rel, 0xF0); add("bmi", AddrMode.rel, 0x30); + add("bne", AddrMode.rel, 0xD0); add("bpl", AddrMode.rel, 0x10); + add("bvc", AddrMode.rel, 0x50); add("bvs", AddrMode.rel, 0x70); + // BIT + add("bit", AddrMode.zp, 0x24); add("bit", AddrMode.abs, 0x2C); + // BRK + add("brk", AddrMode.imp, 0x00); + // Flags + add("clc", AddrMode.imp, 0x18); add("cld", AddrMode.imp, 0xD8); + add("cli", AddrMode.imp, 0x58); add("clv", AddrMode.imp, 0xB8); + // CMP + add("cmp", AddrMode.imm, 0xC9); add("cmp", AddrMode.zp, 0xC5); + add("cmp", AddrMode.zpx, 0xD5); add("cmp", AddrMode.abs, 0xCD); + add("cmp", AddrMode.absx, 0xDD); add("cmp", AddrMode.absy, 0xD9); + add("cmp", AddrMode.izx, 0xC1); add("cmp", AddrMode.izy, 0xD1); + // CPX / CPY + add("cpx", AddrMode.imm, 0xE0); add("cpx", AddrMode.zp, 0xE4); + add("cpx", AddrMode.abs, 0xEC); + add("cpy", AddrMode.imm, 0xC0); add("cpy", AddrMode.zp, 0xC4); + add("cpy", AddrMode.abs, 0xCC); + // DEC / DEX / DEY + add("dec", AddrMode.zp, 0xC6); add("dec", AddrMode.zpx, 0xD6); + add("dec", AddrMode.abs, 0xCE); add("dec", AddrMode.absx, 0xDE); + add("dex", AddrMode.imp, 0xCA); add("dey", AddrMode.imp, 0x88); + // EOR + add("eor", AddrMode.imm, 0x49); add("eor", AddrMode.zp, 0x45); + add("eor", AddrMode.zpx, 0x55); add("eor", AddrMode.abs, 0x4D); + add("eor", AddrMode.absx, 0x5D); add("eor", AddrMode.absy, 0x59); + add("eor", AddrMode.izx, 0x41); add("eor", AddrMode.izy, 0x51); + // INC / INX / INY + add("inc", AddrMode.zp, 0xE6); add("inc", AddrMode.zpx, 0xF6); + add("inc", AddrMode.abs, 0xEE); add("inc", AddrMode.absx, 0xFE); + add("inx", AddrMode.imp, 0xE8); add("iny", AddrMode.imp, 0xC8); + // JMP / JSR + add("jmp", AddrMode.abs, 0x4C); add("jmp", AddrMode.ind, 0x6C); + add("jsr", AddrMode.abs, 0x20); + // LDA + add("lda", AddrMode.imm, 0xA9); add("lda", AddrMode.zp, 0xA5); + add("lda", AddrMode.zpx, 0xB5); add("lda", AddrMode.abs, 0xAD); + add("lda", AddrMode.absx, 0xBD); add("lda", AddrMode.absy, 0xB9); + add("lda", AddrMode.izx, 0xA1); add("lda", AddrMode.izy, 0xB1); + // LDX / LDY + add("ldx", AddrMode.imm, 0xA2); add("ldx", AddrMode.zp, 0xA6); + add("ldx", AddrMode.zpy, 0xB6); add("ldx", AddrMode.abs, 0xAE); + add("ldx", AddrMode.absy, 0xBE); + add("ldy", AddrMode.imm, 0xA0); add("ldy", AddrMode.zp, 0xA4); + add("ldy", AddrMode.zpx, 0xB4); add("ldy", AddrMode.abs, 0xAC); + add("ldy", AddrMode.absx, 0xBC); + // LSR + add("lsr", AddrMode.acc, 0x4A); add("lsr", AddrMode.zp, 0x46); + add("lsr", AddrMode.zpx, 0x56); add("lsr", AddrMode.abs, 0x4E); + add("lsr", AddrMode.absx, 0x5E); + // NOP + add("nop", AddrMode.imp, 0xEA); + // ORA + add("ora", AddrMode.imm, 0x09); add("ora", AddrMode.zp, 0x05); + add("ora", AddrMode.zpx, 0x15); add("ora", AddrMode.abs, 0x0D); + add("ora", AddrMode.absx, 0x1D); add("ora", AddrMode.absy, 0x19); + add("ora", AddrMode.izx, 0x01); add("ora", AddrMode.izy, 0x11); + // Stack + add("pha", AddrMode.imp, 0x48); add("php", AddrMode.imp, 0x08); + add("pla", AddrMode.imp, 0x68); add("plp", AddrMode.imp, 0x28); + // ROL / ROR + add("rol", AddrMode.acc, 0x2A); add("rol", AddrMode.zp, 0x26); + add("rol", AddrMode.zpx, 0x36); add("rol", AddrMode.abs, 0x2E); + add("rol", AddrMode.absx, 0x3E); + add("ror", AddrMode.acc, 0x6A); add("ror", AddrMode.zp, 0x66); + add("ror", AddrMode.zpx, 0x76); add("ror", AddrMode.abs, 0x6E); + add("ror", AddrMode.absx, 0x7E); + // RTI / RTS + add("rti", AddrMode.imp, 0x40); add("rts", AddrMode.imp, 0x60); + // SBC + add("sbc", AddrMode.imm, 0xE9); add("sbc", AddrMode.zp, 0xE5); + add("sbc", AddrMode.zpx, 0xF5); add("sbc", AddrMode.abs, 0xED); + add("sbc", AddrMode.absx, 0xFD); add("sbc", AddrMode.absy, 0xF9); + add("sbc", AddrMode.izx, 0xE1); add("sbc", AddrMode.izy, 0xF1); + // Flags set + add("sec", AddrMode.imp, 0x38); add("sed", AddrMode.imp, 0xF8); + add("sei", AddrMode.imp, 0x78); + // STA + add("sta", AddrMode.zp, 0x85); add("sta", AddrMode.zpx, 0x95); + add("sta", AddrMode.abs, 0x8D); add("sta", AddrMode.absx, 0x9D); + add("sta", AddrMode.absy, 0x99); add("sta", AddrMode.izx, 0x81); + add("sta", AddrMode.izy, 0x91); + // STX / STY + add("stx", AddrMode.zp, 0x86); add("stx", AddrMode.zpy, 0x96); + add("stx", AddrMode.abs, 0x8E); + add("sty", AddrMode.zp, 0x84); add("sty", AddrMode.zpx, 0x94); + add("sty", AddrMode.abs, 0x8C); + // Transfers + add("tax", AddrMode.imp, 0xAA); add("tay", AddrMode.imp, 0xA8); + add("tsx", AddrMode.imp, 0xBA); add("txa", AddrMode.imp, 0x8A); + add("txs", AddrMode.imp, 0x9A); add("tya", AddrMode.imp, 0x98); +} + +// --------------------------------------------------------------------------- +// Character helpers +// --------------------------------------------------------------------------- + +private bool isIdentStart(char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; +} + +private bool isIdentChar(char c) +{ + return isIdentStart(c) || (c >= '0' && c <= '9'); +} + +private int hexVal(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return 0; +} + +private long parseHex(string s) +{ + long v = 0; + foreach (c; s) v = v * 16 + hexVal(c); + return v; +} + +private void skipWs(string s, ref size_t pos) +{ + while (pos < s.length && (s[pos] == ' ' || s[pos] == '\t')) pos++; +} + +private string stripComment(string s) +{ + bool inS = false, inD = false; + for (size_t i = 0; i < s.length; i++) + { + char c = s[i]; + if (inS) + { + if (c == '\\') i++; + else if (c == '\'') inS = false; + continue; + } + if (inD) + { + if (c == '\\') i++; + else if (c == '"') inD = false; + continue; + } + if (c == '\'') inS = true; + else if (c == '"') inD = true; + else if (c == ';') return s[0 .. i]; + } + return s; +} + +private int findTopLevelComma(string s) +{ + int depth = 0; + bool inS = false, inD = false; + for (size_t i = 0; i < s.length; i++) + { + char c = s[i]; + if (inS) + { + if (c == '\\') i++; + else if (c == '\'') inS = false; + continue; + } + if (inD) + { + if (c == '\\') i++; + else if (c == '"') inD = false; + continue; + } + if (c == '\'') inS = true; + else if (c == '"') inD = true; + else if (c == '(') depth++; + else if (c == ')') depth--; + else if (c == ',' && depth == 0) return cast(int)i; + } + return -1; +} + +private int matchParen(string s, size_t start) +{ + int depth = 0; + bool inS = false, inD = false; + for (size_t i = start; i < s.length; i++) + { + char c = s[i]; + if (inS) + { + if (c == '\\') i++; + else if (c == '\'') inS = false; + continue; + } + if (inD) + { + if (c == '\\') i++; + else if (c == '"') inD = false; + continue; + } + if (c == '\'') inS = true; + else if (c == '"') inD = true; + else if (c == '(') depth++; + else if (c == ')') + { + depth--; + if (depth == 0) return cast(int)i; + } + } + return -1; +} + +private string[] splitCommas(string s) +{ + string[] buf; + size_t start = 0; + int depth = 0; + bool inS = false, inD = false; + for (size_t i = 0; i < s.length; i++) + { + char c = s[i]; + if (inS) + { + if (c == '\\') i++; + else if (c == '\'') inS = false; + continue; + } + if (inD) + { + if (c == '\\') i++; + else if (c == '"') inD = false; + continue; + } + if (c == '\'') inS = true; + else if (c == '"') inD = true; + else if (c == '(') depth++; + else if (c == ')') depth--; + else if (c == ',' && depth == 0) + { + buf ~= s[start .. i]; + start = i + 1; + } + } + buf ~= s[start .. $]; + return buf; +} + +// --------------------------------------------------------------------------- +// The assembler +// --------------------------------------------------------------------------- + +private struct Asm +{ + string filename; + Line[] lines; + SymEntry[string] syms; + string[] defOrder; + string[] impOrder; + long[string] equates; + string entryName; + SegState[] segs; + int activeSeg; + int[string] segIndex; + Reloc[] relocs; + + void err(int lineNo, string msg) + { + throw new Exception(format("%s:%d: %s", filename, lineNo, msg)); + } + + // ---- driver ---------------------------------------------------------- + + Object assemble(string source) + { + parseLines(source); + prepass(); + assignIndices(); + sizing(); + emit(); + return buildObject(); + } + + // ---- line parsing ---------------------------------------------------- + + void parseLines(string source) + { + auto raw = source.split("\n"); + foreach (i, r; raw) + { + string s = r; + if (s.length && s[$ - 1] == '\r') s = s[0 .. $ - 1]; + Line l; + l.lineNo = cast(int)(i + 1); + l.text = s; + parseLine(s, l); + if (l.op.length == 0 && l.label.length == 0 && !l.isEquate && !l.isDirective) + continue; + lines ~= l; + } + } + + void parseLine(string src, ref Line l) + { + string s = strip(stripComment(src)); + if (s.length == 0) return; + + // label? + if (isIdentStart(s[0])) + { + size_t p = 0; + while (p < s.length && isIdentChar(s[p])) p++; + if (p < s.length && s[p] == ':') + { + l.label = s[0 .. p]; + addLabel(l.label, l.lineNo); + s = strip(s[p + 1 .. $]); + if (s.length == 0) + { + l.labelOnly = true; + return; + } + } + } + + // equate: ident '=' expr (not '==') + if (isIdentStart(s[0])) + { + size_t p = 0; + while (p < s.length && isIdentChar(s[p])) p++; + size_t q = p; + while (q < s.length && (s[q] == ' ' || s[q] == '\t')) q++; + if (q < s.length && s[q] == '=' && (q + 1 >= s.length || s[q + 1] != '=')) + { + string name = s[0 .. p]; + string rhs = strip(s[q + 1 .. $]); + if (rhs.length == 0) err(l.lineNo, "missing value in equate"); + l.isEquate = true; + l.eqName = name; + l.expr = parseExpr(rhs, l.lineNo); + return; + } + } + + if (s[0] == '.') + { + l.isDirective = true; + parseDirective(s, l); + return; + } + parseInstruction(s, l); + } + + void addLabel(string name, int lineNo) + { + if (auto p = name in syms) + { + if (p.imported) err(lineNo, "label '" ~ name ~ "' conflicts with imported symbol"); + err(lineNo, "duplicate symbol '" ~ name ~ "'"); + } + SymEntry e; + e.name = name; + e.defined = true; + syms[name] = e; + defOrder ~= name; + } + + void parseInstruction(string s, ref Line l) + { + size_t p = 0; + while (p < s.length && isIdentChar(s[p])) p++; + l.op = toLower(s[0 .. p]); + parseOperand(strip(s[p .. $]), l); + } + + void parseOperand(string rest, ref Line l) + { + if (rest.length == 0) + { + l.form = OpForm.imp; + return; + } + if (toLower(rest) == "a") + { + l.form = OpForm.acc; + return; + } + if (rest[0] == '#') + { + l.form = OpForm.imm; + l.expr = parseExpr(strip(rest[1 .. $]), l.lineNo); + return; + } + if (rest[0] == '(') + { + int close = matchParen(rest, 0); + if (close < 0) err(l.lineNo, "missing ')' in operand"); + string inner = strip(rest[1 .. close]); + string trailing = strip(rest[close + 1 .. $]); + if (trailing.length == 0) + { + int ci = findTopLevelComma(inner); + if (ci >= 0) + { + string a = strip(inner[0 .. ci]); + string b = strip(inner[ci + 1 .. $]); + if (toLower(b) != "x") err(l.lineNo, "expected ',x' in (zp,x) operand"); + l.form = OpForm.izx; + l.expr = parseExpr(a, l.lineNo); + } + else + { + l.form = OpForm.ind; + l.expr = parseExpr(inner, l.lineNo); + } + } + else if (toLower(trailing) == ",y") + { + l.form = OpForm.izy; + l.expr = parseExpr(inner, l.lineNo); + } + else + err(l.lineNo, "invalid operand '" ~ rest ~ "'"); + return; + } + int ci = findTopLevelComma(rest); + if (ci >= 0) + { + string a = strip(rest[0 .. ci]); + string b = strip(rest[ci + 1 .. $]); + if (toLower(b) == "x") l.indexX = true; + else if (toLower(b) == "y") l.indexY = true; + else err(l.lineNo, "invalid index register '" ~ b ~ "'"); + l.form = OpForm.direct; + l.expr = parseExpr(a, l.lineNo); + } + else + { + l.form = OpForm.direct; + l.expr = parseExpr(rest, l.lineNo); + } + } + + void parseDirective(string s, ref Line l) + { + size_t p = 1; + while (p < s.length && (isIdentChar(s[p]))) p++; + l.op = toLower(s[0 .. p]); + string rest = strip(s[p .. $]); + switch (l.op) + { + case ".org": + l.expr = parseExpr(rest, l.lineNo); + break; + case ".entry": + l.entrySym = parseIdentName(rest, l.lineNo); + break; + case ".code", ".data", ".bss": + break; + case ".byte", ".db": + parseDataList(rest, l); + break; + case ".word", ".dw": + parseExprList(rest, l); + break; + case ".res", ".ds": + parseRes(rest, l); + break; + case ".ascii", ".asciiz": + l.strBytes = parseString(rest, l.lineNo); + break; + case ".export", ".import", ".zp": + l.names = parseNameList(rest, l.lineNo); + break; + default: + err(l.lineNo, "unknown directive '" ~ l.op ~ "'"); + } + } + + void parseDataList(string rest, ref Line l) + { + if (strip(rest).length == 0) err(l.lineNo, "missing operand"); + foreach (raw; splitCommas(rest)) + { + string pt = strip(raw); + if (pt.length == 0) err(l.lineNo, "empty .byte item"); + DataItem it; + if (pt.length >= 2 && pt[0] == '"' && pt[$ - 1] == '"') + { + it.isStr = true; + it.bytes = decodeString(pt, l.lineNo); + } + else + { + it.isStr = false; + it.expr = parseExpr(pt, l.lineNo); + } + l.items ~= it; + } + } + + void parseExprList(string rest, ref Line l) + { + if (strip(rest).length == 0) err(l.lineNo, "missing operand"); + foreach (raw; splitCommas(rest)) + { + string pt = strip(raw); + if (pt.length == 0) err(l.lineNo, "empty expression"); + l.exprs ~= parseExpr(pt, l.lineNo); + } + } + + void parseRes(string rest, ref Line l) + { + auto parts = splitCommas(rest); + if (parts.length == 0 || strip(parts[0]).length == 0) err(l.lineNo, "missing count"); + l.exprs ~= parseExpr(strip(parts[0]), l.lineNo); + if (parts.length >= 2) + { + l.hasFill = true; + l.exprFill = parseExpr(strip(parts[1]), l.lineNo); + } + if (parts.length > 2) err(l.lineNo, "too many operands for .res"); + } + + ubyte[] parseString(string rest, int lineNo) + { + rest = strip(rest); + if (rest.length < 2 || rest[0] != '"' || rest[$ - 1] != '"') + err(lineNo, "expected a string literal"); + return decodeString(rest, lineNo); + } + + ubyte[] decodeString(string s, int lineNo) + { + ubyte[] buf; + for (size_t i = 1; i + 1 < s.length; i++) + { + char c = s[i]; + if (c == '\\') + { + i++; + if (i >= s.length - 1) err(lineNo, "unterminated escape"); + switch (s[i]) + { + case 'n': buf ~= '\n'; break; + case 'r': buf ~= '\r'; break; + case 't': buf ~= '\t'; break; + case '0': buf ~= 0; break; + case '\\': buf ~= '\\'; break; + case '\'': buf ~= '\''; break; + case '"': buf ~= '"'; break; + default: err(lineNo, "unknown escape '\\" ~ s[i] ~ "'"); + } + } + else + buf ~= cast(ubyte)c; + } + return buf; + } + + string[] parseNameList(string rest, int lineNo) + { + string[] buf; + if (strip(rest).length == 0) err(lineNo, "missing name"); + foreach (raw; splitCommas(rest)) + { + string n = strip(raw); + if (n.length == 0) err(lineNo, "empty name"); + buf ~= n; + } + return buf; + } + + string parseIdentName(string rest, int lineNo) + { + rest = strip(rest); + if (rest.length == 0 || !isIdentStart(rest[0])) err(lineNo, "expected identifier"); + size_t p = 0; + while (p < rest.length && isIdentChar(rest[p])) p++; + if (p != rest.length) err(lineNo, "invalid identifier '" ~ rest ~ "'"); + return rest; + } + + // ---- expressions ----------------------------------------------------- + + Expr parseExpr(string s, int lineNo) + { + size_t pos = 0; + Expr e = parseAddSub(s, pos, lineNo); + skipWs(s, pos); + if (pos != s.length) err(lineNo, "unexpected '" ~ s[pos .. $] ~ "' in expression"); + return e; + } + + Expr parseAddSub(string s, ref size_t pos, int lineNo) + { + Expr left = parseTerm(s, pos, lineNo); + while (true) + { + skipWs(s, pos); + if (pos < s.length && (s[pos] == '+' || s[pos] == '-')) + { + char op = s[pos]; + pos++; + Expr right = parseTerm(s, pos, lineNo); + left = combine(left, right, op, lineNo); + } + else + break; + } + return left; + } + + Expr parseTerm(string s, ref size_t pos, int lineNo) + { + skipWs(s, pos); + bool low = false, high = false; + if (pos < s.length && (s[pos] == '<' || s[pos] == '>')) + { + if (s[pos] == '<') low = true; + else high = true; + pos++; + } + skipWs(s, pos); + bool neg = false; + if (pos < s.length && s[pos] == '-') + { + neg = true; + pos++; + } + Expr e = parsePrimary(s, pos, lineNo); + if (neg) + { + if (!e.isNum) err(lineNo, "cannot negate a symbol"); + e.num = -e.num; + } + if (low) e.low = true; + if (high) e.high = true; + return e; + } + + Expr parsePrimary(string s, ref size_t pos, int lineNo) + { + skipWs(s, pos); + if (pos >= s.length) err(lineNo, "unexpected end of expression"); + char c = s[pos]; + if (c == '(') + { + pos++; + Expr e = parseAddSub(s, pos, lineNo); + skipWs(s, pos); + if (pos >= s.length || s[pos] != ')') err(lineNo, "missing ')'"); + pos++; + return e; + } + if (c == '\'') + { + pos++; + int v; + if (pos < s.length && s[pos] == '\\') + { + pos++; + if (pos >= s.length) err(lineNo, "unterminated char literal"); + switch (s[pos]) + { + case 'n': v = '\n'; break; + case 'r': v = '\r'; break; + case 't': v = '\t'; break; + case '0': v = 0; break; + case '\\': v = '\\'; break; + case '\'': v = '\''; break; + case '"': v = '"'; break; + default: err(lineNo, "unknown escape in char literal"); + } + pos++; + } + else + { + if (pos >= s.length) err(lineNo, "unterminated char literal"); + v = cast(int)cast(ubyte)s[pos]; + pos++; + } + if (pos >= s.length || s[pos] != '\'') err(lineNo, "unterminated char literal"); + pos++; + Expr e; + e.isNum = true; + e.num = v; + return e; + } + if (c == '$') + { + pos++; + size_t st = pos; + while (pos < s.length && isHexDigit(s[pos])) pos++; + if (pos == st) err(lineNo, "invalid hex literal"); + Expr e; + e.isNum = true; + e.num = cast(int)parseHex(s[st .. pos]); + return e; + } + if (c == '%') + { + pos++; + size_t st = pos; + while (pos < s.length && (s[pos] == '0' || s[pos] == '1')) pos++; + if (pos == st) err(lineNo, "invalid binary literal"); + int v = 0; + foreach (ch; s[st .. pos]) v = v * 2 + (ch - '0'); + Expr e; + e.isNum = true; + e.num = v; + return e; + } + if (c >= '0' && c <= '9') + { + size_t st = pos; + if (c == '0' && pos + 1 < s.length && (s[pos + 1] == 'x' || s[pos + 1] == 'X')) + { + pos += 2; + size_t hs = pos; + while (pos < s.length && isHexDigit(s[pos])) pos++; + if (pos == hs) err(lineNo, "invalid hex literal"); + Expr e; + e.isNum = true; + e.num = cast(int)parseHex(s[hs .. pos]); + return e; + } + while (pos < s.length && s[pos] >= '0' && s[pos] <= '9') pos++; + Expr e; + e.isNum = true; + e.num = to!int(s[st .. pos]); + return e; + } + if (isIdentStart(c)) + { + size_t st = pos; + while (pos < s.length && isIdentChar(s[pos])) pos++; + Expr e; + e.isNum = false; + e.sym = s[st .. pos]; + e.offset = 0; + return e; + } + err(lineNo, "invalid character '" ~ c ~ "' in expression"); + return Expr.init; + } + + Expr combine(Expr a, Expr b, char op, int lineNo) + { + int sign = (op == '+') ? 1 : -1; + Expr r; + if (a.isNum && b.isNum) + { + r.isNum = true; + r.num = a.num + sign * b.num; + return r; + } + if (!a.isNum && b.isNum) + { + r.isNum = false; + r.sym = a.sym; + r.offset = a.offset + sign * b.num; + r.low = a.low; + r.high = a.high; + return r; + } + if (a.isNum && !b.isNum) + { + if (op == '-') err(lineNo, "cannot subtract a symbol from a constant"); + r.isNum = false; + r.sym = b.sym; + r.offset = b.offset + a.num; + r.low = b.low; + r.high = b.high; + return r; + } + err(lineNo, "cannot combine two symbols in an expression"); + return Expr.init; + } + + long evalConstExpr(Expr e, int lineNo) + { + if (e.isNum) return e.num; + if (e.sym.length == 0) err(lineNo, "empty expression"); + if (auto p = e.sym in equates) return *p + e.offset; + err(lineNo, "symbol '" ~ e.sym ~ "' is not a constant"); + return 0; + } + + bool resolveValue(Expr e, out int v) + { + if (e.isNum) + { + v = e.num; + return true; + } + if (e.sym.length == 0) return false; + if (auto p = e.sym in equates) + { + v = cast(int)*p + e.offset; + return true; + } + if (auto s = e.sym in syms) + { + if (s.defined && s.valueKnown) + { + v = cast(int)s.value + e.offset; + return true; + } + } + return false; + } + + // ---- prepass --------------------------------------------------------- + + void prepass() + { + foreach (ref l; lines) + { + if (l.isEquate) + { + if (l.eqName in syms) err(l.lineNo, "duplicate symbol '" ~ l.eqName ~ "'"); + equates[l.eqName] = evalConstExpr(l.expr, l.lineNo); + } + else if (l.isDirective) + { + switch (l.op) + { + case ".import": + foreach (n; l.names) + { + if (n in syms) err(l.lineNo, "symbol '" ~ n ~ "' already defined"); + SymEntry e; + e.name = n; + e.imported = true; + e.global = true; + syms[n] = e; + impOrder ~= n; + } + break; + case ".export": + foreach (n; l.names) + { + auto p = n in syms; + if (p is null) err(l.lineNo, "exported symbol '" ~ n ~ "' is not defined"); + p.global = true; + } + break; + case ".entry": + entryName = l.entrySym; + break; + default: + break; + } + } + } + foreach (ref l; lines) + { + if (l.isDirective && l.op == ".zp") + { + foreach (n; l.names) + { + auto p = n in syms; + if (p is null) err(l.lineNo, "zp symbol '" ~ n ~ "' is not defined"); + p.zeropage = true; + } + } + } + } + + void assignIndices() + { + int idx = 0; + foreach (n; defOrder) + { + if (auto p = n in syms) p.index = idx++; + } + foreach (n; impOrder) + { + if (auto p = n in syms) p.index = idx++; + } + } + + // ---- segments -------------------------------------------------------- + + ubyte segFlags(string name) + { + switch (name) + { + case ".code": return SEG_CODE; + case ".data": return SEG_DATA; + case ".bss": return SEG_BSS; + default: return SEG_CODE; + } + } + + void switchSeg(string name) + { + if (name !in segIndex) + { + SegState s; + s.name = name; + s.flags = segFlags(name); + segIndex[name] = cast(int)segs.length; + segs ~= s; + } + activeSeg = segIndex[name]; + } + + // ---- sizing ---------------------------------------------------------- + + void sizing() + { + switchSeg(".code"); + foreach (_; 0 .. 1000) + { + if (!layoutOnce()) break; + } + } + + bool layoutOnce() + { + foreach (ref s; segs) s.len = 0; + foreach (ref e; syms) e.valueKnown = false; + activeSeg = segIndex[".code"]; + bool changed = false; + foreach (ref l; lines) + { + if (l.label.length) + { + auto p = l.label in syms; + p.value = cast(ushort)segs[activeSeg].len; + p.seg = activeSeg; + p.defined = true; + p.valueKnown = true; + } + if (l.isEquate || l.labelOnly) continue; + if (l.isDirective) + { + sizingDirective(l); + continue; + } + AddrMode m = resolveInstMode(l); + if (!l.modeSet || l.mode != m) + { + if (l.modeSet && l.mode != m) changed = true; + l.mode = m; + l.modeSet = true; + } + segs[activeSeg].len += instSize(m); + } + return changed; + } + + void sizingDirective(ref Line l) + { + switch (l.op) + { + case ".org": + doOrgLen(cast(int)evalConstExpr(l.expr, l.lineNo), l.lineNo); + break; + case ".code": switchSeg(".code"); break; + case ".data": switchSeg(".data"); break; + case ".bss": switchSeg(".bss"); break; + case ".byte", ".db": + foreach (it; l.items) segs[activeSeg].len += it.isStr ? it.bytes.length : 1; + break; + case ".word", ".dw": + segs[activeSeg].len += l.exprs.length * 2; + break; + case ".res", ".ds": + segs[activeSeg].len += cast(size_t)evalConstExpr(l.exprs[0], l.lineNo); + break; + case ".ascii": + segs[activeSeg].len += l.strBytes.length; + break; + case ".asciiz": + segs[activeSeg].len += l.strBytes.length + 1; + break; + case ".export", ".import", ".zp", ".entry": + break; + default: + err(l.lineNo, "unknown directive '" ~ l.op ~ "'"); + } + } + + void doOrgLen(int newAbs, int lineNo) + { + ref SegState s = segs[activeSeg]; + if (s.len == 0) + { + s.origin = cast(ushort)newAbs; + return; + } + int off = newAbs - cast(int)s.origin; + if (off < cast(int)s.len) err(lineNo, ".org moves backwards"); + s.len = cast(size_t)off; + } + + AddrMode resolveInstMode(ref Line l) + { + auto op = l.op in gOpTable; + if (op is null) err(l.lineNo, "unknown instruction '" ~ l.op ~ "'"); + auto ops = *op; + switch (l.form) + { + case OpForm.imp: + if (AddrMode.acc in ops) return AddrMode.acc; + if (AddrMode.imp in ops) return AddrMode.imp; + err(l.lineNo, "instruction '" ~ l.op ~ "' has no implied form"); + break; + case OpForm.acc: + if (AddrMode.acc in ops) return AddrMode.acc; + err(l.lineNo, "instruction '" ~ l.op ~ "' has no accumulator form"); + break; + case OpForm.imm: + if (AddrMode.imm in ops) return AddrMode.imm; + err(l.lineNo, "instruction '" ~ l.op ~ "' has no immediate form"); + break; + case OpForm.izx: + if (AddrMode.izx in ops) return AddrMode.izx; + err(l.lineNo, "instruction '" ~ l.op ~ "' has no (zp,x) form"); + break; + case OpForm.izy: + if (AddrMode.izy in ops) return AddrMode.izy; + err(l.lineNo, "instruction '" ~ l.op ~ "' has no (zp),y form"); + break; + case OpForm.ind: + if (AddrMode.ind in ops) return AddrMode.ind; + err(l.lineNo, "instruction '" ~ l.op ~ "' has no indirect form"); + break; + case OpForm.direct: + if (l.indexX) return pickZpAbs(AddrMode.zpx, AddrMode.absx, l, ops); + if (l.indexY) return pickZpAbs(AddrMode.zpy, AddrMode.absy, l, ops); + if (AddrMode.rel in ops) return AddrMode.rel; + return pickZpAbs(AddrMode.zp, AddrMode.abs, l, ops); + default: + break; + } + return AddrMode.imp; + } + + AddrMode pickZpAbs(AddrMode zp, AddrMode ab, ref Line l, ubyte[AddrMode] ops) + { + bool zpOk = (zp in ops) !is null; + bool abOk = (ab in ops) !is null; + if (zpOk && isZp(l.expr)) return zp; + if (abOk) return ab; + if (zpOk) return zp; + err(l.lineNo, "instruction '" ~ l.op ~ "' has no such addressing mode"); + return ab; + } + + bool isZp(Expr e) + { + if (e.isNum) return e.num >= 0 && e.num <= 0xFF; + if (auto q = e.sym in equates) + { + int v = cast(int)*q + e.offset; + return v >= 0 && v <= 0xFF; + } + auto s = e.sym in syms; + if (s is null) return false; + if (s.imported) return s.zeropage; + if (s.defined && s.valueKnown && s.seg >= 0 && segs[s.seg].origin != 0) + { + int addr = cast(int)segs[s.seg].origin + cast(int)s.value + e.offset; + return addr >= 0 && addr <= 0xFF; + } + return false; + } + + int instSize(AddrMode m) + { + switch (m) + { + case AddrMode.imp, AddrMode.acc: + return 1; + case AddrMode.imm, AddrMode.zp, AddrMode.zpx, AddrMode.zpy, + AddrMode.izx, AddrMode.izy, AddrMode.rel: + return 2; + default: + return 3; + } + } + + // ---- emission -------------------------------------------------------- + + void emit() + { + foreach (ref s; segs) + { + s.len = 0; + s.data = null; + } + activeSeg = segIndex[".code"]; + foreach (ref l; lines) + { + if (l.label.length) + { + auto p = l.label in syms; + p.value = cast(ushort)segs[activeSeg].len; + p.seg = activeSeg; + p.defined = true; + p.valueKnown = true; + } + if (l.isEquate || l.labelOnly) continue; + if (l.isDirective) + { + emitDirective(l); + continue; + } + emitInstruction(l); + } + } + + void emitByte(ubyte b) + { + segs[activeSeg].data ~= b; + segs[activeSeg].len = segs[activeSeg].data.length; + } + + void emitInstruction(ref Line l) + { + ubyte op = gOpTable[l.op][l.mode]; + emitByte(op); + switch (l.mode) + { + case AddrMode.imp, AddrMode.acc: + break; + case AddrMode.imm, AddrMode.zp, AddrMode.zpx, AddrMode.zpy, + AddrMode.izx, AddrMode.izy: + emitRef(l.expr, 1, l.lineNo); + break; + case AddrMode.rel: + emitBranch(l.expr, l.lineNo); + break; + default: + emitRef(l.expr, 2, l.lineNo); + break; + } + } + + void emitConst(int v, int size, bool low, bool high) + { + if (size == 1) + emitByte(high ? cast(ubyte)((v >> 8) & 0xFF) : cast(ubyte)(v & 0xFF)); + else + { + emitByte(cast(ubyte)(v & 0xFF)); + emitByte(cast(ubyte)((v >> 8) & 0xFF)); + } + } + + void emitRef(Expr e, int size, int lineNo) + { + bool isConst = e.isNum || (e.sym in equates) !is null; + if (isConst) + { + int v; + resolveValue(e, v); + emitConst(v, size, e.low, e.high); + return; + } + auto p = e.sym in syms; + if (p is null) err(lineNo, "undefined symbol '" ~ e.sym ~ "'"); + // Resolve defined symbols that live in a fixed-origin segment. + if (p.defined && p.seg >= 0 && segs[p.seg].origin != 0) + { + int addr = cast(int)segs[p.seg].origin + cast(int)p.value + e.offset; + emitConst(addr, size, e.low, e.high); + return; + } + if (e.offset != 0) + err(lineNo, "relocation with offset is not supported for symbol '" ~ e.sym ~ "'"); + Reloc r; + r.seg = cast(ushort)activeSeg; + r.sym = cast(ushort)p.index; + r.offset = cast(ushort)segs[activeSeg].len; + if (size == 2) + { + if (e.low) r.type = RelocType.lo8; + else if (e.high) r.type = RelocType.hi8; + else r.type = RelocType.abs16; + relocs ~= r; + emitByte(0); + emitByte(0); + } + else + { + r.type = e.high ? RelocType.hi8 : RelocType.lo8; + relocs ~= r; + emitByte(0); + } + } + + void emitBranch(Expr e, int lineNo) + { + int operandOff = cast(int)segs[activeSeg].len; + if (e.isNum || (e.sym in equates) !is null) + { + if (segs[activeSeg].origin == 0) + err(lineNo, "branch to a constant in a relocatable segment"); + int v; + resolveValue(e, v); + int endAddr = cast(int)segs[activeSeg].origin + operandOff + 1; + int disp = v - endAddr; + if (disp < -128 || disp > 127) err(lineNo, "branch out of range"); + emitByte(cast(ubyte)(disp & 0xFF)); + return; + } + auto p = e.sym in syms; + if (p is null) err(lineNo, "undefined symbol '" ~ e.sym ~ "'"); + if (p.defined && p.seg == activeSeg) + { + int disp = cast(int)p.value + e.offset - (operandOff + 1); + if (disp < -128 || disp > 127) err(lineNo, "branch out of range"); + emitByte(cast(ubyte)(disp & 0xFF)); + return; + } + if (p.defined && p.seg >= 0 && segs[p.seg].origin != 0 && segs[activeSeg].origin != 0) + { + int targetAddr = cast(int)segs[p.seg].origin + cast(int)p.value + e.offset; + int endAddr = cast(int)segs[activeSeg].origin + operandOff + 1; + int disp = targetAddr - endAddr; + if (disp < -128 || disp > 127) err(lineNo, "branch out of range"); + emitByte(cast(ubyte)(disp & 0xFF)); + return; + } + if (e.offset != 0) err(lineNo, "relocation with offset is not supported"); + Reloc r; + r.seg = cast(ushort)activeSeg; + r.sym = cast(ushort)p.index; + r.offset = cast(ushort)operandOff; + r.type = RelocType.rel8; + relocs ~= r; + emitByte(0); + } + + void emitDirective(ref Line l) + { + switch (l.op) + { + case ".org": + doOrgEmit(cast(int)evalConstExpr(l.expr, l.lineNo), l.lineNo); + break; + case ".code": switchSeg(".code"); break; + case ".data": switchSeg(".data"); break; + case ".bss": switchSeg(".bss"); break; + case ".byte", ".db": + foreach (it; l.items) + { + if (it.isStr) + foreach (b; it.bytes) emitByte(b); + else + emitRef(it.expr, 1, l.lineNo); + } + break; + case ".word", ".dw": + foreach (e; l.exprs) emitRef(e, 2, l.lineNo); + break; + case ".res", ".ds": + { + int count = cast(int)evalConstExpr(l.exprs[0], l.lineNo); + ubyte fill = 0; + if (l.hasFill) fill = cast(ubyte)evalConstExpr(l.exprFill, l.lineNo); + foreach (_; 0 .. count) emitByte(fill); + break; + } + case ".ascii": + foreach (b; l.strBytes) emitByte(b); + break; + case ".asciiz": + foreach (b; l.strBytes) emitByte(b); + emitByte(0); + break; + case ".export", ".import", ".zp", ".entry": + break; + default: + err(l.lineNo, "unknown directive '" ~ l.op ~ "'"); + } + } + + void doOrgEmit(int newAbs, int lineNo) + { + ref SegState s = segs[activeSeg]; + if (s.len == 0) + { + s.origin = cast(ushort)newAbs; + return; + } + int off = newAbs - cast(int)s.origin; + if (off < cast(int)s.len) err(lineNo, ".org moves backwards"); + while (cast(int)s.len < off) emitByte(0); + } + + // ---- object construction --------------------------------------------- + + Object buildObject() + { + Object obj; + int[] remap; + remap.length = segs.length; + foreach (i, ref s; segs) remap[i] = -1; + + foreach (i, ref s; segs) + { + if (s.len == 0 && s.data.length == 0) continue; + Segment seg; + seg.name = s.name; + seg.flags = s.flags; + seg.origin = s.origin; + if ((s.flags & 0x08) != 0) // bss bit only (SEG_BSS also carries alloc|write) + seg.data = new ubyte[s.len]; + else + seg.data = s.data; + remap[i] = cast(int)obj.segments.length; + obj.segments ~= seg; + } + + foreach (n; defOrder) + { + auto p = n in syms; + if (p is null) continue; + Symbol sym; + sym.name = p.name; + sym.value = p.value; + sym.flags = SymFlag.defined; + if (p.global) sym.flags |= SymFlag.global; + bool zp = false; + if (p.seg >= 0 && segs[p.seg].origin != 0) + zp = (cast(int)segs[p.seg].origin + cast(int)p.value) <= 0xFF; + if (zp) sym.flags |= SymFlag.zeropage; + sym.seg = (p.seg >= 0 && remap[p.seg] >= 0) ? cast(ushort)remap[p.seg] : NoIndex; + obj.symbols ~= sym; + } + foreach (n; impOrder) + { + auto p = n in syms; + if (p is null) continue; + Symbol sym; + sym.name = p.name; + sym.value = 0; + sym.flags = SymFlag.imported | SymFlag.global; + if (p.zeropage) sym.flags |= SymFlag.zeropage; + sym.seg = NoIndex; + obj.symbols ~= sym; + } + + foreach (r; relocs) + { + Reloc rr = r; + if (r.seg >= 0 && remap[r.seg] >= 0) rr.seg = cast(ushort)remap[r.seg]; + obj.relocs ~= rr; + } + + if (entryName.length) + { + auto p = entryName in syms; + if (p is null || !p.defined) err(0, "entry symbol '" ~ entryName ~ "' is not defined"); + obj.entry = p.value; + obj.hasEntry = true; + } + else if (auto sp = "start" in syms) + { + if (sp.defined) + { + obj.entry = sp.value; + obj.hasEntry = true; + } + } + return obj; + } +} + +// --------------------------------------------------------------------------- +// Unittests +// --------------------------------------------------------------------------- + +unittest +{ + // LDA immediate + auto o = assembleSource("lda #$42\n"); + assert(o.segments.length == 1); + assert(o.segments[0].data == [0xA9, 0x42]); + + // zero page numeric + auto o2 = assembleSource(".org $0000\nlda $10\n"); + assert(o2.segments[0].data == [0xA5, 0x10]); + + // absolute numeric (fixed origin) + auto o3 = assembleSource(".org $0600\nlda $1234\n"); + assert(o3.segments[0].data == [0xAD, 0x34, 0x12]); + + // zp,x + auto o4 = assembleSource(".org $0000\nlda $10,x\n"); + assert(o4.segments[0].data == [0xB5, 0x10]); + + // abs,x + auto o5 = assembleSource(".org $0000\nlda $1234,x\n"); + assert(o5.segments[0].data == [0xBD, 0x34, 0x12]); + + // (zp,x) + auto o6 = assembleSource(".org $0000\nlda ($10,x)\n"); + assert(o6.segments[0].data == [0xA1, 0x10]); + + // (zp),y + auto o7 = assembleSource(".org $0000\nlda ($10),y\n"); + assert(o7.segments[0].data == [0xB1, 0x10]); + + // (abs) jmp + auto o8 = assembleSource(".org $0000\njmp ($1234)\n"); + assert(o8.segments[0].data == [0x6C, 0x34, 0x12]); + + // backward branch displacement + auto o9 = assembleSource(".org $0000\nloop:\ndex\nbne loop\n"); + assert(o9.segments[0].data == [0xCA, 0xD0, 0xFD]); + + // directives + auto o10 = assembleSource(".org $0000\n.byte 1,2,3\n.word $1234\n.res 2\n.ascii \"A\"\n.asciiz \"B\"\n"); + assert(o10.segments[0].data == [1, 2, 3, 0x34, 0x12, 0, 0, 'A', 'B', 0]); + + // export / import reloc + auto o11 = assembleSource(".export start\n.import putc\nstart:\njsr putc\nrts\n"); + assert(o11.segments[0].data[0] == 0x20); + assert(o11.relocs.length == 1); + assert(o11.relocs[0].type == RelocType.abs16); + assert(o11.relocs[0].offset == 1); + int pi = o11.findSymbol("putc"); + assert(pi >= 0); + assert(o11.relocs[0].sym == pi); + assert(o11.findSymbol("start") >= 0); + + // accumulator form + auto o12 = assembleSource(".org $0000\nasl a\nlsr\n"); + assert(o12.segments[0].data == [0x0A, 0x4A]); + + // equate + lo/hi + auto o13 = assembleSource(".org $0000\nV = $1234\nlda #V\n"); + assert(o13.segments[0].data == [0xA9, 0x34, 0xA9, 0x12]); + + // char literal + auto o14 = assembleSource(".org $0000\nlda #'A'\n"); + assert(o14.segments[0].data == [0xA9, 0x41]); +} diff --git a/src/assembler/main.d b/src/assembler/main.d index b0e361f..602d50d 100644 --- a/src/assembler/main.d +++ b/src/assembler/main.d @@ -1,6 +1,75 @@ -import std.stdio; +module assembler.main; -void main() +import std.format : format; +import std.getopt : getopt; +import std.path : setExtension; +import std.stdio : File, stderr, stdout, writeln; +import assembler.assembler : assembleFile; +import wcore.objfmt : Object, writeObject; + +private void printHelp() { - writeln("same deal"); + writeln("wiasm - small 6502 assembler"); + writeln(); + writeln("Usage: wiasm [options] "); + writeln(); + writeln(" -o, --output output object file (default: input with .wo)"); + writeln(" -l, --listing write a listing file"); + writeln(" -h, --help show this help"); +} + +private void writeListing(string path, string input, ref Object obj) +{ + auto f = File(path, "w"); + f.writeln("; listing for ", input); + foreach (s; obj.segments) + { + f.writeln(format("; segment %s flags=$%02X origin=$%04X size=%d", + s.name, s.flags, s.origin, s.data.length)); + for (size_t i = 0; i < s.data.length; i += 16) + { + size_t end = (i + 16 < s.data.length) ? i + 16 : s.data.length; + string hexs; + foreach (b; s.data[i .. end]) hexs ~= format("%02X ", b); + f.writeln(format(" %04X %s", i, hexs)); + } + } + f.writeln("; symbols"); + foreach (sym; obj.symbols) + f.writeln(format(" %-24s = $%04X flags=$%02X seg=%d", + sym.name, sym.value, sym.flags, sym.seg == ushort.max ? -1 : cast(int)sym.seg)); +} + +int main(string[] args) +{ + string outPath; + string listPath; + try + { + auto helpInfo = getopt(args, + "o|output", "output object file", &outPath, + "l|listing", "listing file", &listPath, + ); + if (helpInfo.helpWanted) + { + printHelp(); + return 0; + } + if (args.length < 2) + { + printHelp(); + return 1; + } + string input = args[1]; + if (outPath.length == 0) outPath = setExtension(input, ".wo"); + auto obj = assembleFile(input); + writeObject(outPath, obj); + if (listPath.length) writeListing(listPath, input, obj); + return 0; + } + catch (Exception e) + { + stderr.writeln("wiasm: ", e.msg); + return 1; + } } diff --git a/src/emulator/bus.d b/src/emulator/bus.d new file mode 100644 index 0000000..0219d2a --- /dev/null +++ b/src/emulator/bus.d @@ -0,0 +1,142 @@ +/** + * 6502 system bus: 64 KiB RAM plus memory-mapped terminal I/O. + * + * Memory-mapped I/O registers: + * $F001 write: emit byte to stdout (raw char); read: 0 + * $F002 read: next byte of buffered stdin (0 when exhausted); write: ignored + * $F003 write: set halted flag; read: 0 + * $F004 read: 0x80 when buffered input remains, else 0; write: ignored + */ +module emulator.bus; + +import std.stdio : stdout; + +/// Transmit register: writing a byte prints it to stdout. +enum ushort IO_TX = 0xF001; +/// Receive register: reading consumes the next buffered input byte (0 when empty). +enum ushort IO_RX = 0xF002; +/// Halt register: writing any byte sets the halted flag. +enum ushort IO_HALT = 0xF003; +/// Receive-ready register: 0x80 when input is still available. +enum ushort IO_RXRDY = 0xF004; + +class Bus +{ + /// 64 KiB of RAM. + ubyte[65536] ram; + + /// Set when the program writes to IO_HALT. + bool halted = false; + + private ubyte[] input; + private size_t inputPos = 0; + + /// Load a byte array at an address (wraps within 64 KiB). + void loadAt(ushort addr, const(ubyte)[] data) + { + foreach (i, ubyte b; data) + ram[(addr + i) & 0xFFFF] = b; + } + + /// Set the buffered input stream consumed by reads of IO_RX. + void setInput(const(ubyte)[] data) + { + input = data.dup; + inputPos = 0; + } + + /// True when more buffered input remains. + bool inputAvailable() const + { + return inputPos < input.length; + } + + /// Read a byte from memory or an I/O register. + ubyte read(ushort addr) + { + switch (addr) + { + case IO_TX: + return 0; + case IO_RX: + if (inputPos < input.length) + return input[inputPos++]; + return 0; + case IO_HALT: + return 0; + case IO_RXRDY: + return inputAvailable() ? 0x80 : 0; + default: + return ram[addr]; + } + } + + /// Write a byte to memory or an I/O register. + void write(ushort addr, ubyte val) + { + switch (addr) + { + case IO_TX: + { + ubyte[1] buf; + buf[0] = val; + stdout.rawWrite(buf[]); + stdout.flush(); + break; + } + case IO_RX: + break; // ignored + case IO_HALT: + halted = true; + break; + case IO_RXRDY: + break; // ignored + default: + ram[addr] = val; + break; + } + } +} + +// --------------------------------------------------------------------------- +// unit tests +// --------------------------------------------------------------------------- + +unittest +{ + auto b = new Bus(); + + // plain RAM round trip + b.write(0x0200, 0xAB); + assert(b.read(0x0200) == 0xAB); + + // loadAt writes a block + b.loadAt(0x0300, [0x01, 0x02, 0x03, 0x04]); + assert(b.read(0x0300) == 0x01); + assert(b.read(0x0301) == 0x02); + assert(b.read(0x0303) == 0x04); + + // $F003 write sets halted + assert(b.halted == false); + b.write(0xF003, 0x00); + assert(b.halted == true); + + // $F001 / $F003 / $F004 reads return 0 / 0 / status + b.write(0xF001, 0x00); + assert(b.read(0xF001) == 0); + assert(b.read(0xF003) == 0); + + // input buffer handling + assert(b.inputAvailable() == false); + assert(b.read(0xF004) == 0x00); + assert(b.read(0xF002) == 0x00); // exhausted -> 0 + + b.setInput([0x41, 0x42]); + assert(b.inputAvailable() == true); + assert(b.read(0xF004) == 0x80); + assert(b.read(0xF002) == 0x41); + assert(b.read(0xF004) == 0x80); + assert(b.read(0xF002) == 0x42); + assert(b.read(0xF004) == 0x00); + assert(b.read(0xF002) == 0x00); +} diff --git a/src/emulator/cpu.d b/src/emulator/cpu.d new file mode 100644 index 0000000..c0d0c95 --- /dev/null +++ b/src/emulator/cpu.d @@ -0,0 +1,1014 @@ +/** + * NMOS 6502 CPU core. + * + * Implements all 151 documented legal opcodes with correct flags and base + * cycle counts (including page-cross and branch-taken penalties where the + * real 6502 applies them). Undocumented/illegal opcodes are treated as a + * 1-byte NOP (2 cycles) and never crash. + * + * Decimal (BCD) mode is implemented for ADC and SBC. The N and V flags in + * BCD mode are set from the equivalent binary result (an approximation of + * the hardware's undefined behaviour); Z and C are exact. + * + * The JMP (indirect) page-boundary bug is implemented (low byte is read from + * ptr, high byte from (ptr & 0xFF00) | ((ptr + 1) & 0xFF)). + */ +module emulator.cpu; + +import std.string : format; + +import emulator.bus; + +/// Processor status flag bit masks. +enum : ubyte +{ + F_N = 0x80, /// negative + F_V = 0x40, /// overflow + F_U = 0x20, /// unused; always set + F_B = 0x10, /// break + F_D = 0x08, /// decimal (BCD) + F_I = 0x04, /// interrupt disable + F_Z = 0x02, /// zero + F_C = 0x01, /// carry +} + +class Cpu +{ + Bus bus; + + ubyte A, X, Y; /// accumulator, index registers + ubyte SP; /// stack pointer (page 1) + ushort PC; /// program counter + ubyte P; /// processor status + ulong cycles; /// total cycle counter + + this(Bus b) + { + bus = b; + } + + /// Reset registers (does NOT set PC; the caller sets the entry point). + void reset() + { + A = 0; + X = 0; + Y = 0; + SP = 0xFD; + P = 0x24; // I | unused + cycles = 0; + } + + /// Set the program counter (entry point). + void setPC(ushort pc) + { + PC = pc; + } + + // -- flag helpers ------------------------------------------------------- + + private bool getF(ubyte m) const + { + return (P & m) != 0; + } + + private void setF(ubyte m, bool v) + { + if (v) + P |= m; + else + P &= cast(ubyte) ~m; + } + + private void setNZ(ubyte v) + { + setF(F_Z, v == 0); + setF(F_N, (v & 0x80) != 0); + } + + private void setZ(bool v) { setF(F_Z, v); } + private void setN(bool v) { setF(F_N, v); } + private void setC(bool v) { setF(F_C, v); } + private void setV(bool v) { setF(F_V, v); } + + // -- bus/stack helpers -------------------------------------------------- + + private ubyte fetch() + { + return bus.read(PC++); + } + + private ushort fetch16() + { + ubyte lo = fetch(); + ubyte hi = fetch(); + return cast(ushort) (lo | (hi << 8)); + } + + private ushort read16(ushort a) + { + ubyte lo = bus.read(a); + ubyte hi = bus.read(cast(ushort) (a + 1)); + return cast(ushort) (lo | (hi << 8)); + } + + private void push(ubyte v) + { + bus.write(cast(ushort) (0x0100 + SP), v); + SP--; + } + + private ubyte pop() + { + SP++; + return bus.read(cast(ushort) (0x0100 + SP)); + } + + private void setPFromStack(ubyte v) + { + P = cast(ubyte) (v | F_U); + } + + // -- addressing modes --------------------------------------------------- + + private ushort addrZP() { return fetch(); } + private ushort addrZPX() { return cast(ushort) ((fetch() + X) & 0xFF); } + private ushort addrZPY() { return cast(ushort) ((fetch() + Y) & 0xFF); } + private ushort addrAbs() { return fetch16(); } + + private ushort addrAbsX(out bool cross) + { + ushort b = fetch16(); + ushort r = cast(ushort) (b + X); + cross = (b & 0xFF00) != (r & 0xFF00); + return r; + } + + private ushort addrAbsY(out bool cross) + { + ushort b = fetch16(); + ushort r = cast(ushort) (b + Y); + cross = (b & 0xFF00) != (r & 0xFF00); + return r; + } + + private ushort addrIzx() + { + ushort zp = cast(ushort) ((fetch() + X) & 0xFF); + return read16(zp); + } + + private ushort addrIzy(out bool cross) + { + ushort zp = fetch(); + ushort b = read16(zp); + ushort r = cast(ushort) (b + Y); + cross = (b & 0xFF00) != (r & 0xFF00); + return r; + } + + // -- ALU / logic operations -------------------------------------------- + + private void ldaOp(ubyte m) { A = m; setNZ(A); } + private void ldxOp(ubyte m) { X = m; setNZ(X); } + private void ldyOp(ubyte m) { Y = m; setNZ(Y); } + private void oraOp(ubyte m) { A = cast(ubyte) (A | m); setNZ(A); } + private void andOp(ubyte m) { A = cast(ubyte) (A & m); setNZ(A); } + private void eorOp(ubyte m) { A = cast(ubyte) (A ^ m); setNZ(A); } + private void cmpOp(ubyte m) { setC(A >= m); setNZ(cast(ubyte) (A - m)); } + private void cpxOp(ubyte m) { setC(X >= m); setNZ(cast(ubyte) (X - m)); } + private void cpyOp(ubyte m) { setC(Y >= m); setNZ(cast(ubyte) (Y - m)); } + + private void bitOp(ubyte m) + { + setF(F_Z, (A & m) == 0); + setF(F_N, (m & 0x80) != 0); + setF(F_V, (m & 0x40) != 0); + } + + private void adcOp(ubyte m) + { + if (getF(F_D)) + adcDec(m); + else + adcBin(m); + } + + private void sbcOp(ubyte m) + { + if (getF(F_D)) + sbcDec(m); + else + sbcBin(m); + } + + private void adcBin(ubyte m) + { + uint a = A; + uint b = m; + uint c = getF(F_C) ? 1 : 0; + uint sum = a + b + c; + bool v = ((~(a ^ b)) & (a ^ sum) & 0x80) != 0; + A = cast(ubyte) (sum & 0xFF); + setC(sum > 0xFF); + setV(v); + setNZ(A); + } + + private void sbcBin(ubyte m) + { + uint a = A; + uint b = m; + uint c = getF(F_C) ? 1 : 0; + uint inv = b ^ 0xFF; + uint sum = a + inv + c; + bool v = ((~(a ^ inv)) & (a ^ sum) & 0x80) != 0; + A = cast(ubyte) (sum & 0xFF); + setC(sum > 0xFF); + setV(v); + setNZ(A); + } + + private void adcDec(ubyte m) + { + uint a = A; + uint b = m; + uint carry = getF(F_C) ? 1 : 0; + uint sum = a + b + carry; + uint lo = (a & 0x0F) + (b & 0x0F) + carry; + if (lo > 0x09) + sum += 0x06; + bool c = sum > 0x99; + if (c) + sum += 0x60; + A = cast(ubyte) (sum & 0xFF); + setC(c); + setZ(A == 0); + setN((A & 0x80) != 0); + uint bin = a + b + carry; + setV((~(a ^ b) & (a ^ bin) & 0x80) != 0); + } + + private void sbcDec(ubyte m) + { + int a = A; + int b = m; + int borrow = getF(F_C) ? 0 : 1; + int result = a - b - borrow; + bool c = result >= 0; + if (result < 0) + result += 0x100; + uint res = cast(uint) result; + uint lo = res & 0x0F; + if (lo >= 0x0A) + res -= 0x06; + if (res >= 0xA0) + res -= 0x60; + A = cast(ubyte) (res & 0xFF); + setC(c); + setZ(A == 0); + setN((A & 0x80) != 0); + uint bin = cast(uint) (a - b - borrow) & 0xFF; + setV(((a ^ b) & (a ^ bin) & 0x80) != 0); + } + + // -- read-modify-write value helpers ----------------------------------- + + private ubyte aslVal(ubyte v) + { + setC((v & 0x80) != 0); + ubyte r = cast(ubyte) (v << 1); + setNZ(r); + return r; + } + + private ubyte lsrVal(ubyte v) + { + setC((v & 0x01) != 0); + ubyte r = cast(ubyte) (v >> 1); + setNZ(r); + return r; + } + + private ubyte rolVal(ubyte v) + { + bool c = (v & 0x80) != 0; + ubyte r = cast(ubyte) ((v << 1) | (getF(F_C) ? 1 : 0)); + setC(c); + setNZ(r); + return r; + } + + private ubyte rorVal(ubyte v) + { + bool c = (v & 0x01) != 0; + ubyte r = cast(ubyte) ((v >> 1) | (getF(F_C) ? 0x80 : 0)); + setC(c); + setNZ(r); + return r; + } + + private ubyte incVal(ubyte v) + { + ubyte r = cast(ubyte) (v + 1); + setNZ(r); + return r; + } + + private ubyte decVal(ubyte v) + { + ubyte r = cast(ubyte) (v - 1); + setNZ(r); + return r; + } + + // -- branches / jumps / interrupts ------------------------------------- + + private void branch(bool cond) + { + int off = cast(byte) fetch(); + if (cond) + { + cycles += 1; + ushort old = PC; + PC = cast(ushort) (PC + off); + if ((old & 0xFF00) != (PC & 0xFF00)) + cycles += 1; + } + } + + private void brk() + { + ushort ret = cast(ushort) (PC + 1); + push(cast(ubyte) (ret >> 8)); + push(cast(ubyte) (ret & 0xFF)); + push(cast(ubyte) (P | F_B | F_U)); + setF(F_I, true); + PC = read16(0xFFFE); + cycles += 7; + } + + private void jsr() + { + ushort target = fetch16(); + ushort ret = cast(ushort) (PC - 1); + push(cast(ubyte) (ret >> 8)); + push(cast(ubyte) (ret & 0xFF)); + PC = target; + cycles += 6; + } + + private void rts() + { + ubyte lo = pop(); + ubyte hi = pop(); + PC = cast(ushort) (((hi << 8) | lo) + 1); + cycles += 6; + } + + private void rti() + { + setPFromStack(pop()); + ubyte lo = pop(); + ubyte hi = pop(); + PC = cast(ushort) ((hi << 8) | lo); + cycles += 6; + } + + private void jmpInd() + { + ushort ptr = fetch16(); + ushort lo = bus.read(ptr); + ushort hi = bus.read(cast(ushort) ((ptr & 0xFF00) | ((ptr + 1) & 0xFF))); + PC = cast(ushort) ((hi << 8) | lo); + cycles += 5; + } + + // -- execute exactly one instruction ----------------------------------- + + void step() + { + ubyte op = fetch(); + switch (op) + { + // BRK + case 0x00: brk(); break; + // ORA + case 0x01: cycles += 6; oraOp(bus.read(addrIzx())); break; + case 0x05: cycles += 3; oraOp(bus.read(addrZP())); break; + case 0x09: cycles += 2; oraOp(fetch()); break; + case 0x0D: cycles += 4; oraOp(bus.read(addrAbs())); break; + case 0x11: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; oraOp(bus.read(a)); } break; + case 0x15: cycles += 4; oraOp(bus.read(addrZPX())); break; + case 0x19: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; oraOp(bus.read(a)); } break; + case 0x1D: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; oraOp(bus.read(a)); } break; + // ASL + case 0x06: cycles += 5; { ushort a = addrZP(); bus.write(a, aslVal(bus.read(a))); } break; + case 0x0A: cycles += 2; A = aslVal(A); break; + case 0x0E: cycles += 6; { ushort a = addrAbs(); bus.write(a, aslVal(bus.read(a))); } break; + case 0x16: cycles += 6; { ushort a = addrZPX(); bus.write(a, aslVal(bus.read(a))); } break; + case 0x1E: cycles += 7; { bool x; ushort a = addrAbsX(x); bus.write(a, aslVal(bus.read(a))); } break; + // PHP + case 0x08: cycles += 3; push(cast(ubyte) (P | F_B | F_U)); break; + // BPL + case 0x10: cycles += 2; branch(!getF(F_N)); break; + // CLC + case 0x18: cycles += 2; setC(false); break; + // JSR + case 0x20: jsr(); break; + // AND + case 0x21: cycles += 6; andOp(bus.read(addrIzx())); break; + case 0x25: cycles += 3; andOp(bus.read(addrZP())); break; + case 0x29: cycles += 2; andOp(fetch()); break; + case 0x2D: cycles += 4; andOp(bus.read(addrAbs())); break; + case 0x31: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; andOp(bus.read(a)); } break; + case 0x35: cycles += 4; andOp(bus.read(addrZPX())); break; + case 0x39: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; andOp(bus.read(a)); } break; + case 0x3D: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; andOp(bus.read(a)); } break; + // BIT + case 0x24: cycles += 3; bitOp(bus.read(addrZP())); break; + case 0x2C: cycles += 4; bitOp(bus.read(addrAbs())); break; + // ROL + case 0x26: cycles += 5; { ushort a = addrZP(); bus.write(a, rolVal(bus.read(a))); } break; + case 0x2A: cycles += 2; A = rolVal(A); break; + case 0x2E: cycles += 6; { ushort a = addrAbs(); bus.write(a, rolVal(bus.read(a))); } break; + case 0x36: cycles += 6; { ushort a = addrZPX(); bus.write(a, rolVal(bus.read(a))); } break; + case 0x3E: cycles += 7; { bool x; ushort a = addrAbsX(x); bus.write(a, rolVal(bus.read(a))); } break; + // PLP + case 0x28: cycles += 4; setPFromStack(pop()); break; + // BMI + case 0x30: cycles += 2; branch(getF(F_N)); break; + // SEC + case 0x38: cycles += 2; setC(true); break; + // RTI + case 0x40: rti(); break; + // EOR + case 0x41: cycles += 6; eorOp(bus.read(addrIzx())); break; + case 0x45: cycles += 3; eorOp(bus.read(addrZP())); break; + case 0x49: cycles += 2; eorOp(fetch()); break; + case 0x4D: cycles += 4; eorOp(bus.read(addrAbs())); break; + case 0x51: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; eorOp(bus.read(a)); } break; + case 0x55: cycles += 4; eorOp(bus.read(addrZPX())); break; + case 0x59: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; eorOp(bus.read(a)); } break; + case 0x5D: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; eorOp(bus.read(a)); } break; + // LSR + case 0x46: cycles += 5; { ushort a = addrZP(); bus.write(a, lsrVal(bus.read(a))); } break; + case 0x4A: cycles += 2; A = lsrVal(A); break; + case 0x4E: cycles += 6; { ushort a = addrAbs(); bus.write(a, lsrVal(bus.read(a))); } break; + case 0x56: cycles += 6; { ushort a = addrZPX(); bus.write(a, lsrVal(bus.read(a))); } break; + case 0x5E: cycles += 7; { bool x; ushort a = addrAbsX(x); bus.write(a, lsrVal(bus.read(a))); } break; + // PHA + case 0x48: cycles += 3; push(A); break; + // JMP + case 0x4C: cycles += 3; PC = fetch16(); break; + case 0x6C: jmpInd(); break; + // BVC + case 0x50: cycles += 2; branch(!getF(F_V)); break; + // CLI + case 0x58: cycles += 2; setF(F_I, false); break; + // RTS + case 0x60: rts(); break; + // ADC + case 0x61: cycles += 6; adcOp(bus.read(addrIzx())); break; + case 0x65: cycles += 3; adcOp(bus.read(addrZP())); break; + case 0x69: cycles += 2; adcOp(fetch()); break; + case 0x6D: cycles += 4; adcOp(bus.read(addrAbs())); break; + case 0x71: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; adcOp(bus.read(a)); } break; + case 0x75: cycles += 4; adcOp(bus.read(addrZPX())); break; + case 0x79: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; adcOp(bus.read(a)); } break; + case 0x7D: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; adcOp(bus.read(a)); } break; + // ROR + case 0x66: cycles += 5; { ushort a = addrZP(); bus.write(a, rorVal(bus.read(a))); } break; + case 0x6A: cycles += 2; A = rorVal(A); break; + case 0x6E: cycles += 6; { ushort a = addrAbs(); bus.write(a, rorVal(bus.read(a))); } break; + case 0x76: cycles += 6; { ushort a = addrZPX(); bus.write(a, rorVal(bus.read(a))); } break; + case 0x7E: cycles += 7; { bool x; ushort a = addrAbsX(x); bus.write(a, rorVal(bus.read(a))); } break; + // PLA + case 0x68: cycles += 4; A = pop(); setNZ(A); break; + // BVS + case 0x70: cycles += 2; branch(getF(F_V)); break; + // SEI + case 0x78: cycles += 2; setF(F_I, true); break; + // STA + case 0x81: cycles += 6; bus.write(addrIzx(), A); break; + case 0x85: cycles += 3; bus.write(addrZP(), A); break; + case 0x8D: cycles += 4; bus.write(addrAbs(), A); break; + case 0x91: cycles += 6; { bool x; ushort a = addrIzy(x); bus.write(a, A); } break; + case 0x95: cycles += 4; bus.write(addrZPX(), A); break; + case 0x99: cycles += 5; { bool x; ushort a = addrAbsY(x); bus.write(a, A); } break; + case 0x9D: cycles += 5; { bool x; ushort a = addrAbsX(x); bus.write(a, A); } break; + // STY + case 0x84: cycles += 3; bus.write(addrZP(), Y); break; + case 0x8C: cycles += 4; bus.write(addrAbs(), Y); break; + case 0x94: cycles += 4; bus.write(addrZPX(), Y); break; + // STX + case 0x86: cycles += 3; bus.write(addrZP(), X); break; + case 0x8E: cycles += 4; bus.write(addrAbs(), X); break; + case 0x96: cycles += 4; bus.write(addrZPY(), X); break; + // DEY + case 0x88: cycles += 2; Y = cast(ubyte) (Y - 1); setNZ(Y); break; + // TXA + case 0x8A: cycles += 2; A = X; setNZ(A); break; + // BCC + case 0x90: cycles += 2; branch(!getF(F_C)); break; + // TYA + case 0x98: cycles += 2; A = Y; setNZ(A); break; + // TXS + case 0x9A: cycles += 2; SP = X; break; + // LDY + case 0xA0: cycles += 2; ldyOp(fetch()); break; + case 0xA4: cycles += 3; ldyOp(bus.read(addrZP())); break; + case 0xAC: cycles += 4; ldyOp(bus.read(addrAbs())); break; + case 0xB4: cycles += 4; ldyOp(bus.read(addrZPX())); break; + case 0xBC: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; ldyOp(bus.read(a)); } break; + // LDA + case 0xA1: cycles += 6; ldaOp(bus.read(addrIzx())); break; + case 0xA5: cycles += 3; ldaOp(bus.read(addrZP())); break; + case 0xA9: cycles += 2; ldaOp(fetch()); break; + case 0xAD: cycles += 4; ldaOp(bus.read(addrAbs())); break; + case 0xB1: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; ldaOp(bus.read(a)); } break; + case 0xB5: cycles += 4; ldaOp(bus.read(addrZPX())); break; + case 0xB9: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; ldaOp(bus.read(a)); } break; + case 0xBD: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; ldaOp(bus.read(a)); } break; + // LDX + case 0xA2: cycles += 2; ldxOp(fetch()); break; + case 0xA6: cycles += 3; ldxOp(bus.read(addrZP())); break; + case 0xAE: cycles += 4; ldxOp(bus.read(addrAbs())); break; + case 0xB6: cycles += 4; ldxOp(bus.read(addrZPY())); break; + case 0xBE: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; ldxOp(bus.read(a)); } break; + // TAY + case 0xA8: cycles += 2; Y = A; setNZ(Y); break; + // TAX + case 0xAA: cycles += 2; X = A; setNZ(X); break; + // BCS + case 0xB0: cycles += 2; branch(getF(F_C)); break; + // CLV + case 0xB8: cycles += 2; setV(false); break; + // TSX + case 0xBA: cycles += 2; X = SP; setNZ(X); break; + // CPY + case 0xC0: cycles += 2; cpyOp(fetch()); break; + case 0xC4: cycles += 3; cpyOp(bus.read(addrZP())); break; + case 0xCC: cycles += 4; cpyOp(bus.read(addrAbs())); break; + // CMP + case 0xC1: cycles += 6; cmpOp(bus.read(addrIzx())); break; + case 0xC5: cycles += 3; cmpOp(bus.read(addrZP())); break; + case 0xC9: cycles += 2; cmpOp(fetch()); break; + case 0xCD: cycles += 4; cmpOp(bus.read(addrAbs())); break; + case 0xD1: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; cmpOp(bus.read(a)); } break; + case 0xD5: cycles += 4; cmpOp(bus.read(addrZPX())); break; + case 0xD9: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; cmpOp(bus.read(a)); } break; + case 0xDD: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; cmpOp(bus.read(a)); } break; + // DEC + case 0xC6: cycles += 5; { ushort a = addrZP(); bus.write(a, decVal(bus.read(a))); } break; + case 0xCE: cycles += 6; { ushort a = addrAbs(); bus.write(a, decVal(bus.read(a))); } break; + case 0xD6: cycles += 6; { ushort a = addrZPX(); bus.write(a, decVal(bus.read(a))); } break; + case 0xDE: cycles += 7; { bool x; ushort a = addrAbsX(x); bus.write(a, decVal(bus.read(a))); } break; + // INY + case 0xC8: cycles += 2; Y = cast(ubyte) (Y + 1); setNZ(Y); break; + // DEX + case 0xCA: cycles += 2; X = cast(ubyte) (X - 1); setNZ(X); break; + // BNE + case 0xD0: cycles += 2; branch(!getF(F_Z)); break; + // CLD + case 0xD8: cycles += 2; setF(F_D, false); break; + // CPX + case 0xE0: cycles += 2; cpxOp(fetch()); break; + case 0xE4: cycles += 3; cpxOp(bus.read(addrZP())); break; + case 0xEC: cycles += 4; cpxOp(bus.read(addrAbs())); break; + // SBC + case 0xE1: cycles += 6; sbcOp(bus.read(addrIzx())); break; + case 0xE5: cycles += 3; sbcOp(bus.read(addrZP())); break; + case 0xE9: cycles += 2; sbcOp(fetch()); break; + case 0xED: cycles += 4; sbcOp(bus.read(addrAbs())); break; + case 0xF1: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; sbcOp(bus.read(a)); } break; + case 0xF5: cycles += 4; sbcOp(bus.read(addrZPX())); break; + case 0xF9: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; sbcOp(bus.read(a)); } break; + case 0xFD: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; sbcOp(bus.read(a)); } break; + // INC + case 0xE6: cycles += 5; { ushort a = addrZP(); bus.write(a, incVal(bus.read(a))); } break; + case 0xEE: cycles += 6; { ushort a = addrAbs(); bus.write(a, incVal(bus.read(a))); } break; + case 0xF6: cycles += 6; { ushort a = addrZPX(); bus.write(a, incVal(bus.read(a))); } break; + case 0xFE: cycles += 7; { bool x; ushort a = addrAbsX(x); bus.write(a, incVal(bus.read(a))); } break; + // INX + case 0xE8: cycles += 2; X = cast(ubyte) (X + 1); setNZ(X); break; + // NOP + case 0xEA: cycles += 2; break; + // BEQ + case 0xF0: cycles += 2; branch(getF(F_Z)); break; + // SED + case 0xF8: cycles += 2; setF(F_D, true); break; + + default: + // Undocumented/illegal opcode: treat as a 1-byte NOP. + cycles += 2; + break; + } + } + + // -- disassembly (for --trace) ----------------------------------------- + + string disasm(ushort pc) + { + ubyte op = bus.read(pc); + Am mode = opMode(op); + string mn = opMnemonic(op); + if (mn == "???") + return "???"; + + auto rb = (int off) => bus.read(cast(ushort) ((pc + off) & 0xFFFF)); + auto rw = (int off) => cast(ushort) (rb(off) | (rb(off + 1) << 8)); + + string operands; + switch (mode) + { + case Am.imp: operands = ""; break; + case Am.acc: operands = "A"; break; + case Am.imm: operands = format("#$%02X", rb(1)); break; + case Am.zp: operands = format("$%02X", rb(1)); break; + case Am.zpx: operands = format("$%02X,X", rb(1)); break; + case Am.zpy: operands = format("$%02X,Y", rb(1)); break; + case Am.abs: operands = format("$%04X", rw(1)); break; + case Am.abx: operands = format("$%04X,X", rw(1)); break; + case Am.aby: operands = format("$%04X,Y", rw(1)); break; + case Am.ind: operands = format("($%04X)", rw(1)); break; + case Am.izx: operands = format("($%02X,X)", rb(1)); break; + case Am.izy: operands = format("($%02X),Y", rb(1)); break; + case Am.rel: + { + int off = cast(byte) rb(1); + operands = format("$%04X", cast(ushort) (pc + 2 + off)); + break; + } + default: operands = ""; break; + } + return operands.length ? (mn ~ " " ~ operands) : mn; + } + + string regs() const + { + return format("A=%02X X=%02X Y=%02X SP=%02X P=%02X", A, X, Y, SP, P); + } +} + +// Addressing modes for disassembly only. +private enum Am : ubyte { imp, acc, imm, zp, zpx, zpy, abs, abx, aby, ind, izx, izy, rel, ill } + +private static string[256] mnTab; +private static Am[256] modeTab; + +private string opMnemonic(ubyte op) +{ + auto s = mnTab[op]; + return s.length ? s : "???"; +} + +private Am opMode(ubyte op) +{ + return modeTab[op]; +} + +shared static this() +{ + void d(ubyte op, string mn, Am m) + { + mnTab[op] = mn; + modeTab[op] = m; + } + + d(0x00, "BRK", Am.imp); + d(0x01, "ORA", Am.izx); + d(0x05, "ORA", Am.zp); + d(0x06, "ASL", Am.zp); + d(0x08, "PHP", Am.imp); + d(0x09, "ORA", Am.imm); + d(0x0A, "ASL", Am.acc); + d(0x0D, "ORA", Am.abs); + d(0x0E, "ASL", Am.abs); + d(0x10, "BPL", Am.rel); + d(0x11, "ORA", Am.izy); + d(0x15, "ORA", Am.zpx); + d(0x16, "ASL", Am.zpx); + d(0x18, "CLC", Am.imp); + d(0x19, "ORA", Am.aby); + d(0x1D, "ORA", Am.abx); + d(0x1E, "ASL", Am.abx); + d(0x20, "JSR", Am.abs); + d(0x21, "AND", Am.izx); + d(0x24, "BIT", Am.zp); + d(0x25, "AND", Am.zp); + d(0x26, "ROL", Am.zp); + d(0x28, "PLP", Am.imp); + d(0x29, "AND", Am.imm); + d(0x2A, "ROL", Am.acc); + d(0x2C, "BIT", Am.abs); + d(0x2D, "AND", Am.abs); + d(0x2E, "ROL", Am.abs); + d(0x30, "BMI", Am.rel); + d(0x31, "AND", Am.izy); + d(0x35, "AND", Am.zpx); + d(0x36, "ROL", Am.zpx); + d(0x38, "SEC", Am.imp); + d(0x39, "AND", Am.aby); + d(0x3D, "AND", Am.abx); + d(0x3E, "ROL", Am.abx); + d(0x40, "RTI", Am.imp); + d(0x41, "EOR", Am.izx); + d(0x45, "EOR", Am.zp); + d(0x46, "LSR", Am.zp); + d(0x48, "PHA", Am.imp); + d(0x49, "EOR", Am.imm); + d(0x4A, "LSR", Am.acc); + d(0x4C, "JMP", Am.abs); + d(0x4D, "EOR", Am.abs); + d(0x4E, "LSR", Am.abs); + d(0x50, "BVC", Am.rel); + d(0x51, "EOR", Am.izy); + d(0x55, "EOR", Am.zpx); + d(0x56, "LSR", Am.zpx); + d(0x58, "CLI", Am.imp); + d(0x59, "EOR", Am.aby); + d(0x5D, "EOR", Am.abx); + d(0x5E, "LSR", Am.abx); + d(0x60, "RTS", Am.imp); + d(0x61, "ADC", Am.izx); + d(0x65, "ADC", Am.zp); + d(0x66, "ROR", Am.zp); + d(0x68, "PLA", Am.imp); + d(0x69, "ADC", Am.imm); + d(0x6A, "ROR", Am.acc); + d(0x6C, "JMP", Am.ind); + d(0x6D, "ADC", Am.abs); + d(0x6E, "ROR", Am.abs); + d(0x70, "BVS", Am.rel); + d(0x71, "ADC", Am.izy); + d(0x75, "ADC", Am.zpx); + d(0x76, "ROR", Am.zpx); + d(0x78, "SEI", Am.imp); + d(0x79, "ADC", Am.aby); + d(0x7D, "ADC", Am.abx); + d(0x7E, "ROR", Am.abx); + d(0x81, "STA", Am.izx); + d(0x84, "STY", Am.zp); + d(0x85, "STA", Am.zp); + d(0x86, "STX", Am.zp); + d(0x88, "DEY", Am.imp); + d(0x8A, "TXA", Am.imp); + d(0x8C, "STY", Am.abs); + d(0x8D, "STA", Am.abs); + d(0x8E, "STX", Am.abs); + d(0x90, "BCC", Am.rel); + d(0x91, "STA", Am.izy); + d(0x94, "STY", Am.zpx); + d(0x95, "STA", Am.zpx); + d(0x96, "STX", Am.zpy); + d(0x98, "TYA", Am.imp); + d(0x99, "STA", Am.aby); + d(0x9A, "TXS", Am.imp); + d(0x9D, "STA", Am.abx); + d(0xA0, "LDY", Am.imm); + d(0xA1, "LDA", Am.izx); + d(0xA2, "LDX", Am.imm); + d(0xA4, "LDY", Am.zp); + d(0xA5, "LDA", Am.zp); + d(0xA6, "LDX", Am.zp); + d(0xA8, "TAY", Am.imp); + d(0xA9, "LDA", Am.imm); + d(0xAA, "TAX", Am.imp); + d(0xAC, "LDY", Am.abs); + d(0xAD, "LDA", Am.abs); + d(0xAE, "LDX", Am.abs); + d(0xB0, "BCS", Am.rel); + d(0xB1, "LDA", Am.izy); + d(0xB4, "LDY", Am.zpx); + d(0xB5, "LDA", Am.zpx); + d(0xB6, "LDX", Am.zpy); + d(0xB8, "CLV", Am.imp); + d(0xB9, "LDA", Am.aby); + d(0xBA, "TSX", Am.imp); + d(0xBC, "LDY", Am.abx); + d(0xBD, "LDA", Am.abx); + d(0xBE, "LDX", Am.aby); + d(0xC0, "CPY", Am.imm); + d(0xC1, "CMP", Am.izx); + d(0xC4, "CPY", Am.zp); + d(0xC5, "CMP", Am.zp); + d(0xC6, "DEC", Am.zp); + d(0xC8, "INY", Am.imp); + d(0xC9, "CMP", Am.imm); + d(0xCA, "DEX", Am.imp); + d(0xCC, "CPY", Am.abs); + d(0xCD, "CMP", Am.abs); + d(0xCE, "DEC", Am.abs); + d(0xD0, "BNE", Am.rel); + d(0xD1, "CMP", Am.izy); + d(0xD5, "CMP", Am.zpx); + d(0xD6, "DEC", Am.zpx); + d(0xD8, "CLD", Am.imp); + d(0xD9, "CMP", Am.aby); + d(0xDD, "CMP", Am.abx); + d(0xDE, "DEC", Am.abx); + d(0xE0, "CPX", Am.imm); + d(0xE1, "SBC", Am.izx); + d(0xE4, "CPX", Am.zp); + d(0xE5, "SBC", Am.zp); + d(0xE6, "INC", Am.zp); + d(0xE8, "INX", Am.imp); + d(0xE9, "SBC", Am.imm); + d(0xEA, "NOP", Am.imp); + d(0xEC, "CPX", Am.abs); + d(0xED, "SBC", Am.abs); + d(0xEE, "INC", Am.abs); + d(0xF0, "BEQ", Am.rel); + d(0xF1, "SBC", Am.izy); + d(0xF5, "SBC", Am.zpx); + d(0xF6, "INC", Am.zpx); + d(0xF8, "SED", Am.imp); + d(0xF9, "SBC", Am.aby); + d(0xFD, "SBC", Am.abx); + d(0xFE, "INC", Am.abx); +} + +// --------------------------------------------------------------------------- +// unit tests +// --------------------------------------------------------------------------- + +version (unittest) +{ + private Cpu testCpu(const(ubyte)[] prog, ushort addr = 0x0200) + { + auto b = new Bus(); + b.loadAt(addr, prog); + auto c = new Cpu(b); + c.reset(); + c.setPC(addr); + return c; + } +} + +unittest +{ + // LDA #$42 sets A, clears Z and N + auto c = testCpu([0xA9, 0x42]); + c.step(); + assert(c.A == 0x42); + assert((c.P & F_Z) == 0); + assert((c.P & F_N) == 0); + assert(c.cycles == 2); + + // LDA #$00 sets Z + auto c2 = testCpu([0xA9, 0x00]); + c2.step(); + assert(c2.A == 0x00); + assert((c2.P & F_Z) != 0); +} + +unittest +{ + // $7F + $01 = $80: V set, C clear + auto c = testCpu([0xA9, 0x7F, 0x69, 0x01]); + c.step(); // LDA #$7F + c.step(); // ADC #$01 + assert(c.A == 0x80); + assert((c.P & F_V) != 0); + assert((c.P & F_C) == 0); + + // $FF + $01 = $00: C set, Z set + auto c2 = testCpu([0xA9, 0xFF, 0x69, 0x01]); + c2.step(); + c2.step(); + assert(c2.A == 0x00); + assert((c2.P & F_C) != 0); + assert((c2.P & F_Z) != 0); +} + +unittest +{ + // SBC: 5 - 3 = 2 with carry set (no borrow) + auto c = testCpu([0xA9, 0x05, 0x38, 0xE9, 0x03]); + c.step(); // LDA #$05 + c.step(); // SEC + c.step(); // SBC #$03 + assert(c.A == 0x02); + assert((c.P & F_C) != 0); + assert((c.P & F_Z) == 0); + + // SBC: 5 - 6 = $FF with borrow (carry clear) + auto c2 = testCpu([0xA9, 0x05, 0x38, 0xE9, 0x06]); + c2.step(); + c2.step(); + c2.step(); + assert(c2.A == 0xFF); + assert((c2.P & F_C) == 0); + assert((c2.P & F_N) != 0); +} + +unittest +{ + // JSR/RTS round trip + // 0200: JSR $0208 (20 08 02) + // 0203: LDA #$11 (A9 11) + // 0205: BRK? -> instead just leave; RTS returns to 0203 + // 0208: RTS (60) + auto c = testCpu([0x20, 0x08, 0x02, 0xA9, 0x11, 0x00, 0x00, 0x00, 0x60]); + c.step(); // JSR -> PC=0x0208, SP=0xFB + assert(c.PC == 0x0208); + assert(c.SP == 0xFB); + c.step(); // RTS -> PC=0x0203 + assert(c.PC == 0x0203); + c.step(); // LDA #$11 + assert(c.A == 0x11); +} + +unittest +{ + // Branch taken (forward) and not-taken + // 0200: LDA #$00 (A9 00) + // 0202: BNE +2 (D0 02) -> not taken + // 0204: LDA #$01 (A9 01) + // 0206: NOP + auto c = testCpu([0xA9, 0x00, 0xD0, 0x02, 0xA9, 0x01, 0xEA]); + c.step(); // LDA #$00 + c.step(); // BNE not taken -> PC=0x0204 + assert(c.PC == 0x0204); + c.step(); // LDA #$01 + assert(c.A == 0x01); + + // Backward branch (loop) + // 0200: LDA #$03 (A9 03) + // 0202: DEX (CA) + // 0203: BNE $0202 (D0 FD) -> taken back to 0202 + auto c2 = testCpu([0xA9, 0x03, 0xCA, 0xD0, 0xFD]); + c2.step(); // LDA #$03 + c2.step(); // DEX -> X=$FF (not zero) + c2.step(); // BNE taken -> PC back to 0x0202 + assert(c2.PC == 0x0202); + assert((c2.P & F_Z) == 0); +} + +unittest +{ + // Zero-page,X wrap-around: operand $FF with X=1 reads $00 + auto b = new Bus(); + b.write(0x0000, 0x77); + auto c = new Cpu(b); + c.reset(); + c.setPC(0x0200); + b.loadAt(0x0200, [0xB5, 0xFF]); // LDA $FF,X + c.X = 1; + c.step(); + assert(c.A == 0x77); +} + +unittest +{ + // STA/LDA absolute round trip through the bus + auto c = testCpu([0xA9, 0xAB, 0x8D, 0x34, 0x12, 0xA9, 0x00, 0xAD, 0x34, 0x12]); + c.step(); // LDA #$AB + c.step(); // STA $1234 + assert(c.bus.read(0x1234) == 0xAB); + c.step(); // LDA #$00 + c.step(); // LDA $1234 + assert(c.A == 0xAB); +} + +unittest +{ + // PHA/PLA and PHP/PLP round trip + auto c = testCpu([0xA9, 0x5A, 0x48, 0xA9, 0x00, 0x68]); + c.step(); // LDA #$5A + c.step(); // PHA + assert(c.SP == 0xFC); + assert(c.bus.read(0x01FD) == 0x5A); + c.step(); // LDA #$00 + c.step(); // PLA -> A=$5A + assert(c.A == 0x5A); + assert(c.SP == 0xFD); + + // PHP/PLP: push P (carry set), clear carry, pull -> carry set again + auto c2 = testCpu([0x38, 0x08, 0x18, 0x28]); + c2.step(); // SEC + c2.step(); // PHP + c2.step(); // CLC + assert((c2.P & F_C) == 0); + c2.step(); // PLP -> carry restored + assert((c2.P & F_C) != 0); +} + +unittest +{ + // A short program that computes something and halts via $F003. + // 0200: LDA #$05 (A9 05) + // 0202: CLC (18) + // 0203: ADC #$03 (69 03) -> A = 8 + // 0205: STA $40 (85 40) + // 0207: STA $F003 (8D 03 F0) -> halt + auto c = testCpu([0xA9, 0x05, 0x18, 0x69, 0x03, 0x85, 0x40, 0x8D, 0x03, 0xF0]); + while (!c.bus.halted) + { + c.step(); + assert(c.cycles < 1000); + } + assert(c.A == 0x08); + assert(c.bus.read(0x0040) == 0x08); + assert(c.bus.halted == true); +} diff --git a/src/emulator/main.d b/src/emulator/main.d new file mode 100644 index 0000000..29babfc --- /dev/null +++ b/src/emulator/main.d @@ -0,0 +1,230 @@ +/** + * weirdcpu — NMOS 6502 emulator CLI. + * + * Loads a .wimg image (via wcore.objfmt.readImage) or a raw binary, runs it + * until the program halts ($F003 write) or the cycle budget is exhausted. + * + * Usage: weirdcpu [options] + */ +module emulator.main; + +import std.stdio : stderr, stdout, stdin, write, writef, writefln, writeln; +import std.file : read; +import std.conv : to, ConvException; + +import core.sys.posix.unistd : isatty; + +import wcore.objfmt; +import emulator.bus; +import emulator.cpu; + +private void usage() +{ + stderr.writeln("weirdcpu — NMOS 6502 emulator"); + stderr.writeln("Usage: weirdcpu [options] "); + stderr.writeln(); + stderr.writeln("Options:"); + stderr.writeln(" --raw Load as a raw binary (default: parse .wimg)"); + stderr.writeln(" --origin Load address for --raw (default 0x0600)"); + stderr.writeln(" --entry Entry point (default: image entry, or origin for raw)"); + stderr.writeln(" --max-cycles Cycle budget (default 100000000)"); + stderr.writeln(" --trace Disassemble each instruction to stderr"); + stderr.writeln(" --dump Hex-dump memory around load area after halt"); + stderr.writeln(" -h, --help Show this help"); +} + +private bool parseAddr(string s, out ushort v, string opt) +{ + long n; + try + { + if (s.length > 2 && (s[0 .. 2] == "0x" || s[0 .. 2] == "0X")) + n = to!long(s[2 .. $], 16); + else + n = to!long(s, 10); + } + catch (ConvException e) + { + stderr.writeln("error: invalid value for ", opt, ": ", s); + return false; + } + if (n < 0 || n > 0xFFFF) + { + stderr.writeln("error: ", opt, " out of range (0..65535): ", s); + return false; + } + v = cast(ushort) n; + return true; +} + +private bool parseULong(string s, out ulong v, string opt) +{ + ulong n; + try + { + n = to!ulong(s, 10); + } + catch (ConvException e) + { + stderr.writeln("error: invalid value for ", opt, ": ", s); + return false; + } + v = n; + return true; +} + +private void hexDump(Bus bus, ushort start) +{ + const uint count = 256; + for (uint i = 0; i < count; i += 16) + { + ushort a = cast(ushort) (start + i); + writef("%04X ", a); + foreach (j; 0 .. 16) + { + ushort aa = cast(ushort) (a + j); + writef("%02X ", bus.read(aa)); + } + write(" "); + foreach (j; 0 .. 16) + { + ushort aa = cast(ushort) (a + j); + ubyte b = bus.read(aa); + char ch = (b >= 32 && b < 127) ? cast(char) b : '.'; + write(ch); + } + writeln(); + } +} + +int main(string[] args) +{ + bool raw = false; + bool trace = false; + bool dump = false; + bool hasEntry = false; + ushort origin = 0x0600; + ushort entry = 0; + ulong maxCycles = 100_000_000; + string file; + + for (size_t i = 1; i < args.length; i++) + { + string a = args[i]; + if (a == "-h" || a == "--help") + { + usage(); + return 0; + } + else if (a == "--raw") + raw = true; + else if (a == "--trace") + trace = true; + else if (a == "--dump") + dump = true; + else if (a == "--origin") + { + if (++i >= args.length || !parseAddr(args[i], origin, "--origin")) + return 1; + } + else if (a == "--entry") + { + if (++i >= args.length || !parseAddr(args[i], entry, "--entry")) + return 1; + hasEntry = true; + } + else if (a == "--max-cycles") + { + if (++i >= args.length || !parseULong(args[i], maxCycles, "--max-cycles")) + return 1; + } + else if (a.length > 1 && a[0] == '-') + { + stderr.writeln("error: unknown option: ", a); + usage(); + return 1; + } + else + file = a; + } + + if (file.length == 0) + { + usage(); + return 1; + } + + Bus bus = new Bus(); + ushort loadAddr; + ushort entryPoint; + ubyte[] data; + + if (raw) + { + try + { + data = cast(ubyte[]) read(file); + } + catch (Exception e) + { + stderr.writeln("error: cannot read ", file, ": ", e.msg); + return 1; + } + loadAddr = origin; + entryPoint = hasEntry ? entry : origin; + } + else + { + ubyte[] bytes; + try + { + bytes = cast(ubyte[]) read(file); + } + catch (Exception e) + { + stderr.writeln("error: cannot read ", file, ": ", e.msg); + return 1; + } + if (bytes.length < 4 || bytes[0] != 'W' || bytes[1] != 'I' || bytes[2] != 'M' || bytes[3] != 'G') + { + stderr.writeln("error: ", file, " is not a .wimg image (missing WIMG magic); use --raw for raw binaries"); + return 1; + } + Image img = readImage(file); + loadAddr = img.loadAddr; + entryPoint = hasEntry ? entry : img.entry; + data = img.data; + } + + bus.loadAt(loadAddr, data); + + // Read buffered stdin lazily; never block on a TTY. + if (isatty(0) == 0) + { + ubyte[] input; + foreach (ubyte[] chunk; stdin.byChunk(4096)) + input ~= chunk; + bus.setInput(input); + } + + Cpu cpu = new Cpu(bus); + cpu.reset(); + cpu.setPC(entryPoint); + + while (!bus.halted) + { + if (cpu.cycles >= maxCycles) + { + stderr.writeln("error: cycle budget exceeded (max ", maxCycles, " cycles)"); + return 2; + } + if (trace) + stderr.writefln("%04X: %-12s %s", cpu.PC, cpu.disasm(cpu.PC), cpu.regs()); + cpu.step(); + } + + if (dump) + hexDump(bus, loadAddr); + + return 0; +} diff --git a/src/linker/linker.d b/src/linker/linker.d new file mode 100644 index 0000000..bbd6265 --- /dev/null +++ b/src/linker/linker.d @@ -0,0 +1,442 @@ +/** + * Small 6502 linker for the weirdcpu toolchain. + * + * Reads one or more `.wo` objects (see `wcore.objfmt`), lays out their + * segments, resolves cross-object symbols, applies relocations, and produces + * a `.wimg` loadable image plus an optional human-readable symbol map. + * + * Everything here operates on the in-memory `Object`/`Image` structs defined + * in `wcore.objfmt`; the CLI wrapper in `main.d` handles file I/O and argument + * parsing. + */ +module linker.linker; + +import wcore.objfmt; +import std.algorithm : sort; +import std.array : join; +import std.string : format; + +/// Disambiguate the object-file `Object` from the root `object.Object` class. +alias Object = wcore.objfmt.Object; + +/// Options controlling layout and entry resolution. +struct LinkOptions +{ + ushort origin = 0x0600; /// base address for relocatable segments + string entrySymbol; /// optional: -e symbol + bool verbose = false; /// extra diagnostics (currently unused) +} + +/// Result of a link: the loadable image plus a printable symbol map. +struct LinkResult +{ + Image image; + string mapText; +} + +/// A segment (or bss reservation) after layout. +private struct Placed +{ + uint base; + uint size; + string name; /// for diagnostics +} + +/// A symbol line in the map, for sorting. +private struct SymLine +{ + uint address; + string name; +} + +/// Link `objects` into a loadable image. +LinkResult link(Object[] objects, LinkOptions opts = LinkOptions.init) +{ + // ------------------------------------------------------------------ + // 1. Place segments. + // ------------------------------------------------------------------ + uint cursor = opts.origin; + Placed[] placed; + uint[][] segBase = new uint[][objects.length]; + + foreach (objIdx, ref obj; objects) + { + segBase[objIdx] = new uint[obj.segments.length]; + foreach (segIdx, ref seg; obj.segments) + { + uint size = cast(uint) seg.data.length; + uint base; + + if (seg.origin != 0) + base = seg.origin; + else + { + base = cursor; + cursor += size; + if (cursor > 0x10000) + throw new Exception(format( + "image exceeds 64 KiB while placing segment '%s'", seg.name)); + } + + if (base + size > 0x10000) + throw new Exception(format( + "segment '%s' exceeds 64 KiB (0x%04X..0x%04X)", seg.name, base, base + size)); + + foreach (p; placed) + { + if (size > 0 && base < p.base + p.size && p.base < base + size) + throw new Exception(format( + "segment '%s' at 0x%04X overlaps segment '%s' at 0x%04X", + seg.name, base, p.name, p.base)); + } + + placed ~= Placed(base, size, format("obj%d:%s", objIdx, seg.name)); + segBase[objIdx][segIdx] = base; + } + } + + // ------------------------------------------------------------------ + // 2. Compute defined-symbol addresses; build the global symbol table. + // ------------------------------------------------------------------ + uint[][] symAddr = new uint[][objects.length]; + uint[string] globalAddr; /// name -> address for defined global symbols + uint[string] allDefined; /// name -> address for any defined symbol (entry fallback) + + foreach (objIdx, ref obj; objects) + { + symAddr[objIdx] = new uint[obj.symbols.length]; + foreach (symIdx, ref sym; obj.symbols) + { + if (!sym.isDefined()) + continue; + + if (sym.seg != NoIndex && sym.seg >= obj.segments.length) + throw new Exception(format( + "symbol '%s' references invalid segment %d", sym.name, sym.seg)); + + uint a = (sym.seg != NoIndex) + ? segBase[objIdx][sym.seg] + cast(uint) sym.value + : cast(uint) sym.value; + symAddr[objIdx][symIdx] = a; + + if (sym.isGlobal()) + { + if (auto p = sym.name in globalAddr) + { + if (*p != a) + throw new Exception(format( + "duplicate global symbol '%s' defined at 0x%04X and 0x%04X", + sym.name, *p, a)); + } + else + globalAddr[sym.name] = a; + } + + if (sym.name !in allDefined) + allDefined[sym.name] = a; + } + } + + // ------------------------------------------------------------------ + // 3. Resolve relocations. + // ------------------------------------------------------------------ + foreach (objIdx, ref obj; objects) + { + foreach (rel; obj.relocs) + { + if (rel.seg >= obj.segments.length) + throw new Exception(format("relocation references invalid segment %d", rel.seg)); + if (rel.sym >= obj.symbols.length) + throw new Exception(format("relocation references invalid symbol %d", rel.sym)); + + Segment* seg = &obj.segments[rel.seg]; + uint base = segBase[objIdx][rel.seg]; + Symbol* sym = &obj.symbols[rel.sym]; + + uint target; + if (sym.isDefined()) + target = symAddr[objIdx][rel.sym]; + else if (auto p = sym.name in globalAddr) + target = *p; + else + throw new Exception(format( + "undefined symbol '%s' referenced by object %d", sym.name, objIdx)); + + uint off = rel.offset; + ubyte[] data = seg.data; + + final switch (cast(RelocType) rel.type) + { + case RelocType.abs16: + if (off + 2 > data.length) + throw new Exception(format( + "abs16 relocation out of range in segment '%s'", seg.name)); + data[off] = cast(ubyte) (target & 0xFF); + data[off + 1] = cast(ubyte) ((target >> 8) & 0xFF); + break; + case RelocType.lo8: + if (off + 1 > data.length) + throw new Exception(format( + "lo8 relocation out of range in segment '%s'", seg.name)); + data[off] = cast(ubyte) (target & 0xFF); + break; + case RelocType.hi8: + if (off + 1 > data.length) + throw new Exception(format( + "hi8 relocation out of range in segment '%s'", seg.name)); + data[off] = cast(ubyte) ((target >> 8) & 0xFF); + break; + case RelocType.rel8: + if (off + 1 > data.length) + throw new Exception(format( + "rel8 relocation out of range in segment '%s'", seg.name)); + { + int disp = cast(int) target - cast(int) (base + off + 1); + if (disp < -128 || disp > 127) + throw new Exception(format( + "rel8 displacement %d out of range for symbol '%s'", disp, sym.name)); + data[off] = cast(ubyte) (disp & 0xFF); + } + break; + } + } + } + + // ------------------------------------------------------------------ + // 4. Determine entry address. + // ------------------------------------------------------------------ + uint load = opts.origin; + uint high = opts.origin; + bool any = false; + foreach (p; placed) + { + if (!any || p.base < load) + load = p.base; + uint h = p.base + p.size; + if (!any || h > high) + high = h; + any = true; + } + + ushort entry; + if (opts.entrySymbol.length > 0) + { + if (auto p = opts.entrySymbol in globalAddr) + entry = cast(ushort) *p; + else if (auto q = opts.entrySymbol in allDefined) + entry = cast(ushort) *q; + else + throw new Exception(format("entry symbol '%s' not defined", opts.entrySymbol)); + } + else if (auto p = "start" in globalAddr) + entry = cast(ushort) *p; + else if (any) + entry = cast(ushort) load; + else + entry = opts.origin; + + // ------------------------------------------------------------------ + // 5. Build the output image. + // ------------------------------------------------------------------ + ubyte[] imgdata = new ubyte[any ? (high - load) : 0]; + foreach (objIdx, ref obj; objects) + { + foreach (segIdx, ref seg; obj.segments) + { + if (seg.isBss()) + continue; + uint base = segBase[objIdx][segIdx]; + uint off = base - load; + imgdata[off .. off + seg.data.length] = seg.data[]; + } + } + + Image image; + image.loadAddr = cast(ushort) load; + image.entry = entry; + image.data = imgdata; + + // ------------------------------------------------------------------ + // 6. Build the symbol map. + // ------------------------------------------------------------------ + string[] lines; + lines ~= format("load = $%04X", load); + lines ~= format("entry = $%04X", entry); + lines ~= ""; + lines ~= "segments:"; + foreach (objIdx, ref obj; objects) + { + foreach (segIdx, ref seg; obj.segments) + { + uint base = segBase[objIdx][segIdx]; + uint size = cast(uint) seg.data.length; + lines ~= format(" $%04X-$%04X %s%s", base, base + size, + (seg.isBss() ? "[bss] " : ""), seg.name); + } + } + lines ~= ""; + lines ~= "symbols:"; + + SymLine[] syms; + foreach (objIdx, ref obj; objects) + { + foreach (symIdx, sym; obj.symbols) + { + if (sym.isDefined()) + syms ~= SymLine(symAddr[objIdx][symIdx], sym.name); + } + } + syms.sort!((a, b) => a.address != b.address ? a.address < b.address : a.name < b.name); + foreach (s; syms) + lines ~= format("$%04X %s", s.address, s.name); + + LinkResult res; + res.image = image; + res.mapText = lines.join("\n"); + return res; +} + +// =========================================================================== +// tests +// =========================================================================== + +unittest +{ + // --- abs16 across objects: A exports `foo`, B patches an abs16 to it --- + Object a; + a.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc), + 0, cast(ubyte[]) [0xEA, 0xEA, 0xEA]); + a.symbols ~= Symbol("foo", 0, cast(ubyte) (SymFlag.defined | SymFlag.global), 0); + + Object b; + b.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc), + 0, cast(ubyte[]) [0x00, 0x00]); + b.symbols ~= Symbol("foo", 0, cast(ubyte) (SymFlag.imported | SymFlag.global), NoIndex); + b.relocs ~= Reloc(0, 0, 0, RelocType.abs16); + + LinkResult r = link([a, b]); + + assert(r.image.loadAddr == 0x0600); + // foo -> 0x0600; B's segment placed at 0x0603, patched little-endian. + assert(r.image.data.length == 5); + assert(r.image.data[3] == 0x00, "abs16 low byte"); + assert(r.image.data[4] == 0x06, "abs16 high byte"); + // no entrySymbol and no `start` -> lowest placed base. + assert(r.image.entry == 0x0600); +} + +unittest +{ + // --- rel8 computes signed displacement; out-of-range is rejected --- + Object o; + o.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc), + 0, cast(ubyte[]) [0x00]); + o.symbols ~= Symbol("here", 0, cast(ubyte) (SymFlag.defined | SymFlag.global), 0); + o.relocs ~= Reloc(0, 0, 0, RelocType.rel8); + + LinkResult r = link([o]); + // disp = 0x0600 - (0x0600 + 0 + 1) = -1 -> 0xFF + assert(r.image.data[0] == 0xFF, "rel8 -1 displacement"); + + // out of range: target 0x8000 absolute + Object p; + p.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc), + 0, cast(ubyte[]) [0x00]); + p.symbols ~= Symbol("far", 0x8000, cast(ubyte) (SymFlag.defined | SymFlag.global), NoIndex); + p.relocs ~= Reloc(0, 0, 0, RelocType.rel8); + + bool threw = false; + try + link([p]); + catch (Exception e) + threw = true; + assert(threw, "out-of-range rel8 must be rejected"); +} + +unittest +{ + // --- fixed-origin placement and overlap detection --- + Object o; + o.segments ~= Segment(".fixed", cast(ubyte) (SegFlag.exec | SegFlag.alloc), + 0x4000, cast(ubyte[]) [0x01, 0x02]); + o.symbols ~= Symbol("target", 1, cast(ubyte) (SymFlag.defined | SymFlag.global), 0); + + LinkResult r = link([o]); + assert(r.image.loadAddr == 0x4000); + assert(r.image.data.length == 2); + // symbol address = segment base + value = 0x4000 + 1 + assert(r.image.entry == 0x4000); // no entrySymbol, no start -> lowest base + + // two fixed-origin segments sharing an address must overlap + Object ov; + ov.segments ~= Segment(".a", cast(ubyte) SegFlag.alloc, 0x2000, cast(ubyte[]) [0x01]); + ov.segments ~= Segment(".b", cast(ubyte) SegFlag.alloc, 0x2000, cast(ubyte[]) [0x02]); + + bool threw = false; + try + link([ov]); + catch (Exception e) + threw = true; + assert(threw, "overlapping fixed-origin segments must be rejected"); +} + +unittest +{ + // --- entry resolution via `start` --- + Object o; + o.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc), + 0, cast(ubyte[]) [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + o.symbols ~= Symbol("start", 5, cast(ubyte) (SymFlag.defined | SymFlag.global), 0); + + LinkResult r = link([o]); + assert(r.image.entry == 0x0605, "entry should come from `start`"); +} + +unittest +{ + // --- undefined symbol produces an error --- + Object o; + o.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc), + 0, cast(ubyte[]) [0x00, 0x00]); + o.symbols ~= Symbol("missing", 0, cast(ubyte) (SymFlag.imported | SymFlag.global), NoIndex); + o.relocs ~= Reloc(0, 0, 0, RelocType.abs16); + + bool threw = false; + try + link([o]); + catch (Exception e) + threw = true; + assert(threw, "undefined symbol must be an error"); +} + +unittest +{ + // --- writeObject/readObject round-trip --- + Object o; + o.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc), + 0x1000, cast(ubyte[]) [0xA9, 0x01, 0xEA]); + o.segments ~= Segment(".bss", cast(ubyte) (SegFlag.alloc | SegFlag.bss), + 0, cast(ubyte[]) [0, 0, 0, 0]); + o.symbols ~= Symbol("main", 0, cast(ubyte) (SymFlag.defined | SymFlag.global), 0); + o.relocs ~= Reloc(0, 1, 0, RelocType.lo8); + o.entry = 0x1000; + o.hasEntry = true; + + string path = "/tmp/link_roundtrip_test.wo"; + writeObject(path, o); + Object back = readObject(path); + + assert(back.segments.length == 2); + assert(back.segments[0].name == ".code"); + assert(back.segments[0].origin == 0x1000); + assert(back.segments[0].data == cast(ubyte[]) [0xA9, 0x01, 0xEA]); + assert(back.segments[1].isBss()); + assert(back.segments[1].data.length == 4); + assert(back.symbols.length == 1); + assert(back.symbols[0].name == "main"); + assert(back.symbols[0].isGlobal()); + assert(back.relocs.length == 1); + assert(back.relocs[0].type == RelocType.lo8); + assert(back.hasEntry); + assert(back.entry == 0x1000); +} diff --git a/src/linker/main.d b/src/linker/main.d index 54c0db1..3276806 100644 --- a/src/linker/main.d +++ b/src/linker/main.d @@ -1,6 +1,116 @@ -import std.stdio; +/** + * CLI wrapper for the weirdcpu linker. + * + * Usage: + * wlinker [options] -o [b.wo ...] + */ +module linker.main; -void main() +import linker.linker; +import wcore.objfmt; +import std.conv : to; +import std.file; +import std.stdio; +import core.stdc.stdlib : exit; + +/// Disambiguate the object-file `Object` from the root `object.Object` class. +alias Object = wcore.objfmt.Object; + +/// Parse an address argument: `0x`-prefixed hex or plain decimal. +int parseAddr(string s) { - writefln("stub for now until i get the actual emulator done"); + if (s.length > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) + { + int v = 0; + foreach (c; s[2 .. $]) + { + v <<= 4; + if (c >= '0' && c <= '9') + v |= (c - '0'); + else if (c >= 'a' && c <= 'f') + v |= (c - 'a' + 10); + else if (c >= 'A' && c <= 'F') + v |= (c - 'A' + 10); + else + throw new Exception("invalid hex digit in address '" ~ s ~ "'"); + } + return v; + } + return to!int(s); +} + +/// Return the argument following `args[i]`, advancing `i`; error if absent. +string needArg(string[] args, ref size_t i) +{ + if (i + 1 >= args.length) + throw new Exception("missing argument after '" ~ args[i] ~ "'"); + i++; + return args[i]; +} + +void usage() +{ + writeln("usage: wlinker [options] -o [b.wo ...]"); + writeln(" -o output image (required)"); + writeln(" --origin base address for relocatable segments (default 0x0600)"); + writeln(" -e, --entry entry symbol"); + writeln(" --map write the symbol map"); + writeln(" -h, --help"); +} + +void main(string[] args) +{ + try + { + string outPath; + string mapPath; + LinkOptions opts; + string[] inputs; + bool wantHelp = false; + + size_t i = 1; + while (i < args.length) + { + string a = args[i]; + if (a == "-h" || a == "--help") + wantHelp = true; + else if (a == "-o") + outPath = needArg(args, i); + else if (a == "--origin") + opts.origin = cast(ushort) parseAddr(needArg(args, i)); + else if (a == "-e" || a == "--entry") + opts.entrySymbol = needArg(args, i); + else if (a == "--map") + mapPath = needArg(args, i); + else if (a.length > 0 && a[0] == '-') + throw new Exception("unknown option: " ~ a); + else + inputs ~= a; + i++; + } + + if (wantHelp) + { + usage(); + return; + } + if (outPath.length == 0) + throw new Exception("no output file specified (use -o)"); + if (inputs.length == 0) + throw new Exception("no input object files"); + + Object[] objs; + foreach (f; inputs) + objs ~= readObject(f); + + LinkResult res = link(objs, opts); + writeImage(outPath, res.image); + if (mapPath.length) + std.file.write(mapPath, res.mapText ~ "\n"); + } + catch (Exception e) + { + stderr.writeln("wlinker: error: " ~ e.msg); + exit(1); + } } diff --git a/src/main.d b/src/main.d deleted file mode 100644 index 524c4f9..0000000 --- a/src/main.d +++ /dev/null @@ -1,6 +0,0 @@ -import std.stdio; - -void main() -{ - writeln("hello world!"); -} diff --git a/src/wcore/objfmt.d b/src/wcore/objfmt.d new file mode 100644 index 0000000..609c2df --- /dev/null +++ b/src/wcore/objfmt.d @@ -0,0 +1,420 @@ +/** + * Shared object-file / loadable-image formats for the weirdcpu toolchain. + * + * The assembler (wiasm) emits `.wo` objects. The linker (wlinker) reads one or + * more `.wo` objects and emits a `.wimg` loadable image. The emulator + * (weirdcpu) loads `.wimg` images (or raw binaries with an explicit origin). + * + * Everything is little-endian. + * + * Object format "WO01" + * -------------------- + * Header (16 bytes): + * char[4] magic = "WO01" + * u16 segCount + * u16 symCount + * u16 relCount + * u16 entry ; entry address, meaningless unless flags bit0 set + * u16 flags ; bit0 = hasEntry + * u16 reserved + * + * Segment record: + * char[8] name ; NUL padded, e.g. ".code" + * u8 flags ; SegFlag bits + * u8 reserved + * u16 origin ; requested origin, 0 = relocatable + * u32 size ; byte length of data + * u8[size] data ; omitted when SegFlag.bss is set + * + * Symbol record: + * char[32] name ; NUL padded + * u16 value + * u8 flags ; SymFlag bits + * u8 reserved + * u16 seg ; segment index, 0xFFFF if none + * + * Relocation record: + * u16 seg ; segment index the patch lives in + * u16 offset ; offset within that segment's data + * u16 sym ; symbol index + * u8 type ; RelocType + * u8 reserved + * + * Image format "WIMG" + * ------------------- + * char[4] magic = "WIMG" + * u16 loadAddr + * u16 entry + * u32 length + * u8[length] data + */ +module wcore.objfmt; + +import std.file : read, write; +import std.conv : to; +import std.string : format; + +/// Object file magic. +enum ObjMagic = "WO01"; +/// Loadable image magic. +enum ImgMagic = "WIMG"; +/// No segment / no symbol sentinel. +enum NoIndex = ushort.max; + +/// Segment attribute bits. +enum SegFlag : ubyte +{ + alloc = 1, /// occupies memory at runtime + exec = 2, /// executable + write = 4, /// writable + bss = 8, /// zero-initialised, no bytes stored on disk +} + +/// Symbol attribute bits. +enum SymFlag : ubyte +{ + defined = 1, /// has a value in this object + global = 2, /// visible to / referenceable by other objects + imported = 4, /// undefined here, resolved by the linker + zeropage = 8, /// value fits in one byte (0x00..0xFF) +} + +/// Relocation kinds. +enum RelocType : ubyte +{ + abs16 = 0, /// patch a 16-bit little-endian absolute address + lo8 = 1, /// patch the low byte of an address + hi8 = 2, /// patch the high byte of an address + rel8 = 3, /// patch an 8-bit signed PC-relative displacement +} + +/// A named chunk of bytes (or bss reservation). +struct Segment +{ + string name; /// e.g. ".code" + ubyte flags; /// SegFlag bits + ushort origin; /// requested origin, 0 = relocatable + ubyte[] data; /// contents; empty for bss + + /// True when the segment reserves memory but stores no bytes. + bool isBss() const pure nothrow @nogc + { + return (flags & SegFlag.bss) != 0; + } +} + +/// A symbol defined in, imported by, or exported from an object. +struct Symbol +{ + string name; + ushort value; + ubyte flags; /// SymFlag bits + ushort seg = NoIndex; + + /// True when this object provides the symbol's value. + bool isDefined() const pure nothrow @nogc + { + return (flags & SymFlag.defined) != 0; + } + + /// True when the symbol is visible outside its object. + bool isGlobal() const pure nothrow @nogc + { + return (flags & SymFlag.global) != 0; + } +} + +/// A request for the linker to patch bytes in a segment. +struct Reloc +{ + ushort seg; /// segment index the patch lives in + ushort offset; /// byte offset inside the segment + ushort sym; /// symbol index + ubyte type; /// RelocType +} + +/// A parsed `.wo` object file. +struct Object +{ + Segment[] segments; + Symbol[] symbols; + Reloc[] relocs; + ushort entry; + bool hasEntry; + + /// Find a symbol by name, or NoIndex when absent. + int findSymbol(string name) const + { + foreach (i, s; symbols) + if (s.name == name) + return cast(int) i; + return -1; + } +} + +/// A loadable image produced by the linker. +struct Image +{ + ushort loadAddr; + ushort entry; + ubyte[] data; +} + +// --------------------------------------------------------------------------- +// little-endian serialisation helpers +// --------------------------------------------------------------------------- + +private void putU8(ref ubyte[] b, ubyte v) +{ + b ~= v; +} + +private void putU16(ref ubyte[] b, ushort v) +{ + b ~= cast(ubyte) (v & 0xFF); + b ~= cast(ubyte) ((v >> 8) & 0xFF); +} + +private void putU32(ref ubyte[] b, uint v) +{ + b ~= cast(ubyte) (v & 0xFF); + b ~= cast(ubyte) ((v >> 8) & 0xFF); + b ~= cast(ubyte) ((v >> 16) & 0xFF); + b ~= cast(ubyte) ((v >> 24) & 0xFF); +} + +private void putFixed(ref ubyte[] b, string s, size_t n) +{ + foreach (i; 0 .. n) + b ~= (i < s.length) ? cast(ubyte) s[i] : cast(ubyte) 0; +} + +/// Extract a NUL-terminated fixed-width name from a byte slice. +string cstr(const(ubyte)[] buf) +{ + size_t n = 0; + while (n < buf.length && buf[n] != 0) + n++; + return cast(string) buf[0 .. n].dup; +} + +private struct Reader +{ + const(ubyte)[] buf; + size_t pos; + + void need(size_t n) const + { + if (pos + n > buf.length) + throw new Exception("unexpected end of file in object/image data"); + } + + ubyte u8() + { + need(1); + return buf[pos++]; + } + + ushort u16() + { + need(2); + ushort v = cast(ushort) (buf[pos] | (buf[pos + 1] << 8)); + pos += 2; + return v; + } + + uint u32() + { + need(4); + uint v = cast(uint) buf[pos] + | (cast(uint) buf[pos + 1] << 8) + | (cast(uint) buf[pos + 2] << 16) + | (cast(uint) buf[pos + 3] << 24); + pos += 4; + return v; + } + + ubyte[] bytes(size_t n) + { + need(n); + auto v = buf[pos .. pos + n].dup; + pos += n; + return v; + } + + string fixed(size_t n) + { + need(n); + auto v = cstr(buf[pos .. pos + n]); + pos += n; + return v; + } +} + +// --------------------------------------------------------------------------- +// object I/O +// --------------------------------------------------------------------------- + +/// Serialise an object to its `.wo` byte representation. +ubyte[] encodeObject(const ref Object obj) +{ + ubyte[] b; + putFixed(b, ObjMagic, 4); + putU16(b, cast(ushort) obj.segments.length); + putU16(b, cast(ushort) obj.symbols.length); + putU16(b, cast(ushort) obj.relocs.length); + putU16(b, obj.entry); + putU16(b, obj.hasEntry ? 1 : 0); + putU16(b, 0); + + foreach (seg; obj.segments) + { + putFixed(b, seg.name, 8); + putU8(b, seg.flags); + putU8(b, 0); + putU16(b, seg.origin); + putU32(b, cast(uint) seg.data.length); + if (!seg.isBss()) + b ~= seg.data; + } + + foreach (sym; obj.symbols) + { + putFixed(b, sym.name, 32); + putU16(b, sym.value); + putU8(b, sym.flags); + putU8(b, 0); + putU16(b, sym.seg); + } + + foreach (r; obj.relocs) + { + putU16(b, r.seg); + putU16(b, r.offset); + putU16(b, r.sym); + putU8(b, r.type); + putU8(b, 0); + } + + return b; +} + +/// Decode a `.wo` object from bytes. +Object decodeObject(const(ubyte)[] bytes) +{ + Reader r = Reader(bytes, 0); + string magic = cast(string) r.bytes(4); + if (magic != ObjMagic) + throw new Exception(format("bad object magic %s (expected %s)", magic, ObjMagic)); + + ushort segCount = r.u16(); + ushort symCount = r.u16(); + ushort relCount = r.u16(); + ushort entry = r.u16(); + ushort flags = r.u16(); + r.u16(); // reserved + + Object obj; + obj.entry = entry; + obj.hasEntry = (flags & 1) != 0; + + foreach (_; 0 .. segCount) + { + Segment seg; + seg.name = r.fixed(8); + seg.flags = r.u8(); + r.u8(); + seg.origin = r.u16(); + uint size = r.u32(); + if ((seg.flags & SegFlag.bss) != 0) + { + seg.data = new ubyte[size]; + seg.data[] = 0; + } + else + { + seg.data = r.bytes(size); + } + obj.segments ~= seg; + } + + foreach (_; 0 .. symCount) + { + Symbol sym; + sym.name = r.fixed(32); + sym.value = r.u16(); + sym.flags = r.u8(); + r.u8(); + sym.seg = r.u16(); + obj.symbols ~= sym; + } + + foreach (_; 0 .. relCount) + { + Reloc rel; + rel.seg = r.u16(); + rel.offset = r.u16(); + rel.sym = r.u16(); + rel.type = r.u8(); + r.u8(); + obj.relocs ~= rel; + } + + return obj; +} + +/// Write an object to `path`. +void writeObject(string path, const ref Object obj) +{ + write(path, encodeObject(obj)); +} + +/// Read an object from `path`. +Object readObject(string path) +{ + return decodeObject(cast(const(ubyte)[]) read(path)); +} + +// --------------------------------------------------------------------------- +// image I/O +// --------------------------------------------------------------------------- + +/// Serialise a loadable image to its `.wimg` byte representation. +ubyte[] encodeImage(const ref Image img) +{ + ubyte[] b; + putFixed(b, ImgMagic, 4); + putU16(b, img.loadAddr); + putU16(b, img.entry); + putU32(b, cast(uint) img.data.length); + b ~= img.data; + return b; +} + +/// Decode a `.wimg` image from bytes. +Image decodeImage(const(ubyte)[] bytes) +{ + Reader r = Reader(bytes, 0); + string magic = cast(string) r.bytes(4); + if (magic != ImgMagic) + throw new Exception(format("bad image magic %s (expected %s)", magic, ImgMagic)); + + Image img; + img.loadAddr = r.u16(); + img.entry = r.u16(); + uint len = r.u32(); + img.data = r.bytes(len); + return img; +} + +/// Write a loadable image to `path`. +void writeImage(string path, const ref Image img) +{ + write(path, encodeImage(img)); +} + +/// Read a loadable image from `path`. +Image readImage(string path) +{ + return decodeImage(cast(const(ubyte)[]) read(path)); +} diff --git a/tests/e2e.sh b/tests/e2e.sh new file mode 100755 index 0000000..b93185f --- /dev/null +++ b/tests/e2e.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# End-to-end test: assemble -> link -> emulate, and check program output. +# Usage: tests/e2e.sh +set -euo pipefail +cd "$(dirname "$0")/.." + +BIN="build/linux/x86_64/debug" +OUT="build/e2e" +mkdir -p "$OUT" + +echo "== building ==" +xmake -y >/dev/null + +WASM="$BIN/wiasm" +WLINK="$BIN/wlinker" +WCPU="$BIN/weirdcpu" + +for t in "$WASM" "$WLINK" "$WCPU"; do + [ -x "$t" ] || { echo "missing binary: $t"; exit 1; } +done + +fail=0 +check() { # name expected actual + if [ "$2" = "$3" ]; then + echo "PASS $1" + else + echo "FAIL $1" + echo " expected: $(printf '%q' "$2")" + echo " actual: $(printf '%q' "$3")" + fail=1 + fi +} + +echo +echo "== single object: hello ==" +"$WASM" -o "$OUT/hello.wo" examples/hello.asm +"$WLINK" -o "$OUT/hello.wimg" --map "$OUT/hello.map" "$OUT/hello.wo" +check "hello" "Hello, World!" "$("$WCPU" "$OUT/hello.wimg")" + +echo +echo "== cross object: putc + main ==" +"$WASM" -o "$OUT/putc.wo" examples/putc.asm +"$WASM" -o "$OUT/main.wo" examples/main.asm +"$WLINK" -o "$OUT/main.wimg" -e start --map "$OUT/main.map" "$OUT/putc.wo" "$OUT/main.wo" +check "cross-object" "Hello, 6502!" "$("$WCPU" "$OUT/main.wimg")" + +echo +echo "== arithmetic loop: count ==" +"$WASM" -o "$OUT/count.wo" examples/count.asm +"$WLINK" -o "$OUT/count.wimg" "$OUT/count.wo" +check "count" "0123456789" "$("$WCPU" "$OUT/count.wimg")" + +echo +if [ "$fail" -ne 0 ]; then + echo "e2e: FAILED" + exit 1 +fi +echo "e2e: OK" diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..9d22d1c --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Build and run the D unit tests for the toolchain modules. +# Usage: tests/run.sh +set -euo pipefail +cd "$(dirname "$0")/.." + +DMD="${DMD:-dmd}" +OUT="build/wtests" + +mkdir -p build + +"$DMD" -unittest -main -Isrc -of"$OUT" \ + src/wcore/objfmt.d \ + src/emulator/cpu.d \ + src/emulator/bus.d \ + src/assembler/assembler.d \ + src/linker/linker.d + +"$OUT" diff --git a/xmake.lua b/xmake.lua index 9ecbbb4..be6ba05 100644 --- a/xmake.lua +++ b/xmake.lua @@ -8,88 +8,32 @@ else set_strip("all") end +-- Shared formats used by more than one tool (object files, images). +local core = "src/wcore/*.d" + +-- 6502 emulator: loads a .wimg image (or raw binary) and runs it. target("weirdcpu") do set_kind("binary") - add_files("src/*.d") -end - -target("wlinker") -do - set_kind("binary") - add_files("src/linker/*.d") + add_includedirs("src") + add_files("src/emulator/*.d") + add_files(core) end +-- Assembler: .asm source -> .wo object. target("wiasm") do set_kind("binary") + add_includedirs("src") add_files("src/assembler/*.d") + add_files(core) +end + +-- Linker: one or more .wo objects -> .wimg image + .map. +target("wlinker") +do + set_kind("binary") + add_includedirs("src") + add_files("src/linker/*.d") + add_files(core) end --- --- If you want to known more usage about xmake, please see https://xmake.io --- --- ## FAQ --- --- You can enter the project directory firstly before building project. --- --- $ cd projectdir --- --- 1. How to build project? --- --- $ xmake --- --- 2. How to configure project? --- --- $ xmake f -p [macosx|linux|iphoneos ..] -a [x86_64|i386|arm64 ..] -m [debug|release] --- --- 3. Where is the build output directory? --- --- The default output directory is `./build` and you can configure the output directory. --- --- $ xmake f -o outputdir --- $ xmake --- --- 4. How to run and debug target after building project? --- --- $ xmake run [targetname] --- $ xmake run -d [targetname] --- --- 5. How to install target to the system directory or other output directory? --- --- $ xmake install --- $ xmake install -o installdir --- --- 6. Add some frequently-used compilation flags in xmake.lua --- --- @code --- -- add debug and release modes --- add_rules("mode.debug", "mode.release") --- --- -- add macro definition --- add_defines("NDEBUG", "_GNU_SOURCE=1") --- --- -- set warning all as error --- set_warnings("all", "error") --- --- -- set language: c99, c++11 --- set_languages("c99", "c++11") --- --- -- set optimization: none, faster, fastest, smallest --- set_optimize("fastest") --- --- -- add include search directories --- add_includedirs("/usr/include", "/usr/local/include") --- --- -- add link libraries and search directories --- add_links("tbox") --- add_linkdirs("/usr/local/lib", "/usr/lib") --- --- -- add system link libraries --- add_syslinks("z", "pthread") --- --- -- add compilation and link flags --- add_cxflags("-stdnolib", "-fno-strict-aliasing") --- add_ldflags("-L/usr/local/lib", "-lpthread", {force = true}) --- --- @endcode ---