Files
sacred_k/tools/rtti.mjs
T
khallaandClaude Opus 5.5 b2dc1338d2 Map the world view sizing; add RE tooling
- 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>
2026-10-14 00:19:52 +02:00

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)}`);
}
}
}
}