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)