• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SamboyCoding / Cpp2IL / 28725945259

05 Jul 2026 01:40AM UTC coverage: 36.238% (+0.1%) from 36.139%
28725945259

Pull #549

github

web-flow
Merge 5f32b622e into b1cb5091b
Pull Request #549: Ensure the base type of generic instance types is instantiated

2691 of 8492 branches covered (31.69%)

Branch coverage included in aggregate %.

92 of 118 new or added lines in 27 files covered. (77.97%)

2 existing lines in 2 files now uncovered.

5078 of 12947 relevant lines covered (39.22%)

171675.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/Cpp2IL.Core/Analysis/MetadataResolver.cs
1
using System.Collections.Generic;
2
using System.Linq;
3
using Cpp2IL.Core.Extensions;
4
using Cpp2IL.Core.Graphs;
5
using Cpp2IL.Core.Il2CppApiFunctions;
6
using Cpp2IL.Core.ISIL;
7
using Cpp2IL.Core.Model.Contexts;
8
using Cpp2IL.Core.Utils;
9
using LibCpp2IL;
10

11
namespace Cpp2IL.Core.Analysis;
12

13
public static class MetadataResolver
14
{
15
    public static void ResolveAll(MethodAnalysisContext method)
16
    {
17
        ResolveCalls(method);
×
18
        ResolveGetter(method);
×
19
        ResolveMetadataUsages(method);
×
20
    }
×
21

22
    /// <summary>
23
    /// Resolves <c>Move local, [absoluteAddress]</c> loads of IL2CPP metadata-usage globals into a
24
    /// strongly-typed operand: a string literal, a <see cref="TypeAnalysisContext"/> (an Il2CppType*/
25
    /// Il2CppClass* usage) or, for a MethodInfo* usage, a <see cref="RuntimeMethodInfoAnalysisContext"/>
26
    /// naming the method it refers to (also used to type the local - see <see cref="LocalVariables"/>).
27
    /// </summary>
28
    private static void ResolveMetadataUsages(MethodAnalysisContext method)
29
    {
30
        var libContext = method.AppContext.LibCpp2IlContext;
×
31

32
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
33
        {
34
            if (instruction.OpCode != OpCode.Move)
×
35
                continue;
36

37
            // Only an absolute-address load [addr] (no base/index/scale) can be a metadata-usage global.
38
            if (instruction.Operands[0] is not LocalVariable
×
39
                || instruction.Operands[1] is not MemoryOperand { Base: null, Index: null, Scale: 0 } memory)
×
40
                continue;
41

42
            var address = (ulong)memory.Addend;
×
43

44
            // String literal.
45
            var stringLiteral = libContext.GetLiteralByAddress(address);
×
46
            if (stringLiteral != null)
×
47
            {
48
                instruction.Operands[1] = stringLiteral;
×
49
                continue;
×
50
            }
51

52
            // Type metadata usage (Il2CppType* / Il2CppClass*).
53
            if (method.DeclaringType is { } declaringType)
×
54
            {
NEW
55
                var typeContext = libContext.GetTypeGlobalByAddress(address)?.ToContext(declaringType.AppContext);
×
56
                if (typeContext != null)
×
57
                {
58
                    instruction.Operands[1] = typeContext;
×
59
                    continue;
×
60
                }
61
            }
62

63
            // Method metadata usage (MethodInfo*). On metadata v27+ GetMethodGlobalByAddress can return
64
            // any global, so confirm it is actually a method before resolving - the resolver's switch
65
            // throws on other usage kinds.
66
            var methodUsage = libContext.GetMethodGlobalByAddress(address);
×
67
            if (methodUsage?.Type is MetadataUsageType.MethodDef or MetadataUsageType.MethodRef
×
68
                && method.AppContext.ResolveContextForMethod(methodUsage) is { DeclaringType: { } methodDeclaringType } methodContext)
×
69
                instruction.Operands[1] = new RuntimeMethodInfoAnalysisContext(methodContext, methodDeclaringType.DeclaringAssembly);
×
70
        }
71
    }
×
72

73
    /// <summary>
74
    /// Replaces every <c>[base + addend]</c> memory operand whose base is a typed local with a
75
    /// <see cref="FieldReference"/> to the field at that offset. Returns whether any operand was
76
    /// resolved this pass, so the type/field fixpoint can detect convergence: as more bases become
77
    /// typed (a field load types its result, which is the base of the next load), more offsets
78
    /// resolve, so this is re-run until it stops finding new fields.
79
    /// </summary>
80
    public static bool ResolveFieldOffsets(MethodAnalysisContext method)
81
    {
82
        var changed = false;
×
83

84
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
85
        {
86
            for (var i = 0; i < instruction.Operands.Count; i++)
×
87
            {
88
                var operand = instruction.Operands[i];
×
89

90
                if (operand is not MemoryOperand memory)
×
91
                    continue;
92

93
                // Has to be [base (local) + addend (field offset)]
94
                if (memory.Index != null || memory.Scale != 0)
×
95
                    continue;
96

97
                if (memory.Base is not LocalVariable local || local?.Type == null)
×
98
                    continue;
99

100
                var field = local.Type.Fields.FirstOrDefault(f => f.BackingData?.FieldOffset == memory.Addend);
×
101

102
                if (field == null) // TODO: Support nested fields (Field1.Field2.Field3)
×
103
                    continue;
104

105
                instruction.Operands[i] = new FieldReference(field, local, (int)memory.Addend);
×
106
                changed = true;
×
107
            }
108
        }
109

110
        return changed;
×
111
    }
112

113
    private static void ResolveCalls(MethodAnalysisContext method)
114
    {
115
        foreach (var block in method.ControlFlowGraph!.Blocks)
×
116
        {
117
            if (block.BlockType != BlockType.Call && block.BlockType != BlockType.TailCall)
×
118
                continue;
119

120
            var callInstruction = block.Instructions[^1];
×
121
            var dest = callInstruction.Operands[0];
×
122

123
            if (!dest.IsNumeric())
×
124
                continue;
125

126
            var target = (ulong)dest;
×
127

128
            var keyFunctionAddresses = method.AppContext.GetOrCreateKeyFunctionAddresses();
×
129

130
            if (keyFunctionAddresses.IsKeyFunctionAddress(target))
×
131
            {
132
                HandleKeyFunction(method.AppContext, callInstruction, target, keyFunctionAddresses);
×
133
                continue;
×
134
            }
135

136
            //Non-key function call. Try to find a single match
137
            if (!method.AppContext.MethodsByAddress.TryGetValue(target, out var targetMethods))
×
138
                continue;
139

140
            // Duplicated/Shared method bodies are resolved later in ResolveCallsViaMethodInfo/ResolveAmbiguousCalls.
141
            if (targetMethods is not [{ } singleTargetMethod])
×
142
                continue;
143

144
            callInstruction.Operands[0] = singleTargetMethod;
×
145
        }
146

147
        method.ControlFlowGraph.MergeCallBlocks();
×
148
    }
×
149

150
    /// <summary>
151
    /// Resolves calls whose address maps to more than one method by matching the receiver's known
152
    /// type against the candidates' declaring types. Runs inside the type/field fixpoint and so
153
    /// re-fires as receivers become typed - a resolved call types its return value, which can type
154
    /// the receiver of a further call. Returns whether any call was resolved this pass.
155
    ///
156
    /// Conservative by design: it commits only when exactly one non-static candidate's declaring
157
    /// type matches the receiver's type. Anything still untyped or ambiguous is left for a later
158
    /// pass, or left unresolved - it never guesses.
159
    /// </summary>
160
    public static bool ResolveAmbiguousCalls(MethodAnalysisContext method)
161
    {
162
        var changed = false;
×
163

164
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
165
        {
166
            if (!instruction.IsCall)
×
167
                continue;
168

169
            var target = instruction.Operands[0];
×
170

171
            // A resolved call's target is a method/key-function name; only unresolved ones are still numeric.
172
            if (!target.IsNumeric())
×
173
                continue;
174

175
            if (!method.AppContext.MethodsByAddress.TryGetValue((ulong)target, out var candidates) || candidates.Count < 2)
×
176
                continue;
177

178
            if (GetReceiver(instruction) is not { Type: { } receiverType })
×
179
                continue;
180

181
            MethodAnalysisContext? match = null;
×
182
            var ambiguous = false;
×
183

184
            foreach (var candidate in candidates)
×
185
            {
186
                if (candidate.IsStatic || !ReferenceEquals(candidate.DeclaringType, receiverType))
×
187
                    continue;
188

189
                if (match != null)
×
190
                {
191
                    ambiguous = true;
×
192
                    break;
×
193
                }
194

195
                match = candidate;
×
196
            }
197

198
            if (ambiguous || match == null)
×
199
                continue;
200

201
            instruction.Operands[0] = match;
×
202
            changed = true;
×
203
        }
204

205
        return changed;
×
206
    }
207

208
    // The receiver ('this') of a call is the first integer-slot argument: operand 1 for CallVoid
209
    // (after the target), operand 2 for Call (after the target and the return value).
210
    private static LocalVariable? GetReceiver(Instruction call)
211
    {
212
        var index = call.OpCode == OpCode.CallVoid ? 1 : 2;
×
213
        return index < call.Operands.Count ? call.Operands[index] as LocalVariable : null;
×
214
    }
215

216
    /// <summary>
217
    /// Resolves any Call (theoretically should always be a CallVoid) target directly after a Newobj to a constructor call.
218
    /// </summary>
219
    public static bool ResolveConstructorCalls(MethodAnalysisContext method)
220
    {
221
        var definitions = new Dictionary<LocalVariable, Instruction>();
×
222
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
223
            if (instruction.Destination is LocalVariable definition)
×
224
                definitions[definition] = instruction;
×
225

226
        var changed = false;
×
227

228
        foreach (var instruction in method.ControlFlowGraph.Instructions)
×
229
        {
230
            if (!instruction.IsCall || !instruction.Operands[0].IsNumeric())
×
231
                continue;
232

233
            if (!method.AppContext.MethodsByAddress.TryGetValue((ulong)instruction.Operands[0], out var candidates))
×
234
                continue;
235

236
            if (GetReceiver(instruction) is not { } receiver || AllocatedType(receiver, definitions) is not { } allocatedType)
×
237
                continue;
238

239
            var constructor = candidates.FirstOrDefault(c => !c.IsStatic && c.Name == ".ctor" && ReferenceEquals(c.DeclaringType, allocatedType));
×
240
            if (constructor == null)
×
241
                continue;
242

243
            instruction.Operands[0] = constructor;
×
244
            changed = true;
×
245
        }
246

247
        return changed;
×
248
    }
249

250
    // Follow SSA copies from a local back to the Newobj that produced the value
251
    private static TypeAnalysisContext? AllocatedType(LocalVariable local, Dictionary<LocalVariable, Instruction> definitions)
252
    {
253
        var visited = new HashSet<LocalVariable>();
×
254

255
        while (visited.Add(local) && definitions.TryGetValue(local, out var definition))
×
256
        {
257
            switch (definition.OpCode)
×
258
            {
259
                case OpCode.Newobj:
260
                    return (definition.Operands[0] as LocalVariable)?.Type;
×
261
                case OpCode.Move when definition.Operands[1] is LocalVariable source:
×
262
                    local = source;
×
263
                    continue;
×
264
            }
265

266
            break;
267
        }
268

269
        return null;
×
270
    }
271

272
    /// <summary>
273
    /// Resolves calls whose address maps to more than one method by reading the runtime
274
    /// <c>MethodInfo*</c> the caller passes in, if there is one.
275
    /// </summary>
276
    public static bool ResolveCallsViaMethodInfo(MethodAnalysisContext method)
277
    {
278
        var changed = false;
×
279

280
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
281
        {
282
            if (!instruction.IsCall)
×
283
                continue;
284

285
            var target = instruction.Operands[0];
×
286

287
            if (!target.IsNumeric())
×
288
                //Already resolved
289
                continue;
290

291
            if (!method.AppContext.MethodsByAddress.TryGetValue((ulong)target, out var candidates) || candidates.Count < 2)
×
292
                //Not a managed method at all
293
                continue;
294

295
            if (GetMethodInfoArgument(instruction) is not { RepresentedMethod: { } representedMethod })
×
296
                //No MethodInfo to work with
297
                continue;
298

299
            //Try to actually match on the method name so we don't just replace a call with something else.
300
            var representedBase = BaseMethodOf(representedMethod);
×
301
            if (!candidates.Any(candidate => ReferenceEquals(BaseMethodOf(candidate), representedBase)))
×
302
                continue;
303

304
            instruction.Operands[0] = representedMethod;
×
305
            changed = true;
×
306
        }
307

308
        return changed;
×
309
    }
310

311
    private static MethodAnalysisContext BaseMethodOf(MethodAnalysisContext method) =>
312
        method is ConcreteGenericMethodAnalysisContext { BaseMethodContext: { } baseMethod } ? baseMethod : method;
×
313

314
    private static RuntimeMethodInfoAnalysisContext? GetMethodInfoArgument(Instruction call)
315
    {
316
        var firstArg = call.OpCode == OpCode.CallVoid ? 1 : 2;
×
317

318
        for (var i = call.Operands.Count - 1; i >= firstArg; i--)
×
319
        {
320
            switch (call.Operands[i])
×
321
            {
322
                case RuntimeMethodInfoAnalysisContext methodInfo:
323
                    return methodInfo;
×
324
                case LocalVariable { Type: RuntimeMethodInfoAnalysisContext methodInfoLocal }:
325
                    return methodInfoLocal;
×
326
            }
327
        }
328

329
        return null;
×
330
    }
331

332
    private static void HandleKeyFunction(ApplicationAnalysisContext appContext, Instruction instruction, ulong target, BaseKeyFunctionAddresses kFA)
333
    {
334
        var method = "";
×
335
        if (target == kFA.il2cpp_codegen_initialize_method || target == kFA.il2cpp_codegen_initialize_runtime_metadata)
×
336
        {
337
            if (appContext.MetadataVersion < 27)
×
338
            {
339
                method = nameof(kFA.il2cpp_codegen_initialize_method);
×
340
            }
341
            else
342
            {
343
                method = nameof(kFA.il2cpp_codegen_initialize_runtime_metadata);
×
344
            }
345
        }
346
        else
347
        {
348
            var pairs = kFA.Pairs.ToList();
×
349
            var key = pairs.FirstOrDefault(pair => pair.Value == target).Key;
×
350
            if (key == null)
×
351
                return;
×
352
            method = key;
×
353
        }
354

355
        if (method != "")
×
356
        {
357
            instruction.Operands[0] = method;
×
358
        }
359
    }
×
360

361
    // Because of il2cpp fields (like cctor_finished_or_no_cctor) [local @ reg+offset] sometimes can't be resolved, but this works for now
362
    private static void ResolveGetter(MethodAnalysisContext method)
363
    {
364
        if (!method.Name.StartsWith("get_"))
×
365
            return;
×
366

367
        // Default get: Return [this @ reg+offset]
368
        var instructions = method.ControlFlowGraph!.Instructions;
×
369
        if (instructions.Count == 1)
×
370
        {
371
            var instr = instructions[0];
×
372

373
            if (instr.OpCode != OpCode.Return
×
374
                || instr.Operands.Count < 1
×
375
                || instr.Operands[0] is not MemoryOperand memory
×
376
                || memory.Index != null || memory.Scale != 0
×
377
                || memory.Base is not LocalVariable local)
×
378
                return;
×
379

380
            var fieldName = $"<{method.Name[4..]}>k__BackingField";
×
381

382
            var field = method.DeclaringType!.Fields.Find(f => f.Name == fieldName);
×
383
            if (field == null)
×
384
                return;
×
385

386
            instr.Operands[0] = new FieldReference(field, local, (int)memory.Addend);
×
387
        }
388
    }
×
389
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc