using System.Collections.Generic; using System.Diagnostics; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.Util; using System; using System.Linq; namespace ICSharpCode.Decompiler.IL.ControlFlow { /// /// C# switch statements are not necessarily compiled into IL switch instructions. /// For example, when the label values are not contiguous, the C# compiler /// will generate if statements similar to a binary search. /// /// This class analyses such sequences of if statements to reconstruct the original switch. /// /// /// This analysis expects to be run on basic blocks (not extended basic blocks). /// class SwitchAnalysis { /// /// The variable that is used to represent the switch expression. /// null while analyzing the first block. /// ILVariable switchVar; /// /// The variable to be used as the argument of the switch instruction. /// public ILVariable SwitchVariable { get { return switchVar; } } /// /// Whether at least one the analyzed blocks contained an IL switch constructors. /// public bool ContainsILSwitch { get; private set; } /// /// Gets the sections that were detected by the previous AnalyzeBlock() call. /// public readonly List> Sections = new List>(); /// /// Used to de-duplicate sections with a branch instruction. /// Invariant: (Sections[targetBlockToSectionIndex[branch.TargetBlock]].Instruction as Branch).TargetBlock == branch.TargetBlock /// readonly Dictionary targetBlockToSectionIndex = new Dictionary(); /// /// Used to de-duplicate sections with a value-less leave instruction. /// Invariant: (Sections[targetBlockToSectionIndex[leave.TargetContainer]].Instruction as Leave).TargetContainer == leave.TargetContainer /// readonly Dictionary targetContainerToSectionIndex = new Dictionary(); /// /// Blocks that can be deleted if the tail of the initial block is replaced with a switch instruction. /// public readonly List InnerBlocks = new List(); public Block RootBlock { get; private set; } /// /// Analyze the last two statements in the block and see if they can be turned into a /// switch instruction. /// /// true if the block could be analyzed successfully; false otherwise public bool AnalyzeBlock(Block block) { switchVar = null; RootBlock = block; targetBlockToSectionIndex.Clear(); targetContainerToSectionIndex.Clear(); Sections.Clear(); InnerBlocks.Clear(); ContainsILSwitch = false; return AnalyzeBlock(block, LongSet.Universe, tailOnly: true); } /// /// Analyzes the tail end (last two instructions) of a block. /// /// /// Sets switchVar and defaultInstruction if they are null, /// and adds found sections to sectionLabels and sectionInstructions. /// /// If the function returns false, sectionLabels and sectionInstructions are unmodified. /// /// The block to analyze. /// The possible values of the "interesting" variable /// when control flow reaches this block. /// If true, analyze only the tail (last two instructions). /// If false, analyze the whole block. bool AnalyzeBlock(Block block, LongSet inputValues, bool tailOnly = false) { if (block.Instructions.Count == 0) { // might happen if the block was already marked for deletion in SwitchDetection return false; } if (tailOnly) { Debug.Assert(block == RootBlock); } else { Debug.Assert(switchVar != null); // switchVar should always be determined by the top-level call if (block.IncomingEdgeCount != 1 || block == RootBlock) return false; // for now, let's only consider if-structures that form a tree if (block.Parent != RootBlock.Parent) return false; // all blocks should belong to the same container } LongSet trueValues; if (block.Instructions.Count >= 2 && block.Instructions[block.Instructions.Count - 2].MatchIfInstruction(out var condition, out var trueInst) && AnalyzeCondition(condition, out trueValues) ) { if (!(tailOnly || block.Instructions.Count == 2)) return false; trueValues = trueValues.IntersectWith(inputValues); if (trueValues.SetEquals(inputValues) || trueValues.IsEmpty) return false; Block trueBlock; if (trueInst.MatchBranch(out trueBlock) && AnalyzeBlock(trueBlock, trueValues)) { // OK, true block was further analyzed. InnerBlocks.Add(trueBlock); } else { // Create switch section for trueInst. AddSection(trueValues, trueInst); } } else if (block.Instructions.Last() is SwitchInstruction switchInst) { if (!(tailOnly || block.Instructions.Count == 1)) return false; if (AnalyzeSwitch(switchInst, inputValues)) { ContainsILSwitch = true; // OK return true; } else { // switch analysis failed (e.g. switchVar mismatch) return false; } } else { // unknown inst return false; } var remainingValues = inputValues.ExceptWith(trueValues); ILInstruction falseInst = block.Instructions.Last(); Block falseBlock; if (falseInst.MatchBranch(out falseBlock) && AnalyzeBlock(falseBlock, remainingValues)) { // OK, false block was further analyzed. InnerBlocks.Add(falseBlock); } else { // Create switch section for falseInst. AddSection(remainingValues, falseInst); } return true; } private bool AnalyzeSwitch(SwitchInstruction inst, LongSet inputValues) { Debug.Assert(!inst.IsLifted); long offset; if (MatchSwitchVar(inst.Value)) { offset = 0; } else if (inst.Value is BinaryNumericInstruction bop) { if (bop.CheckForOverflow) return false; if (MatchSwitchVar(bop.Left) && bop.Right.MatchLdcI(out long val)) { switch (bop.Operator) { case BinaryNumericOperator.Add: offset = unchecked(-val); break; case BinaryNumericOperator.Sub: offset = val; break; default: // unknown bop.Operator return false; } } else { // unknown bop.Left return false; } } else { // unknown inst.Value return false; } foreach (var section in inst.Sections) { var matchValues = section.Labels.AddOffset(offset).IntersectWith(inputValues); if (matchValues.Count() > 1 && section.Body.MatchBranch(out var targetBlock) && AnalyzeBlock(targetBlock, matchValues)) { InnerBlocks.Add(targetBlock); } else { AddSection(matchValues, section.Body); } } return true; } /// /// Adds a new section to the Sections list. /// /// If the instruction is a branch instruction, unify the new section with an existing section /// that also branches to the same target. /// void AddSection(LongSet values, ILInstruction inst) { if (values.IsEmpty) { return; } if (inst.MatchBranch(out Block targetBlock)) { if (targetBlockToSectionIndex.TryGetValue(targetBlock, out int index)) { Sections[index] = new KeyValuePair( Sections[index].Key.UnionWith(values), inst ); } else { targetBlockToSectionIndex.Add(targetBlock, Sections.Count); Sections.Add(new KeyValuePair(values, inst)); } } else if (inst.MatchLeave(out BlockContainer targetContainer)) { if (targetContainerToSectionIndex.TryGetValue(targetContainer, out int index)) { Sections[index] = new KeyValuePair( Sections[index].Key.UnionWith(values), inst ); } else { targetContainerToSectionIndex.Add(targetContainer, Sections.Count); Sections.Add(new KeyValuePair(values, inst)); } } else { Sections.Add(new KeyValuePair(values, inst)); } } bool MatchSwitchVar(ILInstruction inst) { if (switchVar != null) return inst.MatchLdLoc(switchVar); else return inst.MatchLdLoc(out switchVar); } bool MatchSwitchVar(ILInstruction inst, out long sub) { if (inst is BinaryNumericInstruction bn && bn.Operator == BinaryNumericOperator.Sub && !bn.CheckForOverflow && !bn.IsLifted && bn.Right.MatchLdcI(out sub)) { return MatchSwitchVar(bn.Left); } sub = 0; return MatchSwitchVar(inst); } /// /// Analyzes the boolean condition, returning the set of values of the interesting /// variable for which the condition evaluates to true. /// private bool AnalyzeCondition(ILInstruction condition, out LongSet trueValues) { if (condition is Comp comp && MatchSwitchVar(comp.Left, out var sub) && comp.Right.MatchLdcI(out long val)) { // if (comp((V - sub) OP val)) trueValues = MakeSetWhereComparisonIsTrue(comp.Kind, val, comp.Sign); trueValues = trueValues.AddOffset(sub); return true; } else if (MatchSwitchVar(condition)) { // if (ldloc V) --> branch for all values except 0 trueValues = new LongSet(0).Invert(); return true; } else if (condition.MatchLogicNot(out ILInstruction arg)) { // if (logic.not(X)) --> branch for all values where if (X) does not branch bool res = AnalyzeCondition(arg, out LongSet falseValues); trueValues = falseValues.Invert(); return res; } else { trueValues = LongSet.Empty; return false; } } /// /// Create the LongSet that contains a value x iff x compared with value is true. /// internal static LongSet MakeSetWhereComparisonIsTrue(ComparisonKind kind, long val, Sign sign) { switch (kind) { case ComparisonKind.Equality: return new LongSet(val); case ComparisonKind.Inequality: return new LongSet(val).Invert(); case ComparisonKind.LessThan: return MakeGreaterThanOrEqualSet(val, sign).Invert(); case ComparisonKind.LessThanOrEqual: return MakeLessThanOrEqualSet(val, sign); case ComparisonKind.GreaterThan: return MakeLessThanOrEqualSet(val, sign).Invert(); case ComparisonKind.GreaterThanOrEqual: return MakeGreaterThanOrEqualSet(val, sign); default: throw new ArgumentException("Invalid ComparisonKind"); } } private static LongSet MakeGreaterThanOrEqualSet(long val, Sign sign) { if (sign == Sign.Signed) { return new LongSet(LongInterval.Inclusive(val, long.MaxValue)); } else { Debug.Assert(sign == Sign.Unsigned); if (val >= 0) { // The range val to ulong.MaxValue expressed with signed longs // is not a single contiguous range, but two ranges: return new LongSet(LongInterval.Inclusive(val, long.MaxValue)) .UnionWith(new LongSet(new LongInterval(long.MinValue, 0))); } else { return new LongSet(new LongInterval(val, 0)); } } } private static LongSet MakeLessThanOrEqualSet(long val, Sign sign) { if (sign == Sign.Signed) { return new LongSet(LongInterval.Inclusive(long.MinValue, val)); } else { Debug.Assert(sign == Sign.Unsigned); if (val >= 0) { return new LongSet(LongInterval.Inclusive(0, val)); } else { // The range 0 to (ulong)val expressed with signed longs // is not a single contiguous range, but two ranges: return new LongSet(LongInterval.Inclusive(0, long.MaxValue)) .UnionWith(new LongSet(LongInterval.Inclusive(long.MinValue, val))); } } } } }