Ported from sacredK with module-relative addresses, hooks by exported name and a SHA-256 check of the six game modules (Community Patch 2.65.1). Borderless windowed at monitor size replaces exclusive fullscreen, whose focus loss during loading made the game quit and crash on exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
61 lines
2.3 KiB
JavaScript
61 lines
2.3 KiB
JavaScript
// 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 <module> <va> [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 <exe> <va> [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();
|