Emulator basics
const fs = require('fs'); | |
function parse(program) { | |
const labels = {}; | |
const instructions = []; | |
const lines = program.split('\n'); | |
for (let i = 0; i < lines.length; i++) { | |
let line = lines[i].trim(); // Remove any trailing, leading whitespace | |
if (line.startsWith('.') || line.startsWith(';') || line.startsWith('#')) { | |
continue; | |
} | |
if (line.includes(';')) { | |
line = line.split(';')[0]; | |
} | |
if (line.includes('#')) { | |
line = line.split('#')[0]; | |
} | |
if (!line) { | |
continue; | |
} | |
if (line.includes(':')) { | |
const label = line.split(':')[0]; | |
labels[label] = instructions.length; | |
continue; | |
} | |
const operation = line.split(/\s/)[0].toLowerCase(); | |
const operands = line.substring(operation.length).split(',').map(t => t.trim().toUpperCase()); | |
instructions.push({ | |
operation, | |
operands, | |
}); | |
} | |
return { labels, instructions }; | |
} | |
const REGISTERS = [ | |
'RDI', 'RSI', 'RSP', 'RBP', 'RAX', 'RBX', 'RCX', 'RDX', 'RIP', 'R8', | |
'R9', 'R10', 'R11', 'R12', 'R13', 'R14', 'R15', 'CS', 'DS', 'FS', | |
'SS', 'ES', 'GS', 'CF', 'ZF', 'PF', 'AF', 'SF', 'TF', 'IF', 'DF', 'OF', | |
]; | |
const SYSCALLS_BY_ID = { | |
1: function sys_write(process) { | |
const msg = BigInt(process.registers.RSI); | |
const bytes = Number(process.registers.RDX); | |
for (let i = 0; i < bytes; i++) { | |
const byte = readMemoryBytes(process, msg + BigInt(i), 1); | |
const char = String.fromCharCode(Number(byte)); | |
process.fd[Number(process.registers.RDI)].write(char); | |
} | |
}, | |
60: function sys_exit(process) { | |
global.process.exit(Number(process.registers.RDI)); | |
}, | |
}; | |
function writeMemoryBytes(process, address, value, size) { | |
for (let i = 0n; i < size; i++) { | |
value >>= i * 8n; | |
process.memory[address + i] = value & 0xFFn; | |
} | |
} | |
function readMemoryBytes(process, address, size) { | |
let value = 0n; | |
for (let i = 0n; i < size; i++) { | |
value |= (process.memory[address + i] || 0n) << (i * 8n); | |
} | |
return value; | |
} | |
function interpretValue(process, value, { lhs } = { lhs: false }) { | |
if (REGISTERS.includes(value)) { | |
if (lhs) { | |
return value; | |
} else { | |
return process.registers[value]; | |
} | |
} | |
if (value.startsWith('QWORD PTR [')) { | |
const offsetString = value.substring('QWORD PTR ['.length, value.length - 1).trim(); | |
if (offsetString.includes('-')) { | |
const [l, r] = offsetString.split('-').map(l => interpretValue(process, l.trim())); | |
const address = l - r; | |
// qword is 8 bytes | |
const bytes = 8; | |
if (lhs) { | |
return { address, size: bytes }; | |
} else { | |
return readMemoryBytes(process, address, bytes); | |
} | |
} | |
throw new Error('Unsupported offset calculation: ' + value); | |
} | |
return BigInt.asIntN(64, value); | |
} | |
function interpret(process) { | |
do { | |
const instruction = process.instructions[process.registers.RIP]; | |
switch (instruction.operation.toLowerCase()) { | |
case 'mov': { | |
const lhs = interpretValue(process, instruction.operands[0], { lhs: true }); | |
const rhs = interpretValue(process, instruction.operands[1]); | |
if (REGISTERS.includes(lhs)) { | |
process.registers[lhs] = rhs; | |
} else { | |
writeMemoryBytes(process, lhs.address, rhs, lhs.size); | |
} | |
process.registers.RIP++; | |
break; | |
} | |
case 'add': { | |
const lhs = interpretValue(process, instruction.operands[0], { lhs: true }); | |
const rhs = interpretValue(process, instruction.operands[1]); | |
process.registers[lhs] += rhs; | |
process.registers.RIP++; | |
break; | |
} | |
case 'call': { | |
process.registers.RSP -= 8n; | |
writeMemoryBytes(process, process.registers.RSP, process.registers.RIP + 1n, 8); | |
const label = instruction.operands[0]; | |
process.registers.RIP = process.labels[label]; | |
break; | |
} | |
case 'ret': { | |
const value = readMemoryBytes(process, process.registers.RSP, 8); | |
process.registers.RSP += 8n; | |
process.registers.RIP = value; | |
break; | |
} | |
case 'push': { | |
const value = interpretValue(process, instruction.operands[0]); | |
process.registers.RSP -= 8n; | |
writeMemoryBytes(process, process.registers.RSP, value, 8); | |
process.registers.RIP++; | |
break; | |
} | |
case 'pop': { | |
const lhs = interpretValue(process, instruction.operands[0], { lhs: true }); | |
const value = readMemoryBytes(process, process.registers.RSP, 8); | |
process.registers.RSP += 8n; | |
process.registers[lhs] = value; | |
process.registers.RIP++; | |
break; | |
} | |
case 'syscall': { | |
const idNumber = Number(process.registers.RAX); | |
SYSCALLS_BY_ID[idNumber](process); | |
process.registers.RIP++; | |
break; | |
} | |
} | |
} while (process.registers.RIP != BigInt(readMemoryBytes(process, BigInt(process.memory.length - 9), 8))); | |
} | |
function main(file) { | |
const memory = new Array(10000); | |
const code = fs.readFileSync(file).toString(); | |
const { instructions, labels } = parse(code); | |
const registers = REGISTERS.reduce((rs, r) => ({ ...rs, [r]: 0n }), {}); | |
registers.RIP = BigInt(labels.main === undefined ? labels._main : labels.main); | |
registers.RSP = BigInt(memory.length - 9); | |
const process = { | |
registers, | |
memory, | |
instructions, | |
labels, | |
fd: { | |
// stdout | |
1: global.process.stdout, | |
} | |
}; | |
writeMemoryBytes(process, registers.RSP, BigInt(memory.length + 1), 8); | |
interpret(process); | |
return Number(process.registers.RAX); | |
} | |
process.exit(main(process.argv[2])); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment