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

SamboyCoding / Cpp2IL / 30857607998

03 Aug 2026 10:09PM UTC coverage: 32.946% (-1.4%) from 34.341%
30857607998

push

github

SamboyCoding
Decompiler: Handle arrays of structs, struct return buffers, interface method calls

2940 of 10661 branches covered (27.58%)

Branch coverage included in aggregate %.

20 of 467 new or added lines in 12 files covered. (4.28%)

8 existing lines in 6 files now uncovered.

5375 of 14577 relevant lines covered (36.87%)

151659.48 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.SetOperand(1, new StringLiteral(stringLiteral));
×
49
                continue;
×
50
            }
51

52
            // Type metadata usage (Il2CppType* / Il2CppClass*).
53
            if (method.DeclaringType is { } declaringType)
×
54
            {
55
                var typeGlobal = libContext.GetTypeGlobalByAddress(address);
×
56
                if (typeGlobal != null)
×
57
                {
58
                    instruction.SetOperand(1, declaringType.AppContext.ResolveIl2CppType(typeGlobal));
×
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.SetOperand(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
                // check if static field access
101
                var staticOwner = (local.Type as StaticFieldStorageTypeAnalysisContext)?.OwnerType;
×
102
                var owner = staticOwner ?? local.Type;
×
103
                var genericOwner = owner as GenericInstanceTypeAnalysisContext;
×
104

105
                FieldAnalysisContext? field;
106
                if (genericOwner != null && staticOwner == null)
×
107
                {
108
                    // metadata has all-0 offsets for generic definitions, so recompute layout
109
                    // TODO support user-defined value types
110
                    if (genericOwner.GenericArguments.Any(a => a.IsValueType))
×
111
                        continue;
112

113
                    field = GenericInstanceFieldLayout.FindFieldAtOffset(genericOwner, memory.Addend);
×
114
                }
115
                else
116
                {
117
                    // an inherited field exists on the base type but sits at the same offset in the
118
                    // derived layout, so the whole chain is searched
119
                    field = null;
×
120
                    for (var candidateOwner = genericOwner?.GenericType ?? owner; candidateOwner != null && field == null; candidateOwner = candidateOwner.BaseType)
×
121
                        field = candidateOwner.Fields.FirstOrDefault(f => f.IsStatic == (staticOwner != null) && f.BackingData?.FieldOffset == memory.Addend);
×
122
                }
123

124
                if (field == null) // TODO: Support nested fields (Field1.Field2.Field3)
×
125
                    continue;
126

127
                // make sure we have a full GIT for field access. open type is bad.
128
                if (genericOwner != null)
×
129
                    field = new ConcreteGenericFieldAnalysisContext(field, genericOwner);
×
130

131
                instruction.SetOperand(i, new FieldReference(field, local, (int)memory.Addend));
×
132
                changed = true;
×
133
            }
134
        }
135

136
        return changed;
×
137
    }
138

139
    private static void ResolveCalls(MethodAnalysisContext method)
140
    {
141
        foreach (var block in method.ControlFlowGraph!.Blocks)
×
142
        {
143
            if (block.BlockType != BlockType.Call && block.BlockType != BlockType.TailCall)
×
144
                continue;
145

146
            var callInstruction = block.Instructions[^1];
×
147
            if (callInstruction.Operands[0] is not Immediate dest)
×
148
                continue;
149

150
            var target = dest.UnsignedValue;
×
151

152
            var keyFunctionAddresses = method.AppContext.GetOrCreateKeyFunctionAddresses();
×
153

154
            if (keyFunctionAddresses.IsKeyFunctionAddress(target))
×
155
            {
156
                HandleKeyFunction(method.AppContext, callInstruction, target, keyFunctionAddresses);
×
157
                continue;
×
158
            }
159

160
            //Non-key function call. Try to find a single match
161
            if (!method.AppContext.MethodsByAddress.TryGetValue(target, out var targetMethods))
×
162
            {
163
                // Not a managed method at all. It may be one of the runtime helpers that exist purely to
164
                // throw, in which case restore the throw itself
165
                if (ThrowHelperRecovery.GetThrownException(method.AppContext, target) is { } thrown)
×
166
                {
167
                    callInstruction.OpCode = OpCode.Throw;
×
168
                    callInstruction.SetOperands(thrown);
×
169
                }
170

171
                continue;
×
172
            }
173

174
            // Duplicated/Shared method bodies are resolved later in ResolveCallsViaMethodInfo/ResolveAmbiguousCalls.
175
            if (targetMethods is not [{ } singleTargetMethod])
×
176
                continue;
177

178
            callInstruction.SetOperand(0, singleTargetMethod);
×
NEW
179
            X64CallingConventionResolver.RemapRawArguments(callInstruction, singleTargetMethod);
×
180
        }
181

182
        method.ControlFlowGraph.MergeCallBlocks();
×
183
    }
