From 4a24835dc845b161f7b83f2855f1e0c5000b4845 Mon Sep 17 00:00:00 2001 From: gompoc <91314780+gompoc@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:41:18 +0100 Subject: [PATCH] Analysis: Experimental Switch Recovery --- Cpp2IL.Core/Analysis/JumpTableRestoration.cs | 300 ++++++++++++++++++ Cpp2IL.Core/Graphs/Block.cs | 3 +- Cpp2IL.Core/Graphs/ISILControlFlowGraph.cs | 26 ++ Cpp2IL.Core/ISIL/Instruction.cs | 8 +- Cpp2IL.Core/ISIL/OpCode.cs | 7 +- Cpp2IL.Core/ISIL/SwitchTargets.cs | 12 + Cpp2IL.Core/IlGenerator.cs | 45 ++- .../InstructionSets/NewArmV8InstructionSet.cs | 2 +- .../InstructionSets/X86InstructionSet.cs | 2 +- .../Model/Contexts/MethodAnalysisContext.cs | 3 + Cpp2IL.Core/Utils/X86Utils.cs | 70 ++-- 11 files changed, 447 insertions(+), 31 deletions(-) create mode 100644 Cpp2IL.Core/Analysis/JumpTableRestoration.cs create mode 100644 Cpp2IL.Core/ISIL/SwitchTargets.cs diff --git a/Cpp2IL.Core/Analysis/JumpTableRestoration.cs b/Cpp2IL.Core/Analysis/JumpTableRestoration.cs new file mode 100644 index 000000000..8fe5cbefc --- /dev/null +++ b/Cpp2IL.Core/Analysis/JumpTableRestoration.cs @@ -0,0 +1,300 @@ +using System.Collections.Generic; +using System.Buffers.Binary; +using System.Linq; +using Cpp2IL.Core.Graphs; +using Cpp2IL.Core.ISIL; +using Cpp2IL.Core.Logging; +using Cpp2IL.Core.Model.Contexts; + +namespace Cpp2IL.Core.Analysis; + +/// +/// Finds the address calculation used by relative switch jump tables. +/// +public static class JumpTableRestoration +{ + public static void Run(MethodAnalysisContext method) { + var candidates = FindCandidates(method.ControlFlowGraph!); + + foreach (var candidate in candidates) + { + if (candidate.TryRestore(method.AppContext, method.ControlFlowGraph!)) {} + //Logger.InfoNewline($"Restored {candidate.TableLength}-entry jump table at {candidate.LoadBase.Index} in {method.FullName}"); + } + } + + public static List FindCandidates(ISILControlFlowGraph cfg) + { + var candidates = new List(); + + foreach (var block in cfg.Blocks) + { + for (var index = 0; index <= block.Instructions.Count - 4; index++) + { + if (TryMatch(block, index, out var candidate)) + candidates.Add(candidate); + } + } + + return candidates; + } + + + /// + /// Looks for x86 msvc compiler generated jump table patterns + /// TODO: Abstract this in some way so that we can have a separate implementation for gcc etc. + /// + private static bool TryMatch(Block dispatchBlock, int index, out JumpTableCandidate candidate) + { + candidate = default; + + var instructions = dispatchBlock.Instructions; + + var loadBase = instructions[index]; + var loadOffset = instructions[index + 1]; + var addBase = instructions[index + 2]; + var jump = instructions[index + 3]; + + if (loadBase is not { OpCode: OpCode.Move, Operands.Count: 2 } || + //if (index != 0 || instructions.Count != 4 || + // loadBase is not { OpCode: OpCode.Move, Operands.Count: 2 } || + loadBase.Destination is not Register baseRegister || + loadBase.Operands[1] is not Immediate tableBase || + loadOffset is not { OpCode: OpCode.Move, Operands.Count: 2 } || + loadOffset.Destination is not Register offsetRegister || + loadOffset.Operands[1] is not MemoryOperand tableOffset || + !Equals(tableOffset.Base, baseRegister) || + addBase is not { OpCode: OpCode.Add, Operands.Count: 3 } || + addBase.Destination is not Register targetRegister || + !targetRegister.Equals(offsetRegister) || + !Equals(addBase.Operands[1], offsetRegister) || + !Equals(addBase.Operands[2], baseRegister) || + jump is not { OpCode: OpCode.IndirectJump, Operands.Count: > 0 } || + !Equals(jump.Operands[0], targetRegister)) + { + return false; + } + + if (dispatchBlock.Predecessors is not [{ BlockType: BlockType.TwoWay } boundsBlock] || + boundsBlock.Instructions.Count == 0 || + boundsBlock.Instructions[^1] is not { OpCode: OpCode.ConditionalJump } boundsJump || + !boundsBlock.Successors.Contains(dispatchBlock) || + boundsJump.Operands[0] is not Block branchTarget || + ReferenceEquals(branchTarget, dispatchBlock) || + !TryGetTableLength(boundsBlock, boundsJump, out var boundsCheck, out var tableIndex, out var tableLength)) + { + return false; + } + + candidate = new JumpTableCandidate( + dispatchBlock, loadBase, loadOffset, addBase, jump, tableBase, tableOffset, + boundsCheck, tableIndex, tableLength); + return true; + } + + private static bool TryGetTableLength(Block boundsBlock, Instruction boundsJump, out Instruction boundsCheck, + out IOperand tableIndex, out int tableLength) + { + boundsCheck = null!; + tableIndex = null!; + tableLength = 0; + + if (boundsJump.Operands.Count < 2 || boundsJump.Operands[1] is not Register condition) + return false; + + var instructions = boundsBlock.Instructions; + if (!TryGetDefinition(condition, instructions, instructions.Count, OpCode.And, out var greaterThan) || + !TryGetGreaterThanOperands(greaterThan, instructions, out tableIndex, out var upperBound)) + { + return false; + } + + if (upperBound is not Immediate { Value: >= 0 and < int.MaxValue } bound) + return false; + + boundsCheck = greaterThan; + tableLength = checked((int)bound.Value + 1); + return true; + } + + private static bool TryGetGreaterThanOperands(Instruction condition, IReadOnlyList instructions, + out IOperand index, out IOperand upperBound) + { + index = upperBound = null!; + + if (condition.Operands.Count != 3 || + condition.Operands[1] is not Register left || + condition.Operands[2] is not Register right) + { + return false; + } + + var before = GetInstructionIndex(instructions, condition); + return TryGetSignEqualsOverflow(left, instructions, before, out index, out upperBound) && + IsNotZeroFlag(right, instructions, before) || + TryGetSignEqualsOverflow(right, instructions, before, out index, out upperBound) && + IsNotZeroFlag(left, instructions, before); + } + + private static bool TryGetSignEqualsOverflow(Register condition, IReadOnlyList instructions, int before, + out IOperand index, out IOperand upperBound) + { + index = upperBound = null!; + + if (!TryGetDefinition(condition, instructions, before, OpCode.CheckEqual, out var signEqualsOverflow) || + signEqualsOverflow.Operands.Count != 3) + { + return false; + } + + foreach (var operand in new[] { signEqualsOverflow.Operands[1], signEqualsOverflow.Operands[2] }) + { + if (operand is Register signFlag && TryGetSignFlagOperands(signFlag, instructions, + GetInstructionIndex(instructions, signEqualsOverflow), out index, out upperBound)) + return true; + } + + return false; + } + + private static bool IsNotZeroFlag(Register condition, IReadOnlyList instructions, int before) + { + return TryGetDefinition(condition, instructions, before, OpCode.Not, out var notZero) && + notZero.Operands.Count == 2 && + notZero.Operands[1] is Register zeroFlag && + TryGetDefinition(zeroFlag, instructions, GetInstructionIndex(instructions, notZero), OpCode.CheckEqual, out var checkZero) && + checkZero.Operands.Count == 3 && + checkZero.Operands[1] is Register subtraction && + checkZero.Operands[2] is Immediate { Value: 0 } && + TryGetDefinition(subtraction, instructions, GetInstructionIndex(instructions, checkZero), OpCode.Subtract, out _); + } + + private static bool TryGetSignFlagOperands(Register condition, IReadOnlyList instructions, int before, + out IOperand index, out IOperand upperBound) + { + index = upperBound = null!; + + if (!TryGetDefinition(condition, instructions, before, OpCode.CheckLess, out var signFlag) || + signFlag.Operands.Count != 3 || + signFlag.Operands[1] is not Register subtraction || + signFlag.Operands[2] is not Immediate { Value: 0 } || + !TryGetDefinition(subtraction, instructions, GetInstructionIndex(instructions, signFlag), OpCode.Subtract, out var subtract) || + subtract.Operands.Count != 3) + { + return false; + } + + index = subtract.Operands[1]; + upperBound = subtract.Operands[2]; + return true; + } + + private static bool TryGetDefinition(Register register, IReadOnlyList instructions, int before, OpCode opCode, + out Instruction instruction) + { + for (var index = before - 1; index >= 0; index--) + { + if (instructions[index].Destination is not Register destination || !destination.Equals(register)) + continue; + + instruction = instructions[index]; + return instruction.OpCode == opCode; + } + + instruction = null!; + return false; + } + + private static int GetInstructionIndex(IReadOnlyList instructions, Instruction instruction) + { + for (var index = 0; index < instructions.Count; index++) + { + if (ReferenceEquals(instructions[index], instruction)) + return index; + } + + return -1; + } +} + +public readonly record struct JumpTableCandidate( + Block DispatchBlock, + Instruction LoadBase, + Instruction LoadOffset, + Instruction AddBase, + Instruction Jump, + Immediate TableBase, + MemoryOperand TableOffset, + Instruction BoundsCheck, + IOperand TableIndex, + int TableLength) +{ + /// + /// The address of the first jump table entry. The index and scale select an entry within it. + /// + public ulong TableAddress => checked((ulong)checked(TableBase.Value + TableOffset.Addend)); + + /// + /// Reads the signed 32-bit relative offsets stored in this jump table. + /// + public bool TryReadEntries(ApplicationAnalysisContext appContext, out int[] entries) + { + entries = []; + + if (TableOffset.Scale != sizeof(int) || + !appContext.Binary.TryMapVirtualAddressToRaw(TableAddress, out var rawAddress)) + { + return false; + } + + var byteLength = checked((long)TableLength * sizeof(int)); + var content = appContext.Binary.GetRawBinaryContent(); + if (rawAddress < 0 || byteLength > content.Length || rawAddress > content.Length - byteLength) + return false; + + entries = new int[TableLength]; + var tableBytes = content.Slice((int)rawAddress, (int)byteLength); + for (var index = 0; index < entries.Length; index++) + entries[index] = BinaryPrimitives.ReadInt32LittleEndian(tableBytes.Slice(index * sizeof(int), sizeof(int))); + + return true; + } + + /// + /// Replaces the relative-offset dispatch sequence with a switch whose ordered targets correspond to table entries. + /// + public bool TryRestore(ApplicationAnalysisContext appContext, ISILControlFlowGraph cfg) + { + if (!TryReadEntries(appContext, out var entries)) + return false; + + var instructions = cfg.Blocks.SelectMany(block => block.Instructions).ToList(); + var targetInstructions = new List(entries.Length); + foreach (var entry in entries) + { + var targetIp = checked((ulong)checked(TableBase.Value + entry)); + var targetInstruction = instructions.FirstOrDefault(instruction => instruction.IP == targetIp); + if (targetInstruction == null || DispatchBlock.Instructions.Contains(targetInstruction)) + return false; + + targetInstructions.Add(targetInstruction); + } + + var targetBlocks = new List(targetInstructions.Count); + foreach (var targetInstruction in targetInstructions) + { + var targetBlock = cfg.GetOrSplitBlockForInstruction(targetInstruction); + if (targetBlock == null) + return false; + + targetBlocks.Add(targetBlock); + } + + DispatchBlock.Instructions.Clear(); + DispatchBlock.AddInstruction(new Instruction(Jump.Index, OpCode.Switch, + TableIndex, new SwitchTargets(targetBlocks)) { IP = Jump.IP }); + cfg.ReplaceSuccessors(DispatchBlock, targetBlocks); + DispatchBlock.CalculateBlockType(); + return true; + } +} diff --git a/Cpp2IL.Core/Graphs/Block.cs b/Cpp2IL.Core/Graphs/Block.cs index de17f4322..bffb3e4ff 100644 --- a/Cpp2IL.Core/Graphs/Block.cs +++ b/Cpp2IL.Core/Graphs/Block.cs @@ -47,7 +47,8 @@ public void CalculateBlockType() { OpCode.Jump => BlockType.OneWay, OpCode.ConditionalJump => BlockType.TwoWay, - OpCode.IndirectJump => BlockType.TailCall, //TODO this is wrong for switch statements but that's better than tail calls to virtual methods stopping the stack settling + OpCode.IndirectJump => BlockType.TailCall, + OpCode.Switch => BlockType.NWay, OpCode.Call or OpCode.CallVoid => BlockType.Call, OpCode.Return => BlockType.Return, _ => BlockType.Fall, diff --git a/Cpp2IL.Core/Graphs/ISILControlFlowGraph.cs b/Cpp2IL.Core/Graphs/ISILControlFlowGraph.cs index 467ed5453..4d198ffc1 100644 --- a/Cpp2IL.Core/Graphs/ISILControlFlowGraph.cs +++ b/Cpp2IL.Core/Graphs/ISILControlFlowGraph.cs @@ -477,6 +477,32 @@ private void FixBlock(Block block, bool removeJmp = false) return null; } + /// + /// Gets the block beginning at , splitting its current block when needed. + /// + internal Block? GetOrSplitBlockForInstruction(Instruction instruction) + { + var block = FindBlockByInstruction(instruction); + if (block == null) + return null; + + var index = block.Instructions.FindIndex(candidate => ReferenceEquals(candidate, instruction)); + return index < 0 ? null : SplitAndCreate(block, index); + } + + /// + /// Replaces every outgoing edge from with edges to . + /// + internal void ReplaceSuccessors(Block from, IEnumerable successors) + { + foreach (var successor in from.Successors) + successor.Predecessors.Remove(from); + from.Successors.Clear(); + + foreach (var successor in successors.Distinct()) + AddDirectedEdge(from, successor); + } + private Block SplitAndCreate(Block target, int index) { if (index < 0 || index >= target.Instructions.Count) diff --git a/Cpp2IL.Core/ISIL/Instruction.cs b/Cpp2IL.Core/ISIL/Instruction.cs index 3dc6151c7..d90d8aad1 100644 --- a/Cpp2IL.Core/ISIL/Instruction.cs +++ b/Cpp2IL.Core/ISIL/Instruction.cs @@ -11,6 +11,11 @@ public class Instruction : IOperand { public int Index; + /// + /// Virtual address of the native instruction that produced this ISIL instruction, or 0 when synthetic. + /// + public ulong IP; + public OpCode OpCode { get; @@ -31,7 +36,7 @@ public OpCode OpCode public bool IsFallThrough => OpCode switch { - OpCode.Return or OpCode.Jump or OpCode.ConditionalJump or OpCode.IndirectJump or OpCode.Throw => false, + OpCode.Return or OpCode.Jump or OpCode.ConditionalJump or OpCode.IndirectJump or OpCode.Throw or OpCode.Switch => false, _ => true }; @@ -162,6 +167,7 @@ or OpCode.And or OpCode.Or or OpCode.Xor : _operands.Take(1).ToList(), OpCode.CallVoid or OpCode.Phi => _operands.Skip(1).ToList(), + OpCode.Switch => [_operands[0]], OpCode.CheckEqual or OpCode.CheckGreater or OpCode.CheckLess or OpCode.CheckNotEqual or OpCode.CheckGreaterOrEqual or OpCode.CheckLessOrEqual => [_operands[1], _operands[2]], diff --git a/Cpp2IL.Core/ISIL/OpCode.cs b/Cpp2IL.Core/ISIL/OpCode.cs index 8aed12bc7..3416ed831 100644 --- a/Cpp2IL.Core/ISIL/OpCode.cs +++ b/Cpp2IL.Core/ISIL/OpCode.cs @@ -117,5 +117,10 @@ public enum OpCode /// /// Throws a new instance of the exception type described by op 1. /// - Throw + Throw, + + /// + /// Switches on op 1 and jumps to one of the case targets in op 2 + /// + Switch, } diff --git a/Cpp2IL.Core/ISIL/SwitchTargets.cs b/Cpp2IL.Core/ISIL/SwitchTargets.cs new file mode 100644 index 000000000..4b1eea9ed --- /dev/null +++ b/Cpp2IL.Core/ISIL/SwitchTargets.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Cpp2IL.Core.ISIL; + +/// +/// Ordered case targets +/// +/// +public class SwitchTargets(List blocks) : IOperand +{ + public List Blocks { get; } = blocks; +} diff --git a/Cpp2IL.Core/IlGenerator.cs b/Cpp2IL.Core/IlGenerator.cs index 64bd4ddc2..a71c2e0e6 100644 --- a/Cpp2IL.Core/IlGenerator.cs +++ b/Cpp2IL.Core/IlGenerator.cs @@ -111,6 +111,7 @@ public static void GenerateIl(MethodAnalysisContext context, MethodDefinition de Dictionary> instructionMap = []; Dictionary blockEntryMap = []; List<(CilInstruction BranchInstruction, Block TargetBlock)> pendingBlockBranchFixups = []; + List<(CilInstruction SwitchInstruction, SwitchTargets Targets)> pendingSwitchFixups = []; foreach (var block in context.ControlFlowGraph!.Blocks) { @@ -125,6 +126,13 @@ public static void GenerateIl(MethodAnalysisContext context, MethodDefinition de var generated = GenerateInstructions(instruction, context, definition, locals, writeLine, stringCtor); instructionMap.Add(instruction, generated); + if (instruction is { OpCode: OpCode.Switch, Operands: [_, SwitchTargets targets] }) + { + var switchInstruction = generated.FirstOrDefault(il => il.OpCode == CilOpCodes.Switch); + if (switchInstruction != null) + pendingSwitchFixups.Add((switchInstruction, targets)); + } + if (!blockEntryMap.ContainsKey(block) && generated.Count > 0) blockEntryMap[block] = generated[0]; } @@ -141,7 +149,8 @@ public static void GenerateIl(MethodAnalysisContext context, MethodDefinition de pendingBlockBranchFixups.Add((bridge, falseSuccessor)); } - else if (lastInstruction.OpCode != OpCode.Jump && lastInstruction.OpCode != OpCode.Return && lastInstruction.OpCode != OpCode.IndirectJump) + else if (lastInstruction.OpCode != OpCode.Jump && lastInstruction.OpCode != OpCode.Return && + lastInstruction.OpCode != OpCode.IndirectJump && lastInstruction.OpCode != OpCode.Switch) { var successor = block.Successors.FirstOrDefault(s => s != context.ControlFlowGraph.ExitBlock); if (successor == null) continue; @@ -196,6 +205,27 @@ public static void GenerateIl(MethodAnalysisContext context, MethodDefinition de branchInstruction.Operand = new CilInstructionLabel(target); } + foreach (var (switchInstruction, targets) in pendingSwitchFixups) + { + var labels = new CilInstructionLabel[targets.Blocks.Count]; + for (var index = 0; index < targets.Blocks.Count; index++) + { + var target = ResolveBlockEntryInstruction(targets.Blocks[index], blockEntryMap); + if (target == null) + { + context.AddWarning($"Unable to resolve switch target block: {targets.Blocks[index]}"); + switchInstruction.OpCode = CilOpCodes.Nop; + switchInstruction.Operand = null; + break; + } + + labels[index] = new CilInstructionLabel(target); + } + + if (switchInstruction.OpCode == CilOpCodes.Switch) + switchInstruction.Operand = labels; + } + // Add analysis warnings var instructions = body.Instructions; foreach (var warning in context.AnalysisWarnings) @@ -420,6 +450,19 @@ private static List GenerateInstructions(Instruction instruction instructions.Add(CilOpCodes.Brtrue, new CilInstructionLabel()); break; + case OpCode.Switch: + if (instruction.Operands is [var selector, SwitchTargets targets]) + { + LoadOperand(selector, method, locals, writeLine, stringCtor); + instructions.Add(CilOpCodes.Switch, new CilInstructionLabel[targets.Blocks.Count]); + } + else + { + instructions.Add(CilOpCodes.Ldstr, $"Invalid switch instruction: {instruction}"); + instructions.Add(CilOpCodes.Call, importer.ImportMethod(writeLine)); + } + break; + case OpCode.IndirectJump: instructions.Add(CilOpCodes.Ldstr, $"Indirect jump: {instruction} (should have been resolved before IL gen)"); instructions.Add(CilOpCodes.Call, importer.ImportMethod(writeLine)); diff --git a/Cpp2IL.Core/InstructionSets/NewArmV8InstructionSet.cs b/Cpp2IL.Core/InstructionSets/NewArmV8InstructionSet.cs index 06ee78f21..afc8be84f 100644 --- a/Cpp2IL.Core/InstructionSets/NewArmV8InstructionSet.cs +++ b/Cpp2IL.Core/InstructionSets/NewArmV8InstructionSet.cs @@ -104,7 +104,7 @@ private void ConvertInstructionStatement(Arm64Instruction instruction, List operands) { addresses.Add(address); - var newInstruction = new Instruction(instructions.Count, opCode, operands); + var newInstruction = new Instruction(instructions.Count, opCode, operands) { IP = address }; instructions.Add(newInstruction); return newInstruction; } diff --git a/Cpp2IL.Core/InstructionSets/X86InstructionSet.cs b/Cpp2IL.Core/InstructionSets/X86InstructionSet.cs index 1ba04e621..0541a81cf 100644 --- a/Cpp2IL.Core/InstructionSets/X86InstructionSet.cs +++ b/Cpp2IL.Core/InstructionSets/X86InstructionSet.cs @@ -115,7 +115,7 @@ private void ConvertInstructionStatement(Instruction instruction, List operands) { addresses.Add(address); - var newInstruction = new ISIL.Instruction(instructions.Count, opCode, operands); + var newInstruction = new ISIL.Instruction(instructions.Count, opCode, operands) { IP = address }; instructions.Add(newInstruction); return newInstruction; } diff --git a/Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs b/Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs index 6ac28b9fb..c8e538ce9 100644 --- a/Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs +++ b/Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs @@ -368,6 +368,9 @@ public void Analyze() return; //Nothing to do, empty function ControlFlowGraph = new ISILControlFlowGraph(ConvertedIsil); + + // Detect switch-style jump table dispatches before stack analysis. + JumpTableRestoration.Run(this); // Indirect jumps/calls should probably be resolved here before stack analysis diff --git a/Cpp2IL.Core/Utils/X86Utils.cs b/Cpp2IL.Core/Utils/X86Utils.cs index 9fd5a78dd..f005ebfde 100644 --- a/Cpp2IL.Core/Utils/X86Utils.cs +++ b/Cpp2IL.Core/Utils/X86Utils.cs @@ -109,46 +109,66 @@ public static BinarySlice GetRawManagedOrCaCacheGenMethodBody(ulong ptr, bool is var span = rawBinary.Slice((int)rawAddr, (int)(lastPos - rawAddr + 1)); - if (TryFindJumpTableStart(span, ptr, virtStartNextFunc, out var startIndex, out var jumpTableElements)) + if (TryCheckForJumpTables(span, ptr, virtStartNextFunc, binary, out var startIndex)) { - // TODO: Figure out what to do with jumpTableElements, how do we handle returning it from this function? - // we might need to return the address it was found at in TryFindJumpTableStart function too - // Should clean up the way we handle the bytes array too - /* - foreach (var element in jumpTableElements) - //Logger.InfoNewline($"Jump table element: 0x{element:x8}."); - */ - return new BinarySlice(binary, (int)rawAddr, startIndex); + + span = span[..startIndex]; + var methodRva = binary.GetRva(ptr); + var methodRaw = binary.MapVirtualAddressToRaw(ptr, false); + var nextMethodRaw = binary.MapVirtualAddressToRaw(virtStartNextFunc, false); + Logger.InfoNewline($"Jump table scan: method VA=0x{ptr:X}, RVA=0x{methodRva:X}, raw={(methodRaw >= 0 ? $"0x{methodRaw:X}" : methodRaw.ToString())}; next VA=0x{virtStartNextFunc:X}, raw={(nextMethodRaw >= 0 ? $"0x{nextMethodRaw:X}" : nextMethodRaw.ToString())}; length=0x{span.Length:X}"); + //Debugger.Break(); } return new BinarySlice(binary, (int)rawAddr, span.Length); } - - private static bool TryFindJumpTableStart(ReadOnlySpan methodBytes, ulong methodPtr, ulong nextMethodPtr, out int startIndex, out List jumpTableElements) + + /// + /// Attempts to identify a jump table at the end of the method. This pattern is typically + /// generated by MSVC but not by GCC. + /// + private static bool TryCheckForJumpTables(ReadOnlySpan methodBytes, ulong methodPtr, ulong nextMethodPtr, Il2CppBinary binary, out int startIndex) { - bool foundTable = false; + // DynamicAtlas_IsTextureFormatSupported example of a 2 case jump table where the compiler optimized out unused cases + const int minimumConsecutiveEntries = 2; startIndex = 0; - jumpTableElements = []; - for (int i = (int)(methodPtr % 4); i < methodBytes.Length; i += 4) + + var currentRunStart = -1; + var currentRunLength = 0; + + var imageBase = methodPtr - binary.GetRva(methodPtr); + + for (int i = (int)(methodPtr % 4); i <= methodBytes.Length - sizeof(uint); i += 4) { var result = (ulong)methodBytes.ReadUInt(i); - var possibleJumpAddress = result + 0x180000000; // image base + var possibleJumpAddress = imageBase + result; if (possibleJumpAddress > methodPtr && possibleJumpAddress < nextMethodPtr) { - // Sound the alarms, we've more than likely ran into a jump table - if (!foundTable) - { - startIndex = i; - foundTable = true; - } - - jumpTableElements.Add(result); + if (currentRunStart == -1) + currentRunStart = i; + + currentRunLength++; + continue; + } + + if (currentRunLength >= minimumConsecutiveEntries) + { + startIndex = currentRunStart; + return true; } + + currentRunStart = -1; + currentRunLength = 0; } - return foundTable; - } + if (currentRunLength >= minimumConsecutiveEntries) + { + startIndex = currentRunStart; + return true; + } + return false; + } public static InstructionList GetMethodBodyAtVirtAddressNew(ulong addr, bool peek, Il2CppBinary binary) => GetMethodBodyAtVirtAddressNew(addr, peek, binary, out _); public static InstructionList GetMethodBodyAtVirtAddressNew(ulong addr, bool peek, Il2CppBinary binary, out BinarySlice rawBytes)