• 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/InterfaceDispatchRecovery.cs
1
using System.Collections.Generic;
2
using System.Linq;
3
using Cpp2IL.Core.Graphs;
4
using Cpp2IL.Core.ISIL;
5
using Cpp2IL.Core.Model.Contexts;
6
using Cpp2IL.Core.Utils;
7

8
namespace Cpp2IL.Core.Analysis;
9

10
// Recovers interface calls from GetInterfaceInvokeData which is usually inlined. that method scans klass->interfaceOffsets
11
// for the declaring interface, indexes the vtable with (entryOffset + slot), or falls back to a slow path
12
// helper when the scan fails.
13
public static class InterfaceDispatchRecovery
14
{
15
    public static void Run(MethodAnalysisContext method)
16
    {
17
        // offsets below are the 64-bit Il2CppClass layout
NEW
18
        if (method.AppContext.Binary.PointerSizeBytes != 8)
×
NEW
19
            return;
×
20

NEW
21
        var cfg = method.ControlFlowGraph!;
×
22

NEW
23
        var definitions = new Dictionary<LocalVariable, Instruction>();
×
NEW
24
        var homeBlock = new Dictionary<Instruction, Block>();
×
25

NEW
26
        foreach (var block in cfg.Blocks)
×
27
        {
NEW
28
            foreach (var instruction in block.Instructions)
×
29
            {
NEW
30
                homeBlock[instruction] = block;
×
NEW
31
                if (instruction.Destination is LocalVariable destination)
×
NEW
32
                    definitions[destination] = instruction;
×
33
            }
34
        }
35

NEW
36
        var changed = false;
×
37

NEW
38
        foreach (var block in cfg.Blocks.ToList())
×
39
        {
NEW
40
            foreach (var instruction in block.Instructions.ToList())
×
41
            {
NEW
42
                if (instruction.OpCode is not (OpCode.IndirectCall or OpCode.IndirectJump))
×
43
                    continue;
44

NEW
45
                if (MatchDispatch(method, instruction, definitions, homeBlock) is not { } match)
×
46
                    continue;
47

NEW
48
                RewriteDispatch(method, instruction, block, match, definitions);
×
NEW
49
                TryExciseLookup(cfg, match, definitions, homeBlock);
×
NEW
50
                changed = true;
×
51
            }
52
        }
53

NEW
54
        if (changed)
×
NEW
55
            DeadCodeEliminator.Run(method);
×
NEW
56
    }
×
57

58
    private const long VTableOffset = 0x138;
59
    private const int InvokeDataShift = 4; // sizeof(VirtualInvokeData) == 16
60

61
    private record struct Match(
NEW
62
        MethodAnalysisContext Resolved,
×
NEW
63
        Instruction InvokeDataPhi,
×
NEW
64
        Block Merge,
×
NEW
65
        Instruction SlowCall,
×
NEW
66
        LocalVariable KlassLocal);
×
67

68
    private static Match? MatchDispatch(MethodAnalysisContext method, Instruction dispatch, Dictionary<LocalVariable, Instruction> definitions, Dictionary<Instruction, Block> homeBlock)
69
    {
70
        // the call target loads VirtualInvokeData::methodPtr, separately or folded in
NEW
71
        var targetLoad = dispatch.Operands[0] switch
×
NEW
72
        {
×
NEW
73
            MemoryOperand folded => folded,
×
NEW
74
            LocalVariable target when Definition(definitions, target) is { OpCode: OpCode.Move, Operands: [_, MemoryOperand loaded] } => loaded,
×
NEW
75
            _ => default(MemoryOperand?)
×
NEW
76
        };
×
77

NEW
78
        if (targetLoad is not { Index: null, Scale: 0, Addend: 0, Base: LocalVariable invokeData })
×
NEW
79
            return null;
×
80

NEW
81
        if (Definition(definitions, invokeData) is not { OpCode: OpCode.Phi, Operands: [_, LocalVariable first, LocalVariable second] } phi)
×
NEW
82
            return null;
×
83

NEW
84
        var firstDefinition = Definition(definitions, first);
×
NEW
85
        var secondDefinition = Definition(definitions, second);
×
86

NEW
87
        var slowCall = firstDefinition is { OpCode: OpCode.Call } 
×
NEW
88
            ? firstDefinition
×
NEW
89
            : secondDefinition is { OpCode: OpCode.Call } ? secondDefinition : null;
×
NEW
90
        var vtableEntry = ReferenceEquals(slowCall, firstDefinition) ? secondDefinition : firstDefinition;
×
91

92
        // slow path is GetInterfaceInvokeDataFromVTableSlowPath(obj, interface, slot), never resolved
NEW
93
        if (slowCall is not { Operands: [Immediate, _, _, LocalVariable interfaceArg, LocalVariable slotArg, ..] })
×
NEW
94
            return null;
×
95

NEW
96
        if (ChaseCopies(definitions, interfaceArg) is not { OpCode: OpCode.Move, Operands: [_, TypeAnalysisContext declaringInterface] })
×
NEW
97
            return null;
×
98

NEW
99
        if (declaringInterface is RuntimeMethodInfoAnalysisContext
×
NEW
100
            || !(declaringInterface is GenericInstanceTypeAnalysisContext { GenericType.IsInterface: true } || declaringInterface.IsInterface))
×
NEW
101
            return null;
×
102

NEW
103
        if (ChaseCopies(definitions, slotArg) is not { OpCode: OpCode.Move, Operands: [_, Immediate slotImmediate] }
×
NEW
104
            || slotImmediate.Value is < 0 or > ushort.MaxValue)
×
NEW
105
            return null;
×
106

NEW
107
        var slot = (int)slotImmediate.Value;
×
108

109
        // fast path computes klass + vtableOffset + ((entryOffset + slot) << 4) (the +slot folds away for slot 0)
NEW
110
        if (MatchVTableEntryChain(definitions, vtableEntry, slot) is not { } klassLocal)
×
NEW
111
            return null;
×
112

NEW
113
        if (ResolveInterfaceSlot(declaringInterface, slot) is not { } resolved)
×
NEW
114
            return null;
×
115

NEW
116
        if (!homeBlock.TryGetValue(phi, out var merge))
×
NEW
117
            return null;
×
118

NEW
119
        return new Match(resolved, phi, merge, slowCall, klassLocal);
×
120
    }
121

122
    private static LocalVariable? MatchVTableEntryChain(Dictionary<LocalVariable, Instruction> definitions, Instruction? vtableEntry, int slot)
123
    {
NEW
124
        if (vtableEntry is not { OpCode: OpCode.Add, Operands: [_, LocalVariable addLeft, LocalVariable addRight] })
×
NEW
125
            return null;
×
126

NEW
127
        var (klassCandidate, sum) = Definition(definitions, addRight) is { OpCode: OpCode.Move, Operands: [_, MemoryOperand { Index: null, Scale: 0, Addend: 0 }] }
×
NEW
128
            ? (addRight, addLeft)
×
NEW
129
            : (addLeft, addRight);
×
130

NEW
131
        if (Definition(definitions, klassCandidate) is not { OpCode: OpCode.Move, Operands: [_, MemoryOperand { Index: null, Scale: 0, Addend: 0, Base: LocalVariable }] })
×
NEW
132
            return null;
×
133

NEW
134
        if (ChaseCopies(definitions, sum) is not { OpCode: OpCode.Add, Operands: [_, LocalVariable shifted, Immediate { Value: VTableOffset }] })
×
NEW
135
            return null;
×
136

NEW
137
        if (ChaseCopies(definitions, shifted) is not { OpCode: OpCode.ShiftLeft, Operands: [_, LocalVariable index, Immediate { Value: InvokeDataShift }] })
×
NEW
138
            return null;
×
139

NEW
140
        var entryOffset = ChaseCopies(definitions, index);
×
141

NEW
142
        if (entryOffset is { OpCode: OpCode.Add, Operands: [_, LocalVariable beforeSlot, Immediate slotAddend] })
×
143
        {
NEW
144
            if (slotAddend.Value != slot)
×
NEW
145
                return null;
×
146

NEW
147
            entryOffset = ChaseCopies(definitions, beforeSlot);
×
148
        }
NEW
149
        else if (slot != 0)
×
NEW
150
            return null;
×
151

NEW
152
        if (entryOffset is not { OpCode: OpCode.Move, Operands: [_, MemoryOperand { Base: not null }] })
×
NEW
153
            return null;
×
154

NEW
155
        return klassCandidate;
×
156
    }
157

158
    private static Instruction? Definition(Dictionary<LocalVariable, Instruction> definitions, LocalVariable local)
NEW
159
        => definitions.TryGetValue(local, out var definition) ? definition : null;
×
160

161
    private static Instruction? ChaseCopies(Dictionary<LocalVariable, Instruction> definitions, LocalVariable local)
162
    {
NEW
163
        var visited = new HashSet<LocalVariable>();
×
164

NEW
165
        while (visited.Add(local))
×
166
        {
NEW
167
            if (Definition(definitions, local) is not { } definition)
×
NEW
168
                return null;
×
169

NEW
170
            if (definition is { OpCode: OpCode.Move, Operands: [_, LocalVariable source] })
×
171
            {
NEW
172
                local = source;
×
NEW
173
                continue;
×
174
            }
175

NEW
176
            return definition;
×
177
        }
178

NEW
179
        return null;
×
180
    }
181

182
    private static MethodAnalysisContext? ResolveInterfaceSlot(TypeAnalysisContext declaringInterface, int slot)
183
    {
NEW
184
        if (declaringInterface is GenericInstanceTypeAnalysisContext genericInstance)
×
185
        {
NEW
186
            var baseMethod = genericInstance.GenericType.Methods.FirstOrDefault(m => m.Definition?.slot == slot);
×
NEW
187
            return baseMethod == null ? null : new ConcreteGenericMethodAnalysisContext(baseMethod, genericInstance.GenericArguments, []);
×
188
        }
189

NEW
190
        return declaringInterface.Methods.FirstOrDefault(m => m.Definition?.slot == slot);
×
191
    }
192

193
    private static void RewriteDispatch(MethodAnalysisContext method, Instruction dispatch, Block block, Match match, Dictionary<LocalVariable, Instruction> definitions)
194
    {
NEW
195
        var resolved = match.Resolved;
×
NEW
196
        var isTailCall = dispatch.OpCode == OpCode.IndirectJump;
×
197

198
        // an IndirectJump's rax operand is a stale use rather than a return slot, so rebuild from scratch
NEW
199
        if (isTailCall)
×
200
        {
NEW
201
            var operands = new List<IOperand> { resolved };
×
202

NEW
203
            if (!resolved.IsVoid)
×
NEW
204
                operands.Add(new LocalVariable("interfaceTailCallResult", new Register(null, "rax")));
×
205

NEW
206
            operands.AddRange(dispatch.Operands.Skip(2));
×
NEW
207
            dispatch.SetOperands(operands);
×
208
        }
209
        else
210
        {
NEW
211
            if (resolved.IsVoid)
×
NEW
212
                dispatch.RemoveOperandAt(1);
×
213

NEW
214
            dispatch.SetOperand(0, resolved);
×
215
        }
216

NEW
217
        dispatch.OpCode = resolved.IsVoid ? OpCode.CallVoid : OpCode.Call;
×
NEW
218
        X64CallingConventionResolver.RemapRawArguments(dispatch, resolved);
×
219

220
        // name [phi+8] as the hidden MethodInfo param, like ResolveVirtualCalls. A tail call's target
221
        // register doubles as an argument slot, so a stale [phi] load can turn up as an argument too,
222
        // and gets a placeholder so the VirtualInvokeData pointer still dies.
NEW
223
        var assembly = resolved.DeclaringType?.DeclaringAssembly ?? method.DeclaringType?.DeclaringAssembly;
×
NEW
224
        for (var i = 1; i < dispatch.Operands.Count; i++)
×
225
        {
NEW
226
            if (dispatch.Operands[i] is not LocalVariable argument
×
NEW
227
                || Definition(definitions, argument) is not { OpCode: OpCode.Move, Operands: [_, MemoryOperand { Index: null, Scale: 0, Base: LocalVariable loadBase } load] }
×
NEW
228
                || !ReferenceEquals(Definition(definitions, loadBase), match.InvokeDataPhi))
×
229
                continue;
230

NEW
231
            if (load.Addend == 8 && assembly != null)
×
NEW
232
                dispatch.SetOperand(i, new RuntimeMethodInfoAnalysisContext(resolved, assembly));
×
NEW
233
            else if (load.Addend == 0)
×
NEW
234
                dispatch.SetOperand(i, new Immediate(0));
×
235
        }
236

NEW
237
        if (isTailCall)
×
238
        {
NEW
239
            var returnOperands = !method.IsVoid && !resolved.IsVoid
×
NEW
240
                ? new List<IOperand> { dispatch.Operands[1] }
×
NEW
241
                : [];
×
242

NEW
243
            block.AddInstruction(new Instruction(-1, OpCode.Return, returnOperands));
×
NEW
244
            block.CalculateBlockType();
×
245
        }
NEW
246
    }
×
247

248
    // Bailing here is fine, it just leaves the (already resolved) call with dead lookup around it
249
    private static void TryExciseLookup(ISILControlFlowGraph cfg, Match match, Dictionary<LocalVariable, Instruction> definitions, Dictionary<Instruction, Block> homeBlock)
250
    {
NEW
251
        var merge = match.Merge;
×
252

NEW
253
        if (!homeBlock.TryGetValue(match.SlowCall, out var slowBlock))
×
NEW
254
            return;
×
255

NEW
256
        if (Definition(definitions, match.KlassLocal) is not { } klassDefinition
×
NEW
257
            || !homeBlock.TryGetValue(klassDefinition, out var head) || head == merge)
×
NEW
258
            return;
×
259

NEW
260
        if (!TryCollectRegion(cfg, head, merge, out var region) || !region.Contains(slowBlock))
×
NEW
261
            return;
×
262

NEW
263
        if (!RegionIsSideEffectFree(region, match.SlowCall) || AnyValueEscapes(cfg, region, merge))
×
NEW
264
            return;
×
265

NEW
266
        if (!MergePhisAreDead(cfg, merge, out var removable))
×
NEW
267
            return;
×
268

NEW
269
        foreach (var instruction in removable)
×
270
        {
NEW
271
            instruction.OpCode = OpCode.Nop;
×
NEW
272
            instruction.SetOperands();
×
273
        }
274

NEW
275
        foreach (var successor in head.Successors)
×
NEW
276
            successor.Predecessors.Remove(head);
×
NEW
277
        head.Successors.Clear();
×
NEW
278
        head.Successors.Add(merge);
×
279

NEW
280
        var terminator = head.Instructions[^1];
×
NEW
281
        if (terminator.OpCode is OpCode.Jump or OpCode.ConditionalJump)
×
282
        {
NEW
283
            terminator.OpCode = OpCode.Jump;
×
NEW
284
            terminator.SetOperands(merge);
×
285
        }
286
        else
NEW
287
            head.AddInstruction(new Instruction(-1, OpCode.Jump, merge));
×
288

NEW
289
        head.CalculateBlockType();
×
290

NEW
291
        merge.Predecessors.RemoveAll(region.Contains);
×
NEW
292
        merge.Predecessors.Add(head);
×
293

NEW
294
        foreach (var block in region)
×
295
        {
NEW
296
            foreach (var successor in block.Successors)
×
NEW
297
                successor.Predecessors.Remove(block);
×
NEW
298
            foreach (var predecessor in block.Predecessors)
×
NEW
299
                predecessor.Successors.Remove(block);
×
300

NEW
301
            block.Successors.Clear();
×
NEW
302
            block.Predecessors.Clear();
×
NEW
303
            cfg.Blocks.Remove(block);
×
304
        }
NEW
305
    }
×
306

307
    // The region has to be closed, so nothing else may enter or leave it
308
    private static bool TryCollectRegion(ISILControlFlowGraph cfg, Block head, Block merge, out HashSet<Block> region)
309
    {
NEW
310
        region = [];
×
311

NEW
312
        var queue = new Queue<Block>(merge.Predecessors);
×
313

NEW
314
        while (queue.Count > 0)
×
315
        {
NEW
316
            var block = queue.Dequeue();
×
317

NEW
318
            if (block == head)
×
319
                continue;
320

NEW
321
            if (block == merge || block == cfg.EntryBlock || block == cfg.ExitBlock || region.Count > 64)
×
NEW
322
                return false;
×
323

NEW
324
            if (!region.Add(block))
×
325
                continue;
326

NEW
327
            foreach (var predecessor in block.Predecessors)
×
NEW
328
                queue.Enqueue(predecessor);
×
329
        }
330

NEW
331
        if (region.Count == 0)
×
NEW
332
            return false;
×
333

NEW
334
        var collected = region;
×
NEW
335
        foreach (var block in collected)
×
336
        {
NEW
337
            if (block.Predecessors.Any(p => p != head && !collected.Contains(p)))
×
NEW
338
                return false;
×
NEW
339
            if (block.Successors.Any(s => s != merge && !collected.Contains(s)))
×
NEW
340
                return false;
×
341
        }
342

343
        // we rewrite the head's terminator, so it can't branch anywhere else
NEW
344
        return head.Successors.All(s => s == merge || collected.Contains(s));
×
NEW
345
    }
×
346

347
    private static bool RegionIsSideEffectFree(HashSet<Block> region, Instruction slowCall)
348
    {
NEW
349
        foreach (var block in region)
×
350
        {
NEW
351
            foreach (var instruction in block.Instructions)
×
352
            {
NEW
353
                if (ReferenceEquals(instruction, slowCall))
×
354
                    continue;
355

NEW
356
                var harmless = instruction.OpCode switch
×
NEW
357
                {
×
NEW
358
                    OpCode.Nop or OpCode.Jump or OpCode.ConditionalJump or OpCode.Phi => true,
×
NEW
359
                    OpCode.Move or OpCode.Add or OpCode.Subtract or OpCode.Multiply or OpCode.Divide
×
NEW
360
                        or OpCode.ShiftLeft or OpCode.ShiftRight or OpCode.And or OpCode.Or or OpCode.Xor
×
NEW
361
                        or OpCode.Not or OpCode.Negate
×
NEW
362
                        or (>= OpCode.CheckEqual and <= OpCode.CheckLessOrEqual)
×
NEW
363
                        => instruction.Destination is LocalVariable,
×
NEW
364
                    _ => false,
×
NEW
365
                };
×
366

NEW
367
                if (!harmless)
×
NEW
368
                    return false;
×
369
            }
370
        }
371

NEW
372
        return true;
×
NEW
373
    }
×
374

375
    // Merge phis are exempt, their deadness gets checked separately
376
    private static bool AnyValueEscapes(ISILControlFlowGraph cfg, HashSet<Block> region, Block merge)
377
    {
NEW
378
        var regionDefs = new HashSet<LocalVariable>();
×
NEW
379
        foreach (var block in region)
×
NEW
380
            foreach (var instruction in block.Instructions)
×
NEW
381
                if (instruction.Destination is LocalVariable destination)
×
NEW
382
                    regionDefs.Add(destination);
×
383

NEW
384
        foreach (var block in cfg.Blocks)
×
385
        {
NEW
386
            if (region.Contains(block))
×
387
                continue;
388

NEW
389
            foreach (var instruction in block.Instructions)
×
390
            {
NEW
391
                if (instruction.OpCode == OpCode.Phi && block == merge)
×
392
                    continue;
393

NEW
394
                if (Uses(instruction, regionDefs))
×
NEW
395
                    return true;
×
396
            }
397
        }
398

NEW
399
        return false;
×
NEW
400
    }
×
401

402
    // They may only feed loads off the VirtualInvokeData pointer, which must themselves be dead
403
    private static bool MergePhisAreDead(ISILControlFlowGraph cfg, Block merge, out List<Instruction> removable)
404
    {
NEW
405
        removable = [];
×
406

NEW
407
        var useSites = new Dictionary<LocalVariable, List<Instruction>>();
×
NEW
408
        foreach (var block in cfg.Blocks)
×
409
        {
NEW
410
            foreach (var instruction in block.Instructions)
×
411
            {
NEW
412
                foreach (var used in UsedLocals(instruction))
×
413
                {
NEW
414
                    if (!useSites.TryGetValue(used, out var sites))
×
NEW
415
                        useSites[used] = sites = [];
×
NEW
416
                    sites.Add(instruction);
×
417
                }
418
            }
419
        }
420

NEW
421
        foreach (var phi in merge.Instructions)
×
422
        {
NEW
423
            if (phi.OpCode != OpCode.Phi)
×
424
                continue;
425

NEW
426
            if (phi.Operands[0] is not LocalVariable phiDest)
×
NEW
427
                return false;
×
428

NEW
429
            foreach (var use in useSites.TryGetValue(phiDest, out var phiUses) ? phiUses : [])
×
430
            {
NEW
431
                if (use is not { OpCode: OpCode.Move, Operands: [LocalVariable loaded, MemoryOperand] }
×
NEW
432
                    || (useSites.TryGetValue(loaded, out var loadUses) && loadUses.Count > 0))
×
NEW
433
                    return false;
×
434

NEW
435
                removable.Add(use);
×
436
            }
437

NEW
438
            removable.Add(phi);
×
439
        }
440

NEW
441
        return true;
×
NEW
442
    }
×
443

444
    private static bool Uses(Instruction instruction, HashSet<LocalVariable> candidates)
NEW
445
        => UsedLocals(instruction).Any(candidates.Contains);
×
446

447
    private static IEnumerable<LocalVariable> UsedLocals(Instruction instruction)
448
    {
NEW
449
        for (var i = 0; i < instruction.Operands.Count; i++)
×
450
        {
NEW
451
            if (ReferenceEquals(instruction.Operands[i], instruction.Destination))
×
452
                continue;
453

NEW
454
            switch (instruction.Operands[i])
×
455
            {
456
                case LocalVariable local:
NEW
457
                    yield return local;
×
NEW
458
                    break;
×
459
                case AddressOf { Target: LocalVariable addressed }:
NEW
460
                    yield return addressed;
×
NEW
461
                    break;
×
462
                case MemoryOperand memory:
NEW
463
                    if (memory.Base is LocalVariable baseLocal)
×
NEW
464
                        yield return baseLocal;
×
NEW
465
                    if (memory.Index is LocalVariable indexLocal)
×
NEW
466
                        yield return indexLocal;
×
467
                    break;
468
            }
469
        }
NEW
470
    }
×
471
}
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