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>
This commit is contained in:
1 parent
1549464af6
commit
b2dc1338d2
8 files changed
+493
No files matched your search
@@ -0,0 +1,84 @@
|
||||
// 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user