Skip to content

Commit

Permalink
small refactoring, implemented jmp in Tracer.ts
Browse files Browse the repository at this point in the history
  • Loading branch information
christo committed Dec 20, 2024
1 parent 02a468d commit ca76696
Show file tree
Hide file tree
Showing 15 changed files with 316 additions and 198 deletions.
3 changes: 2 additions & 1 deletion client/src/machine/FileBlob.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Addr, ArrayMemory, BE, Byteable, Endian} from "./core";
import {Addr, BE, Byteable, Endian} from "./core";
import {ArrayMemory} from "./Memory.ts";

/**
* Abstraction over a file-like thing which stores binary content and has a name and size. Contents can be accessed
Expand Down
134 changes: 134 additions & 0 deletions client/src/machine/Memory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {Addr, Byteable, Endian, MB_8} from "./core.ts";

/**
* Contiguous, fixed-sized 0-based Memory with {@link Endian Endianness}.
*/
interface Memory<T extends Endian> {

writeable(): boolean;

executable(): boolean;

/**
* Read from the offset a 16 bit word in the right {@link Endian Endianness}.
* @param byteOffset
*/
read16(byteOffset: Addr): number;


read8(byteOffset: Addr): number;

/**
* Gets the {@link Endian endianness}.
*/
endianness(): T;

getLength(): number;

submatch(seq: Uint8Array, atOffset: number): boolean;

contains(location: Addr): boolean;
}

/**
* Represents a contiguous, {@link Endian} Memory, backed by an array.
*/
class ArrayMemory<T extends Endian> implements Memory<T>, Byteable {
/** Arbitrary size, plenty for retro computers. */
private static MAX: number = MB_8;
private readonly _bytes: number[];
private readonly endian: T;
private readonly _writeable: boolean;
private readonly _executable: boolean;

/**
* Construct with an array of values or a desired size.
*
* @param bytes if a size, must be sensible, if an array, we use that.
* @param endian byte order for word interpretation.
* @param writeable whether this memory is marked as writeable by user code (does not imply immutable)
* @param executable whether this memory is marked as executable for user code
*/
constructor(bytes: number | number[], endian: T, writeable = true, executable = true) {
this._writeable = writeable;
this._executable = executable;
if (typeof bytes === "number") {
if (bytes < 0 || bytes > ArrayMemory.MAX) {
throw Error(`Memory size ${bytes} is not supported`);
}
this._bytes = new Array<number>(bytes);
// arbitrary conspicuous (0b1010 = 0xa = 10) double-endian fill constant to aid debugging
this._bytes.fill(0b1010);
} else {
if (bytes.length > ArrayMemory.MAX) {
throw Error(`Memory size ${bytes.length} is greater than maximum ${ArrayMemory.MAX}`);
}
this._bytes = bytes;
}
this.endian = endian;
}

static zeroes<T extends Endian>(size: number, endian: T, writeable: boolean, executable: boolean): ArrayMemory<T> {
return new ArrayMemory(Array(size).fill(0), endian, writeable, executable);
}

executable = (): boolean => this._executable;

writeable = (): boolean => this._writeable;

getLength = (): number => this._bytes.length;

getBytes = () => this._bytes;

submatch(seq: Uint8Array, atOffset: number): boolean {
if (seq.length + atOffset <= this._bytes.length && seq.length > 0) {
for (let i = 0; i < seq.length; i++) {
if (seq[i] !== this._bytes[i + atOffset]) {
return false;
}
}
// sequence has matched at offset
return true;
} else {
console.log("Sequence length out of range (" + seq.length + ") for memory size " + this._bytes.length);
return false;
}
}

read16(byteOffset: Addr) {
// last possible word offset must fit word
const lastWordAddress = this._bytes.length - 2;
if (byteOffset < 0 || byteOffset > lastWordAddress) {
throw Error("offset out of range for word read");
}
return this.endian.twoBytesToWord([this._bytes[byteOffset], this._bytes[byteOffset + 1]]);
}

read8(byteOffset: Addr): number {
if (!this.contains(byteOffset)) {
throw Error("offset out of range for vector read");
}
return this._bytes[byteOffset];
}

endianness = (): T => this.endian;

contains = (location: Addr) => location >= 0 && location < this._bytes.length;

/**
* Loads the given data to the given address.
* @param data
* @param location
*/
load(data: number[], location: Addr) {
if (data.length + location > this._bytes.length) {
throw Error(`Not enough room to load ${data.length} bytes at ${location}`);
}
data.forEach((b, index) => {
this._bytes[index + location] = b;
})
}
}

export {ArrayMemory};
export {type Memory};
42 changes: 37 additions & 5 deletions client/src/machine/Op.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,50 @@
export enum OpSemantics {
IS_UNCONDITIONAL_JUMP, // will modify PC
IS_CONDITIONAL_JUMP, // may modify PC
IS_BREAK, // legit break
IS_JAM, // illegal break
IS_ILLEGAL, // undocumented but may execute
IS_STORE, // modifies memory
IS_RETURN, // return from subroutine or interrupt
}