×
184

185
    /// <summary>
186
    /// Resolves calls whose address maps to more than one method by matching the receiver's known
187
    /// type against the candidates' declaring types. Runs inside the type/field fixpoint and so
188
    /// re-fires as receivers become typed - a resolved call types its return value, which can type
189
    /// the receiver of a further call. Returns whether any call was resolved this pass.
190
    ///
191
    /// Conservative by design: it commits only when exactly one non-static candidate's declaring
192
    /// type matches the receiver's type. Anything still untyped or ambiguous is left for a later
193
    /// pass, or left unresolved - it never guesses.
194
    /// </summary>
195
    public static bool ResolveAmbiguousCalls(MethodAnalysisContext method)
196
    {
197
        var changed = false;
×
198

199
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
200
        {
201
            if (!instruction.IsCall)
×
202
                continue;
203

204
            // A resolved call's target is a method/key-function name; only unresolved ones are still numeric.
205
            if (instruction.Operands[0] is not Immediate target)
×
206
                continue;
207

208
            if (!method.AppContext.MethodsByAddress.TryGetValue(target.UnsignedValue, out var candidates) || candidates.Count < 2)
×
209
                continue;
210

211
            // e.g. string.Equals and string.op_Equality, identical params, instance type, and bodies are shared
212
            // we can't differentiate which is being called but it doesn't matter
213
            if (AreInterchangeable(candidates))
×
214
            {
NEW
215
                var preferred = PreferredOf(candidates);
×
NEW
216
                instruction.SetOperand(0, preferred);
×
NEW
217
                X64CallingConventionResolver.RemapRawArguments(instruction, preferred);
×
218
                changed = true;
×
219
                continue;
×
220
            }
221

222
            if (GetReceiver(instruction) is not { Type: { } receiverType } receiver)
×
223
                continue;
224

225
            // Prefer picking base ctor if we are a ctor
226
            var callerIsCtor = method.Name == ".ctor" && receiver.IsThis;
×
227

228
            // Handle methods with shared bodies
229
            var match = default(MethodAnalysisContext);
×
230

231
            for (var type = receiverType; type != null && match == null; type = type.BaseType)
×
232
            {
233
                var matches = candidates.Where(c => !c.IsStatic && ReferenceEquals(c.DeclaringType, type)).ToList();
×
234

235
                if (matches.Count > 1 && callerIsCtor)
×
236
                    matches = matches.Where(c => c.Name == ".ctor").ToList();
×
237

238
                if (matches.Count > 1)
×
239
                    break;
240

241
                match = matches.SingleOrDefault();
×
242
            }
243

244
            if (match == null)
×
245
                continue;
246

247
            instruction.SetOperand(0, match);
×
NEW
248
            X64CallingConventionResolver.RemapRawArguments(instruction, match);
×
UNCOV
249
            changed = true;
×
250
        }
251

252
        return changed;
×
253
    }
254

255
    private static bool AreInterchangeable(List<MethodAnalysisContext> candidates)
256
    {
257
        var first = candidates[0];
×
258

259
        return candidates.All(c => c.IsStatic == first.IsStatic
×
260
            && ReferenceEquals(c.DeclaringType, first.DeclaringType)
×
261
            && ReferenceEquals(c.ReturnType, first.ReturnType)
×
262
            && c.Parameters.Count == first.Parameters.Count
×
263
            && SameParameterTypes(c, first));
×
264
    }
265

266
    private static bool SameParameterTypes(MethodAnalysisContext a, MethodAnalysisContext b)
267
    {
268
        for (var i = 0; i < a.Parameters.Count; i++)
×
269
        {
270
            if (!ReferenceEquals(a.Parameters[i].ParameterType, b.Parameters[i].ParameterType))
×
271
                return false;
×
272
        }
273

274
        return true;
×
275
    }
276

