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

SamboyCoding / Cpp2IL / 30769998286

02 Aug 2026 10:23PM UTC coverage: 34.169% (-1.8%) from 35.984%
30769998286

push

github

SamboyCoding
Decompiler: Support szarray, virtual calls, address of local, plus clean up ILSpy output by adjusting the IL

2899 of 9930 branches covered (29.19%)

Branch coverage included in aggregate %.

27 of 614 new or added lines in 19 files covered. (4.4%)

9 existing lines in 4 files now uncovered.

5316 of 14112 relevant lines covered (37.67%)

156656.13 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.Collections.Generic;
2
using System.Linq;
3
using Cpp2IL.Core.Graphs;
4
using Cpp2IL.Core.ISIL;
5
using Cpp2IL.Core.Model.Contexts;
6

7
namespace Cpp2IL.Core.Analysis;
8

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

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

18
    public static void Run(MethodAnalysisContext method)
19
    {
NEW
20
        RecoverAccesses(method);
×
NEW
21
        GroupInitialisers(method.ControlFlowGraph!);
×
NEW
22
    }
×
23

24
    private static void RecoverAccesses(MethodAnalysisContext method)
25
    {
NEW
26
        var pointerSize = method.AppContext.Binary.PointerSizeBytes;
×
27

NEW
28
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
29
        {
NEW
30
            RecoverAllocation(instruction);
×
31

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

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

NEW
44
                if (ElementIndex(memory, arrayType, pointerSize) is { } index)
×
NEW
45
                    instruction.SetOperand(i, new ArrayAccess(array, index));
×
46
            }
47
        }
NEW
48
    }
×
49

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

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

NEW
62
                var stores = new List<(Block Block, Instruction Instruction)>();
×
NEW
63
                var current = block;
×
NEW
64
                var index = current.Instructions.IndexOf(allocation) + 1;
×
65

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

NEW
74
                        current = current.Successors[0];
×
NEW
75
                        index = 0;
×
NEW
76
                        continue;
×
77
                    }
78

NEW
79
                    var instruction = current.Instructions[index];
×
80

NEW
81
                    if (IsElementStore(instruction, array))
×
82
                    {
NEW
83
                        stores.Add((current, instruction));
×
NEW
84
                        index++;
×
NEW
85
                        continue;
×
86
                    }
87

NEW
88
                    if (!ReadsArray(instruction, array))
×
89
                    {
NEW
90
                        index++;
×
NEW
91
                        continue;
×
92
                    }
93

94
                    // 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.
NEW
95
                    if (stores.Count > 1)
×
96
                    {
NEW
97
                        foreach (var (storeBlock, store) in stores)
×
NEW
98
                            storeBlock.Instructions.Remove(store);
×
99

NEW
100
                        block.Instructions.Remove(allocation);
×
101

NEW
102
                        var moved = new List<Instruction> { allocation };
×
NEW
103
                        moved.AddRange(stores.Select(s => s.Instruction));
×
104

NEW
105
                        current.Instructions.InsertRange(current.Instructions.IndexOf(instruction), moved);
×
NEW
106
                        movedAny = true;
×
107
                    }
108

109
                    break;
110
                }
111
            }
112
        }
113

114
        // Emptying a block out entirely leaves branches pointing at nothing to jump to
NEW
115
        if (movedAny)
×
NEW
116
            cfg.RemoveEmptyBlocks();
×
NEW
117
    }
×
118

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

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

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

NEW
141
            if (reads)
×
NEW
142
                return true;
×
143
        }
144

NEW
145
        return false;
×
146
    }
147

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

NEW
154
        instruction.OpCode = OpCode.NewArr;
×
NEW
155
        instruction.SetOperands(result, type, length);
×
NEW
156
        result.Type ??= type;
×
NEW
157
    }
×
158

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

NEW
164
        if (offset < 0 || elementSize == 0 || offset % elementSize != 0)
×
NEW
165
            return null;
×
166

NEW
167
        if (memory.Index == null)
×
NEW
168
            return memory.Scale == 0 ? new Immediate(offset / elementSize) : null;
×
169

NEW
170
        return memory.Scale == elementSize && offset == 0 ? memory.Index : null;
×
171
    }
172

173
    private static long ElementSize(TypeAnalysisContext elementType, int pointerSize)
174
    {
NEW
175
        if (!elementType.IsValueType)
×
NEW
176
            return pointerSize;
×
177

NEW
178
        return elementType.FullName switch
×
NEW
179
        {
×
NEW
180
            "System.Boolean" or "System.Byte" or "System.SByte" => 1,
×
NEW
181
            "System.Int16" or "System.UInt16" or "System.Char" => 2,
×
NEW
182
            "System.Int32" or "System.UInt32" or "System.Single" => 4,
×
NEW
183
            "System.Int64" or "System.UInt64" or "System.Double" => 8,
×
NEW
184
            "System.IntPtr" or "System.UIntPtr" => pointerSize,
×
NEW
185
            _ => 0 // todo: support size calculation for user-defined value types
×
NEW
186
        };
×
187
    }
188
}
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