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

SamboyCoding / Cpp2IL / 25641285720

10 May 2026 10:18PM UTC coverage: 35.104% (-0.2%) from 35.33%
25641285720

Pull #542

github

web-flow
Merge 9249bcb5b into 6af99f218
Pull Request #542: Remove static mutable state from LibCpp2IL 2: Electric Boogaloo

1877 of 6693 branches covered (28.04%)

Branch coverage included in aggregate %.

303 of 569 new or added lines in 66 files covered. (53.25%)

12 existing lines in 11 files now uncovered.

4394 of 11171 relevant lines covered (39.33%)

268486.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/Cpp2IL.Core/Il2CppApiFunctions/Arm64KeyFunctionAddresses.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Linq;
4
using Cpp2IL.Core.Extensions;
5
using Cpp2IL.Core.Logging;
6
using Cpp2IL.Core.Model.Contexts;
7
using Cpp2IL.Core.Utils;
8
using Gee.External.Capstone;
9
using Gee.External.Capstone.Arm64;
10
using LibCpp2IL;
11
using LibCpp2IL.NintendoSwitch;
12

13
namespace Cpp2IL.Core.Il2CppApiFunctions;
14

15
public class Arm64KeyFunctionAddresses : BaseKeyFunctionAddresses
16
{
17
    private List<Arm64Instruction> _allInstructions = [];
×
18

19
    protected override void Init(ApplicationAnalysisContext context)
20
    {
21
        var disassembler = CapstoneDisassembler.CreateArm64Disassembler(context.Binary.IsBigEndian ? Arm64DisassembleMode.BigEndian : Arm64DisassembleMode.LittleEndian);
×
22
        disassembler.EnableInstructionDetails = true;
×
23
        disassembler.DisassembleSyntax = DisassembleSyntax.Intel;
×
24
        disassembler.EnableSkipDataMode = true;
×
25

26
        var primaryExecutableSection = context.Binary.GetEntirePrimaryExecutableSection();
×
27
        var primaryExecutableSectionVa = context.Binary.GetVirtualAddressOfPrimaryExecutableSection();
×
28
        var endOfTextSection = primaryExecutableSectionVa + (ulong)primaryExecutableSection.Length;
×
29

30
        Logger.InfoNewline("\tRunning entire .text section through Arm64 disassembler, this might take up to several minutes for large games, and may fail on large games if you have <16GB ram...");
×
31

32
        Logger.VerboseNewline($"\tPrimary executable section is {primaryExecutableSection.Length} bytes, starting at 0x{primaryExecutableSectionVa:X} and extending to 0x{endOfTextSection:X}");
×
33
        var attributeGeneratorList = SharedState.AttributeGeneratorStarts.ToList();
×
34
        attributeGeneratorList.SortByExtractedKey(a => a);
×
35

36
        if (attributeGeneratorList.Count > 0)
×
37
        {
38
            if (context.Binary is not NsoFile)
×
39
            {
40
                Logger.VerboseNewline($"\tLast attribute generator function is at address 0x{attributeGeneratorList[^1]:X}. Skipping everything before that.");
×
41

42
                //Optimisation: We can skip all bytes up to and including the last attribute restoration function
43
                //However we don't know how long the last restoration function is, so just skip up to it, we'd only be saving a further 100 instructions or so
44
                //These come at the beginning of the .text section usually and the only thing that comes before them is unmanaged finalizers and initializers.
45
                //This may not be correct on v29 which uses the Bee compiler, which may do things differently
46
                var oldLength = primaryExecutableSection.Length;
×
47

48
                var toRemove = (int)(attributeGeneratorList[^1] - primaryExecutableSectionVa);
×
49
                primaryExecutableSection = primaryExecutableSection.Skip(toRemove).ToArray();
×
50

51
                primaryExecutableSectionVa = attributeGeneratorList[^1];
×
52

53
                Logger.VerboseNewline($"\tBy trimming out attribute generator functions, reduced decompilation work by {toRemove} of {oldLength} bytes (a {toRemove * 100 / (float)oldLength:f1}% saving)");
×
54

55
                //Some games (e.g. Muse Dash APK) contain the il2cpp-ified methods in the .text section instead of their own dedicated one.
56
                //That makes this very slow
57
                //Try and detect the first function
58
                var methodAddresses = new List<ulong>();
×
59
                methodAddresses.SortByExtractedKey(a => a);
×
60

61
                if (methodAddresses[0] < endOfTextSection && context.Binary.GetVirtualAddressOfExportedFunctionByName("il2cpp_object_new") != 0)
×
62
                {
63
                    var exportAddresses = new[] { "il2cpp_object_new", "il2cpp_value_box", "il2cpp_runtime_class_init", "il2cpp_array_new_specific", "il2cpp_type_get_object", "il2cpp_resolve_icall", "il2cpp_string_new", "il2cpp_string_new_wrapper", "il2cpp_raise_exception" }.Select(context.Binary.GetVirtualAddressOfExportedFunctionByName).Where(a => a > 0).ToArray();
×
64

65
                    var lastExport = exportAddresses.Max();
×
66
                    var firstExport = exportAddresses.Min();
×
67

68
                    Logger.VerboseNewline($"\tDetected that the il2cpp-ified managed methods are in the .text section and api functions are available. Attempting to trim out managed methods for KFA scanning - the first managed method is at 0x{methodAddresses[0]:X} and the last at 0x{methodAddresses[^1]:X}, " +
×
69
                                          $"the first export function is at 0x{firstExport:X} and the last at 0x{lastExport:X}");
×
70

71
                    //I am assuming, arbitrarily, that the exports are always towards the end of the managed methods, in this case.
72
                    var startFrom = Math.Min(firstExport, methodAddresses[^1]);
×
73

74
                    //Just in case we didn't get the first export, let's subtract a little
75
                    if (startFrom > 0x100_0000)
×
76
                        startFrom -= 0x10_0000;
×
77

78
                    Logger.VerboseNewline($"\tTrimming everything before 0x{startFrom:X}.");
×
79
                    oldLength = primaryExecutableSection.Length;
×
80

81
                    toRemove = (int)(startFrom - primaryExecutableSectionVa);
×
82
                    primaryExecutableSection = primaryExecutableSection.Skip(toRemove).ToArray();
×
83

84
                    primaryExecutableSectionVa = startFrom;
×
85

86
                    Logger.VerboseNewline($"\tBy trimming out most of the il2cpp-ified managed methods, reduced decompilation work by {toRemove} of {oldLength} bytes (a {toRemove * 100L / (float)oldLength:f1}% saving)");
×
87
                }
88
                else if (methodAddresses[0] < endOfTextSection)
×
89
                {
90
                    Logger.VerboseNewline($"\tDetected that the il2cpp-ified managed methods are in the .text section, but api functions are not available. Attempting to (conservatively) trim out managed methods for KFA scanning - the first managed method is at 0x{methodAddresses[0]:X} and the last at 0x{methodAddresses[^1]:X}");
×
91

92
                    var startFrom = methodAddresses[^1];
×
93

94
                    //Just in case the exports are mixed in with the end of the managed methods, let's subtract a little
95
                    if (startFrom > 0x100_0000)
×
96
                        startFrom -= 0x10_0000;
×
97

98
                    Logger.VerboseNewline($"\tTrimming everything before 0x{startFrom:X}.");
×
99
                    oldLength = primaryExecutableSection.Length;
×
100

101
                    toRemove = (int)(startFrom - primaryExecutableSectionVa);
×
102
                    primaryExecutableSection = primaryExecutableSection.Skip(toRemove).ToArray();
×
103

104
                    primaryExecutableSectionVa = startFrom;
×
105

106
                    Logger.VerboseNewline($"\tBy trimming out most of the il2cpp-ified managed methods, reduced decompilation work by {toRemove} of {oldLength} bytes (a {toRemove * 100L / (float)oldLength:f1}% saving)");
×
107
                }
108
            }
109
            else
110
            {
111
                //For now we skip everything after the last attribute generator. Not sure this is always reliable but in test binaries it works.
112
                //We choose last not first to include all the generators, so that we hopefully have some context for api function detection.
113
                Logger.VerboseNewline($"\tNSO: Last attribute generator function is at address 0x{attributeGeneratorList[^1]:X}. Skipping everything after that.");
×
114

115
                var oldLength = primaryExecutableSection.Length;
×
116

117
                var toKeep = (int)(attributeGeneratorList[^1] - primaryExecutableSectionVa);
×
118
                primaryExecutableSection = primaryExecutableSection.SubArray(..toKeep);
×
119

120
                //This doesn't change, we've trimmed the end, not the beginning
121
                // primaryExecutableSectionVa = primaryExecutableSectionVa;
122

123
                Logger.VerboseNewline($"\tBy trimming out everything after and including attribute generator functions, reduced decompilation work by {oldLength - toKeep} of {oldLength} bytes (a {(oldLength - toKeep) * 100L / (float)oldLength:f1}% saving)");
×
124
            }
125
        }
126

127
        _allInstructions = disassembler.Disassemble(primaryExecutableSection, (long)primaryExecutableSectionVa).ToList();
×
128
    }
×
129

130
    protected override IEnumerable<ulong> FindAllThunkFunctions(ulong addr, uint maxBytesBack = 0, params ulong[] addressesToIgnore)
131
    {
132
        var allBranchesToAddr = _allInstructions.Where(i => i.Mnemonic is "b" or "bl")
133
            .Where(i => i.Details.Operands[0].IsImmediate() && i.Details.Operands[0].Immediate == (long)addr)
×
134
            .ToList();
×
135

136
        foreach (var potentialBranch in allBranchesToAddr)
×
137
        {
138
            if (addressesToIgnore.Contains((ulong)potentialBranch.Address))
×
139
                continue;
140

141
            var backtrack = 0;
×
142
            var idx = _allInstructions.IndexOf(potentialBranch);
×
143

144
            do
145
            {
146
                //About the only way we have of checking for a thunk is if there is an all-zero instruction or another unconditional branch just before this
147
                //Or a ret, but that's less reliable.
148
                //if so, this is probably a thunk.
149
                if (idx - backtrack > 0)
×
150
                {
151
                    var prevInstruction = _allInstructions[idx - backtrack - 1];
×
152

153
                    if (addressesToIgnore.Contains((ulong)prevInstruction.Address))
×
154
                    {
155
                        backtrack++;
×
156
                        continue;
×
157
                    }
158

159
                    if (prevInstruction.IsSkippedData && prevInstruction.Bytes.All(b => b == 0))
×
160
                    {
161
                        //All-zero instructions are a match
162
                        yield return (ulong)potentialBranch.Address - (ulong)(backtrack * 4);
×
163
                        break;
×
164
                    }
165

166
                    if (prevInstruction.Mnemonic is "adrp" or "str")
×
167
                    {
168
                        //ADRP instructions are a deal breaker - this means we're loading something from memory, so it's not a simple thunk
169
                        break;
170
                    }
171

172
                    if (prevInstruction.Mnemonic is "b" or "bl")
×
173
                    {
174
                        //Previous branches are a match
175
                        yield return (ulong)potentialBranch.Address - (ulong)(backtrack * 4);
×
176
                        break;
×
177
                    }
178

179
                    if (prevInstruction.Mnemonic is "ret")
×
180
                    {
181
                        //Previous rets are a match
182
                        yield return (ulong)potentialBranch.Address - (ulong)(backtrack * 4);
×
183
                        break;
×
184
                    }
185
                }
186

187
                //We're working in the .text section here so we have few symbols, so there's no point looking for the previous one.
188

189
                backtrack++;
×
190
            } while (backtrack * 4 < maxBytesBack);
×
191
        }
192
    }
×
193

194
    protected override ulong GetObjectIsInstFromSystemType()
195
    {
196
        Logger.Verbose("\tTrying to use System.Type::IsInstanceOfType to find il2cpp::vm::Object::IsInst...");
×
NEW
197
        var typeIsInstanceOfType = ReflectionCache.GetType("Type", "System")?.Methods?.FirstOrDefault(m => m.Name == "IsInstanceOfType");
×
198
        if (typeIsInstanceOfType == null)
×
199
        {
200
            Logger.VerboseNewline("Type or method not found, aborting.");
×
201
            return 0;
×
202
        }
203

204
        //IsInstanceOfType is a very simple ICall, that looks like this:
205
        //  Il2CppClass* klass = vm::Class::FromIl2CppType(type->type.type);
206
        //  return il2cpp::vm::Object::IsInst(obj, klass) != NULL;
207
        //The last call is to Object::IsInst
208

209
        Logger.Verbose($"IsInstanceOfType found at 0x{typeIsInstanceOfType.MethodPointer:X}...");
×
NEW
210
        var instructions = Arm64Utils.GetArm64MethodBodyAtVirtualAddress(_appContext.Binary, typeIsInstanceOfType.MethodPointer, false);
×
211

212
        var lastCall = instructions.LastOrDefault(i => i.Mnemonic == "bl");
×
213

214
        if (lastCall == null)
×
215
        {
216
            Logger.VerboseNewline("Method does not match expected signature. Aborting.");
×
217
            return 0;
×
218
        }
219

220
        Logger.VerboseNewline($"Success. IsInst found at 0x{lastCall.Details.Operands[0].Immediate:X}");
×
221
        return (ulong)lastCall.Details.Operands[0].Immediate;
×
222
    }
223

224
    protected override ulong FindFunctionThisIsAThunkOf(ulong thunkPtr, bool prioritiseCall = false)
225
    {
226
        var idx = _allInstructions.FindIndex(i => i.Address == (long)thunkPtr);
×
227

228
        if (idx < 0)
×
229
            return 0;
×
230

231
        //Easy case, we have an unconditional jump at that address, just return what it points at
232
        if (_allInstructions[idx].Mnemonic is "b" or "bl")
×
233
            return (ulong)_allInstructions[idx].Details.Operands[0].Immediate;
×
234

235
        //Max number of instructions to check is 12. I use this because we check 50 bytes in x86 and 4 * 12 is 48.
236

237
        for (var i = 0; i <= 12; i++)
×
238
        {
239
            idx++;
×
240
            if (_allInstructions[idx].Mnemonic is "b" or "bl")
×
241
                return (ulong)_allInstructions[idx].Details.Operands[0].Immediate;
×
242
        }
243

244
        return 0;
×
245
    }
246

247
    protected override int GetCallerCount(ulong toWhere) => _allInstructions.Where(i => i.Mnemonic is "b" or "bl")
×
248
        .Count(i => i.Details.Operands[0].IsImmediate() && i.Details.Operands[0].Immediate == (long)toWhere);
×
249

250
    protected override void AttemptInstructionAnalysisToFillGaps()
251
    {
252
        if (il2cpp_object_new == 0)
×
253
        {
254
            Logger.Verbose("\tAttempting to use Array GetEnumerator to find il2cpp_codegen_object_new...");
×
NEW
255
            if (ReflectionCache.GetType("Array", "System") is { } arrayTypeDef)
×
256
            {
257
                if (arrayTypeDef.Methods!.FirstOrDefault(m => m.Name == "GetEnumerator") is { } methodDef)
×
258
                {
259
                    var ptr = methodDef.MethodPointer;
×
NEW
260
                    var body = Arm64Utils.GetArm64MethodBodyAtVirtualAddress(_appContext.Binary, ptr);
×
261

262
                    //Looking for adrp, ldr, ldr, bl. Probably more than one - the first will be initializing the method, second will be the constructor call
263
                    var probableResult = 0L;
×
264
                    var numSeen = 0;
×
265
                    for (var i = 0; i < body.Count - 4; i++)
×
266
                    {
267
                        if (body[i].Mnemonic != "adrp" || body[i + 1].Mnemonic != "ldr" || body[i + 2].Mnemonic != "ldr" || body[i + 3].Mnemonic != "bl")
×
268
                            continue;
269

270
                        if (numSeen++ < 2) //Only store first one or second one
×
271
                            probableResult = body[i + 3].Details.Operands[0].Immediate;
×
272
                    }
273

274
                    if (probableResult > 0)
×
275
                    {
276
                        Logger.Verbose($"Probably found at 0x{probableResult:X}...");
×
277

278
                        //This is *codegen*_object_new. Probably. Check it
279
                        var thunk = FindFunctionThisIsAThunkOf((ulong)probableResult);
×
280
                        long addrVmObjectNew;
281
                        if (thunk > 0)
×
282
                            //We've found codegen_object_new, map to vm::Object::New, then try and get back to object_new
283
                            addrVmObjectNew = (long)thunk;
×
284
                        else
285
                            //Looks like we've been inlined and this is just vm::Object::New.
286
                            addrVmObjectNew = probableResult;
×
287

288
                        var allThunks = FindAllThunkFunctions((ulong)addrVmObjectNew, 16, (ulong)probableResult).ToList();
×
289

290
                        allThunks.SortByExtractedKey(GetCallerCount); //Sort in ascending order by caller count
×
291
                        allThunks.Reverse(); //Reverse to be in descending order
×
292

293
                        il2cpp_object_new = allThunks.FirstOrDefault(); //Take first (i.e. most called)
×
294

295
                        Logger.VerboseNewline($"Leaving il2cpp_object_new at 0x{il2cpp_object_new:X}");
×
296
                    }
297
                    else
298
                        Logger.VerboseNewline("Couldn't find a matching instruction, can't be used.");
×
299
                }
300
                else
301
                    Logger.VerboseNewline("Method stripped, can't be used.");
×
302
            }
303
            else
304
                Logger.VerboseNewline("Type stripped, can't be used.");
×
305
        }
306
    }
×
307
}
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