mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 18:05:45 +00:00
* feat(il2cpp): read generic containers on demand and render method spec names Generic container indices and records are demand readers on the same metadata snapshot, so links that do not ask for generic instances allocate nothing. Type rendering keeps one graph walk with its workspace, depth and cycle guards; the C#-like names and the Il2CppDumper GetMethodSpecName names are two styles over it. * feat(il2cpp): link generic method instances with admitted rows and kept issues generic_instances links every genericMethodTable row through methodSpecs, genericInsts and genericMethodPointers, choosing the one CodeRegistration layout whose counts bound every row. The table is reserved against the report budget before any row is resolved; rows and issues are admitted before retention, and a contradicting row stays unknown with an address-backed issue instead of failing the link. Views page the rows by definition, state, address and name, unrequested and historical collections stay unknown, and Ghidra seeds fold instances after the definitions. * feat(il2cpp): accept --generic-instances on the CLI and describe it in tool contracts * docs(il2cpp): describe generic method instances and their limits * fix(il2cpp): keep issued generic rows unknown and name their row A method spec that contradicted the metadata still reported its pointer slot as code, while the schema and provider limitation promised unknown. Such rows now keep no address, every issue carries its genericMethodTable row, the domain validator requires unknown exactly on issued rows, and shared specs name one other row instead of all of them. The row-size lower bound moves to the metadata admission owner, the hex helper is shared, and budget wording says checked rather than reserved. * test(il2cpp): reject ambiguous generic layouts and pin budget, yield and limit paths * fix(il2cpp): keep unmapped generic slots per row and reject undeclared arguments Layout selection no longer requires every genericMethodPointers slot to be mapped; each row reads its own slot, so one unmapped slot leaves only that row unknown with an issue. Method specs whose class or method argument count differs from the owner's declared generic container are issued and unnamed. Fixtures now declare the containers their generic rows fill. * fix(il2cpp): page generic method issues instead of retaining them in summaries Summaries keep generic_methods without its issues and count them in counts.generic_method_issues; the generic_method_issues collection pages them by rows, indices and fields, as code_gen_modules already does. Docs also name the method_indices filter the request schema accepts. * fix(il2cpp): read split generic arguments per slot and method containers on demand Generic argument arrays may span adjacent loader ranges: admit them with hasPointerArrayCoverage and read each slot through readPointer, instead of requiring the whole array in one mapping. Method generic container indices are read on demand from the method record, like type containers, so no link path allocates a per-method column. An owner whose container index is out of range now leaves the method spec unnamed with an issue instead of naming it unchecked. * fix(il2cpp): report generic entries past the address space at their row A damaged methodSpecs or genericMethodPointers pointer could make a spec or slot address exceed 64 bits. The issue kept that address, which the address schema rejects, so inspection failed instead of leaving the row unknown. Check the entry range first and attach the issue to the genericMethodTable row (or its slot field). * chore(il2cpp): remove unrelated formatting changes * fix: restore adb float pattern and mobile fixture from main The merge commit's lint-staged run rewrote these two main files: oxlint --fix dropped the escape in [+\-], which is invalid under the RegExp v flag. * fix(il2cpp): preserve unreadable generic method identities * docs(il2cpp): record generic-instance workflow verification * docs(il2cpp): qualify final integration checks --------- Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
269 lines
9.5 KiB
TypeScript
269 lines
9.5 KiB
TypeScript
import type { Il2cppMethodAddress } from "../domain/il2cpp/il2cppMetadata.js";
|
|
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
|
|
import { openImageSteps, readSlot, isExecutable } from "./Il2cppBinaryImage.js";
|
|
import {
|
|
findModules,
|
|
findCodeRegistration,
|
|
findMetadataRegistration,
|
|
} from "./Il2cppBinaryRegistration.js";
|
|
import { createTypeNamer } from "./Il2cppBinaryTypes.js";
|
|
import {
|
|
genericMethodLayouts,
|
|
linkGenericMethodsSteps,
|
|
type Il2cppGenericMethodLink,
|
|
} from "./Il2cppBinaryGenerics.js";
|
|
import type {
|
|
Il2cppMetadataDecoded,
|
|
Il2cppMetadataLinkFacts,
|
|
Il2cppMetadataReadOptions,
|
|
} from "./Il2cppMetadataFile.js";
|
|
/** One linked code-generation module; its name matches a metadata image. */
|
|
export interface Il2cppCodeGenModule {
|
|
readonly image: number;
|
|
readonly address: bigint;
|
|
readonly method_pointer_count: number;
|
|
readonly method_pointers: bigint;
|
|
}
|
|
|
|
type Il2cppMethodCode = bigint | "no-body" | "unknown";
|
|
|
|
export interface Il2cppBinaryLink {
|
|
readonly format: "elf" | "macho" | "pe";
|
|
readonly architecture: string;
|
|
readonly image_base: bigint;
|
|
/**
|
|
* Address of the CodeRegistration codeGenModulesCount field, or null when the
|
|
* static registration pair was unresolved and an executable byte-pattern
|
|
* candidate identified the array; instruction boundaries remain unverified.
|
|
*/
|
|
readonly code_gen_modules_field: bigint | null;
|
|
/** Address of the code-generation module pointer array. */
|
|
readonly code_gen_modules_array: bigint;
|
|
readonly code_gen_modules: readonly Il2cppCodeGenModule[];
|
|
/**
|
|
* Native code address per metadata method index; "no-body" when its module
|
|
* slot is zero, "unknown" when its owner or relocated pointer is unresolved.
|
|
*/
|
|
readonly method_addresses: Il2cppMethodAddress[];
|
|
/** Address of MetadataRegistration's typesCount field. */
|
|
readonly types_field: bigint;
|
|
readonly types_count: number;
|
|
/** Il2CppType table index and display name per method return, field and parameter. */
|
|
readonly return_types: Il2cppTypeReference[];
|
|
readonly field_types: Il2cppTypeReference[];
|
|
readonly parameter_types: Il2cppTypeReference[];
|
|
/** Null unless generic instances were requested. */
|
|
readonly generic_methods: Il2cppGenericMethodLink | null;
|
|
}
|
|
|
|
export type Il2cppTypeReference = {
|
|
readonly type: number;
|
|
readonly name: string | null;
|
|
};
|
|
|
|
export type Il2cppBinaryReadOptions = Pick<
|
|
Il2cppMetadataReadOptions,
|
|
"beforeText" | "onRow"
|
|
> & {
|
|
/** Also link genericMethodTable rows; unrequested paths read no generic table. */
|
|
readonly genericInstances?: boolean;
|
|
};
|
|
|
|
/**
|
|
* Link metadata methods to native code addresses without executing the image.
|
|
* A method token's row id selects its module's methodPointers slot.
|
|
*/
|
|
export function* linkIl2cppBinarySteps(
|
|
bytes: Buffer,
|
|
metadata: Il2cppMetadataDecoded,
|
|
facts: Il2cppMetadataLinkFacts,
|
|
architecture?: string,
|
|
options: Il2cppBinaryReadOptions = {},
|
|
): Generator<void, Il2cppBinaryLink> {
|
|
const version = metadata.metadata_version;
|
|
// Refuse an undecoded version before any table is read.
|
|
if (options.genericInstances === true) genericMethodLayouts(version);
|
|
yield;
|
|
const image = yield* openImageSteps(bytes, architecture);
|
|
const modules = yield* findModules(image, metadata);
|
|
const registration_ = yield* findCodeRegistration(image, modules);
|
|
const methodImage = new Int32Array(metadata.methods.length).fill(-1);
|
|
for (const [index, entry] of metadata.images.entries())
|
|
for (let offset = 0; offset < entry.type_count; offset++) {
|
|
const type = metadata.types[entry.type_start + offset]!;
|
|
if (offset % LINK_BATCH === 0) yield;
|
|
for (let method = 0; method < type.method_count; method++) {
|
|
if (method % LINK_BATCH === 0) yield;
|
|
methodImage[type.method_start + method] = index;
|
|
}
|
|
}
|
|
const methodByImageAndRow = metadata.images.map(
|
|
() => new Map<number, number>(),
|
|
);
|
|
for (let index = 0; index < methodImage.length; index++) {
|
|
if (index % LINK_BATCH === 0) yield;
|
|
const owner = methodImage[index]!;
|
|
if (owner === -1) continue;
|
|
const token = facts.methodTokens[index]!;
|
|
const row = token & 0xffffff;
|
|
if (token >>> 24 !== 0x06 || row === 0) continue;
|
|
const methodsByRow = methodByImageAndRow[owner]!;
|
|
const earlier = methodsByRow.get(row);
|
|
if (earlier !== undefined)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"invalid-method-token",
|
|
`methods ${earlier} and ${index} in image ${metadata.images[owner]!.name} share MethodDef token 0x${token.toString(16)}`,
|
|
);
|
|
methodsByRow.set(row, index);
|
|
}
|
|
yield;
|
|
const methodAddress = (index: number): Il2cppMethodCode => {
|
|
const owner = methodImage[index]!;
|
|
// No image range holds this method, so no module slot describes it.
|
|
if (owner === -1) return "unknown";
|
|
const module = modules[owner]!;
|
|
const token = facts.methodTokens[index]!;
|
|
const row = token & 0xffffff;
|
|
if (token >>> 24 !== 0x06 || row === 0 || row > module.method_pointer_count)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"invalid-method-token",
|
|
`method ${index} token 0x${token.toString(16)} is outside module ${metadata.images[owner]!.name}'s ${module.method_pointer_count} method pointers`,
|
|
);
|
|
const address = readSlot(
|
|
image,
|
|
module.method_pointers + BigInt((row - 1) * 8),
|
|
);
|
|
if (address === null || address === undefined)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"invalid-method-pointer",
|
|
`module ${metadata.images[owner]!.name} method pointer ${row - 1} is not file mapped`,
|
|
);
|
|
if (address === "unknown") return "unknown";
|
|
if (address === 0n) return "no-body";
|
|
if (!isExecutable(image, address))
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"invalid-method-pointer",
|
|
`method ${index} points outside unambiguous executable file data at 0x${address.toString(16)}`,
|
|
);
|
|
return address;
|
|
};
|
|
const method_addresses: Il2cppMethodAddress[] = [];
|
|
for (let index = 0; index < metadata.methods.length; index++) {
|
|
if (index % LINK_BATCH === 0) yield;
|
|
const code = methodAddress(index);
|
|
if (typeof code === "bigint" && code < image.imageBase)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"invalid-method-pointer",
|
|
"method address precedes the link image base",
|
|
);
|
|
const row: Il2cppMethodAddress =
|
|
typeof code === "bigint"
|
|
? {
|
|
state: "code",
|
|
address: `0x${code.toString(16)}`,
|
|
rva: `0x${(code - image.imageBase).toString(16)}`,
|
|
}
|
|
: { state: code, address: null, rva: null };
|
|
options.onRow?.(row);
|
|
method_addresses.push(row);
|
|
}
|
|
const registration = yield* findMetadataRegistration(
|
|
image,
|
|
metadata.types.length,
|
|
);
|
|
const typeName = createTypeNamer(
|
|
image,
|
|
metadata,
|
|
facts,
|
|
version,
|
|
options.beforeText,
|
|
);
|
|
function* reference(type: number): Generator<void, Il2cppTypeReference> {
|
|
if (type === -1) return { type, name: null };
|
|
if (type < 0 || type >= registration.count)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"invalid-type",
|
|
`type index ${type} is outside the ${registration.count} binary types`,
|
|
);
|
|
// 27.0/27.1 and 27.2 share header version 27 but place byref differently.
|
|
if (version === 27) return { type, name: null };
|
|
const address = readSlot(image, registration.types + BigInt(type * 8));
|
|
if (address === "unknown") return { type, name: null };
|
|
if (address === null)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"invalid-type",
|
|
`type ${type} has no mapped type-table slot`,
|
|
);
|
|
if (address === 0n)
|
|
throw new Il2cppBinaryFormatFailure(
|
|
"invalid-type",
|
|
`type ${type} has no Il2CppType record`,
|
|
);
|
|
return { type, name: yield* typeName(address, 0) };
|
|
}
|
|
function* column(types: Int32Array) {
|
|
const rows: Il2cppTypeReference[] = [];
|
|
for (let index = 0; index < types.length; index++) {
|
|
if (index % LINK_BATCH === 0) yield;
|
|
const row = yield* reference(types[index]!);
|
|
options.onRow?.(row);
|
|
rows.push(row);
|
|
}
|
|
return rows;
|
|
}
|
|
const return_types = yield* column(facts.returnTypes);
|
|
const field_types = yield* column(facts.fieldTypes);
|
|
const parameter_types = yield* column(facts.parameterTypes);
|
|
const generic_methods =
|
|
options.genericInstances === true
|
|
? yield* linkGenericMethodsSteps(
|
|
image,
|
|
metadata,
|
|
facts,
|
|
registration_.field,
|
|
// findMetadataRegistration reports typesCount, word 6.
|
|
registration.field - 48n,
|
|
options,
|
|
)
|
|
: null;
|
|
return {
|
|
format: image.container.format,
|
|
architecture: image.container.architecture,
|
|
image_base: image.imageBase,
|
|
code_gen_modules_field: registration_.field,
|
|
code_gen_modules_array: registration_.array,
|
|
code_gen_modules: modules,
|
|
method_addresses,
|
|
types_field: registration.field,
|
|
types_count: registration.count,
|
|
return_types,
|
|
field_types,
|
|
parameter_types,
|
|
generic_methods,
|
|
};
|
|
}
|
|
|
|
/** Rows between cooperative yields while linking. */
|
|
const LINK_BATCH = 256;
|
|
|
|
/** Synchronous linking for byte-level callers. */
|
|
export const linkIl2cppBinary = (
|
|
bytes: Buffer,
|
|
metadata: Il2cppMetadataDecoded,
|
|
facts: Il2cppMetadataLinkFacts,
|
|
architecture?: string,
|
|
options: Il2cppBinaryReadOptions = {},
|
|
): Il2cppBinaryLink => {
|
|
const steps = linkIl2cppBinarySteps(
|
|
bytes,
|
|
metadata,
|
|
facts,
|
|
architecture,
|
|
options,
|
|
);
|
|
for (;;) {
|
|
const step = steps.next();
|
|
if (step.done) return step.value;
|
|
}
|
|
};
|