- docs/widescreen.md: render-thread frame, cWorldView0 layout and methods, view size = zoom * 1024.0/768.0 float constants and all their users, ortho projection extents, open questions (activity radius, buffers). - tools: find-imm (immediate/regex instruction search), rtti (vtables via MSVC RTTI), decompile.ps1 + Ghidra scripts for a persistent project; ApplyRtti recovers 410 vtables / ~1200 functions Ghidra misses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
85 lines
3.3 KiB
JavaScript
85 lines
3.3 KiB
JavaScript
// Find the vtables of an MSVC C++ class in sacred.exe via RTTI and list their
|
|
// entries.
|
|
//
|
|
// node tools/rtti.mjs <exe> <ClassName> [entries=40]
|
|
//
|
|
// MSVC 32-bit RTTI: TypeDescriptor { pVFTable, spare, char name[] } with the
|
|
// name ".?AV<Class>@@"; CompleteObjectLocator { signature=0, offset, cdOffset,
|
|
// pTypeDescriptor, pClassHierarchyDescriptor }; vtable[-1] = &COL.
|
|
// A class has one vtable per (non-virtual) base subobject; `offset` tells which.
|
|
|
|
import { readFileSync } from "node:fs";
|
|
|
|
const [exePath, className, entriesText = "40"] = process.argv.slice(2);
|
|
if (!exePath || !className) {
|
|
console.error("usage: node rtti.mjs <exe> <ClassName> [entries]");
|
|
process.exit(1);
|
|
}
|
|
const maxEntries = Number.parseInt(entriesText, 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;
|
|
|
|
const sections = [];
|
|
for (let i = 0; i < sectionCount; i++) {
|
|
const s = sectionTable + 40 * i;
|
|
sections.push({
|
|
name: file.toString("latin1", s, s + 8).replace(/\0+$/, ""),
|
|
va: imageBase + file.readUInt32LE(s + 12),
|
|
virtualSize: file.readUInt32LE(s + 8),
|
|
rawOffset: file.readUInt32LE(s + 20),
|
|
rawSize: file.readUInt32LE(s + 16),
|
|
});
|
|
}
|
|
const section = (name) => sections.find((s) => s.name === name);
|
|
const toOffset = (va) => {
|
|
for (const s of sections) {
|
|
if (va >= s.va && va < s.va + Math.min(s.virtualSize, s.rawSize)) return s.rawOffset + va - s.va;
|
|
}
|
|
return -1;
|
|
};
|
|
const inSection = (va, s) => va >= s.va && va < s.va + s.virtualSize;
|
|
const hex = (v) => "0x" + v.toString(16).padStart(8, "0");
|
|
|
|
// 1. TypeDescriptor: name string lives 8 bytes into it (.data).
|
|
const mangled = Buffer.from(`.?AV${className}@@\0`, "latin1");
|
|
const typeDescriptors = [];
|
|
for (const s of sections) {
|
|
const raw = file.subarray(s.rawOffset, s.rawOffset + s.rawSize);
|
|
for (let i = raw.indexOf(mangled); i !== -1; i = raw.indexOf(mangled, i + 1)) {
|
|
typeDescriptors.push(s.va + i - 8);
|
|
}
|
|
}
|
|
if (typeDescriptors.length === 0) {
|
|
console.error(`no RTTI type descriptor for ${className}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
// 2. CompleteObjectLocators referencing it, 3. vtables whose [-1] is a COL.
|
|
const rdata = section(".rdata");
|
|
const text = section(".text");
|
|
const rdataRaw = file.subarray(rdata.rawOffset, rdata.rawOffset + rdata.rawSize);
|
|
for (const td of typeDescriptors) {
|
|
console.log(`${className}: TypeDescriptor ${hex(td)}`);
|
|
for (let i = 0; i + 20 <= rdataRaw.length; i += 4) {
|
|
if (rdataRaw.readUInt32LE(i) !== 0 || rdataRaw.readUInt32LE(i + 12) !== td) continue;
|
|
const col = rdata.va + i;
|
|
const subobjectOffset = rdataRaw.readUInt32LE(i + 4);
|
|
for (let j = 0; j + 4 <= rdataRaw.length; j += 4) {
|
|
if (rdataRaw.readUInt32LE(j) !== col) continue;
|
|
const vtable = rdata.va + j + 4;
|
|
console.log(` vtable ${hex(vtable)} (subobject offset 0x${subobjectOffset.toString(16)}, COL ${hex(col)})`);
|
|
for (let k = 0; k < maxEntries; k++) {
|
|
const o = toOffset(vtable + 4 * k);
|
|
const target = file.readUInt32LE(o);
|
|
if (!inSection(target, text)) break;
|
|
console.log(` [+0x${(4 * k).toString(16).padStart(2, "0")}] ${hex(target)}`);
|
|
}
|
|
}
|
|
}
|
|
}
|