277
    // Prefer operators if possible
278
    private static MethodAnalysisContext PreferredOf(List<MethodAnalysisContext> candidates) =>
279
        candidates.FirstOrDefault(c => c.Name.StartsWith("op_")) ?? candidates[0];
×
280

281
    // The receiver ('this') of a call is the first integer-slot argument: operand 1 for CallVoid
282
    // (after the target), operand 2 for Call (after the target and the return value).
283
    private static LocalVariable? GetReceiver(Instruction call)
284
    {
285
        var index = call.OpCode == OpCode.CallVoid ? 1 : 2;
×
286
        return index < call.Operands.Count ? call.Operands[index] as LocalVariable : null;
×
287
    }
288

289
    /// <summary>
290
    /// Resolves any Call (theoretically should always be a CallVoid) target directly after a Newobj to a constructor call.
291
    /// </summary>
292
    public static bool ResolveConstructorCalls(MethodAnalysisContext method)
293
    {
294
        var definitions = new Dictionary<LocalVariable, Instruction>();
×
295
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
296
            if (instruction.Destination is LocalVariable definition)
×
297
                definitions[definition] = instruction;
×
298

299
        var changed = false;
×
300

301
        foreach (var instruction in method.ControlFlowGraph.Instructions)
×
302
        {
303
            if (!instruction.IsCall || instruction.Operands[0] is not Immediate callTarget)
×
304
                continue;
305

306
            if (!method.AppContext.MethodsByAddress.TryGetValue(callTarget.UnsignedValue, out var candidates))
×
307
                continue;
308

309
            if (GetReceiver(instruction) is not { } receiver || AllocatedType(receiver, definitions) is not { } allocatedType)
×
310
                continue;
311

312
            var constructor = candidates.FirstOrDefault(c => !c.IsStatic && c.Name == ".ctor" && ReferenceEquals(c.DeclaringType, allocatedType));
×
313
            if (constructor == null)
×
314
                continue;
315

316
            instruction.SetOperand(0, constructor);
×
NEW
317
            X64CallingConventionResolver.RemapRawArguments(instruction, constructor);
×
UNCOV
318
            changed = true;
×
319
        }
320

321
        return changed;
×
322
    }
323

324
    // Follow SSA copies from a local back to the Newobj that produced the value
325
    private static TypeAnalysisContext? AllocatedType(LocalVariable local, Dictionary<LocalVariable, Instruction> definitions)
326
    {
327
        var visited = new HashSet<LocalVariable>();
×
328

329
        while (visited.Add(local) && definitions.TryGetValue(local, out var definition))
×
330
        {
331
            switch (definition.OpCode)
×
332
            {
333
                case OpCode.Newobj:
334
                    return (definition.Operands[0] as LocalVariable)?.Type;
×
335
                case OpCode.Move when definition.Operands[1] is LocalVariable source:
×
336
                    local = source;
×
337
                    continue;
×
338
            }
339

340
            break;
341
        }
342

343
        return null;
×
344
    }
345

346
    /// <summary>
347
    /// Resolves calls whose address maps to more than one method by reading the runtime
348
    /// <c>MethodInfo*</c> the caller passes in, if there is one.
349
    /// </summary>
350
    public static bool ResolveCallsViaMethodInfo(MethodAnalysisContext method)
351
    {
352
        var changed = false;
×
353

354
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
355
        {
356
            if (!instruction.IsCall)
×
357
                continue;
358

359
            if (instruction.Operands[0] is not Immediate target)
×
360
                //Already resolved
361
                continue;
362

363
            if (GetMethodInfoArgument(instruction) is not { RepresentedMethod: { } representedMethod })
×
364
                //No MethodInfo to work with
365
                continue;
366

367
            if (!method.AppContext.MethodsByAddress.TryGetValue(target.UnsignedValue, out var candidates))
×
368
            {
369
                // Some shared generic bodies aren't in the address map at all (todo investigate?).
370
                // Il2cpp still passes the concrete MethodInfo as the hidden final parameter, so we can use a methodof there if we have one.
371
                var firstArg = instruction.OpCode == OpCode.CallVoid ? 1 : 2;
×
NEW
372
                var hiddenParamIndex = firstArg
×
NEW
373
                    + (X64CallingConventionResolver.ReturnsViaHiddenBuffer(representedMethod) ? 1 : 0)
×
NEW
374
                    + (representedMethod.IsStatic ? 0 : 1) + representedMethod.Parameters.Count;
×
375

376
                if (hiddenParamIndex >= instruction.Operands.Count
×
377
                    || AsMethodInfo(instruction.Operands[hiddenParamIndex]) == null)
×
378
                    continue;
379

380
                instruction.SetOperand(0, representedMethod);
×
NEW
381
                X64CallingConventionResolver.RemapRawArguments(instruction, representedMethod);
×
382
                changed = true;
×
383
                continue;
×
384
            }
385

386
            if (candidates.Count < 2)
×
387
                continue;
388

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

394
            instruction.SetOperand(0, representedMethod);
×
NEW
395
            X64CallingConventionResolver.RemapRawArguments(instruction, representedMethod);
×
UNCOV
396
            changed = true;
×
397
        }
398

399
        return changed;
×
400
    }