export class Op {
mnemonic: string;
description: string;
/** mnemonic category */
cat: string;
private readonly _isJump: boolean;
private semantics: OpSemantics[] = [];


constructor(mnemonic: string, description: string, cat: string, isJump = false) {
constructor(mnemonic: string, description: string, cat: string, semantics: OpSemantics[] = []) {
this.semantics = semantics;
this.mnemonic = mnemonic;
this.description = description;
this.cat = cat;
this._isJump = isJump;
}

get isJump(): boolean {
return this._isJump;
/**
* Returns true only if this {@see Op} has the given semantics.
* @param semantics
*/
has(semantics: OpSemantics): boolean {
return this.semantics.includes(semantics);
}

/**
* Returns true only if this {@see Op} has all the given semantics.
* @param semantics
*/
all(semantics: OpSemantics[]): boolean {
return semantics.every(s => this.semantics.includes(s));
}

/**
* Returns true only if this {@see Op} has at least one of the given semantics.
* @param semantics
*/
any(semantics: OpSemantics[]): boolean {
return semantics.some(s => this.semantics.includes(s));
}

}
36 changes: 26 additions & 10 deletions client/src/machine/Thread.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {Disassembler} from "./asm/Disassembler";
import {Addr, Endian, Memory} from "./core.ts";
import {Addr, Endian} from "./core.ts";
import {OpSemantics} from "./Op.ts";
import {Memory} from "./Memory.ts";

/**
* A single thread of execution which records all executed addresses and all written locations.
Expand Down Expand Up @@ -65,7 +67,6 @@ export class Thread {
throw new Error("cannot step if stopped");
}
this.execute();
this.executed.push(this.pc++);
}

/**
Expand All @@ -77,17 +78,26 @@ export class Thread {
* If an branching instruction occurs, the new {@link Thread} is returned, otherwise undefined.
*/
private execute(): Thread | undefined {
console.log(`executing: ${this.descriptor}`);
// TODO use this.disasm to disassemble current instruction

// TODO disassemble instruction at PC

// console.log(`executing: ${this.descriptor}/${this.pc}`);
// TODO confirm this.pc is the memory offset - what is the base address?
const inst = this.disasm.disassemble1(this.memory, this.pc);
// by default, increment PC by length of this instruction
let nextPc = this.pc + inst.getLength();
if (this.executed.includes(this.pc)) {
console.log(`already executed ${this.pc}, terminating thread ${this}`);
this._running = false;
} else {
const op = inst.instruction.op;
if (op.any([OpSemantics.IS_BREAK, OpSemantics.IS_JAM])) {
this._running = false;
} else if (op.any([OpSemantics.IS_UNCONDITIONAL_JUMP])) {
nextPc = inst.operandValue();
}
}

// TODO handle stop cases, i.e. BRK or CPU JAM
// TODO handle join case, i.e. reaching already traced code
// TODO handle fork case, i.e. conditional branch
// TODO handle simple recording of execution at instruction address, must keep track of first byte and
// instruction length.
// TODO handle tracing interrupt handlers - these are tricky - perhaps we can just always trace them
// TODO edge case: execution at an address could be byte-misaligned with previous execution resulting in
// different instruction decoding, so execution records should hold the first byte of the decoded instruction
// and coverage measurements imply that coverage of any subsequent bytes of the instruction is predicated on
Expand All @@ -96,6 +106,8 @@ export class Thread {
// instructions may be rare enough to simply report as anomalies at first and may even be more likely be a
// theoretical bug in the analysed code. This tracer will not detect all unreachable code paths since only a
// degenerate runtime state is represented.
this.executed.push(this.pc);
this.pc = nextPc; // increment PC by length of this instruction
return undefined;
}

Expand All @@ -106,4 +118,8 @@ export class Thread {
getWritten(): Array<number> {
return [...this.written];
}

getPc() {
return this.pc;
}
}
8 changes: 5 additions & 3 deletions client/src/machine/Tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
* idioms may be recognised this same way across different code bases.
*/

import {Endian, Memory} from "./core";
import {Endian, hex16} from "./core";
import {Disassembler} from "./asm/Disassembler";
import {Thread} from "./Thread.ts";
import {Memory} from "./Memory.ts";

/**
* Analyser that approximates code execution to some degree. Static analysis ignores register and memory contents,
Expand Down Expand Up @@ -39,10 +40,11 @@ class Tracer {
* @param memory the Memory
*/
constructor(disasm: Disassembler, pc: number, memory: Memory<Endian>) {
const relativePc = pc - disasm.getSegmentBaseAddress();
if (Math.round(pc) !== pc) {
throw Error(`startLocation must be integral`);
} else if (pc < 0 || memory.getLength() <= pc) {
throw Error(`startLocation ${pc} not inside memory of size ${memory.getLength()}`);
} else if (relativePc < 0 || memory.getLength() <= relativePc) {
throw Error(`startLocation 0x${hex16(pc)} not inside memory of size ${memory.getLength()} at base 0x${hex16(disasm.getSegmentBaseAddress())}`);
} else if (!memory.executable()) {
throw Error("memory not marked for execution");
}
Expand Down
3 changes: 2 additions & 1 deletion client/src/machine/api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {Addr, BigEndian, hex8, LittleEndian, Memory} from "./core";
import {Addr, BigEndian, hex8, LittleEndian} from "./core";
import {FileBlob} from "./FileBlob";
import {Mos6502} from "./mos6502";
import {InstructionLike} from "./asm/instructions.ts";
import {DataView, DataViewImpl} from "./DataView.ts";
import {BlobSniffer} from "./BlobSniffer.ts";
import {Memory} from "./Memory.ts";

/**
* Renderable output of structured text with html-friendly structure and internal text renderer.
Expand Down
Loading

0 comments on commit ca76696

Please sign in to comment.