fix(il2cpp): link arm64 builds whose registrations were inlined (#6989)

* fix(il2cpp): accept arm64 module arrays loaded through adrp/add pairs

A Unity 2022.3 macOS Master (LTO) arm64 build keeps no static CodeRegistration, so the module-array fallback that only decoded x86-64 RIP-relative references reported code-registration-not-found.
The registration owner now also decodes aligned adrp + add pairs in executable ranges and keeps the same exact-module-set and single-reference rules.
A minimal Mach-O fixture failed on the previous reader (47 passed / 1 failed) and passes here with four fail-closed variants (48/48 binary tests).

* fix(il2cpp): name the matched instruction form in inferred module-array limits

The inferred-array limitation always described RIP-relative LEA/MOV, which is wrong for arm64 results.
The provider now names the adrp/add pair on arm64, and the tool contract and docs cover both forms and state that a located array alone does not finish linking a Master build.
The new arm64 provider case failed before the wording change (1 passed / 1 failed) and both cases pass now.

* fix(il2cpp): accept type tables loaded without a static MetadataRegistration

A Unity 2022.3.62f3 macOS Master build inlines MetadataRegistration as well as CodeRegistration, so linking stopped at metadata-registration-not-found after the module array was found. Registration now infers the only field linking reads, the Il2CppType table, from the one run of type-shaped pointers that covers every referenced type index and whose start is loaded by the existing RIP-relative or adrp/add forms; types_field is null and the Evidence is inferred. The new arm64 and x86-64 fixtures failed on the previous reader (52 passed / 2 failed). Four run-selection variants (interior reference, run extended before the loaded address, two loaded runs, run shorter than the referenced indices) keep metadata-registration-not-found; instruction decoding and record-shape checks are not covered by these fixtures.

* test(il2cpp): pin adrp page deltas, register pairing and type-record shape

Every adrp fixture targeted its own 4 KiB page, so dropping the page delta or its sign extension, the add register check, the type-kind bound or the type-definition bound left all binary tests passing. Fixtures now reach a later and an earlier page, pair an add with a different destination or source register, break the type run with an out-of-range kind or type-definition index, and bound type-table candidates when the metadata references few type indices. Each of those five source mutations now fails 1 to 3 of 85 binary and provider tests.

* refactor(il2cpp): share Il2CppType kind decoding and document type-table bounds

The type-table fallback decoded the Il2CppType kind and class/value-type kinds separately from the type namer; both now use the Types owner. Zero and unaligned words are rejected before mapping lookup (Master sample metadata-registration search 92 to 68 ms). Docs and the limitation state that types_count is an upper bound, that a run spanning mappings or a small referenced type range can fail, and credit Cpp2IL StrippedCodeRegSupport and the data-side MetadataRegistration searches of Il2CppDumper and Cpp2IL. Master sample output is unchanged (11,031 code addresses, 37,025 type rows).

* fix(il2cpp): refuse generic instances when the type table was inferred

An inferred type table has no static MetadataRegistration, so the
genericMethodTable it would anchor cannot be located. Report
generic-methods-unsupported instead of reading from a null field.

* fix(il2cpp): fail closed when an inferred type table is not unique

Adversarial review found that the type-table fallback could pick another
type-shaped pointer table when the real one was unloaded or excluded, could
replace a static MetadataRegistration whose counts matched, and accepted a
run cut short by a mapping end. Require exactly one long-enough run in the
image, ending inside its mapping and loaded by code; skip the fallback when a
record's two counts matched; accept only assigned Il2CppTypeEnum kinds; and
look records up through the indexed file mapping so many mappings cannot
multiply the scan. Docs and limitations now describe the run length as at
least every referenced type index rather than an upper bound.

* fix(il2cpp): keep the type-table fallback cheap and its refusals exact

Review follow-ups: check that an address can lie in a mapping before the
throwing indexed lookup, so words pointing into gaps or zero-fill no longer
build an exception each (64 MiB: 74 s to under 2 s); treat a run starting at
its mapping start like one reaching its end; stop at the second long-enough
run instead of a 64-candidate capacity error; ignore ADRP/ADD pairs on
register 31; and say whether a counted MetadataRegistration was unusable or
no code-loaded run was found. Docs no longer generalize from one build or
state tool history they do not source.

* fix(il2cpp): judge type-table slots by their loaded value

The run scan read stored words, so a dyld-bound slot's placeholder could
decide whether a run qualified. Confirm each slot through readSlot and give
a run bounded by a bound or unresolved slot an unknown extent, so the
placeholder no longer changes the choice.

* docs(il2cpp): keep the failure-reason table layout

Shorten the two changed meanings so the table keeps its column widths and the
diff shows only those rows.

* docs(il2cpp): keep the reason table at its formatted width

* fix(il2cpp): read type-table record fields through the loader too

The previous change confirmed table slots through readSlot but still read
each record's kind and definition index from stored bytes. Read them with
readUInt32 and readSlot, and treat a record field the loader binds or leaves
unresolved like an unresolved slot: the run it bounds has an unknown extent.

* perf(il2cpp): share image mappings across loader-aware reads

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
JANG BYUNGKUN
2026-10-25 05:21:18 +09:00
committed by GitHub
parent 22b88300d0
commit 149b43ee61
17 changed files with 1006 additions and 68 deletions
+26 -3
View File
@@ -45,8 +45,13 @@ export interface Il2cppBinaryLink {
* 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;
/**
* Address of MetadataRegistration's typesCount field, or null when no static
* registration matched and an executable byte-pattern candidate identified
* the type table; types_count is then the inferred run length, at least
* every referenced type index but not an observed typesCount.
*/
readonly types_field: bigint | null;
readonly types_count: number;
/** Il2CppType table index and display name per method return, field and parameter. */
readonly return_types: Il2cppTypeReference[];
@@ -170,6 +175,7 @@ export function* linkIl2cppBinarySteps(
const registration = yield* findMetadataRegistration(
image,
metadata.types.length,
yield* referencedTypeCount(facts),
);
const typeName = createTypeNamer(
image,
@@ -222,7 +228,7 @@ export function* linkIl2cppBinarySteps(
facts,
registration_.field,
// findMetadataRegistration reports typesCount, word 6.
registration.field - 48n,
registration.field === null ? null : registration.field - 48n,
options,
)
: null;
@@ -243,6 +249,23 @@ export function* linkIl2cppBinarySteps(
};
}
/** One past the highest type index any return, field or parameter references. */
function* referencedTypeCount(
facts: Il2cppMetadataLinkFacts,
): Generator<void, number> {
let count = 0;
for (const types of [
facts.returnTypes,
facts.fieldTypes,
facts.parameterTypes,
])
for (let index = 0; index < types.length; index++) {
if (index % LINK_BATCH === 0) yield;
count = Math.max(count, types[index]! + 1);
}
return count;
}
/** Rows between cooperative yields while linking. */
const LINK_BATCH = 256;
+7 -1
View File
@@ -103,7 +103,8 @@ export function* linkGenericMethodsSteps(
metadata: Il2cppMetadataDecoded,
facts: Il2cppMetadataLinkFacts,
modulesField: bigint | null,
metadataRegistration: bigint,
/** Null when the type table was inferred without a static MetadataRegistration. */
metadataRegistration: bigint | null,
options: Il2cppBinaryReadOptions,
): Generator<void, Il2cppGenericMethodLink> {
const version = metadata.metadata_version;
@@ -113,6 +114,11 @@ export function* linkGenericMethodsSteps(
"generic-methods-unsupported",
"this binary keeps no static CodeRegistration, so genericMethodPointers cannot be located",
);
if (metadataRegistration === null)
throw new Il2cppBinaryFormatFailure(
"generic-methods-unsupported",
"this binary keeps no static MetadataRegistration, so genericMethodTable cannot be located",
);
const word = (address: bigint) => readPointer(image, address);
const count = (address: bigint) => {
const value = word(address);
+19 -34
View File
@@ -6,9 +6,11 @@ import { binaryMetadataArchitecture } from "../domain/binaryMetadataArchitecture
import {
BinaryFormatFailure,
BinaryResourceFailure,
createFileMappingIndex,
mappedFileOffset,
isZeroFilledSpan,
type BinaryContainer,
type FileMappingIndex,
} from "../native/metadata/BinaryContainer.js";
import { readBinaryContainer } from "../native/metadata/readBinaryContainer.js";
import {
@@ -74,6 +76,8 @@ const selectImage = (
export interface NativeImage {
readonly bytes: Buffer;
readonly container: BinaryContainer;
/** One image's immutable load mappings, shared by stored and loader-aware reads. */
readonly fileMappings: FileMappingIndex;
readonly imageBase: bigint;
readonly unresolvedPointerSlots: ReadonlyMap<bigint, number>;
readonly zeroFillPointers: ReadonlyMap<bigint, bigint>;
@@ -190,6 +194,10 @@ export function* openImageSteps(
return {
bytes: image,
container,
fileMappings: createFileMappingIndex(
container.mappings,
container.zero_fills,
),
imageBase,
unresolvedPointerSlots: relocated?.unresolved ?? new Map<bigint, number>(),
zeroFillPointers: relocated?.zeroFillPointers ?? new Map<bigint, bigint>(),
@@ -200,12 +208,7 @@ export function* openImageSteps(
/** Stored bytes only; loader interpretation belongs to readSlot. */
const readStoredWord = (image: NativeImage, address: bigint): bigint | null => {
try {
const offset = mappedFileOffset(
image.container.mappings,
address,
8,
image.container.zero_fills,
);
const offset = image.fileMappings.offset(address, 8);
return image.bytes.readBigUInt64LE(offset);
} catch (cause) {
if (cause instanceof BinaryFormatFailure) return null;
@@ -226,14 +229,7 @@ export const readUInt32 = (
const virtual = image.zeroFillPointers.get(address);
if (virtual !== undefined) return Number(virtual & 0xffffffffn);
try {
return image.bytes.readUInt32LE(
mappedFileOffset(
image.container.mappings,
address,
4,
image.container.zero_fills,
),
);
return image.bytes.readUInt32LE(image.fileMappings.offset(address, 4));
} catch (cause) {
if (cause instanceof BinaryFormatFailure) return null;
throw cause;
@@ -245,12 +241,7 @@ export const hexAddress = (value: bigint) => `0x${value.toString(16)}`;
export const isMapped = (image: NativeImage, address: bigint, size: number) => {
try {
mappedFileOffset(
image.container.mappings,
address,
size,
image.container.zero_fills,
);
image.fileMappings.offset(address, size);
return true;
} catch (cause) {
if (cause instanceof BinaryFormatFailure) return false;
@@ -302,12 +293,7 @@ export const hasPointerArrayCoverage = (
if (fileEnd === cursor) {
if (!isZeroFilled(image, cursor, size)) return false;
} else {
mappedFileOffset(
image.container.mappings,
cursor,
size,
image.container.zero_fills,
);
image.fileMappings.offset(cursor, size);
}
} catch (cause) {
if (cause instanceof BinaryFormatFailure) return false;
@@ -321,12 +307,7 @@ export const hasPointerArrayCoverage = (
/** Both executable coverage and the unique full loader mapping must agree. */
export const isExecutable = (image: NativeImage, address: bigint): boolean => {
try {
const offset = mappedFileOffset(
image.container.mappings,
address,
1,
image.container.zero_fills,
);
const offset = image.fileMappings.offset(address, 1);
return (
offset ===
mappedFileOffset(image.container.executable_mappings ?? [], address, 1)
@@ -359,8 +340,12 @@ export const readSlot = (
const virtual = image.zeroFillPointers.get(address);
let raw: bigint | null;
if (virtual !== undefined) raw = virtual;
else if (isZeroFilled(image, address, 8)) raw = 0n;
else raw = readStoredWord(image, address);
else {
// A successful indexed read already excludes zero-fill aliases. Only a
// non-file-backed slot needs the zero-fill ownership walk.
raw = readStoredWord(image, address);
if (raw === null && isZeroFilled(image, address, 8)) raw = 0n;
}
if (raw === null) return null;
if (image.machoPointerFixups !== undefined) {
const resolution = image.machoPointerFixups.decode(address, raw);
+249 -9
View File
@@ -7,10 +7,16 @@ import {
isMapped,
hasPointerArrayCoverage,
readPointer,
readSlot,
readUInt32,
type NativeImage,
} from "./Il2cppBinaryImage.js";
import type { Il2cppMetadataDecoded } from "./Il2cppMetadataFile.js";
import {
IL2CPP_TYPE_KIND_MAX,
il2cppTypeKind,
isDefinitionTypeKind,
} from "./Il2cppBinaryTypes.js";
import type { Il2cppCodeGenModule } from "./Il2cppBinaryFile.js";
// Bound the retained name-address index independently of binary size. No
// repeated name is evidence of ambiguous module ownership by itself.
@@ -245,7 +251,11 @@ export function* findCodeRegistration(
}
}
}
if (matches.length === 0 && image.container.architecture === "amd64") {
const architecture = image.container.architecture;
if (
matches.length === 0 &&
(architecture === "amd64" || architecture === "arm64")
) {
const array = yield* findModuleArrayByCode(image, expected);
if (array !== null) return { field: null, array };
}
@@ -258,12 +268,16 @@ export function* findCodeRegistration(
}
/**
* Some x86-64 builds (a Unity 2023.2 Linux GameAssembly.so) keep no static
* CodeRegistration: the compiler folded the module array and its count into
* the one loop that walks it, so no data word points at the array. Find every
* Some builds keep no static CodeRegistration: the compiler folded the module
* array and its count into the one loop that walks it, so no data word points
* at the array. Seen on x86-64 (a Unity 2023.2 Linux GameAssembly.so) and on
* arm64 (a Unity 2022.3 macOS Master/LTO GameAssembly.dylib). Find every
* pointer array that lists exactly the located modules, then require exactly
* one of them to be the target of a RIP-relative `lea`/`mov` (REX.W 8D/8B with
* a disp32 operand). Anything else stays code-registration-not-found.
* one of them to be the target of an address-forming instruction in an
* executable range: on x86-64 a RIP-relative `lea`/`mov` (REX.W 8D/8B with a
* disp32 operand), on arm64 an `adrp` immediately followed by an `add` of its
* page offset to the same register. Anything else stays
* code-registration-not-found.
*/
const MODULE_ARRAY_CANDIDATES = 64;
@@ -303,6 +317,17 @@ function* findModuleArrayByCode(
}
}
if (arrays.size === 0) return null;
const referenced =
image.container.architecture === "arm64"
? yield* arm64PageReferences(image, arrays)
: yield* ripRelativeReferences(image, arrays);
return referenced.size === 1 ? [...referenced][0]! : null;
}
function* ripRelativeReferences(
image: NativeImage,
arrays: ReadonlySet<bigint>,
): Generator<void, Set<bigint>> {
const referenced = new Set<bigint>();
for (const mapping of image.container.executable_mappings ?? []) {
const bytes = image.bytes;
@@ -319,7 +344,44 @@ function* findModuleArrayByCode(
if (arrays.has(target)) referenced.add(target);
}
}
return referenced.size === 1 ? [...referenced][0]! : null;
return referenced;
}
/**
* arm64 instructions are 4-byte aligned words, so only aligned addresses are
* decoded. `adrp xd, page` forms the 4 KiB page of pc plus a signed 21-bit
* page delta; the next word must be `add xd, xd, #imm12` (64-bit, unshifted),
* which adds the offset within that page. Pairs split by other instructions or
* loads through `ldr` are not followed.
*/
function* arm64PageReferences(
image: NativeImage,
arrays: ReadonlySet<bigint>,
): Generator<void, Set<bigint>> {
const referenced = new Set<bigint>();
const bytes = image.bytes;
for (const mapping of image.container.executable_mappings ?? []) {
const start = Number((4n - (mapping.address % 4n)) % 4n);
for (let at = start; at + 8 <= mapping.size; at += 4) {
if ((at & 0xffff) < 4) yield;
const adrp = bytes.readUInt32LE(mapping.offset + at);
if ((adrp & 0x9f000000) >>> 0 !== 0x90000000) continue;
const add = bytes.readUInt32LE(mapping.offset + at + 4);
if ((add & 0xffc00000) >>> 0 !== 0x91000000) continue;
const register = adrp & 0x1f;
// Register 31 is xzr for ADRP and sp for ADD, so no address is formed.
if (register === 31) continue;
if ((add & 0x1f) !== register || ((add >>> 5) & 0x1f) !== register)
continue;
const pages =
(((((adrp >>> 5) & 0x7ffff) << 2) | ((adrp >>> 29) & 3)) << 11) >> 11;
const pc = mapping.address + BigInt(at);
const target =
(pc & ~0xfffn) + (BigInt(pages) << 12n) + BigInt((add >>> 10) & 0xfff);
if (arrays.has(target)) referenced.add(target);
}
}
return referenced;
}
/**
@@ -327,20 +389,26 @@ function* findModuleArrayByCode(
* fieldOffsetsCount and typeDefinitionsSizesCount (words 10 and 12) both equal
* the metadata type-definition count and are followed by mapped pointers.
* typesCount and types are words 6 and 7. Counts are int32 in 8-byte slots.
* Linking reads only the type table, so when no static registration matches on
* x86-64 or arm64 the table is inferred from code instead (field null).
*/
export function* findMetadataRegistration(
image: NativeImage,
typeDefinitions: number,
requiredTypes: number,
): Generator<
void,
{
readonly field: bigint;
readonly field: bigint | null;
readonly count: number;
readonly types: bigint;
}
> {
const matches: { field: bigint; count: number; types: bigint }[] = [];
const matchedFields = new Set<bigint>();
// A record whose two counts match is static evidence even when its other
// fields are unusable; code is then not asked to stand in for it.
let countsMatched = false;
const low = (offset: number) => image.bytes.readUInt32LE(offset);
for (const mapping of image.container.mappings) {
const start = Number((8n - (mapping.address % 8n)) % 8n);
@@ -355,6 +423,7 @@ export function* findMetadataRegistration(
if (!hasPointerArrayCoverage(image, address, 16)) continue;
if (readUInt32(image, address + 80n) !== typeDefinitions) continue;
if (readUInt32(image, address + 96n) !== typeDefinitions) continue;
countsMatched = true;
const count = readUInt32(image, address + 48n);
const types = readPointer(image, address + 56n);
const fieldOffsets = readPointer(image, address + 88n);
@@ -387,10 +456,181 @@ export function* findMetadataRegistration(
});
}
}
const architecture = image.container.architecture;
let inferred = false;
if (
!countsMatched &&
requiredTypes > 0 &&
(architecture === "amd64" || architecture === "arm64")
) {
inferred = true;
const table = yield* findTypeTableByCode(
image,
typeDefinitions,
requiredTypes,
);
if (table !== null) return { field: null, ...table };
}
if (matches.length !== 1)
throw new Il2cppBinaryFormatFailure(
"metadata-registration-not-found",
`${matches.length} MetadataRegistration candidates count ${typeDefinitions} type definitions`,
countsMatched && matches.length === 0
? `a MetadataRegistration counts ${typeDefinitions} type definitions, but its type table, field offsets or type sizes are not mapped`
: inferred
? `no MetadataRegistration counts ${typeDefinitions} type definitions, and no single code-loaded run of Il2CppType pointers covers every referenced type index`
: `${matches.length} MetadataRegistration candidates count ${typeDefinitions} type definitions`,
);
return matches[0]!;
}
/**
* An inlined MetadataRegistration leaves the Il2CppType pointer table loaded
* directly by code. Candidates are maximal runs of pointers to Il2CppType-shaped
* records (a defined Il2CppTypeEnum kind; class and value-type data below the
* metadata type-definition count) long enough for every type index the
* metadata references. Exactly one such run may exist in the image, it must
* end inside its mapping, and code must load its start with the same
* instruction forms as the module-array fallback; anything else fails closed.
* The run length becomes the count: never below what the metadata needs, but
* not an observed typesCount.
*/
/** Il2CppTypeEnum values 0x01 to 0x1e; 0x17 and 0x1a are unassigned. */
const isDefinedTypeKind = (kind: number) =>
kind >= 1 && kind <= IL2CPP_TYPE_KIND_MAX && kind !== 0x17 && kind !== 0x1a;
function* findTypeTableByCode(
image: NativeImage,
typeDefinitions: number,
requiredTypes: number,
): Generator<void, { readonly count: number; readonly types: bigint } | null> {
const { bytes, container } = image;
// Nomination and loader-aware confirmation share the image's mapping index.
// Most words are not record addresses; reject those without the throwing
// lookup. Starts are sorted, and reach[i] is the furthest end among the
// first i + 1 mappings, so one binary search finds a possible container.
const spans = container.mappings
.filter((mapping) => mapping.size > 0)
.map((mapping) => ({
start: mapping.address,
end: mapping.address + BigInt(mapping.size),
}))
.sort((left, right) =>
left.start < right.start ? -1 : left.start > right.start ? 1 : 0,
);
const reach: bigint[] = [];
for (const [index, span] of spans.entries())
reach.push(
index === 0 || span.end > reach[index - 1]!
? span.end
: reach[index - 1]!,
);
const maybeMapped = (address: bigint) => {
let lowIndex = 0;
let highIndex = spans.length - 1;
let found = -1;
while (lowIndex <= highIndex) {
const middle = (lowIndex + highIndex) >> 1;
if (spans[middle]!.start <= address) {
found = middle;
lowIndex = middle + 1;
} else highIndex = middle - 1;
}
return found >= 0 && reach[found]! >= address + 12n;
};
const recordOffset = (address: bigint) => {
if (!maybeMapped(address)) return -1;
try {
return image.fileMappings.offset(address, 12);
} catch (cause) {
// Unmapped, zero-filled or ambiguously mapped records are not types.
if (cause instanceof BinaryFormatFailure) return -1;
throw cause;
}
};
const isType = (address: bigint) => {
// Records hold an 8-byte pointer first; zero and unaligned words are not types.
if (address === 0n || (address & 7n) !== 0n) return false;
const offset = recordOffset(address);
if (offset < 0) return false;
const kind = il2cppTypeKind(bytes.readUInt32LE(offset + 8));
if (!isDefinedTypeKind(kind)) return false;
if (!isDefinitionTypeKind(kind)) return true;
return bytes.readBigUInt64LE(offset) < BigInt(typeDefinitions);
};
// A run that touches either end of its mapping may continue in a
// neighbouring one, so its length and start are unknown; it still counts as
// a candidate but cannot be chosen.
const runs = new Map<bigint, number | null>();
// Stored words only nominate slots: a slot the loader binds or leaves
// unresolved has no known value, so a run it bounds has an unknown extent
// whatever its placeholder holds.
// The same holds for a record's kind word and its definition index.
const recordState = (target: bigint): "type" | "other" | "unknown" => {
if (recordOffset(target) < 0) return "other";
const attributes = readUInt32(image, target + 8n);
if (attributes === "unknown") return "unknown";
if (attributes === null) return "other";
const kind = il2cppTypeKind(attributes);
if (!isDefinedTypeKind(kind)) return "other";
if (!isDefinitionTypeKind(kind)) return "type";
const data = readSlot(image, target);
if (data === "unknown") return "unknown";
return typeof data === "bigint" && data < BigInt(typeDefinitions)
? "type"
: "other";
};
// A word whose slot or record the loader leaves unknown bounds a run with
// an unknown extent.
const unresolved = (address: bigint) => {
const slot = readSlot(image, address);
return (
slot === "unknown" ||
(typeof slot === "bigint" && recordState(slot) === "unknown")
);
};
for (const mapping of container.mappings) {
const start = Number((8n - (mapping.address % 8n)) % 8n);
let runStart = -1;
let startUnknown = false;
for (let at = start; at <= mapping.size; at += 8) {
if ((at & 0xffff) < 8) yield;
const address = mapping.address + BigInt(at);
let endUnknown = at + 8 > mapping.size;
if (!endUnknown) {
const raw = bytes.readBigUInt64LE(mapping.offset + at);
if (isType(raw)) {
const slot = readSlot(image, address);
const state = slot === raw ? recordState(slot) : "other";
if (state === "type") {
if (runStart < 0) {
runStart = at;
startUnknown = at === start || unresolved(address - 8n);
}
continue;
}
endUnknown = slot === "unknown" || state === "unknown";
} else if (runStart >= 0) endUnknown = unresolved(address);
}
if (runStart >= 0 && (at - runStart) / 8 >= requiredTypes) {
runs.set(
mapping.address + BigInt(runStart),
endUnknown || startUnknown ? null : (at - runStart) / 8,
);
// Another long-enough run could be the real table while code loads
// only this one, so a second candidate fails closed at once.
if (runs.size > 1) return null;
}
runStart = -1;
}
}
if (runs.size !== 1) return null;
const [start, count] = [...runs][0]!;
if (count === null) return null;
const starts = new Set([start]);
const referenced =
container.architecture === "arm64"
? yield* arm64PageReferences(image, starts)
: yield* ripRelativeReferences(image, starts);
if (!referenced.has(start)) return null;
return { count, types: start };
}
+12 -2
View File
@@ -9,6 +9,16 @@ import type {
Il2cppMetadataDecoded,
Il2cppMetadataLinkFacts,
} from "./Il2cppMetadataFile.js";
/** Il2CppType's type enum: bits 16-23 of the word after its 8-byte data. */
export const il2cppTypeKind = (attributes: number): number =>
(attributes >>> 16) & 0xff;
/** Largest Il2CppTypeEnum value read as a type kind (IL2CPP_TYPE_MVAR). */
export const IL2CPP_TYPE_KIND_MAX = 0x1e;
/** Class and value-type kinds, whose data is a TypeDefinitionIndex. */
export const isDefinitionTypeKind = (kind: number): boolean =>
kind === 0x11 || kind === 0x12;
const PRIMITIVES = new Map([
[0x01, "void"],
[0x02, "bool"],
@@ -127,12 +137,12 @@ const createTypeRenderer = (
if (tick()) yield;
const data = pointer(address, "Il2CppType data");
const bits = u32(address + 8n, "Il2CppType attributes");
const kind = (bits >>> 16) & 0xff;
const kind = il2cppTypeKind(bits);
const index = Number(BigInt.asIntN(32, data & 0xffffffffn));
let result: string;
const primitive = rules.primitives.get(kind);
if (primitive !== undefined) result = primitive;
else if (kind === 0x11 || kind === 0x12)
else if (isDefinitionTypeKind(kind))
result = yield* rules.definition(index);
else if (kind === 0x0f || kind === 0x1d)
result = join([
+12 -2
View File
@@ -67,6 +67,11 @@ const INPUT_BYTES = 256 * 1024 * 1024;
const BINARY_BYTES = 1024 * 1024 * 1024;
// A 12.9 MB Unity 6000.4 file with 112,232 methods serializes to 7.8 MB.
const OUTPUT_BYTES = 64 * 1024 * 1024;
/** Instruction form the module-array fallback matched, per architecture. */
const inferredReferenceForm = (architecture: string) =>
architecture === "arm64"
? "an adjacent ADRP+ADD pair"
: "RIP-relative LEA/MOV";
const failureForPrimary = (
facts: ArtifactFilePrimaryFacts,
path: string,
@@ -229,7 +234,7 @@ const binaryReport = (
address: hex(module.address),
method_pointer_count: module.method_pointer_count,
})),
types_field: hex(link.types_field),
types_field: link.types_field === null ? null : hex(link.types_field),
types_count: link.types_count,
generic_methods:
link.generic_methods === null
@@ -587,7 +592,12 @@ export class Il2cppMetadataProvider implements Il2cppMetadataPort {
...(linked.binary !== null &&
linked.binary.code_gen_modules_field === null
? [
"The static CodeRegistration pair was not resolved. The module array is inferred from executable bytes consistent with RIP-relative LEA/MOV referencing one array candidate; instruction boundaries and runtime use were not verified.",
`The static CodeRegistration pair was not resolved. The module array is inferred from executable bytes consistent with ${inferredReferenceForm(linked.binary.architecture)} referencing one array candidate; instruction boundaries and runtime use were not verified.`,
]
: []),
...(linked.binary !== null && linked.binary.types_field === null
? [
`No static MetadataRegistration had the type-definition counts. The type table is inferred from executable bytes consistent with ${inferredReferenceForm(linked.binary.architecture)} referencing the one run of Il2CppType-shaped pointers long enough for every referenced type index; types_count is that run's length, at least every referenced type index but not an observed typesCount, and instruction boundaries and runtime use were not verified.`,
]
: []),
...(binaryPath === undefined