401

402
    // Offset of Il2CppClass::vtable, VirtualInvokeData entries of {methodPtr, MethodInfo*}.
403
    // TODO this is almost certainly not correct on every version
404
    private const long VTableOffset64 = 0x138;
405
    private const long VTableOffset32 = 0xC0;
406
    
407
    // Resolves virtual dispatch through <c>[klass + vtableOffset + slot * sizeof(VirtualInvokeData)]</c>
408
    // as long as the klass local's represented type is known.
409
    public static bool ResolveVirtualCalls(MethodAnalysisContext method)
410
    {
411
        var pointerSize = method.AppContext.Binary.PointerSizeBytes;
×
412
        var vtableOffset = pointerSize == 8 ? VTableOffset64 : VTableOffset32;
×
413
        var invokeDataSize = 2L * pointerSize;
×
414
        var changed = false;
×
415

416
        var loads = new Dictionary<LocalVariable, MemoryOperand>();
×
417
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
418
        {
419
            if (instruction.OpCode == OpCode.Move
×
420
                && instruction.Operands[0] is LocalVariable destination
×
421
                && instruction.Operands[1] is MemoryOperand { Index: null, Scale: 0 } load)
×
422
                loads[destination] = load;
×
423
        }
424

425
        foreach (var instruction in method.ControlFlowGraph.Instructions)
×
426
        {
427
            if (instruction.OpCode != OpCode.IndirectCall)
×
428
                continue;
429

430
            if (SlotLoad(instruction.Operands[0]) is not { } target
×
431
                || target.Base is not LocalVariable { Type: RuntimeClassTypeAnalysisContext { RepresentedType: { } receiverType } } klassLocal)
×
432
                continue;
433

434
            var offset = target.Addend - vtableOffset;
×
435
            if (offset < 0 || offset % invokeDataSize != 0)
×
436
                continue;
437

438
            var slot = (int)(offset / invokeDataSize);
×
439
            if (ResolveVTableSlot(method.AppContext, receiverType, slot) is not { } resolved)
×
440
                continue;
441

442
            var assembly = resolved.DeclaringType?.DeclaringAssembly ?? method.DeclaringType?.DeclaringAssembly;
×
443

NEW
444
            instruction.OpCode = OpCode.Call; // same operand layout as IndirectCall, and we've resolved it now
×
NEW
445
            instruction.SetOperand(0, resolved);
×
NEW
446
            X64CallingConventionResolver.RemapRawArguments(instruction, resolved);
×
447

448
            // the MethodInfo field is also the same method, name it, for cleanliness and so it can
449
            // serve as a hidden final parameter if needed
450
            for (var i = 1; i < instruction.Operands.Count && assembly != null; i++)
×
451
            {
452
                if (SlotLoad(instruction.Operands[i]) is { } methodInfoLoad
×
453
                    && ReferenceEquals(methodInfoLoad.Base, klassLocal)
×
454
                    && methodInfoLoad.Addend == target.Addend + pointerSize)
×
455
                    instruction.SetOperand(i, new RuntimeMethodInfoAnalysisContext(resolved, assembly));
×
456
            }
457

458
            changed = true;
×
459
        }
460

461
        return changed;
×
462

463
        MemoryOperand? SlotLoad(IOperand operand) => operand switch
×
464
        {
×
465
            MemoryOperand { Index: null, Scale: 0 } inlined => inlined,
×
466
            LocalVariable local when loads.TryGetValue(local, out var load) => load,
×
467
            _ => null
×
468
        };
×
469
    }
