From a05ba49f4b7c0b5cf38c0df995a8546d06c24931 Mon Sep 17 00:00:00 2001 From: Keys <70819367+keys-i@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:33:11 +1000 Subject: [PATCH] Add a documented x86-64 instruction subset CPU. Ship LinearMemory, a register/flags file, and an interpreter covering MOV/ALU/JMP/Jcc/CALL/RET/PUSH/POP/LEA/INC/DEC/NOP/SYSCALL for toy programs. Fail closed on unsupported encodings. Closes #18 Co-authored-by: Cursor --- javascripts/cpu/memory.js | 124 ++++++++++++ javascripts/cpu/x86.js | 401 ++++++++++++++++++++++++++++++++++++++ javascripts/index.js | 1 + package.json | 2 + tools/scripts/test.js | 22 +++ 5 files changed, 550 insertions(+) create mode 100644 javascripts/cpu/memory.js create mode 100644 javascripts/cpu/x86.js diff --git a/javascripts/cpu/memory.js b/javascripts/cpu/memory.js new file mode 100644 index 0000000..d979e02 --- /dev/null +++ b/javascripts/cpu/memory.js @@ -0,0 +1,124 @@ +const u64 = (value) => BigInt.asUintN(64, BigInt(value)); +const i64 = (value) => BigInt.asIntN(64, BigInt(value)); + +export class LinearMemory { + #bytes; + + constructor(size = 65536) { + if (!Number.isSafeInteger(size) || size < 4096 || size % 4096) { + throw new TypeError("memory size must be a 4096-aligned integer >= 4096"); + } + this.#bytes = new Uint8Array(size); + } + + get size() { + return this.#bytes.length; + } + + get buffer() { + return this.#bytes.buffer; + } + + bytes() { + return this.#bytes; + } + + load(path, address = 0n) { + const at = Number(u64(address)); + if (at < 0 || at + path.length > this.#bytes.length) throw new RangeError("image exceeds memory"); + this.#bytes.set(path, at); + } + + u8(address, value) { + const at = Number(u64(address)); + if (at >= this.#bytes.length) throw new RangeError("memory fault"); + if (value === undefined) return this.#bytes[at]; + this.#bytes[at] = value & 0xff; + } + + u32(address, value) { + const at = Number(u64(address)); + if (at + 4 > this.#bytes.length) throw new RangeError("memory fault"); + const view = new DataView(this.#bytes.buffer, at, 4); + if (value === undefined) return view.getUint32(0, true); + view.setUint32(0, value >>> 0, true); + } + + u64(address, value) { + const at = Number(u64(address)); + if (at + 8 > this.#bytes.length) throw new RangeError("memory fault"); + const view = new DataView(this.#bytes.buffer, at, 8); + if (value === undefined) return view.getBigUint64(0, true); + view.setBigUint64(0, u64(value), true); + } +} + +export const FLAGS = Object.freeze({ CF: 1n << 0n, ZF: 1n << 6n, SF: 1n << 7n, OF: 1n << 11n }); + +export class RegisterFile { + #regs = new BigUint64Array(16); + rip = 0n; + rflags = 0n; + + get(index) { + return this.#regs[index]; + } + + set(index, value) { + this.#regs[index] = u64(value); + } + + snapshot() { + return { + rax: this.#regs[0], + rcx: this.#regs[1], + rdx: this.#regs[2], + rbx: this.#regs[3], + rsp: this.#regs[4], + rbp: this.#regs[5], + rsi: this.#regs[6], + rdi: this.#regs[7], + r8: this.#regs[8], + r9: this.#regs[9], + r10: this.#regs[10], + r11: this.#regs[11], + r12: this.#regs[12], + r13: this.#regs[13], + r14: this.#regs[14], + r15: this.#regs[15], + rip: this.rip, + rflags: this.rflags, + }; + } +} + +export const setLogicalFlags = (regs, result) => { + const value = u64(result); + regs.rflags = + (regs.rflags & ~(FLAGS.CF | FLAGS.ZF | FLAGS.SF | FLAGS.OF)) | + (value === 0n ? FLAGS.ZF : 0n) | + ((value >> 63n) & 1n ? FLAGS.SF : 0n); +}; + +export const setAddFlags = (regs, left, right, result) => { + const value = u64(result); + const signed = i64(result); + regs.rflags = + (regs.rflags & ~(FLAGS.CF | FLAGS.ZF | FLAGS.SF | FLAGS.OF)) | + (u64(left) + u64(right) > 0xffffffffffffffffn ? FLAGS.CF : 0n) | + (value === 0n ? FLAGS.ZF : 0n) | + ((value >> 63n) & 1n ? FLAGS.SF : 0n) | + (i64(left) >= 0 === i64(right) >= 0 && i64(left) >= 0 !== signed >= 0 ? FLAGS.OF : 0n); +}; + +export const setSubFlags = (regs, left, right, result) => { + const value = u64(result); + regs.rflags = + (regs.rflags & ~(FLAGS.CF | FLAGS.ZF | FLAGS.SF | FLAGS.OF)) | + (u64(left) < u64(right) ? FLAGS.CF : 0n) | + (value === 0n ? FLAGS.ZF : 0n) | + ((value >> 63n) & 1n ? FLAGS.SF : 0n) | + (i64(left) >= 0 !== i64(right) >= 0 && i64(left) >= 0 !== i64(result) >= 0 ? FLAGS.OF : 0n); +}; + +export { u64, i64 }; diff --git a/javascripts/cpu/x86.js b/javascripts/cpu/x86.js new file mode 100644 index 0000000..70c3b6a --- /dev/null +++ b/javascripts/cpu/x86.js @@ -0,0 +1,401 @@ +import { FLAGS, LinearMemory, RegisterFile, i64, setAddFlags, setLogicalFlags, setSubFlags, u64 } from "./memory.js"; + +const REG = Object.freeze({ + rax: 0, + rcx: 1, + rdx: 2, + rbx: 3, + rsp: 4, + rbp: 5, + rsi: 6, + rdi: 7, + r8: 8, + r9: 9, + r10: 10, + r11: 11, + r12: 12, + r13: 13, + r14: 14, + r15: 15, +}); + +const readImm8 = (mem, regs) => { + const value = mem.u8(regs.rip); + regs.rip += 1n; + return value; +}; + +const readImm32 = (mem, regs) => { + const value = mem.u32(regs.rip); + regs.rip += 4n; + return value; +}; + +const readImm64 = (mem, regs) => { + const value = mem.u64(regs.rip); + regs.rip += 8n; + return value; +}; + +const sign8 = (value) => BigInt.asIntN(8, BigInt(value)); +const sign32 = (value) => BigInt.asIntN(32, BigInt(value)); + +export class X86Cpu { + #memory; + #regs = new RegisterFile(); + #syscall; + #halted = false; + #steps = 0; + + constructor({ memory = new LinearMemory(), onSyscall } = {}) { + this.#memory = memory; + this.#syscall = onSyscall ?? (() => 0n); + this.reset(); + } + + get memory() { + return this.#memory; + } + + get halted() { + return this.#halted; + } + + get steps() { + return this.#steps; + } + + registers() { + return this.#regs.snapshot(); + } + + reset({ rip = 0n, rsp = BigInt(this.#memory.size - 8) } = {}) { + this.#regs = new RegisterFile(); + this.#regs.rip = u64(rip); + this.#regs.set(REG.rsp, u64(rsp)); + this.#halted = false; + this.#steps = 0; + } + + load(bytes, address = 0n) { + this.#memory.load(bytes, address); + this.#regs.rip = u64(address); + } + + #modrm(rex) { + const modrm = readImm8(this.#memory, this.#regs); + const mod = modrm >> 6; + const reg = (rex.r ? 8 : 0) | ((modrm >> 3) & 7); + const rm = (rex.b ? 8 : 0) | (modrm & 7); + if (mod === 3) return { reg, rm, mode: "reg" }; + if (rm === 4 || rm === 12) throw new Error("SIB addressing is not in the documented subset"); + let address = this.#regs.get(rm); + if (mod === 1) address = u64(address + sign8(readImm8(this.#memory, this.#regs))); + if (mod === 2) address = u64(address + sign32(readImm32(this.#memory, this.#regs))); + if (mod === 0 && (rm === 5 || rm === 13)) { + address = u64(this.#regs.rip + sign32(readImm32(this.#memory, this.#regs))); + } + return { reg, rm, mode: "mem", address }; + } + + #readOp(op) { + return op.mode === "reg" ? this.#regs.get(op.rm) : this.#memory.u64(op.address); + } + + #writeOp(op, value) { + if (op.mode === "reg") this.#regs.set(op.rm, value); + else this.#memory.u64(op.address, value); + } + + step() { + if (this.#halted) return false; + const start = this.#regs.rip; + let rex = { w: false, r: false, x: false, b: false }; + let opcode = readImm8(this.#memory, this.#regs); + if ((opcode & 0xf0) === 0x40) { + rex = { w: Boolean(opcode & 8), r: Boolean(opcode & 4), x: Boolean(opcode & 2), b: Boolean(opcode & 1) }; + opcode = readImm8(this.#memory, this.#regs); + } + + if (opcode === 0x90) { + this.#steps += 1; + return true; + } + + if (opcode === 0xc3) { + this.#regs.rip = this.#memory.u64(this.#regs.get(REG.rsp)); + this.#regs.set(REG.rsp, this.#regs.get(REG.rsp) + 8n); + this.#steps += 1; + return true; + } + + if (opcode >= 0x50 && opcode <= 0x57) { + const reg = (rex.b ? 8 : 0) | (opcode & 7); + const rsp = this.#regs.get(REG.rsp) - 8n; + this.#regs.set(REG.rsp, rsp); + this.#memory.u64(rsp, this.#regs.get(reg)); + this.#steps += 1; + return true; + } + + if (opcode >= 0x58 && opcode <= 0x5f) { + const reg = (rex.b ? 8 : 0) | (opcode & 7); + const rsp = this.#regs.get(REG.rsp); + this.#regs.set(reg, this.#memory.u64(rsp)); + this.#regs.set(REG.rsp, rsp + 8n); + this.#steps += 1; + return true; + } + + if (opcode === 0x68) { + const imm = sign32(readImm32(this.#memory, this.#regs)); + const rsp = this.#regs.get(REG.rsp) - 8n; + this.#regs.set(REG.rsp, rsp); + this.#memory.u64(rsp, u64(imm)); + this.#steps += 1; + return true; + } + + if (opcode >= 0xb8 && opcode <= 0xbf) { + const reg = (rex.b ? 8 : 0) | (opcode & 7); + this.#regs.set(reg, rex.w ? readImm64(this.#memory, this.#regs) : u64(readImm32(this.#memory, this.#regs))); + this.#steps += 1; + return true; + } + + if (opcode === 0xe8) { + const rel = sign32(readImm32(this.#memory, this.#regs)); + const rsp = this.#regs.get(REG.rsp) - 8n; + this.#regs.set(REG.rsp, rsp); + this.#memory.u64(rsp, this.#regs.rip); + this.#regs.rip = u64(this.#regs.rip + rel); + this.#steps += 1; + return true; + } + + if (opcode === 0xe9) { + this.#regs.rip = u64(this.#regs.rip + sign32(readImm32(this.#memory, this.#regs))); + this.#steps += 1; + return true; + } + + if (opcode === 0xeb) { + this.#regs.rip = u64(this.#regs.rip + sign8(readImm8(this.#memory, this.#regs))); + this.#steps += 1; + return true; + } + + if (opcode === 0x0f) { + const second = readImm8(this.#memory, this.#regs); + if (second >= 0x80 && second <= 0x8f) { + const rel = sign32(readImm32(this.#memory, this.#regs)); + if (this.#condition(second & 0xf)) this.#regs.rip = u64(this.#regs.rip + rel); + this.#steps += 1; + return true; + } + if (second === 0x05) { + const result = this.#syscall({ + nr: this.#regs.get(REG.rax), + args: [ + this.#regs.get(REG.rdi), + this.#regs.get(REG.rsi), + this.#regs.get(REG.rdx), + this.#regs.get(REG.r10), + this.#regs.get(REG.r8), + this.#regs.get(REG.r9), + ], + cpu: this, + }); + if (result === null || result === undefined) this.#halted = true; + else this.#regs.set(REG.rax, u64(result)); + this.#steps += 1; + return !this.#halted; + } + throw new Error(`unsupported opcode 0f ${second.toString(16)} at ${start}`); + } + + if (opcode >= 0x70 && opcode <= 0x7f) { + const rel = sign8(readImm8(this.#memory, this.#regs)); + if (this.#condition(opcode & 0xf)) this.#regs.rip = u64(this.#regs.rip + rel); + this.#steps += 1; + return true; + } + + if (opcode === 0x89 || opcode === 0x8b) { + const op = this.#modrm(rex); + if (opcode === 0x89) this.#writeOp(op, this.#regs.get(op.reg)); + else this.#regs.set(op.reg, this.#readOp(op)); + this.#steps += 1; + return true; + } + + if (opcode === 0x8d) { + const op = this.#modrm(rex); + if (op.mode !== "mem") throw new Error("LEA requires memory operand"); + this.#regs.set(op.reg, op.address); + this.#steps += 1; + return true; + } + + if ( + opcode === 0x01 || + opcode === 0x03 || + opcode === 0x29 || + opcode === 0x2b || + opcode === 0x21 || + opcode === 0x23 || + opcode === 0x09 || + opcode === 0x0b || + opcode === 0x31 || + opcode === 0x33 || + opcode === 0x39 || + opcode === 0x3b + ) { + const op = this.#modrm(rex); + const regValue = this.#regs.get(op.reg); + const rmValue = this.#readOp(op); + const dstIsReg = (opcode & 2) !== 0; + const left = dstIsReg ? regValue : rmValue; + const right = dstIsReg ? rmValue : regValue; + let result; + switch (opcode & 0xfd) { + case 0x01: + result = u64(left + right); + setAddFlags(this.#regs, left, right, result); + break; + case 0x29: + result = u64(left - right); + setSubFlags(this.#regs, left, right, result); + break; + case 0x21: + result = left & right; + setLogicalFlags(this.#regs, result); + break; + case 0x09: + result = left | right; + setLogicalFlags(this.#regs, result); + break; + case 0x31: + result = left ^ right; + setLogicalFlags(this.#regs, result); + break; + case 0x39: + result = u64(left - right); + setSubFlags(this.#regs, left, right, result); + this.#steps += 1; + return true; + default: + throw new Error(`unhandled alu ${opcode.toString(16)}`); + } + if (dstIsReg) this.#regs.set(op.reg, result); + else this.#writeOp(op, result); + this.#steps += 1; + return true; + } + + if (opcode === 0x81 || opcode === 0x83) { + const op = this.#modrm(rex); + const imm = + opcode === 0x83 ? sign8(readImm8(this.#memory, this.#regs)) : sign32(readImm32(this.#memory, this.#regs)); + const left = this.#readOp(op); + const group = op.reg & 7; + let result; + if (group === 0) { + result = u64(left + imm); + setAddFlags(this.#regs, left, imm, result); + } else if (group === 4) { + result = left & u64(imm); + setLogicalFlags(this.#regs, result); + } else if (group === 1) { + result = left | u64(imm); + setLogicalFlags(this.#regs, result); + } else if (group === 5) { + result = u64(left - imm); + setSubFlags(this.#regs, left, imm, result); + } else if (group === 6) { + result = left ^ u64(imm); + setLogicalFlags(this.#regs, result); + } else if (group === 7) { + result = u64(left - imm); + setSubFlags(this.#regs, left, imm, result); + this.#steps += 1; + return true; + } else throw new Error(`unsupported group1 /${group}`); + this.#writeOp(op, result); + this.#steps += 1; + return true; + } + + if (opcode === 0xff) { + const op = this.#modrm(rex); + const group = op.reg & 7; + if (group === 0) { + const before = this.#readOp(op); + const value = u64(before + 1n); + setAddFlags(this.#regs, before, 1n, value); + this.#writeOp(op, value); + } else if (group === 1) { + const before = this.#readOp(op); + const value = u64(before - 1n); + setSubFlags(this.#regs, before, 1n, value); + this.#writeOp(op, value); + } else if (group === 4) { + this.#regs.rip = this.#readOp(op); + } else throw new Error(`unsupported ff /${group}`); + this.#steps += 1; + return true; + } + + throw new Error(`unsupported opcode ${opcode.toString(16)} at ${start}`); + } + + #condition(code) { + const z = (this.#regs.rflags & FLAGS.ZF) !== 0n; + const s = (this.#regs.rflags & FLAGS.SF) !== 0n; + const c = (this.#regs.rflags & FLAGS.CF) !== 0n; + const o = (this.#regs.rflags & FLAGS.OF) !== 0n; + switch (code) { + case 0x0: + return o; + case 0x1: + return !o; + case 0x2: + return c; + case 0x3: + return !c; + case 0x4: + return z; + case 0x5: + return !z; + case 0x6: + return c || z; + case 0x7: + return !c && !z; + case 0x8: + return s; + case 0x9: + return !s; + case 0xc: + return s !== o; + case 0xd: + return s === o; + case 0xe: + return z || s !== o; + case 0xf: + return !z && s === o; + default: + return false; + } + } + + run({ maxSteps = 100_000 } = {}) { + while (!this.#halted && this.#steps < maxSteps) { + if (!this.step()) break; + } + if (!this.#halted && this.#steps >= maxSteps) throw new Error("CPU step limit exceeded"); + return this.registers(); + } +} + +export const createX86 = (options) => new X86Cpu(options); +export { REG, FLAGS, LinearMemory, RegisterFile }; diff --git a/javascripts/index.js b/javascripts/index.js index 9f028bf..ad4f143 100644 --- a/javascripts/index.js +++ b/javascripts/index.js @@ -1,4 +1,5 @@ export { createShell, MemoryFS, profiles } from "./shell.js"; export { BlockDevice, BlockFS, createBlockFS } from "./block.js"; +export { createX86, LinearMemory, X86Cpu } from "./cpu/x86.js"; export { createManuals } from "./man.js"; export { mountShell } from "./ui.js"; diff --git a/package.json b/package.json index 5780f78..19ac7d0 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ }, "files": [ "javascripts/block.js", + "javascripts/cpu/memory.js", + "javascripts/cpu/x86.js", "javascripts/commands.js", "javascripts/fs.js", "javascripts/index.js", diff --git a/tools/scripts/test.js b/tools/scripts/test.js index 2fa26d4..5bb1cef 100644 --- a/tools/scripts/test.js +++ b/tools/scripts/test.js @@ -16,6 +16,7 @@ import { join, relative } from "node:path"; import { runInNewContext } from "node:vm"; import { createManuals, readLimited } from "../../javascripts/man.js"; import { BlockDevice, BlockFS } from "../../javascripts/block.js"; +import { createX86 } from "../../javascripts/cpu/x86.js"; import { MemoryFS, createShell, profiles } from "../../javascripts/shell.js"; import { createWasm } from "../../javascripts/wasm.js"; import { buildManuals, licenseHeader, validateManual } from "./manuals.js"; @@ -422,3 +423,24 @@ console.log("shell.js core: ok"); assert.equal(remount.exists("/home/rad"), false); console.log("shell.js blockfs: ok"); } + +{ + // movabs rax, 1; movabs rdi, 42; syscall (exit-style halt via null syscall result) + const program = Uint8Array.from([ + 0x48, 0xb8, 1, 0, 0, 0, 0, 0, 0, 0, 0x48, 0xbf, 42, 0, 0, 0, 0, 0, 0, 0, 0x48, 0x83, 0xc0, 7, 0x48, 0x83, 0xe8, 3, + 0x0f, 0x05, + ]); + let saw = null; + const cpu = createX86({ + onSyscall: ({ nr, args }) => { + saw = { nr, args: [...args] }; + return null; + }, + }); + cpu.load(program); + cpu.run(); + assert.equal(saw?.nr, 5n); + assert.equal(saw?.args[0], 42n); + assert.equal(cpu.halted, true); + console.log("shell.js x86: ok"); +}