diff --git a/content/toys/triscit/binutils.mjs b/content/toys/triscit/binutils.mjs new file mode 100644 index 0000000..6ae24c4 --- /dev/null +++ b/content/toys/triscit/binutils.mjs @@ -0,0 +1,92 @@ +const glyphs = [ + "\\x00", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\x07", + "\\x08", "\\x09", "\\x0A", "\\x0B", "\\x0C", "\\x0D", "\\x0E", "\\x0F", + "⏵", "⏴", "↕", "‼", "¶", "§", "‽", "↨", "↑", "↓", "→", "←", "∟", "↔", "⏶", "⏷", + " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", + "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", + "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", + "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", + "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "⌂", + + "Ç", "ü", "é", "â", "ä", "à", "å", "ç", "ê", "ë", "è", "ï", "î", "ì", "Ä", "Å", + "É", "æ", "Æ", "ô", "ö", "ò", "û", "ù", "ÿ", "Ö", "Ü", "¢", "£", "¥", "₧", "ƒ", + "á", "í", "ó", "ú", "ñ", "Ñ", "ª", "º", "¿", "⌐", "¬", "½", "¼", "¡", "«", "»", + "░", "▒", "▓", "│", "┤", "╡", "╢", "╖", "╕", "╣", "║", "╗", "╝", "╜", "╛", "┐", + "└", "┴", "┬", "├", "─", "┼", "╞", "╟", "╚", "╔", "╩", "╦", "╠", "═", "╬", "╧", + "╨", "╤", "╥", "╙", "╘", "╒", "╓", "╫", "╪", "┘", "┌", "█", "▄", "▌", "▐", "▀", + "α", "ß", "Γ", "π", "Σ", "σ", "µ", "τ", "Φ", "Θ", "Ω", "δ", "∞", "φ", "ε", "∩", + "≡", "±", "≥", "≤", "⌠", "⌡", "÷", "≈", "°", "∞", "⊻", "√", "ⁿ", "²", "■", "¤", +] + +/** + * Encode a buffer using fluffy glyphs + * + * @param {Uint8Array} buf Buffer to encode + * @returns {String} Glyph-encoded string + */ +function Stringify(buf) { + let ret = [] + for (let i of buf) { + ret.push(glyphs[i]) + } + return ret.join("") +} + + +/** + * Hex encode a buffer + * + * @param {Uint8Array} buf Buffer to encode + * @returns {String} Hexlified version + */ +function Hexlify(buf) { + let ret = [] + for (let b of buf) { + let hex = "00" + b.toString(16) + ret.push(hex.slice(-2)) + } + return ret.join(" ") +} + +/** + * Hex decode a string + * + * @param {String} str String to decode + * @returns {Uint8Array} Decoded array + */ +function Unhexlify(str) { + let a = [] + let match = str.match(/[0-9a-fA-F]{2}/g) || [] + for (let m of match) { + a.push(parseInt(m, 16)) + } + return new Uint8Array(a) +} + +/** + * Read a NULL-Terminated string from a buffer. + * + * @param {Uint8Arrary} buf Buffer to read + * @param {Boolean} includeNull True to include the trailing NUL + * @returns {Uint8Array} String + */ +function CString(buf, includeNull=false) { + let end = buf.indexOf(0) + if (end == -1) { + return buf + } + return buf.slice(0, end + (includeNull?1:0)) +} + +/** + * Unescapes a string with things like "\x02" in it. + * + * @param {String} str String to unescape + * @returns {String} Unescaped string + */ +function Unescape(str) { + return str.replaceAll(/\\x?([0-9]+)/g, (_, p1) => String.fromCharCode(p1)) +} + +export {Stringify, Hexlify, Unhexlify, CString, Unescape} diff --git a/content/toys/triscit/fire.gif b/content/toys/triscit/fire.gif new file mode 100644 index 0000000..4034689 Binary files /dev/null and b/content/toys/triscit/fire.gif differ diff --git a/content/toys/triscit/index.html b/content/toys/triscit/index.html new file mode 100644 index 0000000..ad1c10e --- /dev/null +++ b/content/toys/triscit/index.html @@ -0,0 +1,95 @@ +--- +title: Triscit +description: A CPU simulator for cybersecurity education +type: bare +--- + + + + + Triscit + + + + + + + + +
+
+
+
+
+
+ PC= + + + +
+
+ + + +
+
+
+
+
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + +
+
+ +
+

Instructions

