// List the exports of a game module with their VAs (at the module's // preferred base, as in Ghidra). The game DLLs export most of their C++ // classes with decorated names, so this is a quick index of what is where. // // node tools/exports.mjs [regex] exports whose name matches // node tools/exports.mjs 0x10018c60 the export at (or before) a VA // // Output: "va name", sorted by name (by address for a VA lookup). import { readFileSync } from "node:fs"; const [modulePath, query] = process.argv.slice(2); if (!modulePath) { console.error("usage: node exports.mjs [regex | 0xVA]"); process.exit(1); } const file = readFileSync(modulePath); const peOffset = file.readUInt32LE(0x3c); const sectionCount = file.readUInt16LE(peOffset + 6); const optionalSize = file.readUInt16LE(peOffset + 20); const optional = peOffset + 24; const imageBase = file.readUInt32LE(optional + 28); const sectionTable = optional + optionalSize; function fileOffset(rva) { 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 + Math.max(virtualSize, file.readUInt32LE(s + 16))) { return rawOffset + rva - virtualAddress; } } throw new Error(`rva 0x${rva.toString(16)} is not in a section`); } const exportRva = file.readUInt32LE(optional + 96); if (!exportRva) { console.error("no export table"); process.exit(1); } const dir = fileOffset(exportRva); const functionCount = file.readUInt32LE(dir + 20); const nameCount = file.readUInt32LE(dir + 24); const functions = fileOffset(file.readUInt32LE(dir + 28)); const names = fileOffset(file.readUInt32LE(dir + 32)); const ordinals = fileOffset(file.readUInt32LE(dir + 36)); const exports = []; for (let i = 0; i < nameCount && functionCount; i++) { const nameOffset = fileOffset(file.readUInt32LE(names + 4 * i)); const end = file.indexOf(0, nameOffset); const name = file.toString("latin1", nameOffset, end); const ordinal = file.readUInt16LE(ordinals + 2 * i); const va = imageBase + file.readUInt32LE(functions + 4 * ordinal); exports.push({ va, name }); } const hex = (va) => va.toString(16).padStart(8, "0"); let shown; if (query && /^0x[0-9a-f]+$/i.test(query)) { const va = Number.parseInt(query, 16); const before = exports.filter((e) => e.va <= va).sort((a, b) => b.va - a.va); shown = before.length ? exports.filter((e) => e.va === before[0].va) : []; } else { const pattern = query ? new RegExp(query.replace(/^\/|\/$/g, ""), "i") : null; shown = exports.filter((e) => !pattern || pattern.test(e.name)).sort((a, b) => a.name.localeCompare(b.name)); } for (const e of shown) { console.log(`${hex(e.va)} ${e.name}`); }