stuffy 6502

This commit is contained in:
2026-09-11 17:41:23 -04:00
parent 251e28ffd6
commit 91a7b3b05e
16 changed files with 4219 additions and 87 deletions
+18
View File
@@ -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
+24
View File
@@ -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
+23
View File
@@ -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
+9
View File
@@ -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
File diff suppressed because it is too large Load Diff
+72 -3
View File
@@ -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] <input.asm>");
writeln();
writeln(" -o, --output <file> output object file (default: input with .wo)");
writeln(" -l, --listing <file> 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;
}
}
+142
View File
@@ -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);
}
+1014
View File
File diff suppressed because it is too large Load Diff
+230
View File
@@ -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] <file>
*/
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] <file>");
stderr.writeln();
stderr.writeln("Options:");
stderr.writeln(" --raw Load <file> as a raw binary (default: parse .wimg)");
stderr.writeln(" --origin <n> Load address for --raw (default 0x0600)");
stderr.writeln(" --entry <n> Entry point (default: image entry, or origin for raw)");
stderr.writeln(" --max-cycles <n> 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;
}
+442
View File
@@ -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);
}
+113 -3
View File
@@ -1,6 +1,116 @@
import std.stdio;
/**
* CLI wrapper for the weirdcpu linker.
*
* Usage:
* wlinker [options] -o <out.wimg> <a.wo> [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 <out.wimg> <a.wo> [b.wo ...]");
writeln(" -o <out.wimg> output image (required)");
writeln(" --origin <addr> base address for relocatable segments (default 0x0600)");
writeln(" -e, --entry <sym> entry symbol");
writeln(" --map <file> 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);
}
}
-6
View File
@@ -1,6 +0,0 @@
import std.stdio;
void main()
{
writeln("hello world!");
}
+420
View File
@@ -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));
}
Executable
+58
View File
@@ -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"
Executable
+19
View File
@@ -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"
+19 -75
View File
@@ -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
--