diff --git a/Mono.Cecil.Cil/MethodBody.cs b/Mono.Cecil.Cil/MethodBody.cs index c9236dba4..89bfc905c 100644 --- a/Mono.Cecil.Cil/MethodBody.cs +++ b/Mono.Cecil.Cil/MethodBody.cs @@ -258,7 +258,7 @@ protected override void OnInsert (Instruction item, int index) item.next = current; } - UpdateLocalScopes (null, null); + UpdateDebugInformation (null, null); } protected override void OnSet (Instruction item, int index) @@ -271,7 +271,7 @@ protected override void OnSet (Instruction item, int index) current.previous = null; current.next = null; - UpdateLocalScopes (item, current); + UpdateDebugInformation (item, current); } protected override void OnRemove (Instruction item, int index) @@ -285,7 +285,7 @@ protected override void OnRemove (Instruction item, int index) next.previous = item.previous; RemoveSequencePoint (item); - UpdateLocalScopes (item, next ?? previous); + UpdateDebugInformation (item, next ?? previous); item.previous = null; item.next = null; @@ -306,126 +306,189 @@ void RemoveSequencePoint (Instruction instruction) } } - void UpdateLocalScopes (Instruction removedInstruction, Instruction existingInstruction) + void UpdateDebugInformation (Instruction removedInstruction, Instruction existingInstruction) { - var debug_info = method.debug_info; - if (debug_info == null) - return; - - // Local scopes store start/end pair of "instruction offsets". Instruction offset can be either resolved, in which case it + // Various bits of debug information store instruction offsets (as "pointers" to the IL) + // Instruction offset can be either resolved, in which case it // has a reference to Instruction, or unresolved in which case it stores numerical offset (instruction offset in the body). - // Typically local scopes loaded from PE/PDB files will be resolved, but it's not a requirement. + // Depending on where the InstructionOffset comes from (loaded from PE/PDB or constructed) it can be in either state. // Each instruction has its own offset, which is populated on load, but never updated (this would be pretty expensive to do). // Instructions created during the editting will typically have offset 0 (so incorrect). - // Local scopes created during editing will also likely be resolved (so no numerical offsets). - // So while local scopes which are unresolved are relatively rare if they appear, manipulating them based - // on the offsets allone is pretty hard (since we can't rely on correct offsets of instructions). - // On the other hand resolved local scopes are easy to maintain, since they point to instructions and thus inserting + // Manipulating unresolved InstructionOffsets is pretty hard (since we can't rely on correct offsets of instructions). + // On the other hand resolved InstructionOffsets are easy to maintain, since they point to instructions and thus inserting // instructions is basically a no-op and removing instructions is as easy as changing the pointer. // For this reason the algorithm here is: // - First make sure that all instruction offsets are resolved - if not - resolve them - // - First time this will be relatively expensinve as it will walk the entire method body to convert offsets to instruction pointers - // Almost all local scopes are stored in the "right" order (sequentially per start offsets), so the code uses a simple one-item - // cache instruction<->offset to avoid walking instructions multiple times (that would only happen for scopes which are out of order). - // - Subsequent calls should be cheap as it will only walk all local scopes without doing anything - // - If there was an edit on local scope which makes some of them unresolved, the cost is proportional + // - First time this will be relatively expensive as it will walk the entire method body to convert offsets to instruction pointers + // Within the same debug info, IL offsets are typically stored in the "right" order (sequentially per start offsets), + // so the code uses a simple one-item cache instruction<->offset to avoid walking instructions multiple times + // (that would only happen for scopes which are out of order). + // - Subsequent calls should be cheap as it will only walk all local scopes without doing anything (as it checks that they're resolved) + // - If there was an edit which adds some unresolved, the cost is proportional (the code will only resolve those) // - Then update as necessary by manipulaitng instruction references alone - InstructionOffsetCache cache = new InstructionOffsetCache () { - Offset = 0, - Index = 0, - Instruction = items [0] - }; + InstructionOffsetResolver resolver = new InstructionOffsetResolver (items, removedInstruction, existingInstruction); + + if (method.debug_info != null) + UpdateLocalScope (method.debug_info.Scope, ref resolver); + + var custom_debug_infos = method.custom_infos ?? method.debug_info?.custom_infos; + if (custom_debug_infos != null) { + foreach (var custom_debug_info in custom_debug_infos) { + switch (custom_debug_info) { + case StateMachineScopeDebugInformation state_machine_scope: + UpdateStateMachineScope (state_machine_scope, ref resolver); + break; - UpdateLocalScope (debug_info.Scope, removedInstruction, existingInstruction, ref cache); + case AsyncMethodBodyDebugInformation async_method_body: + UpdateAsyncMethodBody (async_method_body, ref resolver); + break; + + default: + // No need to update the other debug info as they don't store instruction references + break; + } + } + } } - void UpdateLocalScope (ScopeDebugInformation scope, Instruction removedInstruction, Instruction existingInstruction, ref InstructionOffsetCache cache) + void UpdateLocalScope (ScopeDebugInformation scope, ref InstructionOffsetResolver resolver) { if (scope == null) return; - if (!scope.Start.IsResolved) - scope.Start = ResolveInstructionOffset (scope.Start, ref cache); - - if (!scope.Start.IsEndOfMethod && scope.Start.ResolvedInstruction == removedInstruction) - scope.Start = new InstructionOffset (existingInstruction); + scope.Start = resolver.Resolve (scope.Start); if (scope.HasScopes) { foreach (var subScope in scope.Scopes) - UpdateLocalScope (subScope, removedInstruction, existingInstruction, ref cache); + UpdateLocalScope (subScope, ref resolver); } - if (!scope.End.IsResolved) - scope.End = ResolveInstructionOffset (scope.End, ref cache); - - if (!scope.End.IsEndOfMethod && scope.End.ResolvedInstruction == removedInstruction) - scope.End = new InstructionOffset (existingInstruction); + scope.End = resolver.Resolve (scope.End); } - struct InstructionOffsetCache { - public int Offset; - public int Index; - public Instruction Instruction; + void UpdateStateMachineScope (StateMachineScopeDebugInformation debugInfo, ref InstructionOffsetResolver resolver) + { + resolver.Restart (); + foreach (var scope in debugInfo.Scopes) { + scope.Start = resolver.Resolve (scope.Start); + scope.End = resolver.Resolve (scope.End); + } } - InstructionOffset ResolveInstructionOffset(InstructionOffset inputOffset, ref InstructionOffsetCache cache) + void UpdateAsyncMethodBody (AsyncMethodBodyDebugInformation debugInfo, ref InstructionOffsetResolver resolver) { - if (inputOffset.IsResolved) - return inputOffset; + if (!debugInfo.CatchHandler.IsResolved) { + resolver.Restart (); + debugInfo.CatchHandler = resolver.Resolve (debugInfo.CatchHandler); + } - int offset = inputOffset.Offset; + resolver.Restart (); + for (int i = 0; i < debugInfo.Yields.Count; i++) { + debugInfo.Yields [i] = resolver.Resolve (debugInfo.Yields [i]); + } - if (cache.Offset == offset) - return new InstructionOffset (cache.Instruction); + resolver.Restart (); + for (int i = 0; i < debugInfo.Resumes.Count; i++) { + debugInfo.Resumes [i] = resolver.Resolve (debugInfo.Resumes [i]); + } + } - if (cache.Offset > offset) { - // This should be rare - we're resolving offset pointing to a place before the current cache position - // resolve by walking the instructions from start and don't cache the result. - int size = 0; - for (int i = 0; i < items.Length; i++) { - // The array can be larger than the actual size, in which case its padded with nulls at the end - // so when we reach null, treat it as an end of the IL. - if (items [i] == null) - return new InstructionOffset (i == 0 ? items [0] : items [i - 1]); + struct InstructionOffsetResolver { + readonly Instruction [] items; + readonly Instruction removed_instruction; + readonly Instruction existing_instruction; - if (size == offset) - return new InstructionOffset (items [i]); + int cache_offset; + int cache_index; + Instruction cache_instruction; - if (size > offset) - return new InstructionOffset (i == 0 ? items [0] : items [i - 1]); + public int LastOffset { get => cache_offset; } - size += items [i].GetSize (); - } + public InstructionOffsetResolver (Instruction[] instructions, Instruction removedInstruction, Instruction existingInstruction) + { + items = instructions; + removed_instruction = removedInstruction; + existing_instruction = existingInstruction; + cache_offset = 0; + cache_index = 0; + cache_instruction = items [0]; + } - // Offset is larger than the size of the body - so it points after the end - return new InstructionOffset (); - } else { - // The offset points after the current cache position - so continue counting and update the cache - int size = cache.Offset; - for (int i = cache.Index; i < items.Length; i++) { - cache.Index = i; - cache.Offset = size; + public void Restart () + { + cache_offset = 0; + cache_index = 0; + cache_instruction = items [0]; + } - var item = items [i]; + public InstructionOffset Resolve (InstructionOffset inputOffset) + { + var result = ResolveInstructionOffset (inputOffset); + if (!result.IsEndOfMethod && result.ResolvedInstruction == removed_instruction) + result = new InstructionOffset (existing_instruction); - // Allow for trailing null values in the case of - // instructions.Size < instructions.Capacity - if (item == null) - return new InstructionOffset (i == 0 ? items [0] : items [i - 1]); + return result; + } - cache.Instruction = item; + InstructionOffset ResolveInstructionOffset (InstructionOffset inputOffset) + { + if (inputOffset.IsResolved) + return inputOffset; - if (cache.Offset == offset) - return new InstructionOffset (cache.Instruction); + int offset = inputOffset.Offset; - if (cache.Offset > offset) - return new InstructionOffset (i == 0 ? items [0] : items [i - 1]); + if (cache_offset == offset) + return new InstructionOffset (cache_instruction); - size += item.GetSize (); - } + if (cache_offset > offset) { + // This should be rare - we're resolving offset pointing to a place before the current cache position + // resolve by walking the instructions from start and don't cache the result. + int size = 0; + for (int i = 0; i < items.Length; i++) { + // The array can be larger than the actual size, in which case its padded with nulls at the end + // so when we reach null, treat it as an end of the IL. + if (items [i] == null) + return new InstructionOffset (i == 0 ? items [0] : items [i - 1]); + + if (size == offset) + return new InstructionOffset (items [i]); + + if (size > offset) + return new InstructionOffset (i == 0 ? items [0] : items [i - 1]); + + size += items [i].GetSize (); + } + + // Offset is larger than the size of the body - so it points after the end + return new InstructionOffset (); + } else { + // The offset points after the current cache position - so continue counting and update the cache + int size = cache_offset; + for (int i = cache_index; i < items.Length; i++) { + cache_index = i; + cache_offset = size; - return new InstructionOffset (); + var item = items [i]; + + // Allow for trailing null values in the case of + // instructions.Size < instructions.Capacity + if (item == null) + return new InstructionOffset (i == 0 ? items [0] : items [i - 1]); + + cache_instruction = item; + + if (cache_offset == offset) + return new InstructionOffset (cache_instruction); + + if (cache_offset > offset) + return new InstructionOffset (i == 0 ? items [0] : items [i - 1]); + + size += item.GetSize (); + } + + return new InstructionOffset (); + } } } } diff --git a/Test/Mono.Cecil.Tests/ILProcessorTests.cs b/Test/Mono.Cecil.Tests/ILProcessorTests.cs index c1dc13ce8..fd462c365 100644 --- a/Test/Mono.Cecil.Tests/ILProcessorTests.cs +++ b/Test/Mono.Cecil.Tests/ILProcessorTests.cs @@ -163,7 +163,7 @@ public void Clear () [TestCase (RoundtripType.PortablePdb, true, true, false)] public void InsertAfterWithSymbolRoundtrip (RoundtripType roundtripType, bool forceUnresolved, bool reverseScopes, bool padIL) { - var methodBody = CreateTestMethodWithLocalScopes (padIL); + var methodBody = CreateTestMethodWithLocalScopes (roundtripType, padIL); methodBody = RoundtripMethodBody (methodBody, roundtripType, forceUnresolved, reverseScopes); var il = methodBody.GetILProcessor (); @@ -174,6 +174,10 @@ public void InsertAfterWithSymbolRoundtrip (RoundtripType roundtripType, bool fo AssertLocalScope (methodBody, wholeBodyScope.Scopes [0], 1, 3); AssertLocalScope (methodBody, wholeBodyScope.Scopes [1], 4, null); AssertLocalScope (methodBody, wholeBodyScope.Scopes [1].Scopes [0], 5, 6); + AssertStateMachineScope (methodBody, 1, 7); + AssertAsyncMethodSteppingInfo (methodBody, 0, 1, 1); + AssertAsyncMethodSteppingInfo (methodBody, 1, 5, 6); + AssertAsyncMethodSteppingInfo (methodBody, 2, 7, 7); methodBody.Method.Module.Dispose (); } @@ -189,7 +193,7 @@ public void InsertAfterWithSymbolRoundtrip (RoundtripType roundtripType, bool fo [TestCase (RoundtripType.PortablePdb, true, true, false)] public void RemoveWithSymbolRoundtrip (RoundtripType roundtripType, bool forceUnresolved, bool reverseScopes, bool padIL) { - var methodBody = CreateTestMethodWithLocalScopes (padIL); + var methodBody = CreateTestMethodWithLocalScopes (roundtripType, padIL); methodBody = RoundtripMethodBody (methodBody, roundtripType, forceUnresolved, reverseScopes); var il = methodBody.GetILProcessor (); @@ -200,6 +204,10 @@ public void RemoveWithSymbolRoundtrip (RoundtripType roundtripType, bool forceUn AssertLocalScope (methodBody, wholeBodyScope.Scopes [0], 1, 1); AssertLocalScope (methodBody, wholeBodyScope.Scopes [1], 2, null); AssertLocalScope (methodBody, wholeBodyScope.Scopes [1].Scopes [0], 3, 4); + AssertStateMachineScope (methodBody, 1, 5); + AssertAsyncMethodSteppingInfo (methodBody, 0, 1, 1); + AssertAsyncMethodSteppingInfo (methodBody, 1, 3, 4); + AssertAsyncMethodSteppingInfo (methodBody, 2, 5, 5); methodBody.Method.Module.Dispose (); } @@ -215,7 +223,7 @@ public void RemoveWithSymbolRoundtrip (RoundtripType roundtripType, bool forceUn [TestCase (RoundtripType.PortablePdb, true, true, false)] public void ReplaceWithSymbolRoundtrip (RoundtripType roundtripType, bool forceUnresolved, bool reverseScopes, bool padIL) { - var methodBody = CreateTestMethodWithLocalScopes (padIL); + var methodBody = CreateTestMethodWithLocalScopes (roundtripType, padIL); methodBody = RoundtripMethodBody (methodBody, roundtripType, forceUnresolved, reverseScopes); var il = methodBody.GetILProcessor (); @@ -226,6 +234,10 @@ public void ReplaceWithSymbolRoundtrip (RoundtripType roundtripType, bool forceU AssertLocalScope (methodBody, wholeBodyScope.Scopes [0], 1, 2); AssertLocalScope (methodBody, wholeBodyScope.Scopes [1], 3, null); AssertLocalScope (methodBody, wholeBodyScope.Scopes [1].Scopes [0], 4, 5); + AssertStateMachineScope (methodBody, 1, 6); + AssertAsyncMethodSteppingInfo (methodBody, 0, 1, 1); + AssertAsyncMethodSteppingInfo (methodBody, 1, 4, 5); + AssertAsyncMethodSteppingInfo (methodBody, 2, 6, 6); methodBody.Method.Module.Dispose (); } @@ -241,7 +253,7 @@ public void ReplaceWithSymbolRoundtrip (RoundtripType roundtripType, bool forceU [TestCase (RoundtripType.PortablePdb, true, true, false)] public void EditBodyWithScopesAndSymbolRoundtrip (RoundtripType roundtripType, bool forceUnresolved, bool reverseScopes, bool padIL) { - var methodBody = CreateTestMethodWithLocalScopes (padIL); + var methodBody = CreateTestMethodWithLocalScopes (roundtripType, padIL); methodBody = RoundtripMethodBody (methodBody, roundtripType, forceUnresolved, reverseScopes); var il = methodBody.GetILProcessor (); @@ -260,6 +272,10 @@ public void EditBodyWithScopesAndSymbolRoundtrip (RoundtripType roundtripType, b AssertLocalScope (methodBody, wholeBodyScope.Scopes [1], 3, null); AssertLocalScope (methodBody, wholeBodyScope.Scopes [1].Scopes [0], 4, 5); AssertLocalScope (methodBody, wholeBodyScope.Scopes [1].Scopes [1], 5, 6); + AssertStateMachineScope (methodBody, 1, 7); + AssertAsyncMethodSteppingInfo (methodBody, 0, 1, 1); + AssertAsyncMethodSteppingInfo (methodBody, 1, 4, 5); + AssertAsyncMethodSteppingInfo (methodBody, 2, 7, 7); methodBody.Method.Module.Dispose (); } @@ -310,6 +326,21 @@ static MethodBody CreateTestMethod (params OpCode [] opcodes) return method.Body; } + static MethodDefinition CreateEmptyTestMethod (ModuleDefinition module, string name) + { + var method = new MethodDefinition { + Name = name, + Attributes = MethodAttributes.Public | MethodAttributes.Static + }; + + var il = method.Body.GetILProcessor (); + il.Emit (OpCodes.Ret); + + method.ReturnType = module.ImportReference (typeof (void)); + + return method; + } + static ScopeDebugInformation VerifyWholeBodyScope (MethodBody body) { var debug_info = body.Method.DebugInformation; @@ -318,17 +349,51 @@ static ScopeDebugInformation VerifyWholeBodyScope (MethodBody body) return debug_info.Scope; } + static void AssertInstructionOffset (Instruction instruction, InstructionOffset instructionOffset) + { + if (instructionOffset.IsResolved) + Assert.AreEqual (instruction, instructionOffset.ResolvedInstruction); + else + Assert.AreEqual (instruction.Offset, instructionOffset.Offset); + } + + static void AssertEndOfScopeOffset (MethodBody methodBody, InstructionOffset instructionOffset, int? index) + { + if (index.HasValue) + AssertInstructionOffset (methodBody.Instructions [index.Value], instructionOffset); + else + Assert.IsTrue (instructionOffset.IsEndOfMethod); + } + static void AssertLocalScope (MethodBody methodBody, ScopeDebugInformation scope, int startIndex, int? endIndex) { Assert.IsNotNull (scope); - Assert.AreEqual (methodBody.Instructions [startIndex], scope.Start.ResolvedInstruction); - if (endIndex.HasValue) - Assert.AreEqual (methodBody.Instructions [endIndex.Value], scope.End.ResolvedInstruction); - else - Assert.IsTrue (scope.End.IsEndOfMethod); + AssertInstructionOffset (methodBody.Instructions [startIndex], scope.Start); + AssertEndOfScopeOffset (methodBody, scope.End, endIndex); + } + + static void AssertStateMachineScope (MethodBody methodBody, int startIndex, int? endIndex) + { + var customDebugInfo = methodBody.Method.HasCustomDebugInformations ? methodBody.Method.CustomDebugInformations : methodBody.Method.DebugInformation.CustomDebugInformations; + var stateMachineScope = customDebugInfo.OfType ().SingleOrDefault (); + Assert.IsNotNull (stateMachineScope); + Assert.AreEqual (1, stateMachineScope.Scopes.Count); + AssertInstructionOffset (methodBody.Instructions [startIndex], stateMachineScope.Scopes [0].Start); + AssertEndOfScopeOffset (methodBody, stateMachineScope.Scopes [0].End, endIndex); + } + + static void AssertAsyncMethodSteppingInfo (MethodBody methodBody, int infoNumber, int yieldIndex, int resumeIndex) + { + var customDebugInfo = methodBody.Method.HasCustomDebugInformations ? methodBody.Method.CustomDebugInformations : methodBody.Method.DebugInformation.CustomDebugInformations; + var asyncMethodInfo = customDebugInfo.OfType ().SingleOrDefault (); + Assert.IsNotNull (asyncMethodInfo); + Assert.Greater (asyncMethodInfo.Yields.Count, infoNumber); + Assert.AreEqual (asyncMethodInfo.Yields.Count, asyncMethodInfo.Resumes.Count); + AssertInstructionOffset (methodBody.Instructions [yieldIndex], asyncMethodInfo.Yields [infoNumber]); + AssertInstructionOffset (methodBody.Instructions [resumeIndex], asyncMethodInfo.Resumes [infoNumber]); } - static MethodBody CreateTestMethodWithLocalScopes (bool padILWithNulls) + static MethodBody CreateTestMethodWithLocalScopes (RoundtripType roundtripType, bool padILWithNulls) { var module = ModuleDefinition.CreateModule ("TestILProcessor", ModuleKind.Dll); var type = new TypeDefinition ("NS", "TestType", TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Sealed, module.ImportReference (typeof (object))); @@ -342,6 +407,9 @@ static MethodBody CreateTestMethodWithLocalScopes (bool padILWithNulls) method.ReturnType = module.ImportReference (typeof (void)); type.Methods.Add (method); + var emptyMethod = CreateEmptyTestMethod (module, "empty"); + type.Methods.Add (emptyMethod); + methodBody.InitLocals = true; var tempVar1 = new VariableDefinition (module.ImportReference (typeof (string))); methodBody.Variables.Add (tempVar1); @@ -363,6 +431,7 @@ static MethodBody CreateTestMethodWithLocalScopes (bool padILWithNulls) } // The method looks like this: + // Scopes // | Scope | Scope.Scopes[0] | Scope.Scopes[1] | Scope.Scopes[1].Scopes[0] // #0 Nop | > | | | // #1 Ldloc_0 | . | > | | @@ -372,6 +441,17 @@ static MethodBody CreateTestMethodWithLocalScopes (bool padILWithNulls) // #5 Ldloc_2 | . | | . | < // #6 Nop | . | | . | // | < | | < | + // + // Async and state machine infos + // | Catch handler | Yields | Resumes | State machine | + // #0 Nop | | | | | + // #1 Ldloc_0 | | 0 | 0 | > | + // #2 Nop | | | | . | + // #3 Ldloc_1 | | | | . | + // #4 Nop | | 1 | | . | + // #5 Ldloc_2 | | | 1 | . | + // #6 Nop | * | 2 | 2 | < | + // | | | | | var instructions = methodBody.Instructions; debug_info.Scope = new ScopeDebugInformation (instructions[0], null) { @@ -389,6 +469,33 @@ static MethodBody CreateTestMethodWithLocalScopes (bool padILWithNulls) } }; + // For some reason the Native PDB reader/writer store the custom info on the method.DebugInfo.CustomInfo, while portable PDB stores it on method.CustomInfo. + var customDebugInfo = (roundtripType == RoundtripType.Pdb && Platform.HasNativePdbSupport) + ? method.DebugInformation.CustomDebugInformations : method.CustomDebugInformations; + customDebugInfo.Add (new StateMachineScopeDebugInformation () { + Scopes = { + new StateMachineScope(instructions[1], instructions[6]) + } + }); + customDebugInfo.Add (new AsyncMethodBodyDebugInformation () { + CatchHandler = new InstructionOffset (instructions [6]), + Yields = { + new InstructionOffset (instructions [1]), + new InstructionOffset (instructions [4]), + new InstructionOffset (instructions [6]) + }, + Resumes = { + new InstructionOffset (instructions [1]), + new InstructionOffset (instructions [5]), + new InstructionOffset (instructions [6]), + }, + ResumeMethods = { + emptyMethod, + emptyMethod, + emptyMethod + } + }); + return methodBody; }