diff options
author | Vitek Karas <vitek.karas@microsoft.com> | 2021-08-18 17:20:54 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-18 17:20:54 +0300 |
commit | e9403e206860e9569ea59af8f3241922564c8c2b (patch) | |
tree | 8327f75b7e10e4751de1a191c7b6852c3ac5cf56 | |
parent | 5b2391c2c56af47350a5789375e8dbddc692e67f (diff) |
Implement a custom ILProcessor and make it auto-fix IL corruptions (#2213)
Cecil doesn't handle IL editing well. It doesn't patch scopes and branches. Added a linker-private IL processor which does this.
Added tests for the IL corruption when removing branches around code which has exception handlers.
I also removed now unnecessary test attribute, since in my previous change the exception handlers are now validated within the instruction stream, so no need for a second attribute to validate these.
Co-authored-by: Sven Boemer <sbomer@gmail.com>
13 files changed, 372 insertions, 115 deletions
diff --git a/src/linker/Linker.Steps/CodeRewriterStep.cs b/src/linker/Linker.Steps/CodeRewriterStep.cs index aa2279291..bd730eb64 100644 --- a/src/linker/Linker.Steps/CodeRewriterStep.cs +++ b/src/linker/Linker.Steps/CodeRewriterStep.cs @@ -39,7 +39,7 @@ namespace Mono.Linker.Steps void AddFieldsInitializations (TypeDefinition type) { Instruction ret; - ILProcessor processor; + LinkerILProcessor processor; var cctor = type.Methods.FirstOrDefault (MethodDefinitionExtensions.IsStaticConstructor); if (cctor == null) { @@ -51,13 +51,13 @@ namespace Mono.Linker.Steps type.Methods.Add (method); - processor = method.Body.GetILProcessor (); + processor = method.Body.GetLinkerILProcessor (); ret = Instruction.Create (OpCodes.Ret); processor.Append (ret); } else { ret = cctor.Body.Instructions.Last (l => l.OpCode.Code == Code.Ret); var body = cctor.Body; - processor = cctor.Body.GetILProcessor (); + processor = cctor.Body.GetLinkerILProcessor (); for (int i = 0; i < body.Instructions.Count; ++i) { var instr = body.Instructions[i]; @@ -122,7 +122,7 @@ namespace Mono.Linker.Steps MethodBody CreateThrowLinkedAwayBody (MethodDefinition method) { var body = new MethodBody (method); - var il = body.GetILProcessor (); + var il = body.GetLinkerILProcessor (); MethodReference ctor; // Makes the body verifiable @@ -151,7 +151,7 @@ namespace Mono.Linker.Steps if (method.HasParameters && method.Parameters.Any (l => l.IsOut)) throw new NotSupportedException ($"Cannot replace body of method '{method.GetDisplayName ()}' because it has an out parameter."); - var il = body.GetILProcessor (); + var il = body.GetLinkerILProcessor (); if (method.IsInstanceConstructor () && !method.DeclaringType.IsValueType) { var baseType = Context.Resolve (method.DeclaringType.BaseType); if (baseType is null) @@ -184,7 +184,7 @@ namespace Mono.Linker.Steps return body; } - static void StubComplexBody (MethodDefinition method, MethodBody body, ILProcessor il) + static void StubComplexBody (MethodDefinition method, MethodBody body, LinkerILProcessor il) { switch (method.ReturnType.MetadataType) { case MetadataType.MVar: diff --git a/src/linker/Linker.Steps/MarkStep.cs b/src/linker/Linker.Steps/MarkStep.cs index ad2de5f04..ec69753c1 100644 --- a/src/linker/Linker.Steps/MarkStep.cs +++ b/src/linker/Linker.Steps/MarkStep.cs @@ -490,41 +490,14 @@ namespace Mono.Linker.Steps continue; Instruction instr = item.Instr; - ILProcessor ilProcessor = item.Body.GetILProcessor (); + LinkerILProcessor ilProcessor = item.Body.GetLinkerILProcessor (); ilProcessor.InsertAfter (instr, Instruction.Create (OpCodes.Ldnull)); Instruction new_instr = Instruction.Create (OpCodes.Pop); ilProcessor.Replace (instr, new_instr); - UpdateBranchTarget (item.Body, instr, new_instr); _context.LogMessage ($"Removing typecheck of '{type.FullName}' inside {item.Body.Method.GetDisplayName ()} method"); } - - static void UpdateBranchTarget (MethodBody body, Instruction oldTarget, Instruction newTarget) - { - foreach (var instr in body.Instructions) { - switch (instr.OpCode.FlowControl) { - case FlowControl.Branch: - case FlowControl.Cond_Branch: - if (instr.Operand == oldTarget) - instr.Operand = newTarget; - break; - } - } - - foreach (var handler in body.ExceptionHandlers) { - if (handler.TryStart == oldTarget) - handler.TryStart = newTarget; - if (handler.TryEnd == oldTarget) - handler.TryEnd = newTarget; - if (handler.HandlerStart == oldTarget) - handler.HandlerStart = newTarget; - if (handler.HandlerEnd == oldTarget) - handler.HandlerEnd = newTarget; - if (handler.FilterStart == oldTarget) - handler.FilterStart = newTarget; - } - } } void ProcessQueue () diff --git a/src/linker/Linker.Steps/UnreachableBlocksOptimizer.cs b/src/linker/Linker.Steps/UnreachableBlocksOptimizer.cs index 17be828b5..cd9713f98 100644 --- a/src/linker/Linker.Steps/UnreachableBlocksOptimizer.cs +++ b/src/linker/Linker.Steps/UnreachableBlocksOptimizer.cs @@ -662,7 +662,7 @@ namespace Mono.Linker.Steps RemoveUnreachableInstructions (reachableInstrs); if (nopInstructions != null) { - ILProcessor processor = Body.GetILProcessor (); + LinkerILProcessor processor = Body.GetLinkerILProcessor (); foreach (var instr in nopInstructions) processor.Remove (instr); @@ -673,7 +673,7 @@ namespace Mono.Linker.Steps void RemoveUnreachableInstructions (BitArray reachable) { - ILProcessor processor = Body.GetILProcessor (); + LinkerILProcessor processor = Body.GetLinkerILProcessor (); int removed = 0; for (int i = 0; i < reachable.Count; ++i) { @@ -1114,7 +1114,7 @@ namespace Mono.Linker.Steps readonly BitArray reachable; readonly List<ExceptionHandler> unreachableExceptionHandlers; readonly LinkContext context; - ILProcessor ilprocessor; + LinkerILProcessor ilprocessor; public BodySweeper (MethodBody body, BitArray reachable, List<ExceptionHandler> unreachableEH, LinkContext context) { @@ -1155,7 +1155,7 @@ namespace Mono.Linker.Steps } } - ilprocessor = body.GetILProcessor (); + ilprocessor = body.GetLinkerILProcessor (); return true; } diff --git a/src/linker/Linker/LinkerILProcessor.cs b/src/linker/Linker/LinkerILProcessor.cs new file mode 100644 index 000000000..a2ee2c049 --- /dev/null +++ b/src/linker/Linker/LinkerILProcessor.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace Mono.Linker +{ + class LinkerILProcessor + { + readonly ILProcessor _ilProcessor; + + Collections.Generic.Collection<Instruction> Instructions => _ilProcessor.Body.Instructions; + + internal LinkerILProcessor (MethodBody body) + { + _ilProcessor = body.GetILProcessor (); + } + + public void Emit (OpCode opcode) => Append (_ilProcessor.Create (opcode)); + public void Emit (OpCode opcode, TypeReference type) => Append (_ilProcessor.Create (opcode, type)); + public void Emit (OpCode opcode, MethodReference method) => Append (_ilProcessor.Create (opcode, method)); + public void Emit (OpCode opcode, VariableDefinition variable) => Append (_ilProcessor.Create (opcode, variable)); + + public void Emit (OpCode opcode, string value) => Append (_ilProcessor.Create (opcode, value)); + public void InsertBefore (Instruction target, Instruction instruction) + { + // When inserting before, all pointers have to be updated to the new "before" instruction. + RedirectScopeStart (target, instruction); + ReplaceInstructionReference (target, instruction); + _ilProcessor.InsertBefore (target, instruction); + } + + // When inserting after, no redirection is necessary since it will naturally be "appended" to the same blocks + // as the target instruction. + public void InsertAfter (Instruction target, Instruction instruction) + { + RedirectScopeEnd (target, instruction); + _ilProcessor.InsertAfter (target, instruction); + } + + public void Append (Instruction instruction) + { + Instruction lastInstruction = Instructions.Count == 0 ? null : Instructions[Instructions.Count - 1]; + RedirectScopeEnd (lastInstruction, instruction); + _ilProcessor.Append (instruction); + } + + public void Replace (Instruction target, Instruction instruction) + { + RedirectScopeStart (target, instruction); + RedirectScopeEnd (target, instruction); + ReplaceInstructionReference (target, instruction); + _ilProcessor.Replace (target, instruction); + } + + public void Replace (int index, Instruction instruction) => Replace (_ilProcessor.Body.Instructions[index], instruction); + + public void Remove (Instruction instruction) + { + int index = _ilProcessor.Body.Instructions.IndexOf (instruction); + if (index == -1) + throw new ArgumentOutOfRangeException (nameof (instruction)); + + Instruction nextInstruction = Instructions.Count == index + 1 ? null : Instructions[index + 1]; + Instruction previousInstruction = index == 0 ? null : Instructions[index - 1]; + + RedirectScopeStart (instruction, nextInstruction); + RedirectScopeEnd (instruction, previousInstruction); + ReplaceInstructionReference (instruction, nextInstruction); + _ilProcessor.Remove (instruction); + } + + public void RemoveAt (int index) => Remove (Instructions[index]); + + void RedirectScopeStart (Instruction oldTarget, Instruction newTarget) + { + // In Cecil "start" pointers point to the first instruction in a given scope + // and the "end" pointers point to the first instruction after the given block + // so they need to effectively point to the start of the next scope. + // That's why both start and end are handled in the RedirectScopeStart. + foreach (var handler in _ilProcessor.Body.ExceptionHandlers) { + if (handler.TryStart == oldTarget) + handler.TryStart = newTarget; + if (handler.TryEnd == oldTarget) + handler.TryEnd = newTarget; + if (handler.HandlerStart == oldTarget) + handler.HandlerStart = newTarget; + if (handler.HandlerEnd == oldTarget) + handler.HandlerEnd = newTarget; + if (handler.FilterStart == oldTarget) + handler.FilterStart = newTarget; + } + } + +#pragma warning disable IDE0060 // Remove unused parameter + static void RedirectScopeEnd (Instruction oldTarget, Instruction newTarget) +#pragma warning restore IDE0060 // Remove unused parameter + { + // Currently Cecil treats all block boundaries as "starts" + // so nothing to do here. + } + + void ReplaceInstructionReference (Instruction oldTarget, Instruction newTarget) + { + foreach (var instr in Instructions) { + switch (instr.OpCode.FlowControl) { + case FlowControl.Branch: + case FlowControl.Cond_Branch: + if (instr.Operand == oldTarget) + instr.Operand = newTarget; + break; + } + } + } + } + + static class ILProcessorExtensions + { + public static LinkerILProcessor GetLinkerILProcessor (this MethodBody body) + { + return new LinkerILProcessor (body); + } + } +} diff --git a/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectedExceptionHandlerSequenceAttribute.cs b/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectedExceptionHandlerSequenceAttribute.cs deleted file mode 100644 index 0c9f10e4a..000000000 --- a/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectedExceptionHandlerSequenceAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Mono.Linker.Tests.Cases.Expectations.Assertions -{ - [AttributeUsage (AttributeTargets.Method | AttributeTargets.Constructor, Inherited = false, AllowMultiple = false)] - public class ExpectedExceptionHandlerSequenceAttribute : BaseInAssemblyAttribute - { - public ExpectedExceptionHandlerSequenceAttribute (string[] types) - { - if (types == null) - throw new ArgumentNullException (nameof (types)); - } - } -}
\ No newline at end of file diff --git a/test/Mono.Linker.Tests.Cases/BCLFeatures/ETW/StubbedMethodWithExceptionHandlers.cs b/test/Mono.Linker.Tests.Cases/BCLFeatures/ETW/StubbedMethodWithExceptionHandlers.cs index 0a988d986..73c6820a9 100644 --- a/test/Mono.Linker.Tests.Cases/BCLFeatures/ETW/StubbedMethodWithExceptionHandlers.cs +++ b/test/Mono.Linker.Tests.Cases/BCLFeatures/ETW/StubbedMethodWithExceptionHandlers.cs @@ -45,7 +45,6 @@ namespace Mono.Linker.Tests.Cases.BCLFeatures.ETW { "ret" })] - [ExpectedExceptionHandlerSequence (new string[0])] protected override void OnEventCommand (EventCommandEventArgs command) { try { diff --git a/test/Mono.Linker.Tests.Cases/UnreachableBlock/Dependencies/EndScopeOnMethod.il b/test/Mono.Linker.Tests.Cases/UnreachableBlock/Dependencies/EndScopeOnMethod.il new file mode 100644 index 000000000..fe2b7dde5 --- /dev/null +++ b/test/Mono.Linker.Tests.Cases/UnreachableBlock/Dependencies/EndScopeOnMethod.il @@ -0,0 +1,40 @@ +.assembly extern mscorlib +{ +} +.assembly 'library' +{ + .hash algorithm 0x00008004 + .ver 0:0:0:0 +} +.module library.dll + +.namespace Mono.Linker.Tests.Cases.UnreachableBlock.Dependencies +{ + + .class public auto ansi beforefieldinit EndScopeOnMethod + extends [mscorlib]System.Object + { + + .method public hidebysig specialname rtspecialname + instance default void '.ctor' () cil managed + { + IL_0000: ldarg.0 + IL_0001: call instance void class [mscorlib]System.Object::'.ctor'() + IL_0006: ret + } + + .method public static hidebysig object TryFinally() cil managed + { + .try + { + ldnull + ret + } + finally + { + ldnull + ret + } + } + } +}
\ No newline at end of file diff --git a/test/Mono.Linker.Tests.Cases/UnreachableBlock/EndScopeOnMethoEnd.cs b/test/Mono.Linker.Tests.Cases/UnreachableBlock/EndScopeOnMethoEnd.cs new file mode 100644 index 000000000..be542f9b1 --- /dev/null +++ b/test/Mono.Linker.Tests.Cases/UnreachableBlock/EndScopeOnMethoEnd.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Mono.Linker.Tests.Cases.Expectations.Metadata; + +namespace Mono.Linker.Tests.Cases.UnreachableBlock +{ + [SetupLinkerArgument ("--skip-unresolved", "true")] + [Define ("IL_ASSEMBLY_AVAILABLE")] + [SetupCompileBefore ("library.dll", new[] { "Dependencies/EndScopeOnMethod.il" })] + public class EndScopeOnMethoEnd + { + public static void Main () + { +#if IL_ASSEMBLY_AVAILABLE + // For now just have a method where the try/finally is the last thing in the method (no instruction after the + // end of finally - Roslyn doesn't seem to produce such method body. + Mono.Linker.Tests.Cases.UnreachableBlock.Dependencies.EndScopeOnMethod.TryFinally (); +#endif + } + } +} diff --git a/test/Mono.Linker.Tests.Cases/UnreachableBlock/ReplacedReturns.cs b/test/Mono.Linker.Tests.Cases/UnreachableBlock/ReplacedReturns.cs index 1ac92c851..a4728c606 100644 --- a/test/Mono.Linker.Tests.Cases/UnreachableBlock/ReplacedReturns.cs +++ b/test/Mono.Linker.Tests.Cases/UnreachableBlock/ReplacedReturns.cs @@ -241,7 +241,6 @@ namespace Mono.Linker.Tests.Cases.UnreachableBlock } [Kept] - [ExpectedExceptionHandlerSequence (new string[0])] [ExpectedLocalsSequence (new string[0])] [ExpectedInstructionSequence (new[] { "call", diff --git a/test/Mono.Linker.Tests.Cases/UnreachableBlock/TryCatchBlocks.cs b/test/Mono.Linker.Tests.Cases/UnreachableBlock/TryCatchBlocks.cs index bf1f4f852..465c0bf1d 100644 --- a/test/Mono.Linker.Tests.Cases/UnreachableBlock/TryCatchBlocks.cs +++ b/test/Mono.Linker.Tests.Cases/UnreachableBlock/TryCatchBlocks.cs @@ -10,43 +10,84 @@ namespace Mono.Linker.Tests.Cases.UnreachableBlock { public static void Main () { - TestSimpleTryUnreachable (); + TryCatchInRemovedBranch.Test (); + TryCatchInKeptBranchBeforeRemovedBranch.Test (); } - [Kept] - [ExpectedInstructionSequence (new[] { - "call", - "ldc.i4.6", - "beq.s il_8", - "ldc.i4.3", - "ret" - })] - [ExpectedExceptionHandlerSequence (new string[0])] - [ExpectedLocalsSequence (new string[0])] - static int TestSimpleTryUnreachable () + class TryCatchInRemovedBranch { - if (Prop != 6) { - try { - Unreached_1 (); - return 1; - } catch { - return 2; + [Kept] + [ExpectedInstructionSequence (new[] { + "call", + "ldc.i4.6", + "beq.s il_8", + "ldc.i4.3", + "ret" + })] + [ExpectedLocalsSequence (new string[0])] + public static int Test () + { + if (Prop != 6) { + try { + Unreached_1 (); + return 1; + } catch { + return 2; + } } - } - return 3; - } + return 3; + } - [Kept] - static int Prop { [Kept] - get { - return 6; + static int Prop { + [Kept] + get { + return 6; + } + } + + static void Unreached_1 () + { } } - static void Unreached_1 () + class TryCatchInKeptBranchBeforeRemovedBranch { + [Kept] + [ExpectedInstructionSequence (new[] { + "call", + "pop", + ".try", + "call", + "leave.s il_15", + ".endtry", + ".catch", + "pop", + "call", + "leave.s il_15", + ".endcatch", + "ret", + })] + public static void Test () + { + if (Prop == 0) { + try { Reached (); } catch { Reached_2 (); } + } else { + Unreached (); + } + } + + [Kept] + static int Prop { [Kept] get => 0; } + + [Kept] + static void Reached () { } + + [Kept] + static void Reached_2 () { } + + static void Unreached () { } } } }
\ No newline at end of file diff --git a/test/Mono.Linker.Tests.Cases/UnreachableBlock/TryFilterBlocks.cs b/test/Mono.Linker.Tests.Cases/UnreachableBlock/TryFilterBlocks.cs index 2c9c344f5..af5a20821 100644 --- a/test/Mono.Linker.Tests.Cases/UnreachableBlock/TryFilterBlocks.cs +++ b/test/Mono.Linker.Tests.Cases/UnreachableBlock/TryFilterBlocks.cs @@ -35,7 +35,6 @@ namespace Mono.Linker.Tests.Cases.UnreachableBlock "ldc.i4.2", "ret", })] - [ExpectedExceptionHandlerSequence (new string[] { "filter" })] static int TestUnreachableInsideTry () { try { @@ -70,7 +69,6 @@ namespace Mono.Linker.Tests.Cases.UnreachableBlock "ldc.i4.3", "ret", })] - [ExpectedExceptionHandlerSequence (new string[] { "filter" })] static int TestUnreachableInsideFilterCondition () { try { diff --git a/test/Mono.Linker.Tests.Cases/UnreachableBlock/TryFinallyBlocks.cs b/test/Mono.Linker.Tests.Cases/UnreachableBlock/TryFinallyBlocks.cs index 1c1dfdabd..2a62bf523 100644 --- a/test/Mono.Linker.Tests.Cases/UnreachableBlock/TryFinallyBlocks.cs +++ b/test/Mono.Linker.Tests.Cases/UnreachableBlock/TryFinallyBlocks.cs @@ -10,36 +10,107 @@ namespace Mono.Linker.Tests.Cases.UnreachableBlock { public static void Main () { - TestSimpleTry (); + TryFinallyInConstantProperty.Test (); + TryFinallyInRemovedBranch.Test (); + TryFinallyInKeptBranchBeforeRemovedBranch.Test (); } - [Kept] - [ExpectedInstructionSequence (new[] { - "call", - "ldc.i4.3", - "beq.s il_8", - "ret" - })] - static void TestSimpleTry () + class TryFinallyInConstantProperty { - if (Prop != 3) - Unreached_1 (); - } + [Kept] + [ExpectedInstructionSequence (new[] { + "call", + "ldc.i4.3", + "beq.s il_8", + "ret" + })] + public static void Test () + { + if (Prop != 3) + Unreached_1 (); + } - [Kept] - static int Prop { [Kept] - get { - try { - return 3; - } finally { + static int Prop { + [Kept] + get { + try { + return 3; + } finally { + } } } + + static void Unreached_1 () + { + } } - static void Unreached_1 () + class TryFinallyInRemovedBranch { + [Kept] + [ExpectedInstructionSequence (new[] { + "call", + "pop", + "call", + "ret", + })] + public static void Test () + { + if (Prop == 0) { + Reached (); + } else { + try { Unreached (); } finally { Unreached_2 (); } + } + } + + [Kept] + static int Prop { [Kept] get => 0; } + + [Kept] + static void Reached () { } + + static void Unreached () { } + + static void Unreached_2 () { } + } + + class TryFinallyInKeptBranchBeforeRemovedBranch + { + [Kept] + [ExpectedInstructionSequence (new[] { + "call", + "pop", + ".try", + "call", + "leave.s il_13", + ".endtry", + ".catch", + "call", + "endfinally", + ".endcatch", + "ret", + })] + public static void Test () + { + if (Prop == 0) { + try { Reached (); } finally { Reached_2 (); } + } else { + Unreached (); + } + } + + [Kept] + static int Prop { [Kept] get => 0; } + + [Kept] + static void Reached () { } + + [Kept] + static void Reached_2 () { } + + static void Unreached () { } } } }
\ No newline at end of file diff --git a/test/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs b/test/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs index 0bc38dc48..6b0bd75c5 100644 --- a/test/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs +++ b/test/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs @@ -377,7 +377,6 @@ namespace Mono.Linker.Tests.TestCasesRunner if (!src.HasBody) return; - VerifyExceptionHandlers (src, linked); VerifyInstructions (src, linked); VerifyLocals (src, linked); } @@ -406,7 +405,10 @@ namespace Mono.Linker.Tests.TestCasesRunner foreach (var exHandler in body.ExceptionHandlers) { if (existingTryBlocks.Add ((exHandler.TryStart, exHandler.TryEnd))) { InsertBeforeInstruction (exHandler.TryStart, ".try"); - InsertBeforeInstruction (exHandler.TryEnd, ".endtry"); + if (exHandler.TryEnd != null) + InsertBeforeInstruction (exHandler.TryEnd, ".endtry"); + else + Append (".endtry"); } if (exHandler.HandlerStart != null) @@ -414,6 +416,8 @@ namespace Mono.Linker.Tests.TestCasesRunner if (exHandler.HandlerEnd != null) InsertBeforeInstruction (exHandler.HandlerEnd, ".endcatch"); + else + Append (".endcatch"); if (exHandler.FilterStart != null) InsertBeforeInstruction (exHandler.FilterStart, ".filter"); @@ -423,6 +427,9 @@ namespace Mono.Linker.Tests.TestCasesRunner void InsertBeforeInstruction (Instruction instruction, string text) => result.Insert (result.FindIndex (i => i.Item1 == instruction), (null, text)); + + void Append (string text) => + result.Add ((null, text)); } static string FormatInstruction (Instruction instr) @@ -469,18 +476,6 @@ namespace Mono.Linker.Tests.TestCasesRunner } } - static void VerifyExceptionHandlers (MethodDefinition src, MethodDefinition linked) - { - VerifyBodyProperties ( - src, - linked, - nameof (ExpectedExceptionHandlerSequenceAttribute), - nameof (ExpectExceptionHandlersModifiedAttribute), - "exception handlers", - m => m.Body.ExceptionHandlers.Select (h => h.HandlerType.ToString ().ToLower ()).ToArray (), - attr => GetStringArrayAttributeValue (attr).Select (v => v.ToLower ()).ToArray ()); - } - static void VerifyLocals (MethodDefinition src, MethodDefinition linked) { VerifyBodyProperties ( |