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

9
namespace Cpp2IL.Core.Analysis;
10

11
// Turns the raw Il2CppArray layout (header, then length, then inline elements) back into array ops
12
public static class ArrayRecovery
13
{
14
    private const string SzArrayNew = "SzArrayNew";
15

16
    // Il2CppArray is {Il2CppObject obj; void* bounds; il2cpp_array_size_t max_length;} then the elements, on all versions(?)
17
    private static long LengthOffset(int pointerSize) => 3L * pointerSize;
×
18
    private static long ElementsOffset(int pointerSize) => 4L * pointerSize;
×
19

20
    public static void Run(MethodAnalysisContext method)
21
    {
22
        RecoverAccesses(method);
×
NEW
23
        RecoverStructElementAddresses(method);
×
24
        GroupInitialisers(method.ControlFlowGraph!);
×
25
    }
×
26

27
    private static void RecoverAccesses(MethodAnalysisContext method)
28
    {
29
        var pointerSize = method.AppContext.Binary.PointerSizeBytes;
×
30

31
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
32
        {
33
            RecoverAllocation(instruction);
×
34

35
            for (var i = 0; i < instruction.Operands.Count; i++)
×
36
            {
37
                if (instruction.Operands[i] is not MemoryOperand memory
×
38
                    || memory.Base is not LocalVariable { Type: SzArrayTypeAnalysisContext arrayType } array)
×
39
                    continue;
40

41
                if (memory.Index == null && memory.Scale == 0 && memory.Addend == LengthOffset(pointerSize))
×
42
                {
43
                    instruction.SetOperand(i, new ArrayLength(array));
×
44
                    continue;
×
45
                }
46

47
                if (ElementIndex(memory, arrayType, pointerSize) is { } index)
×
48
                    instruction.SetOperand(i, new ArrayAccess(array, index));
×
49
            }
50
        }
51
    }
×
52

53
    // Group initializers after an array allocation together so ILSpy decompiles them better
54
    private static void GroupInitialisers(ISILControlFlowGraph cfg)
55
    {
56
        var movedAny = false;
×
57

58
        foreach (var block in cfg.Blocks.ToList())
×
59
        {
60
            foreach (var allocation in block.Instructions.ToList())
×
61
            {
62
                if (allocation.OpCode != OpCode.NewArr || allocation.Operands[0] is not LocalVariable array)
×
63
                    continue;
64

65
                var stores = new List<(Block Block, Instruction Instruction)>();
×
66
                var current = block;
×
67
                var index = current.Instructions.IndexOf(allocation) + 1;
×
68

69
                while (true)
70
                {
71
                    if (index >= current.Instructions.Count)
×
72
                    {
73
                        // only a straight-line run can be regrouped without changing what runs when
74
                        if (current.Successors.Count != 1 || current.Successors[0].Predecessors.Count != 1)
×
75
                            break;
76

77
                        current = current.Successors[0];
×
78
                        index = 0;
×
79
                        continue;
×
80
                    }
81

82
                    var instruction = current.Instructions[index];
×
83

84
                    if (IsElementStore(instruction, array))
×
85
                    {
86
                        stores.Add((current, instruction));
×
87
                        index++;
×
88
                        continue;
×
89
                    }
90

91
                    if (!ReadsArray(instruction, array))
×
92
                    {
93
                        index++;
×
94
                        continue;
×
95
                    }
96

97
                    // Found the first read. Move the allocation and its stores immediately in front, so the whole array is built in one chain with the elements already computed.
98
                    if (stores.Count > 1)
×
99
                    {
100
                        foreach (var (storeBlock, store) in stores)
×
101
                            storeBlock.Instructions.Remove(store);
×
102

103
                        block.Instructions.Remove(allocation);
×
104

105
                        var moved = new List<Instruction> { allocation };
×
106
                        moved.AddRange(stores.Select(s => s.Instruction));
×
107

108
                        current.Instructions.InsertRange(current.Instructions.IndexOf(instruction), moved);
×
109
                        movedAny = true;
×
110
                    }
111

112
                    break;
113
                }
114
            }
115
        }
116

117
        // Emptying a block out entirely leaves branches pointing at nothing to jump to
118
        if (movedAny)
×
119
            cfg.RemoveEmptyBlocks();
×
120
    }
×
121

122
    private static bool IsElementStore(Instruction instruction, LocalVariable array) =>
123
        instruction.OpCode == OpCode.Move && instruction.Operands[0] is ArrayAccess { Index: Immediate } stored
×
124
                                          && ReferenceEquals(stored.Array, array)
×
125
                                          && !ReadsArray(instruction, array);
×
126

127
    private static bool ReadsArray(Instruction instruction, LocalVariable array)
128
    {
129
        for (var i = 0; i < instruction.Operands.Count; i++)
×
130
        {
131
            if (i == 0 && instruction.OpCode == OpCode.Move)
×
132
                continue;
133

134
            var reads = instruction.Operands[i] switch
×
135
            {
×
136
                LocalVariable local => ReferenceEquals(local, array),
×
137
                ArrayAccess access => ReferenceEquals(access.Array, array),
×
138
                ArrayLength length => ReferenceEquals(length.Array, array),
×
139
                MemoryOperand memory => ReferenceEquals(memory.Base, array) || ReferenceEquals(memory.Index, array),
×
140
                AddressOf { Target: LocalVariable addressed } => ReferenceEquals(addressed, array),
×
141
                _ => false
×
142
            };
×
143

144
            if (reads)
×
145
                return true;
×
146
        }
147

148
        return false;
×
149
    }
150

151
    private static void RecoverAllocation(Instruction instruction)
152
    {
153
        // Call "SzArrayNew", result, typeof(T[]), length, ...
154
        if (!instruction.IsCall || instruction.Operands is not [StringLiteral { Value: SzArrayNew }, LocalVariable result, TypeAnalysisContext type, { } length, ..])
×
155
            return;
×
156

157
        instruction.OpCode = OpCode.NewArr;
×
158
        instruction.SetOperands(result, type, length);
×
159
        result.Type ??= type;
×
160
    }
×
161

162
    private static IOperand? ElementIndex(MemoryOperand memory, SzArrayTypeAnalysisContext arrayType, int pointerSize)
163
    {
164
        var elementSize = ElementSize(arrayType.ElementType, pointerSize);
×
165
        var offset = memory.Addend - ElementsOffset(pointerSize);
×
166

167
        if (offset < 0 || elementSize == 0 || offset % elementSize != 0)
×
168
            return null;
×
169

170
        if (memory.Index == null)
×
171
            return memory.Scale == 0 ? new Immediate(offset / elementSize) : null;
×
172

173
        return memory.Scale == elementSize && offset == 0 ? memory.Index : null;
×
174
    }
175

176
    private static long ElementSize(TypeAnalysisContext elementType, int pointerSize)
177
    {
178
        if (!elementType.IsValueType)
×
179
            return pointerSize;
×
180

181
        return elementType.FullName switch
×
182
        {
×
183
            "System.Boolean" or "System.Byte" or "System.SByte" => 1,
×
184
            "System.Int16" or "System.UInt16" or "System.Char" => 2,
×
185
            "System.Int32" or "System.UInt32" or "System.Single" => 4,
×
186
            "System.Int64" or "System.UInt64" or "System.Double" => 8,
×
187
            "System.IntPtr" or "System.UIntPtr" => pointerSize,
×
NEW
188
            _ => 0 // struct arrays are handled by the element-address path, which sizes them from metadata
×
189
        };
×
190
    }
191

192
    // Recovers &array[i] over struct arrays. Struct elements are never loaded outright, the compiler
193
    // computes their address via lea chains and calls through it. We solve the index as a linear function
194
    // of a local and demand an exact hit on the metadata stride, so a real load can't match by accident.
195
    private static void RecoverStructElementAddresses(MethodAnalysisContext method)
196
    {
NEW
197
        var pointerSize = method.AppContext.Binary.PointerSizeBytes;
×
NEW
198
        var cfg = method.ControlFlowGraph!;
×
199

NEW
200
        var definitions = SingleDefinitions(cfg);
×
NEW
201
        var uses = CollectUses(cfg);
×
202

NEW
203
        foreach (var instruction in cfg.Instructions)
×
204
        {
NEW
205
            if (instruction.IsCall)
×
206
            {
NEW
207
                for (var i = 1; i < instruction.Operands.Count; i++)
×
208
                {
NEW
209
                    if (MatchElementAddress(instruction.Operands[i], pointerSize, definitions) is { } inlined)
×
NEW
210
                        instruction.SetOperand(i, inlined);
×
211
                }
212

NEW
213
                continue;
×
214
            }
215

216
            // a Move from memory could be a load or a lifted lea, and only the uses tell them apart
NEW
217
            if (instruction is not { OpCode: OpCode.Move, Operands: [LocalVariable destination, MemoryOperand] })
×
218
                continue;
219

NEW
220
            if (definitions.TryGetValue(destination, out var single) && single == null)
×
221
                continue;
222

NEW
223
            if (!uses.TryGetValue(destination, out var destinationUses) || destinationUses.Count == 0
×
NEW
224
                || !destinationUses.All(u => u.Instruction.IsCall || IsMemoryBase(u.Instruction.Operands[u.OperandIndex], destination)))
×
225
                continue;
226

NEW
227
            if (MatchElementAddress(instruction.Operands[1], pointerSize, definitions) is { } address)
×
NEW
228
                instruction.SetOperand(1, address);
×
229
        }
NEW
230
    }
×
231

232
    private static AddressOf? MatchElementAddress(IOperand operand, int pointerSize, Dictionary<LocalVariable, Instruction?> definitions)
233
    {
NEW
234
        if (operand is not MemoryOperand memory
×
NEW
235
            || memory.Base is not LocalVariable { Type: SzArrayTypeAnalysisContext arrayType } array)
×
NEW
236
            return null;
×
237

NEW
238
        var elementType = arrayType.ElementType;
×
NEW
239
        if (!elementType.IsValueType || ElementSize(elementType, pointerSize) != 0)
×
NEW
240
            return null;
×
241

NEW
242
        var elementSize = MetadataElementSize(elementType, pointerSize);
×
NEW
243
        if (elementSize <= 0)
×
NEW
244
            return null;
×
245

NEW
246
        return StructElementIndex(memory, array, elementSize, pointerSize, definitions) is { } index
×
NEW
247
            ? new AddressOf(new ArrayAccess(array, index))
×
NEW
248
            : null;
×
249
    }
250

251
    private static bool IsMemoryBase(IOperand operand, LocalVariable local)
NEW
252
        => operand is MemoryOperand { Base: LocalVariable baseLocal } && ReferenceEquals(baseLocal, local);
×
253

254
    private static long MetadataElementSize(TypeAnalysisContext elementType, int pointerSize)
NEW
255
        => TypeSizes.UnboxedSize(elementType, pointerSize);
×
256

257
    private static IOperand? StructElementIndex(MemoryOperand memory, LocalVariable array, long elementSize, int pointerSize,
258
        Dictionary<LocalVariable, Instruction?> definitions)
259
    {
NEW
260
        var indexAffine = memory.Index is LocalVariable indexLocal
×
NEW
261
            ? ScaleBy(Evaluate(indexLocal, definitions, 0), Math.Max(memory.Scale, 1))
×
NEW
262
            : new Affine(null, 0, 0);
×
263

NEW
264
        if (Sum(indexAffine, new Affine(null, 0, memory.Addend)) is not { } address)
×
NEW
265
            return null;
×
266

NEW
267
        if (ReferenceEquals(address.Root, array))
×
NEW
268
            return null;
×
269

NEW
270
        var offset = address.Offset - ElementsOffset(pointerSize);
×
271

NEW
272
        if (address.Root != null)
×
NEW
273
            return address.Multiplier == elementSize && offset == 0 ? address.Root : null;
×
274

NEW
275
        return offset >= 0 && offset % elementSize == 0 ? new Immediate(offset / elementSize) : null;
×
276
    }
277

278
    // value = Multiplier * Root + Offset (a null Root means it's just a constant)
NEW
279
    private readonly record struct Affine(LocalVariable? Root, long Multiplier, long Offset);
×
280

281
    private static Affine? Evaluate(IOperand operand, Dictionary<LocalVariable, Instruction?> definitions, int depth)
282
    {
NEW
283
        if (depth > 8)
×
NEW
284
            return null;
×
285

286
        switch (operand)
287
        {
288
            case Immediate { Value: var value }:
NEW
289
                return new Affine(null, 0, value);
×
290

291
            case LocalVariable local:
292
            {
NEW
293
                if (!definitions.TryGetValue(local, out var definition) || definition == null)
×
NEW
294
                    return new Affine(local, 1, 0);
×
295

NEW
296
                return definition switch
×
NEW
297
                {
×
NEW
298
                    { OpCode: OpCode.Move, Operands: [_, MemoryOperand lea] } => EvaluateLea(lea, definitions, depth + 1),
×
NEW
299
                    { OpCode: OpCode.Move, Operands: [_, var source] } => Evaluate(source, definitions, depth + 1),
×
NEW
300
                    { OpCode: OpCode.Add, Operands: [_, var left, var right] } => Sum(Evaluate(left, definitions, depth + 1), Evaluate(right, definitions, depth + 1)),
×
NEW
301
                    { OpCode: OpCode.ShiftLeft, Operands: [_, var left, Immediate { Value: >= 0 and < 32 } shift] } => ScaleBy(Evaluate(left, definitions, depth + 1), 1L << (int)shift.Value),
×
NEW
302
                    { OpCode: OpCode.Multiply, Operands: [_, var left, Immediate factor] } => ScaleBy(Evaluate(left, definitions, depth + 1), factor.Value),
×
NEW
303
                    _ => new Affine(local, 1, 0)
×
NEW
304
                };
×
305
            }
306

307
            default:
NEW
308
                return null;
×
309
        }
310
    }
311

312
    private static Affine? EvaluateLea(MemoryOperand lea, Dictionary<LocalVariable, Instruction?> definitions, int depth)
313
    {
NEW
314
        var result = (Affine?)new Affine(null, 0, lea.Addend);
×
315

NEW
316
        if (lea.Base != null)
×
NEW
317
            result = Sum(result, Evaluate(lea.Base, definitions, depth));
×
318

NEW
319
        if (lea.Index != null)
×
NEW
320
            result = Sum(result, ScaleBy(Evaluate(lea.Index, definitions, depth), Math.Max(lea.Scale, 1)));
×
321

NEW
322
        return result;
×
323
    }
324

325
    private static Affine? Sum(Affine? left, Affine? right)
326
    {
NEW
327
        if (left is not { } l || right is not { } r)
×
NEW
328
            return null;
×
329

NEW
330
        if (l.Root != null && r.Root != null && !ReferenceEquals(l.Root, r.Root))
×
NEW
331
            return null;
×
332

NEW
333
        return new Affine(l.Root ?? r.Root, l.Multiplier + r.Multiplier, l.Offset + r.Offset);
×
334
    }
335

336
    private static Affine? ScaleBy(Affine? value, long factor)
NEW
337
        => value is { } affine ? new Affine(affine.Root, affine.Multiplier * factor, affine.Offset * factor) : null;
×
338

339
    // null means the local has more than one definition
340
    private static Dictionary<LocalVariable, Instruction?> SingleDefinitions(ISILControlFlowGraph cfg)
341
    {
NEW
342
        var definitions = new Dictionary<LocalVariable, Instruction?>();
×
343

NEW
344
        foreach (var instruction in cfg.Instructions)
×
NEW
345
            if (instruction.Destination is LocalVariable destination)
×
NEW
346
                definitions[destination] = definitions.ContainsKey(destination) ? null : instruction;
×
347

NEW
348
        return definitions;
×
349
    }
350

351
    private static Dictionary<LocalVariable, List<(Instruction Instruction, int OperandIndex)>> CollectUses(ISILControlFlowGraph cfg)
352
    {
NEW
353
        var uses = new Dictionary<LocalVariable, List<(Instruction, int)>>();
×
354

NEW
355
        foreach (var instruction in cfg.Instructions)
×
356
        {
NEW
357
            for (var i = 0; i < instruction.Operands.Count; i++)
×
358
            {
NEW
359
                var operand = instruction.Operands[i];
×
360

NEW
361
                if (ReferenceEquals(operand, instruction.Destination))
×
362
                    continue;
363

NEW
364
                foreach (var local in OperandLocals(operand))
×
365
                {
NEW
366
                    if (!uses.TryGetValue(local, out var sites))
×
NEW
367
                        uses[local] = sites = [];
×
NEW
368
                    sites.Add((instruction, i));
×
369
                }
370
            }
371
        }
372

NEW
373
        return uses;
×
374
    }
375

376
    private static IEnumerable<LocalVariable> OperandLocals(IOperand operand)
377
    {
378
        switch (operand)
379
        {
380
            case LocalVariable direct:
NEW
381
                yield return direct;
×
NEW
382
                break;
×
383
            case MemoryOperand memory:
NEW
384
                if (memory.Base is LocalVariable baseLocal)
×
NEW
385
                    yield return baseLocal;
×
NEW
386
                if (memory.Index is LocalVariable indexLocal)
×
NEW
387
                    yield return indexLocal;
×
NEW
388
                break;
×
389
            case AddressOf { Target: LocalVariable addressed }:
NEW
390
                yield return addressed;
×
391
                break;
392
        }
NEW
393
    }
×
394
}
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