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

SamboyCoding / Cpp2IL / 27876691548

20 Jun 2026 04:12PM UTC coverage: 34.253% (-1.1%) from 35.351%
27876691548

push

github

web-flow
IL Generation: Fix broken branching and omit methodinfo param load (#563)

2410 of 8267 branches covered (29.15%)

Branch coverage included in aggregate %.

0 of 53 new or added lines in 1 file covered. (0.0%)

216 existing lines in 11 files now uncovered.

4788 of 12747 relevant lines covered (37.56%)

218738.72 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.Linq;
2
using Cpp2IL.Core.Extensions;
3
using Cpp2IL.Core.Graphs;
4
using Cpp2IL.Core.Il2CppApiFunctions;
5
using Cpp2IL.Core.ISIL;
6
using Cpp2IL.Core.Model.Contexts;
7
using Cpp2IL.Core.Utils;
8
using LibCpp2IL;
9

10
namespace Cpp2IL.Core.Analysis;
11

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

109
        return changed;
×
110
    }
111

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

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

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

125
            var target = (ulong)dest;
×
126

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

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

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

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

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

UNCOV
146
        method.ControlFlowGraph.MergeCallBlocks();
×
UNCOV
147
    }
×
148

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

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

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

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

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

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

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

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

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

UNCOV
194
                match = candidate;
×
195
            }
196

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

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

UNCOV
204
        return changed;
×
205
    }
206

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

215
    /// <summary>
216
    /// Resolves calls whose address maps to more than one method by reading the runtime
217
    /// <c>MethodInfo*</c> the caller passes in, if there is one.
218
    /// </summary>
219
    public static bool ResolveCallsViaMethodInfo(MethodAnalysisContext method)
220
    {
UNCOV
221
        var changed = false;
×
222

UNCOV
223
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
224
        {
UNCOV
225
            if (!instruction.IsCall)
×
226
                continue;
227

228
            var target = instruction.Operands[0];
×
229

UNCOV
230
            if (!target.IsNumeric())
×
231
                //Already resolved
232
                continue;
233

234
            if (!method.AppContext.MethodsByAddress.TryGetValue((ulong)target, out var candidates) || candidates.Count < 2)
×
235
                //Not a managed method at all
236
                continue;
237

UNCOV
238
            if (GetMethodInfoArgument(instruction) is not { RepresentedMethod: { } representedMethod })
×
239
                //No MethodInfo to work with
240
                continue;
241

242
            //Try to actually match on the method name so we don't just replace a call with something else.
UNCOV
243
            var representedBase = BaseMethodOf(representedMethod);
×
244
            if (!candidates.Any(candidate => ReferenceEquals(BaseMethodOf(candidate), representedBase)))
×
245
                continue;
246

UNCOV
247
            instruction.Operands[0] = representedMethod;
×
UNCOV
248
            changed = true;
×
249
        }
250

UNCOV
251
        return changed;
×
252
    }
253

254
    private static MethodAnalysisContext BaseMethodOf(MethodAnalysisContext method) =>
UNCOV
255
        method is ConcreteGenericMethodAnalysisContext { BaseMethodContext: { } baseMethod } ? baseMethod : method;
×
256

257
    private static RuntimeMethodInfoAnalysisContext? GetMethodInfoArgument(Instruction call)
258
    {
259
        var firstArg = call.OpCode == OpCode.CallVoid ? 1 : 2;
×
260

261
        for (var i = call.Operands.Count - 1; i >= firstArg; i--)
×
262
        {
263
            switch (call.Operands[i])
×
264
            {
265
                case RuntimeMethodInfoAnalysisContext methodInfo:
UNCOV
266
                    return methodInfo;
×
267
                case LocalVariable { Type: RuntimeMethodInfoAnalysisContext methodInfoLocal }:
268
                    return methodInfoLocal;
×
269
            }
270
        }
271

UNCOV
272
        return null;
×
273
    }
274

275
    private static void HandleKeyFunction(ApplicationAnalysisContext appContext, Instruction instruction, ulong target, BaseKeyFunctionAddresses kFA)
276
    {
UNCOV
277
        var method = "";
×
UNCOV
278
        if (target == kFA.il2cpp_codegen_initialize_method || target == kFA.il2cpp_codegen_initialize_runtime_metadata)
×
279
        {
UNCOV
280
            if (appContext.MetadataVersion < 27)
×
281
            {
UNCOV
282
                method = nameof(kFA.il2cpp_codegen_initialize_method);
×
283
            }
284
            else
285
            {
UNCOV
286
                method = nameof(kFA.il2cpp_codegen_initialize_runtime_metadata);
×
287
            }
288
        }
289
        else
290
        {
UNCOV
291
            var pairs = kFA.Pairs.ToList();
×
UNCOV
292
            var key = pairs.FirstOrDefault(pair => pair.Value == target).Key;
×
UNCOV
293
            if (key == null)
×
UNCOV
294
                return;
×
UNCOV
295
            method = key;
×
296
        }
297

UNCOV
298
        if (method != "")
×
299
        {
UNCOV
300
            instruction.Operands[0] = method;
×
301
        }
UNCOV
302
    }
×
303

304
    // Because of il2cpp fields (like cctor_finished_or_no_cctor) [local @ reg+offset] sometimes can't be resolved, but this works for now
305
    private static void ResolveGetter(MethodAnalysisContext method)
306
    {
UNCOV
307
        if (!method.Name.StartsWith("get_"))
×
UNCOV
308
            return;
×
309

310
        // Default get: Return [this @ reg+offset]
UNCOV
311
        var instructions = method.ControlFlowGraph!.Instructions;
×
UNCOV
312
        if (instructions.Count == 1)
×
313
        {
UNCOV
314
            var instr = instructions[0];
×
315

UNCOV
316
            if (instr.OpCode != OpCode.Return
×
UNCOV
317
                || instr.Operands.Count < 1
×
UNCOV
318
                || instr.Operands[0] is not MemoryOperand memory
×
UNCOV
319
                || memory.Index != null || memory.Scale != 0
×
UNCOV
320
                || memory.Base is not LocalVariable local)
×
UNCOV
321
                return;
×
322

UNCOV
323
            var fieldName = $"<{method.Name[4..]}>k__BackingField";
×
324

UNCOV
325
            var field = method.DeclaringType!.Fields.Find(f => f.Name == fieldName);
×
UNCOV
326
            if (field == null)
×
UNCOV
327
                return;
×
328

UNCOV
329
            instr.Operands[0] = new FieldReference(field, local, (int)memory.Addend);
×
330
        }
UNCOV
331
    }
×
332
}
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