470

471
    private static MethodAnalysisContext? ResolveVTableSlot(ApplicationAnalysisContext appContext, TypeAnalysisContext type, int slot)
472
    {
473
        var definition = (type as GenericInstanceTypeAnalysisContext)?.GenericType.Definition ?? type.Definition;
×
474

475
        if (definition == null || slot >= definition.VtableCount)
×
476
            return null;
×
477

478
        if (appContext.ResolveContextForMethod(definition.VTable[slot]) is { } implementation)
×
479
            return implementation;
×
480

481
        // an abstract method has no implementation, try to resolve it
482
        for (var declarer = type; declarer != null; declarer = declarer.BaseType)
×
483
        {
484
            if (declarer.Methods.FirstOrDefault(m => m.Definition?.slot == slot) is { } declaration)
×
485
                return declaration;
×
486
        }
487

488
        return null;
×
489
    }
490

491
    private static MethodAnalysisContext BaseMethodOf(MethodAnalysisContext method) =>
492
        method is ConcreteGenericMethodAnalysisContext { BaseMethodContext: { } baseMethod } ? baseMethod : method;
×
493

494
    private static RuntimeMethodInfoAnalysisContext? GetMethodInfoArgument(Instruction call)
495
    {
496
        var firstArg = call.OpCode == OpCode.CallVoid ? 1 : 2;
×
497

498
        for (var i = call.Operands.Count - 1; i >= firstArg; i--)
×
499
        {
500
            if (AsMethodInfo(call.Operands[i]) is { } methodInfo)
×
501
                return methodInfo;
×
502
        }
503

504
        return null;
×
505
    }
506

507
    private static RuntimeMethodInfoAnalysisContext? AsMethodInfo(IOperand operand) =>
508
        operand switch
×
509
        {
×
510
            RuntimeMethodInfoAnalysisContext methodInfo => methodInfo,
×
511
            LocalVariable { Type: RuntimeMethodInfoAnalysisContext methodInfoLocal } => methodInfoLocal,
×
512
            _ => null
×
513
        };
×
514

515
    private static void HandleKeyFunction(ApplicationAnalysisContext appContext, Instruction instruction, ulong target, BaseKeyFunctionAddresses kFA)
516
    {
517
        var method = "";
×
518
        if (target == kFA.il2cpp_codegen_initialize_method || target == kFA.il2cpp_codegen_initialize_runtime_metadata)
×
519
        {
520
            if (appContext.MetadataVersion < 27)
×
521
            {
522
                method = nameof(kFA.il2cpp_codegen_initialize_method);
×
523
            }
524
            else
525
            {
526
                method = nameof(kFA.il2cpp_codegen_initialize_runtime_metadata);
×
527
            }
528
        }
529
        else
530
        {
531
            var pairs = kFA.Pairs.ToList();
×
532
            var key = pairs.FirstOrDefault(pair => pair.Value == target).Key;
×
533
            if (key == null)
×
534
                return;
×
535
            method = key;
×
536
        }
537

538
        if (method != "")
×
539
        {
540
            instruction.SetOperand(0, new StringLiteral(method));
×
541
        }
542
    }
×
543

544
    // Because of il2cpp fields (like cctor_finished_or_no_cctor) [local @ reg+offset] sometimes can't be resolved, but this works for now
545
    private static void ResolveGetter(MethodAnalysisContext method)
546
    {
547
        if (!method.Name.StartsWith("get_"))
×
548
            return;
×
549

550
        // Default get: Return [this @ reg+offset]
551
        var instructions = method.ControlFlowGraph!.Instructions;
×
552
        if (instructions.Count == 1)
×
553
        {
554
            var instr = instructions[0];
×
555

556
            if (instr.OpCode != OpCode.Return
×
557
                || instr.Operands.Count < 1
×
558
                || instr.Operands[0] is not MemoryOperand memory
×
559
                || memory.Index != null || memory.Scale != 0
×
560
                || memory.Base is not LocalVariable local)
×
561
                return;
×
562

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

565
            var field = method.DeclaringType!.Fields.Find(f => f.Name == fieldName);
×
566
            if (field == null)
×
567
                return;
×
568

569
            instr.SetOperand(0, new FieldReference(field, local, (int)memory.Addend));
×
570
        }
571
    }
×
572
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc