stuffy 6502
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import std.stdio;
|
||||
|
||||
void main()
|
||||
{
|
||||
writeln("hello world!");
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
@@ -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
|
||||
--
|
||||
|
||||
Reference in New Issue
Block a user