+
+
+
+
+
+ +
+
+
+ + + + + + + diff --git a/content/toys/triscit/triscit.mjs b/content/toys/triscit/triscit.mjs new file mode 100644 index 0000000..ba62625 --- /dev/null +++ b/content/toys/triscit/triscit.mjs @@ -0,0 +1,154 @@ +import * as Binutils from "./binutils.mjs" + +const FLAG_NEGATIVE = 1 << 0 +const FLAG_HALT = 1 << 1 +const FLAG_ABLAZE = 1 << 2 + +class Instruction { + constructor(name, args, description) { + this.Name = name + this.Args = args + this.Description = description + } +} + +const Instructions = [ + new Instruction("NOOP", [], "Do nothing"), + new Instruction("PRNT", ["x"], "Print string at x"), + new Instruction("READ", ["x"], "Read input, store in x"), + new Instruction("CMPS", ["x", "y"], "Compare string x to string y"), + new Instruction("JNEQ", ["x"], "If not equal, set PC to x"), + new Instruction("JUMP", ["x"], "Set PC to x"), + new Instruction("HALT", [], "Terminate program"), + new Instruction("HACF", [], "Halt And Catch Fire (never use this!)"), +] + +function disassemble(program) { + let b = program[0] + let inst = Instructions[b] + if (inst) { + let nargs = inst.Args.length + let args = [] + for (let i = 0; i < nargs; i++) { + args.push(program[i+1]) + } + + if (1+nargs <= program.length) { + // Only return an opcode if there was enough data for its arguments as well + return {length: 1+nargs, name: inst.Name, args: args} + } + } + + let data = Binutils.CString(program, true) + let s = Binutils.Stringify(data) + return {length: data.length, name: "DATA", args: [s]} +} + +class CPU { + /** + * @param {Uint8Array} program Program to load + * @param {string} input input string + * @param {Number} pc initial program counter + * @param {Number} flags initial flags + */ + constructor(program, input, output="", pc=0, flags=0) { + this.Program = program + this.Input = input + this.Output = output + this.PC = pc + this.Flags = flags + } + + Clone() { + return new CPU(this.Program, this.Input, this.Output, this.PC, this.Flags) + } + + DisassembleProgram() { + let ret = [] + let prog1 = this.Program.slice(0, this.PC) + let addr = 0 + for (let prog of [prog1, this.Program]) { + while (addr < prog.length) { + let subprog = prog.slice(addr) + let inst = disassemble(subprog) + ret.push({ + addr, + buf: this.Program.slice(addr, addr+inst.length), + name: inst.name, + args: inst.args, + }) + addr += inst.length + } + } + return ret + } + + Step() { + let prog = this.Program.slice(this.PC) + let inst = disassemble(prog) + let flags = this.Flags + this.Flags = 0 + switch (inst.name) { + case "JUMP": + this.PC = inst.args[0] + return + case "JNEQ": + if (flags && FLAG_NEGATIVE) { + this.PC = inst.args[0] + return + } + break + case "READ": { + let addr = inst.args[0] + let b = new TextEncoder().encode(this.Input + "\0") + let newproglen = Math.max(this.Program.length, addr+b.length) + let newprog = new Uint8Array(newproglen) + + for (let i = 0; i < addr; i++) { + newprog[i] = this.Program[i] + } + for (let i = 0; i < b.length; i++) { + newprog[addr+i] = b[i] + } + for (let i = addr+b.length; i < this.Program.length; i++) { + newprog[i] = this.Program[i] + } + this.Program = newprog + break + } + case "PRNT": { + let addr = inst.args[0] + let prog = this.Program.slice(addr) + this.Output = Binutils.CString(prog) + break + } + case "CMPS": { + let decoder = new TextDecoder() + let a = decoder.decode(Binutils.CString(this.Program.slice(inst.args[0]))) + let b = decoder.decode(Binutils.CString(this.Program.slice(inst.args[1]))) + if (a != b) { + this.Flags = FLAG_NEGATIVE + } + break + } + case "HALT": + // Don't modify PC + this.Flags = FLAG_HALT + return + case "HACF": + this.Flags = FLAG_HALT | FLAG_ABLAZE + return + case "DATA": + // Keep stepping through data one byte at a time trying to execute something + inst.length = 1 + break + } + this.PC += inst.length + } +} + +export { + FLAG_NEGATIVE, FLAG_HALT, FLAG_ABLAZE, + Instructions, + CPU, +} \ No newline at end of file diff --git a/content/toys/triscit/ui.css b/content/toys/triscit/ui.css new file mode 100644 index 0000000..d9e9be6 --- /dev/null +++ b/content/toys/triscit/ui.css @@ -0,0 +1,32 @@ + +#instructions th.addr { + text-align: right; + color: brown; +} + +td.name { + font-weight: bold; +} + +td.args { + color: green; +} + +td.hex { + color: #777; +} + +.ablaze { + background: url(fire.gif) bottom repeat-x; +} + +@media screen and (min-width: 768px) { + #cpu-box { + max-height: 95vh; + } + + .table-container { + max-height: calc(100% - 4em); + overflow-y: scroll; + } +} \ No newline at end of file diff --git a/content/toys/triscit/ui.mjs b/content/toys/triscit/ui.mjs new file mode 100644 index 0000000..c9fbf41 --- /dev/null +++ b/content/toys/triscit/ui.mjs @@ -0,0 +1,206 @@ +import * as Binutils from "./binutils.mjs" +import * as Triscit from "./triscit.mjs" + +const SamplePrograms = [ + `05 0d +61 62 63 64 65 66 67 68 69 6a 00 +02 02 +01 02 +06 +`, + `05 31 +61 62 63 64 65 66 67 68 69 6a 00 +70 61 73 73 77 6f 72 64 00 +53 75 70 65 72 20 53 65 63 72 65 74 00 +53 6f 72 72 79 2c 20 77 72 6f 6e 67 2e 00 +02 02 +03 02 0d +04 3b +01 16 +06 +01 23 +06 +` +] + +function fill(element, values) { + for (let e of element.querySelectorAll("[data-fill]")) { + e.textContent = values[e.dataset.fill] + } +} + +class UI { + constructor(program=[]) { + this.program = program + this.Init() + this.Reset() + } + + Init() { + for (let e of document.querySelectorAll("[data-control]")) { + switch (e.dataset.control) { + case "step": + e.addEventListener("click", () => this.Step()) + break + case "back": + e.addEventListener("click", () => this.Unstep()) + break + case "reset": + e.addEventListener("click", () => this.Reset()) + break + case "input": + e.addEventListener("input", () => this.SetInput()) + break + case "program": + e.addEventListener("input", () => this.SetProgram()) + } + } + + let ih = document.querySelector("#instructions-help") + for (let i = 0; i < Triscit.Instructions.length; i++) { + let inst = Triscit.Instructions[i] + let doc = document.querySelector("template#instruction-help").content.cloneNode(true) + let tr = doc.firstElementChild + fill(tr, { + num: Binutils.Hexlify([i]), + args: inst.Args.join(" "), + name: inst.Name, + description: inst.Description, + }) + ih.appendChild(doc) + } + + let fields = new URLSearchParams(window.location.search); + let progNumber = fields.get("p") || 0 + let progElement = document.querySelector('[data-control="program"]') + let prog = SamplePrograms[progNumber] + if (prog) { + progElement.value = SamplePrograms[progNumber] + progElement.parentElement.classList.add("is-hidden") + } + + this.SetInput() + this.SetProgram() + } + + Reset() { + this.cpu = new Triscit.CPU(this.program, this.input) + this.stack = [] + this.Refresh() + } + + Refresh() { + let inste = document.querySelector("#instructions") + while (inste.firstChild) inste.firstChild.remove() + for (let i of this.cpu.DisassembleProgram()) { + let doc = document.querySelector("template#instruction").content.cloneNode(true) + let tr = doc.firstElementChild + fill(tr, { + addr: Binutils.Hexlify([i.addr]), + name: i.name, + hex: Binutils.Hexlify(i.buf), + }) + + let args = tr.querySelector(".args") + for (let a of i.args) { + if (! isNaN(a)) { + a = Binutils.Hexlify([a]) + } + args.textContent += `${a} ` + } + + inste.appendChild(tr) + if (i.addr == this.cpu.PC) { + tr.classList.add("is-selected") + tr.scrollIntoView(false) + // let top = tr.offsetTop + // let parent = tr.parentElement + + // tr.parentElement.scrollTop = top + } + } + + for (let e of document.querySelectorAll("[data-flag]")) { + let mask = 0 + let className = "is-info" + switch (e.dataset.flag) { + case "negative": + mask = Triscit.FLAG_NEGATIVE + break + case "halt": + mask = Triscit.FLAG_HALT + break + case "fire": + mask = Triscit.FLAG_ABLAZE + className = "is-danger" + break + } + if (this.cpu.Flags & mask) { + e.classList.add(className) + } else { + e.classList.remove(className) + } + } + + for (let cpuBox of document.querySelectorAll("#cpu-box")) { + if (this.cpu.Flags & Triscit.FLAG_ABLAZE) { + cpuBox.classList.add("ablaze") + } else { + cpuBox.classList.remove("ablaze") + } + } + + for (let e of document.querySelectorAll("[data-value]")) { + switch (e.dataset.value) { + case "pc": + e.textContent = Binutils.Hexlify([this.cpu.PC]) + break + case "output": + e.textContent = Binutils.Stringify(this.cpu.Output) + break + } + } + } + + Step() { + if (this.cpu.Flags & Triscit.FLAG_HALT) { + return + } + this.stack.push(this.cpu) + this.cpu = this.cpu.Clone() + this.cpu.Step() + this.Refresh() + } + + Unstep() { + if (this.stack.length == 0) { + return + } + this.cpu = this.stack.pop() + this.Refresh() + } + + SetProgram() { + let e =document.querySelector('[data-control="program"]') + this.program = Binutils.Unhexlify(e.value || "") + this.Reset() + } + + SetInput() { + let e = document.querySelector('[data-control="input"]') + let v = e.value || "" + this.input = Binutils.Unescape(v) + this.Reset() + } +} + +function init() { + let app = new UI() + window.app = app +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init) +} else { + init() +}