// List every instruction in a game module's .text that uses one of the given // immediate values (decimal or 0x-hex), e.g. to find hardcoded resolutions. // // node tools/find-imm.mjs 1024 768 [...] // node tools/find-imm.mjs /96AB0h\]/ (regex on instruction text) // // Output: one line per hit, "address instruction". import { readFileSync } from "node:fs"; import { Decoder, DecoderOptions, Formatter, FormatterSyntax, OpKind } from "iced-x86"; const [exePath, ...valueTexts] = process.argv.slice(2); if (!exePath || valueTexts.length === 0) { console.error("usage: node find-imm.mjs [value...]"); process.exit(1); } const textPattern = valueTexts.length === 1 && /^\/.*\/$/.test(valueTexts[0]) ? new RegExp(valueTexts[0].slice(1, -1), "i") : null; const wanted = textPattern ? new Set() : new Set(valueTexts.map((t) => BigInt(Number(t)))); 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; let text = null; for (let i = 0; i < sectionCount; i++) { const s = sectionTable + 40 * i; const name = file.toString("latin1", s, s + 8).replace(/\0+$/, ""); if (name === ".text") { text = { virtualAddress: file.readUInt32LE(s + 12), rawSize: file.readUInt32LE(s + 16), rawOffset: file.readUInt32LE(s + 20), }; } } if (!text) throw new Error("no .text section"); const bytes = file.subarray(text.rawOffset, text.rawOffset + text.rawSize); const decoder = new Decoder(32, bytes, DecoderOptions.None); decoder.ip = BigInt(imageBase + text.virtualAddress); const formatter = new Formatter(FormatterSyntax.Intel); const immediateKinds = new Set([ OpKind.Immediate8, OpKind.Immediate16, OpKind.Immediate32, OpKind.Immediate8to16, OpKind.Immediate8to32, ]); // Linear sweep: data embedded in .text can produce a few bogus hits. while (decoder.canDecode) { const instruction = decoder.decode(); if (textPattern) { const text = formatter.format(instruction); if (textPattern.test(text)) { console.log(`${Number(instruction.ip).toString(16).padStart(8, "0")} ${text}`); } instruction.free(); continue; } for (let op = 0; op < instruction.opCount; op++) { if (immediateKinds.has(instruction.opKind(op)) && wanted.has(instruction.immediate(op) & 0xffffffffn)) { console.log(`${Number(instruction.ip).toString(16).padStart(8, "0")} ${formatter.format(instruction)}`); break; } } instruction.free(); } decoder.free(); formatter.free();