// Disassemble a game module (sacred2.exe, s2logic.dll, ...) at a virtual // address (as in Ghidra: the module's preferred image base), independent of // Ghidra's function boundaries (useful for code only reached via pointers). // // node tools/disasm.mjs [count=60] // // Stops after `count` instructions or at the first RET/INT3 run, whichever // comes later than 8 instructions. import { readFileSync } from "node:fs"; import { Decoder, DecoderOptions, Formatter, FormatterSyntax, Mnemonic } from "iced-x86"; const [exePath, vaText, countText = "60"] = process.argv.slice(2); if (!exePath || !vaText) { console.error("usage: node disasm.mjs [count]"); process.exit(1); } const va = Number.parseInt(vaText, 16); const count = Number.parseInt(countText, 10); const file = readFileSync(exePath); const peOffset = file.readUInt32LE(0x3c); const sectionCount = file.readUInt16LE(peOffset + 6); const optionalSize = file.readUInt16LE(peOffset + 20); const imageBase = file.readUInt32LE(peOffset + 24 + 28); const sectionTable = peOffset + 24 + optionalSize; function fileOffset(address) { const rva = address - imageBase; for (let i = 0; i < sectionCount; i++) { const s = sectionTable + 40 * i; const virtualSize = file.readUInt32LE(s + 8); const virtualAddress = file.readUInt32LE(s + 12); const rawOffset = file.readUInt32LE(s + 20); if (rva >= virtualAddress && rva < virtualAddress + virtualSize) { return rawOffset + rva - virtualAddress; } } throw new Error(`address 0x${address.toString(16)} is not in a section`); } const start = fileOffset(va); const bytes = file.subarray(start, start + count * 15); const decoder = new Decoder(32, bytes, DecoderOptions.None); decoder.ip = BigInt(va); const formatter = new Formatter(FormatterSyntax.Intel); formatter.firstOperandCharIndex = 8; for (let n = 0; n < count && decoder.canDecode; n++) { const instruction = decoder.decode(); const address = Number(instruction.ip).toString(16).padStart(8, "0"); const offset = Number(instruction.ip) - va; const raw = [...bytes.subarray(offset, offset + instruction.length)] .map((b) => b.toString(16).padStart(2, "0")) .join(" "); console.log(`${address} ${raw.padEnd(30)} ${formatter.format(instruction)}`); instruction.free(); } decoder.free(); formatter.free();