diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs index 6ffefade4c..1d6c29e16e 100644 --- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs @@ -339,6 +339,12 @@ public async Task ExtensionEncodingV2() await Run(); } + [Test] + public async Task SortSwitchSections() + { + await Run(settings: new DecompilerSettings { SortSwitchSections = true, FileScopedNamespaces = false }); + } + async Task Run([CallerMemberName] string testName = null, DecompilerSettings settings = null, AssemblerOptions assemblerOptions = AssemblerOptions.Library) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SortSwitchSections.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SortSwitchSections.cs new file mode 100644 index 0000000000..2b0c1b0b72 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SortSwitchSections.cs @@ -0,0 +1,35 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty +{ + public static class SortSwitchSections + { + public static int PickValue(int n) + { + return n switch { + 0 => 100, + 1 => 200, + 2 => 300, + 3 => 400, + 4 => 500, + _ => -1, + }; + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SortSwitchSections.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SortSwitchSections.il new file mode 100644 index 0000000000..a76bf07598 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SortSwitchSections.il @@ -0,0 +1,64 @@ +// Test fixture for the SortSwitchSections decompiler setting. +// +// The IL `switch` table targets are placed at non-monotonic offsets, +// simulating obfuscator block-reordering. With SortSwitchSections=false +// (default) the decompiler would emit cases in IL-offset order (3, 0, 4, 1, 2); +// with the setting enabled they are emitted in label-value order (0..4). +// The matching .cs file is the expected output with the setting enabled. + +.assembly extern mscorlib +{ + .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) + .ver 4:0:0:0 +} +.assembly SortSwitchSections +{ + .ver 1:0:0:0 +} +.module SortSwitchSections.dll +.imagebase 0x10000000 +.file alignment 0x00000200 +.stackreserve 0x00100000 +.subsystem 0x0003 // WINDOWS_CUI +.corflags 0x00000001 // ILONLY + +.class public auto ansi abstract sealed beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SortSwitchSections + extends [mscorlib]System.Object +{ + .method public hidebysig static int32 PickValue(int32 n) cil managed + { + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: switch ( + LBL_0, + LBL_1, + LBL_2, + LBL_3, + LBL_4) + IL_001a: br.s LBL_DEFAULT + + LBL_3: + IL_001c: ldc.i4 400 + IL_0021: ret + + LBL_0: + IL_0022: ldc.i4 100 + IL_0027: ret + + LBL_4: + IL_0028: ldc.i4 500 + IL_002d: ret + + LBL_1: + IL_002e: ldc.i4 200 + IL_0033: ret + + LBL_2: + IL_0034: ldc.i4 300 + IL_0039: ret + + LBL_DEFAULT: + IL_003a: ldc.i4.m1 + IL_003b: ret + } +} diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index e9aeefb733..c9da954d68 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -2375,6 +2375,26 @@ public bool SortCustomAttributes { } } + bool sortSwitchSections = false; + + /// + /// Sort switch sections by their label value instead of by IL offset. + /// Useful when diffing decompiler output across rebuilds of obfuscated assemblies, + /// where IL block layout is unstable but the case-to-value mapping is not. + /// + [Category("DecompilerSettings.Other")] + [Description("DecompilerSettings.SortSwitchSections")] + public bool SortSwitchSections { + get { return sortSwitchSections; } + set { + if (sortSwitchSections != value) + { + sortSwitchSections = value; + OnPropertyChanged(); + } + } + } + bool checkForOverflowUnderflow = false; /// diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/SwitchDetection.cs b/ICSharpCode.Decompiler/IL/ControlFlow/SwitchDetection.cs index 29d16e1ccf..edc2d43b93 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/SwitchDetection.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/SwitchDetection.cs @@ -208,7 +208,7 @@ void ProcessBlock(Block block, ref bool blockContainerNeedsCleanup) controlFlowGraph = null; // control flow graph is no-longer valid blockContainerNeedsCleanup = true; - SortSwitchSections(sw); + SortSwitchSections(sw, context); } else { @@ -249,16 +249,23 @@ internal static void SimplifySwitchInstruction(Block block, ILTransformContext c return false; }); AdjustLabels(sw, context); - SortSwitchSections(sw); + SortSwitchSections(sw, context); } - static void SortSwitchSections(SwitchInstruction sw) + static void SortSwitchSections(SwitchInstruction sw, ILTransformContext context) { - sw.Sections.ReplaceList(sw.Sections.OrderBy(s => s.Body switch { - Branch b => b.TargetILOffset, - Leave l => l.StartILOffset, - _ => (int?)null - }).ThenBy(s => s.Labels.Values.FirstOrDefault())); + if (context.Settings.SortSwitchSections) + { + sw.Sections.ReplaceList(sw.Sections.OrderBy(s => s.Labels.Values.FirstOrDefault())); + } + else + { + sw.Sections.ReplaceList(sw.Sections.OrderBy(s => s.Body switch { + Branch b => b.TargetILOffset, + Leave l => l.StartILOffset, + _ => (int?)null + }).ThenBy(s => s.Labels.Values.FirstOrDefault())); + } } static void AdjustLabels(SwitchInstruction sw, ILTransformContext context) diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index ba7df50e34..a3f3f8f0f3 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1405,7 +1405,16 @@ public static string DecompilerSettings_SortCustomAttributes { return ResourceManager.GetString("DecompilerSettings.SortCustomAttributes", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Sort switch sections by case label value. + /// + public static string DecompilerSettings_SortSwitchSections { + get { + return ResourceManager.GetString("DecompilerSettings.SortSwitchSections", resourceCulture); + } + } + /// /// Looks up a localized string similar to Detect switch on integer even if IL code does not use a jump table. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 5accafe41c..3b94c69b85 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -489,6 +489,9 @@ Are you sure you want to continue? Sort custom attributes + + Sort switch sections by case label value + Detect switch on integer even if IL code does not use a jump table