/* Copyright (C) 2014-2019 de4dot@gmail.com This file is part of dnSpy dnSpy is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. dnSpy is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with dnSpy. If not, see . */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using dnlib.DotNet; using dnSpy.Contracts.Debugger; using dnSpy.Contracts.Debugger.CallStack; using dnSpy.Contracts.Debugger.DotNet.Code; using dnSpy.Contracts.Debugger.DotNet.Evaluation; using dnSpy.Contracts.Debugger.DotNet.Steppers.Engine; using dnSpy.Contracts.Debugger.Engine.Steppers; using dnSpy.Contracts.Debugger.Evaluation; using dnSpy.Debugger.DotNet.Code; using dnSpy.Debugger.DotNet.Metadata; using dnSpy.Debugger.DotNet.Properties; namespace dnSpy.Debugger.DotNet.Steppers.Engine { sealed class DbgEngineStepperImpl : DbgEngineStepper { public override event EventHandler? StepComplete; readonly DbgLanguageService dbgLanguageService; readonly DbgDotNetDebugInfoService dbgDotNetDebugInfoService; readonly DebuggerSettings debuggerSettings; readonly IDbgDotNetRuntime runtime; readonly DbgDotNetEngineStepper stepper; ReturnToAwaiterState? returnToAwaiterState; StepIntoState? stepIntoState; //TODO: Return false if the current decompiler language doesn't support base wrapper methods (i.e., it's not a high level language such as C# or VB) bool IgnoreBaseWrapperMethods => true; //TODO: Return false if the decompiler doesn't decompile iterator state machines bool AreIteratorsDecompiled => true; sealed class ReturnToAwaiterState { public DbgDotNetStepperBreakpoint? breakpoint; public TaskCompletionSource? taskCompletionSource; public DbgDotNetObjectId? taskObjectId; } DbgThread CurrentThread { get => __DONT_USE_currentThread; set => __DONT_USE_currentThread = value ?? throw new InvalidOperationException(); } DbgThread __DONT_USE_currentThread; public DbgEngineStepperImpl(DbgLanguageService dbgLanguageService, DbgDotNetDebugInfoService dbgDotNetDebugInfoService, DebuggerSettings debuggerSettings, IDbgDotNetRuntime runtime, DbgDotNetEngineStepper stepper, DbgThread thread) { this.dbgLanguageService = dbgLanguageService ?? throw new ArgumentNullException(nameof(dbgLanguageService)); this.dbgDotNetDebugInfoService = dbgDotNetDebugInfoService ?? throw new ArgumentNullException(nameof(dbgDotNetDebugInfoService)); this.debuggerSettings = debuggerSettings ?? throw new ArgumentNullException(nameof(debuggerSettings)); this.runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); this.stepper = stepper ?? throw new ArgumentNullException(nameof(stepper)); __DONT_USE_currentThread = thread ?? throw new ArgumentNullException(nameof(thread)); } DbgEvaluationInfo CreateEvaluationInfo(DbgThread thread) { DbgStackFrame? frame = null; try { frame = thread.GetTopStackFrame(); if (frame is null) throw new InvalidOperationException(); Debug2.Assert(frame is not null); return CreateEvaluationInfo(frame); } catch { frame?.Close(); throw; } } DbgEvaluationInfo CreateEvaluationInfo(DbgStackFrame frame) { DbgEvaluationContext? ctx = null; try { var language = dbgLanguageService.GetCurrentLanguage(frame.Runtime.RuntimeKindGuid); ctx = language.CreateContext(frame, DbgEvaluationContextOptions.NoMethodBody); var cancellationToken = CancellationToken.None; return new DbgEvaluationInfo(ctx, frame, cancellationToken); } catch { ctx?.Close(); throw; } } void ClearReturnToAwaiterState() { ClearReturnToAwaiterBreakpoint(); ClearReturnToAwaiterTaskObjectId(); } Task? TryCreateReturnToAwaiterTask(DbgThread? thread, DbgModule module, uint methodToken, uint setResultOffset, uint builderFieldToken) { runtime.Dispatcher.VerifyAccess(); ClearReturnToAwaiterState(); if (!debuggerSettings.AsyncDebugging) return null; if (setResultOffset == uint.MaxValue) return null; if (!TaskEvalUtils.SupportsAsyncStepOut(module.GetReflectionModule()?.AppDomain)) return null; if ((runtime.Features & DbgDotNetRuntimeFeatures.NoAsyncStepObjectId) != 0) return null; if (returnToAwaiterState is null) returnToAwaiterState = new ReturnToAwaiterState(); returnToAwaiterState.breakpoint = stepper.CreateBreakpoint(thread, module, methodToken, setResultOffset); returnToAwaiterState.taskCompletionSource = new TaskCompletionSource(); var tcs = returnToAwaiterState.taskCompletionSource; returnToAwaiterState.breakpoint.Hit += (s, e) => { if (returnToAwaiterState?.taskCompletionSource == tcs) { e.Pause = true; tcs.TrySetResult(e.Thread); } else tcs.TrySetCanceled(); }; return CreateReturnToAwaiterTaskCoreAsync(returnToAwaiterState.taskCompletionSource.Task, module, builderFieldToken); } async Task CreateReturnToAwaiterTaskCoreAsync(Task setResultBreakpointTask, DbgModule builderFieldModule, uint builderFieldToken) { runtime.Dispatcher.VerifyAccess(); var thread = await setResultBreakpointTask; ClearReturnToAwaiterState(); stepper.CancelLastStep(); DbgDotNetValue? taskValue = null; try { if (TryCallSetNotificationForWaitCompletion(thread, builderFieldModule, builderFieldToken, true, out taskValue)) { var notifyDebuggerOfWaitCompletionMethod = TaskEvalUtils.GetNotifyDebuggerOfWaitCompletionMethod(taskValue.Type.AppDomain); Debug2.Assert(notifyDebuggerOfWaitCompletionMethod is not null); thread = await SetNotifyDebuggerOfWaitCompletionBreakpoint(notifyDebuggerOfWaitCompletionMethod, taskValue); } } finally { taskValue?.Dispose(); } return thread; } Task SetNotifyDebuggerOfWaitCompletionBreakpoint(DmdMethodInfo method, DbgDotNetValue taskValue) { try { runtime.Dispatcher.VerifyAccess(); ClearReturnToAwaiterState(); var module = method.Module.GetDebuggerModule() ?? throw new InvalidOperationException(); if (returnToAwaiterState is null) returnToAwaiterState = new ReturnToAwaiterState(); returnToAwaiterState.breakpoint = stepper.CreateBreakpoint(null, module, (uint)method.MetadataToken, 0); returnToAwaiterState.taskCompletionSource = new TaskCompletionSource(); if ((runtime.Features & DbgDotNetRuntimeFeatures.ObjectIds) != 0) returnToAwaiterState.taskObjectId = runtime.CreateObjectId(taskValue, 0); var taskObjId = returnToAwaiterState.taskObjectId; var tcs = returnToAwaiterState.taskCompletionSource; returnToAwaiterState.breakpoint.Hit += (s, e) => { if (tcs != returnToAwaiterState?.taskCompletionSource) { tcs.TrySetCanceled(); return; } bool hit; if (taskObjId is null) hit = true; else { DbgDotNetValueResult taskValue2 = default; DbgEvaluationInfo? evalInfo = null; try { evalInfo = CreateEvaluationInfo(e.Thread); taskValue2 = runtime.GetParameterValue(evalInfo, 0); hit = !taskValue2.IsNormalResult || runtime.Equals(taskObjId, taskValue2.Value!); } finally { taskValue2.Value?.Dispose(); evalInfo?.Close(); } } if (hit) { e.Pause = true; tcs.TrySetResult(e.Thread); } }; return SetNotifyDebuggerOfWaitCompletionBreakpointCoreAsync(returnToAwaiterState.taskCompletionSource.Task); } finally { taskValue?.Dispose(); } } async Task SetNotifyDebuggerOfWaitCompletionBreakpointCoreAsync(Task notifyDebuggerOfWaitCompletionBreakpointTask) { runtime.Dispatcher.VerifyAccess(); stepper.Continue(); var thread = await notifyDebuggerOfWaitCompletionBreakpointTask; ClearReturnToAwaiterState(); // Step out until we reach user code. We could set a BP too, but it's only supported by the CorDebug code. // We don't mark any code as user code so we can't just step once and let the CLR stepper do the work. const int MAX_STEP_OUT = 50; for (int i = 0; i < MAX_STEP_OUT; i++) { DbgStackFrame[]? frames = null; try { frames = thread.GetFrames(2); if (frames.Length <= 1) break; if (IsUserFrame(frames[0])) break; thread.Process.DbgManager.Close(frames); frames = null; var frame = stepper.TryGetFrameInfo(thread); Debug2.Assert(frame is not null); if (frame is null) break; thread = await stepper.StepOutAsync(frame); } finally { if (frames is not null) thread.Process.DbgManager.Close(frames); } } // Step over any hidden instructions so we end up on a statement var newFrame = stepper.TryGetFrameInfo(thread); Debug2.Assert(newFrame is not null); if (newFrame is not null) thread = await StepOverHiddenInstructionsAsync(newFrame); return thread; } bool IsUserFrame(DbgStackFrame frame) { runtime.Dispatcher.VerifyAccess(); DbgEvaluationInfo? evalInfo = null; try { evalInfo = CreateEvaluationInfo(frame); var method = runtime.GetFrameMethod(evalInfo); if (method is null) return false; var type = method.DeclaringType!; while (type.DeclaringType is DmdType declType) type = declType; if (IsNonUserCodeNamespace(type.MetadataNamespace)) return false; // Assume it's user code return true; } finally { evalInfo?.Close(); } } static bool IsNonUserCodeNamespace(string? @namespace) { foreach (var ns in nonUserCodeNamespaces) { if (@namespace == ns) return true; } return false; } static readonly string[] nonUserCodeNamespaces = new string[] { // eg. Task, Task "System.Threading.Tasks", // eg. TaskAwaiter, TaskAwaiter "System.Runtime.CompilerServices", }; void ClearReturnToAwaiterBreakpoint() { runtime.Dispatcher.VerifyAccess(); if (returnToAwaiterState is null) return; returnToAwaiterState.taskCompletionSource?.TrySetCanceled(); returnToAwaiterState.taskCompletionSource = null; if (returnToAwaiterState.breakpoint is not null) { stepper.RemoveBreakpoints(new[] { returnToAwaiterState.breakpoint }); returnToAwaiterState.breakpoint = null; } } void ClearReturnToAwaiterTaskObjectId() { runtime.Dispatcher.VerifyAccess(); if (returnToAwaiterState is null) return; returnToAwaiterState.taskObjectId?.Dispose(); returnToAwaiterState.taskObjectId = null; } bool TryCallSetNotificationForWaitCompletion(DbgThread thread, DbgModule builderFieldModule, uint builderFieldToken, bool value, [NotNullWhen(true)] out DbgDotNetValue? taskValue) { runtime.Dispatcher.VerifyAccess(); DbgEvaluationInfo? evalInfo = null; try { evalInfo = CreateEvaluationInfo(thread); var info = TaskEvalUtils.CallSetNotificationForWaitCompletion(evalInfo, builderFieldModule, builderFieldToken, value); if (info.success && info.taskValue is not null) { taskValue = info.taskValue; return true; } else { taskValue = null; return false; } } finally { evalInfo?.Close(); } } void RaiseStepComplete(object? tag, string? error, bool forciblyCanceled = false) { runtime.Dispatcher.VerifyAccess(); CleanUp(); if (IsClosed) return; var thread = CurrentThread.IsClosed ? null : CurrentThread; Debug2.Assert(StepComplete is not null); StepComplete?.Invoke(this, new DbgEngineStepCompleteEventArgs(thread, tag, error, forciblyCanceled)); } public override void Step(object? tag, DbgEngineStepKind step) { if (!runtime.Dispatcher.TryBeginInvoke(() => Step_EngineThread(tag, step))) { // process has exited } } void Step_EngineThread(object? tag, DbgEngineStepKind step) { runtime.Dispatcher.VerifyAccess(); if (stepper.Session is not null) { Debug.Fail("The previous step hasn't been canceled"); // No need to localize it, if we're here it's a bug RaiseStepComplete(tag, "The previous step hasn't been canceled"); return; } if (!stepper.IsRuntimePaused) { Debug.Fail("Process is not paused"); // No need to localize it, if we're here it's a bug RaiseStepComplete(tag, "Process is not paused"); return; } StepAsync(tag, step).ContinueWith(t => { var ex = t.Exception; Debug2.Assert(ex is null); }); } Task StepAsync(object? tag, DbgEngineStepKind step) { runtime.Dispatcher.VerifyAccess(); switch (step) { case DbgEngineStepKind.StepInto: return StepIntoAsync(tag); case DbgEngineStepKind.StepOver: return StepOverAsync(tag); case DbgEngineStepKind.StepOut: return StepOutAsync(tag); default: RaiseStepComplete(tag, $"Unsupported step kind: {step}"); return Task.CompletedTask; } } async Task StepOverHiddenInstructionsAsync(DbgDotNetEngineStepperFrameInfo frame) { runtime.Dispatcher.VerifyAccess(); var result = await GetStepRangesAsync(frame, returnValues: false); return await StepOverHiddenInstructionsAsync(frame, result); } Task StepOverHiddenInstructionsAsync(DbgDotNetEngineStepperFrameInfo frame, GetStepRangesAsyncResult result) { var thread = frame.Thread; if (result.DebugInfo is not null) { if (!frame.TryGetLocation(out var module, out var token, out var offset)) throw new InvalidOperationException(); bool skipMethod = (offset == 0 || offset == DbgDotNetInstructionOffsetConstants.PROLOG) && AreIteratorsDecompiled && CompilerUtils.IsIgnoredIteratorStateMachineMethod(result.DebugInfo.Method); if (!skipMethod) { var currentStatement = result.DebugInfo.GetSourceStatementByCodeOffset(offset); if (currentStatement is null) { var ranges = CreateStepRanges(result.DebugInfo.GetUnusedRanges()); if (ranges.Length != 0) return stepper.StepOverAsync(frame, ranges); } return Task.FromResult(thread); } } var ilSpans = TryCreateMethodBodySpans(frame); if (ilSpans is not null) { var ranges = CreateStepRanges(ilSpans); if (ranges.Length != 0) return stepper.StepOverAsync(frame, ranges); } return Task.FromResult(thread); } sealed class MethodILSpanState { public DbgILSpan[]? BodyRange; } DbgILSpan[]? TryCreateMethodBodySpans(DbgDotNetEngineStepperFrameInfo frame) { if (!frame.TryGetLocation(out var module, out var token, out _)) return null; DbgEvaluationInfo? evalInfo = null; try { evalInfo = CreateEvaluationInfo(frame.Thread); var method = runtime.GetFrameMethod(evalInfo); if (method is null) return null; var state = method.GetOrCreateData(); if (state.BodyRange is null) { var body = method.GetMethodBody(); state.BodyRange = new DbgILSpan[] { new DbgILSpan(0, (uint)(body?.GetILAsByteArray().Length ?? 0)) }; } return state.BodyRange; } finally { evalInfo?.Close(); } } async Task StepIntoAsync(object? tag) { runtime.Dispatcher.VerifyAccess(); Debug2.Assert(stepper.Session is null); try { var frame = stepper.TryGetFrameInfo(CurrentThread); if (frame is null) { // No frame? Just let the process run. stepper.Continue(); return; } stepper.Session = stepper.CreateSession(tag); CurrentThread = await StepIntoCoreAsync(frame); StepCompleted(null, tag); } catch (ForciblyCanceledException fce) { StepCompleted(fce.Message, tag); } catch (StepErrorException see) { StepError(see.Message, tag); } catch (Exception ex) { if (stepper.IgnoreException(ex)) return; StepFailed(ex, tag); } } async static Task> WhenAny(IEnumerable> tasks) { var list = new List>(tasks); Debug.Assert(list.Count != 0); for (;;) { var task = await Task.WhenAny(list); if (task.Status != TaskStatus.Canceled) return task; list.Remove(task); if (list.Count == 0) return task; } } sealed class StepIntoState { public DbgDotNetStepperBreakpoint? breakpoint; public TaskCompletionSource ?taskCompletionSource; } void ClearStepIntoState() => ClearStepIntoBreakpoint(); void ClearStepIntoBreakpoint() { runtime.Dispatcher.VerifyAccess(); if (stepIntoState is null) return; if (stepIntoState.breakpoint is not null) { stepper.RemoveBreakpoints(new[] { stepIntoState.breakpoint }); stepIntoState.breakpoint = null; } stepIntoState.taskCompletionSource?.TrySetCanceled(); stepIntoState.taskCompletionSource = null; } Task SetStepIntoBreakpoint(DbgThread thread, DbgModule module, uint token, uint offset) { runtime.Dispatcher.VerifyAccess(); ClearStepIntoBreakpoint(); if (stepIntoState is null) stepIntoState = new StepIntoState(); stepIntoState.taskCompletionSource = new TaskCompletionSource(); stepIntoState.breakpoint = stepper.CreateBreakpoint(thread, module, token, offset); var tcs = stepIntoState.taskCompletionSource; stepIntoState.breakpoint.Hit += (s, e) => { if (stepIntoState?.taskCompletionSource != tcs) tcs.TrySetCanceled(); else { e.Pause = true; tcs.TrySetResult(e.Thread); } }; return stepIntoState.taskCompletionSource.Task; } async Task StepIntoCoreAsync(DbgDotNetEngineStepperFrameInfo frame) { runtime.Dispatcher.VerifyAccess(); DbgDotNetEngineStepperFrameInfo? f = frame; var origResult = await GetStepRangesAsync(f, returnValues: true); if (!f.TryGetLocation(out var origModule, out var origToken, out _)) throw new InvalidOperationException(); DbgThread thread; var prevFrame = f; bool inSameFrame; stepper.CollectReturnValues(f, origResult.StatementInstructions); for (;;) { thread = await StepIntoCoreAsync(f, origResult.DebugInfo, origResult.StatementRanges); f = stepper.TryGetFrameInfo(thread); Debug2.Assert(f is not null); if (f is null) return thread; if (!f.TryGetLocation(out var module, out var token, out uint offset)) throw new InvalidOperationException(); bool isBaseWrapperMethod = IgnoreBaseWrapperMethods && CompilerUtils.IsBaseWrapperMethod(module, token); if (isBaseWrapperMethod) continue; // If we're at the start of the method we may need to skip the first hidden instructions. // If it's an async kickoff method, we need to set a BP in MoveNext() and continue the process. if (offset == 0 || offset == DbgDotNetInstructionOffsetConstants.PROLOG) { if (debuggerSettings.AsyncDebugging) { var newResult = await GetStepRangesAsync(f, returnValues: false); if (newResult.DebugInfo is not null && newResult.StateMachineDebugInfo?.AsyncInfo is not null) { var stepIntoTask = SetStepIntoBreakpoint(thread, module, newResult.StateMachineDebugInfo.Method.MDToken.Raw, 0); stepper.Continue(); thread = await stepIntoTask; ClearStepIntoState(); f = stepper.TryGetFrameInfo(thread); Debug2.Assert(f is not null); if (f is null) return thread; } } thread = await StepOverHiddenInstructionsAsync(f); f = stepper.TryGetFrameInfo(thread); Debug2.Assert(f is not null); if (f is null) return thread; if (!f.TryGetLocation(out module, out token, out offset)) throw new InvalidOperationException(); } // Check if we didn't step into a new method. frame.Equals() isn't always 100% reliable // so we also check the offset. If it's not 0, we didn't step into it. inSameFrame = origModule == module && origToken == token && ((offset != 0 && offset != DbgDotNetInstructionOffsetConstants.PROLOG) || prevFrame.Equals(f)); if (inSameFrame && !Contains(origResult.StatementRanges, offset)) break; if (!inSameFrame) { if (debuggerSettings.StepOverPropertiesAndOperators && IsPropertyOrOperatorMethod(thread, out var member)) { thread.Runtime.Process.DbgManager.WriteMessage(PredefinedDbgManagerMessageKinds.StepFilter, GetStepFilterMessage(member)); thread = await stepper.StepOutAsync(f); f = stepper.TryGetFrameInfo(thread); Debug2.Assert(f is not null); if (f is null) return thread; if (!f.TryGetLocation(out module, out token, out offset)) throw new InvalidOperationException(); if (origModule != module || origToken != token) break; } } inSameFrame = origModule == module && origToken == token && ((offset != 0 && offset != DbgDotNetInstructionOffsetConstants.PROLOG) || prevFrame.Equals(f)); if (!inSameFrame || !Contains(origResult.StatementRanges, offset)) break; } if (!inSameFrame) { // Clear return values. These should only be shown if we're still in the same frame. stepper.ClearReturnValues(); } return thread; } string GetStepFilterMessage(DmdMemberInfo member) { var type = member.DeclaringType!; if (type.IsConstructedGenericType) type = type.GetGenericTypeDefinition(); var typeName = type.FullName ?? string.Empty; int index = typeName.IndexOf('`'); if (index >= 0) typeName = typeName.Substring(0, index); var memberName = typeName + "." + member.Name; switch (member) { case DmdPropertyInfo: return string.Format(dnSpy_Debugger_DotNet_Resources.StepFilter_SteppingOverProperty, memberName); case DmdMethodBase: return string.Format(dnSpy_Debugger_DotNet_Resources.StepFilter_SteppingOverOperator, memberName); default: Debug.Fail($"Unknown member: {member}"); return string.Empty; } } static bool Contains(DbgCodeRange[] ranges, uint offset) { foreach (var range in ranges) { if (range.Contains(offset)) return true; } return false; } bool IsPropertyOrOperatorMethod(DbgThread thread, [NotNullWhen(true)] out DmdMemberInfo? member) { DbgEvaluationInfo? evalInfo = null; try { evalInfo = CreateEvaluationInfo(thread); var method = runtime.GetFrameMethod(evalInfo); if (method is not null) { // Operators should have special-name bit set if (method.IsSpecialName && method.Name.StartsWith("op_")) { member = method; return true; } if (GetProperty(method) is DmdPropertyInfo property) { member = property; return true; } } member = null; return false; } finally { evalInfo?.Close(); } } static DmdPropertyInfo? GetProperty(DmdMethodBase method) { foreach (var p in method.DeclaringType!.DeclaredProperties) { if (p.GetGetMethod(DmdGetAccessorOptions.All) == method) return p; if (p.GetSetMethod(DmdGetAccessorOptions.All) == method) return p; } return null; } async Task StepIntoCoreAsync(DbgDotNetEngineStepperFrameInfo frame, DbgMethodDebugInfo? debugInfo, DbgCodeRange[] statementRanges) { if (debugInfo?.AsyncInfo is not null && debugInfo.AsyncInfo.SetResultOffset != uint.MaxValue) { if (!frame.TryGetLocation(out var module, out var token, out _)) throw new InvalidOperationException(); var returnToAwaiterTask = TryCreateReturnToAwaiterTask(frame.Thread, module, token, debugInfo.AsyncInfo.SetResultOffset, debugInfo.AsyncInfo.BuilderField?.MDToken.Raw ?? 0); if (returnToAwaiterTask is not null) { var stepIntoTask = stepper.StepIntoAsync(frame, statementRanges); return await await WhenAny(new[] { returnToAwaiterTask, stepIntoTask }); } } return await stepper.StepIntoAsync(frame, statementRanges); } async Task StepOverAsync(object? tag) { runtime.Dispatcher.VerifyAccess(); Debug2.Assert(stepper.Session is null); try { var frame = stepper.TryGetFrameInfo(CurrentThread); if (frame is null) { // No frame? Just let the process run. stepper.Continue(); return; } stepper.Session = stepper.CreateSession(tag); CurrentThread = await StepOverCoreAsync(frame); StepCompleted(null, tag); } catch (ForciblyCanceledException fce) { StepCompleted(fce.Message, tag); } catch (StepErrorException see) { StepError(see.Message, tag); } catch (Exception ex) { if (stepper.IgnoreException(ex)) return; StepFailed(ex, tag); } } async Task StepOverCoreAsync(DbgDotNetEngineStepperFrameInfo frame) { DbgDotNetEngineStepperFrameInfo? f = frame; DbgThread thread; for (;;) { thread = await StepOverCore2Async(f); bool keepLooping = false; if (IgnoreBaseWrapperMethods) { f = stepper.TryGetFrameInfo(thread); Debug2.Assert(f is not null); if (f is null) return thread; if (!f.TryGetLocation(out var module, out var token, out _)) throw new InvalidOperationException(); keepLooping = CompilerUtils.IsBaseWrapperMethod(module, token); } if (!keepLooping) break; } return thread; } async Task StepOverCore2Async(DbgDotNetEngineStepperFrameInfo frame) { runtime.Dispatcher.VerifyAccess(); Debug2.Assert(stepper.Session is not null); DbgThread thread; var result = await GetStepRangesAsync(frame, returnValues: true); if (!result.Frame.TryGetLocation(out var module, out var token, out _)) throw new InvalidOperationException(); var returnToAwaiterTask = result.DebugInfo?.AsyncInfo is null ? null : TryCreateReturnToAwaiterTask(result.Frame.Thread, module, token, result.DebugInfo.AsyncInfo.SetResultOffset, result.DebugInfo.AsyncInfo.BuilderField?.MDToken.Raw ?? 0); var asyncStepInfos = GetAsyncStepInfos(result); Debug2.Assert(asyncStepInfos is null || asyncStepInfos.Count != 0); if (asyncStepInfos is not null) { try { var asyncState = SetAsyncStepOverState(new AsyncStepOverState(this, stepper, result.DebugInfo!.AsyncInfo!.BuilderField))!; foreach (var stepInfo in asyncStepInfos) asyncState.AddYieldBreakpoint(result.Frame.Thread, module, token, stepInfo); var yieldBreakpointTask = asyncState.Task; stepper.CollectReturnValues(result.Frame, result.StatementInstructions); var stepOverTask = stepper.StepOverAsync(result.Frame, result.StatementRanges); var tasks = returnToAwaiterTask is null ? new[] { stepOverTask, yieldBreakpointTask } : new[] { stepOverTask, yieldBreakpointTask, returnToAwaiterTask }; var completedTask = await Task.WhenAny(tasks); ClearReturnToAwaiterState(); if (completedTask == stepOverTask) { asyncState.Dispose(); thread = stepOverTask.Result; } else if (completedTask == returnToAwaiterTask) { asyncState.Dispose(); thread = returnToAwaiterTask!.Result; } else { stepper.CancelLastStep(); asyncState.ClearYieldBreakpoints(); var resumeBpTask = asyncState.SetResumeBreakpoint(result.Frame.Thread, module); stepper.Continue(); thread = await resumeBpTask; asyncState.Dispose(); var newFrame = stepper.TryGetFrameInfo(thread); Debug2.Assert(newFrame is not null); if (newFrame is not null && newFrame.TryGetLocation(out var newModule, out var newToken, out _)) { Debug.Assert(newModule == module && asyncState.ResumeToken == newToken); if (result.DebugInfo is not null && newModule == module && asyncState.ResumeToken == newToken) thread = await StepOverHiddenInstructionsAsync(newFrame, result); } } } finally { SetAsyncStepOverState(null); ClearReturnToAwaiterState(); stepper.CancelLastStep(); } } else { var tasks = new List>(2); if (returnToAwaiterTask is not null) tasks.Add(returnToAwaiterTask); stepper.CollectReturnValues(result.Frame, result.StatementInstructions); tasks.Add(stepper.StepOverAsync(result.Frame, result.StatementRanges)); thread = await await WhenAny(tasks); ClearReturnToAwaiterState(); } return thread; } List? GetAsyncStepInfos(in GetStepRangesAsyncResult result) { runtime.Dispatcher.VerifyAccess(); if (!debuggerSettings.AsyncDebugging) return null; if (result.DebugInfo?.AsyncInfo is null) return null; List? asyncStepInfos = null; GetAsyncStepInfos(ref asyncStepInfos, result.DebugInfo.Method, result.DebugInfo.AsyncInfo, result.ExactStatementRanges); foreach (var ranges in GetHiddenRanges(result.ExactStatementRanges, result.DebugInfo.GetUnusedRanges())) GetAsyncStepInfos(ref asyncStepInfos, result.DebugInfo.Method, result.DebugInfo.AsyncInfo, ranges); return asyncStepInfos; } AsyncStepOverState? SetAsyncStepOverState(AsyncStepOverState? state) { runtime.Dispatcher.VerifyAccess(); __DONT_USE_asyncStepOverState?.Dispose(); __DONT_USE_asyncStepOverState = state; return state; } AsyncStepOverState? __DONT_USE_asyncStepOverState; sealed class AsyncStepOverState { readonly DbgEngineStepperImpl owner; readonly DbgDotNetEngineStepper stepper; readonly List yieldBreakpoints; readonly TaskCompletionSource yieldTaskCompletionSource; DbgDotNetStepperBreakpoint? resumeBreakpoint; public Task Task => yieldTaskCompletionSource.Task; public uint ResumeToken { get; private set; } DbgModule? builderFieldModule; readonly uint builderFieldToken; DbgDotNetObjectId? taskObjectId; public AsyncStepOverState(DbgEngineStepperImpl owner, DbgDotNetEngineStepper stepper, FieldDef? builderField) { this.owner = owner; this.stepper = stepper; yieldBreakpoints = new List(); yieldTaskCompletionSource = new TaskCompletionSource(); builderFieldToken = builderField?.MDToken.Raw ?? 0; } public void AddYieldBreakpoint(DbgThread thread, DbgModule module, uint token, DbgAsyncStepInfo stepInfo) { var yieldBreakpoint = stepper.CreateBreakpoint(thread, module, token, stepInfo.YieldOffset); try { var bpState = new AsyncBreakpointState(yieldBreakpoint, stepInfo.ResumeMethod, stepInfo.ResumeOffset); bpState.Hit += AsyncBreakpointState_Hit; yieldBreakpoints.Add(bpState); } catch { stepper.RemoveBreakpoints(new[] { yieldBreakpoint }); throw; } } void AsyncBreakpointState_Hit(object? sender, AsyncBreakpointState bpState) => yieldTaskCompletionSource.TrySetResult(bpState); internal Task SetResumeBreakpoint(DbgThread thread, DbgModule module) { Debug.Assert(yieldTaskCompletionSource.Task.IsCompleted); Debug2.Assert(resumeBreakpoint is null); if (resumeBreakpoint is not null) throw new InvalidOperationException(); var bpState = yieldTaskCompletionSource.Task.GetAwaiter().GetResult(); builderFieldModule = module; ResumeToken = bpState.ResumeMethod.MDToken.Raw; DbgDotNetValue? taskObjId = null; try { var runtime = module.Runtime.GetDotNetRuntime(); if ((runtime.Features & DbgDotNetRuntimeFeatures.ObjectIds) != 0 && (runtime.Features & DbgDotNetRuntimeFeatures.NoAsyncStepObjectId) == 0) { taskObjId = TryGetTaskObjectId(thread); if (taskObjId is not null) taskObjectId = runtime.CreateObjectId(taskObjId, 0); } // The thread can change so pass in null == any thread resumeBreakpoint = stepper.CreateBreakpoint(null, module, bpState.ResumeMethod.MDToken.Raw, bpState.ResumeOffset); var tcs = new TaskCompletionSource(); resumeBreakpoint.Hit += (s, e) => { bool hit = false; if (taskObjectId is null) hit = true; else { DbgDotNetValue? taskObjId2 = null; try { taskObjId2 = TryGetTaskObjectId(e.Thread); hit = taskObjId2 is null || runtime.Equals(taskObjectId, taskObjId2); } finally { taskObjId2?.Dispose(); } } if (hit) { e.Pause = true; tcs.TrySetResult(e.Thread); } }; return tcs.Task; } finally { taskObjId?.Dispose(); } } DbgDotNetValue? TryGetTaskObjectId(DbgThread thread) { DbgEvaluationInfo? evalInfo = null; DbgDotNetValue? builderValue = null; try { evalInfo = owner.CreateEvaluationInfo(thread); builderValue = TaskEvalUtils.TryGetBuilder(evalInfo, builderFieldModule!.GetReflectionModule(), builderFieldToken); if (builderValue is null) return null; return TaskEvalUtils.TryGetTaskObjectId(evalInfo, builderValue); } finally { evalInfo?.Close(); builderValue?.Dispose(); } } internal void ClearYieldBreakpoints() { var bps = yieldBreakpoints.Select(a => a.Breakpoint).ToArray(); yieldBreakpoints.Clear(); stepper.RemoveBreakpoints(bps); } internal void Dispose() { ClearYieldBreakpoints(); if (resumeBreakpoint is not null) { stepper.RemoveBreakpoints(new[] { resumeBreakpoint }); resumeBreakpoint = null; } taskObjectId?.Dispose(); taskObjectId = null; } } sealed class AsyncBreakpointState { internal readonly DbgDotNetStepperBreakpoint Breakpoint; internal readonly MethodDef ResumeMethod; internal readonly uint ResumeOffset; public event EventHandler? Hit; public AsyncBreakpointState(DbgDotNetStepperBreakpoint yieldBreakpoint, MethodDef resumeMethod, uint resumeOffset) { Breakpoint = yieldBreakpoint; ResumeMethod = resumeMethod; ResumeOffset = resumeOffset; yieldBreakpoint.Hit += YieldBreakpoint_Hit; } void YieldBreakpoint_Hit(object? sender, DbgDotNetStepperBreakpointEventArgs e) { Debug2.Assert(Hit is not null); e.Pause = true; Hit?.Invoke(this, this); } } static IEnumerable GetHiddenRanges(DbgCodeRange[] statements, DbgILSpan[] unusedSpans) { #if DEBUG for (int i = 1; i < statements.Length; i++) Debug.Assert(statements[i - 1].End <= statements[i].Start); for (int i = 1; i < unusedSpans.Length; i++) Debug.Assert(unusedSpans[i - 1].End <= unusedSpans[i].Start); #endif int si = 0; int ui = 0; while (si < statements.Length && ui < unusedSpans.Length) { while (ui < unusedSpans.Length && statements[si].End > unusedSpans[ui].Start) ui++; if (ui >= unusedSpans.Length) break; // If a hidden range immediately follows a normal statement, the hidden part could be the removed // async code and should be part of this statement. if (statements[si].End == unusedSpans[ui].Start) yield return new[] { new DbgCodeRange(unusedSpans[ui].Start, unusedSpans[ui].End) }; si++; } } static void GetAsyncStepInfos(ref List? result, MethodDef currentMethod, DbgAsyncMethodDebugInfo asyncInfo, DbgCodeRange[] ranges) { var stepInfos = asyncInfo.StepInfos; for (int i = 0; i < stepInfos.Length; i++) { ref readonly var stepInfo = ref stepInfos[i]; if (Contains(currentMethod, ranges, stepInfo)) { if (result is null) result = new List(); result.Add(stepInfo); } } } static bool Contains(MethodDef currentMethod, DbgCodeRange[] ranges, in DbgAsyncStepInfo stepInfo) { for (int i = 0; i < ranges.Length; i++) { ref readonly var range = ref ranges[i]; if (range.Contains(stepInfo.YieldOffset) || (stepInfo.ResumeMethod == currentMethod && range.Contains(stepInfo.ResumeOffset))) return true; } return false; } async Task StepOutAsync(object? tag) { runtime.Dispatcher.VerifyAccess(); Debug2.Assert(stepper.Session is null); try { var frame = stepper.TryGetFrameInfo(CurrentThread); if (frame is null) { // No frame? Just let the process run. stepper.Continue(); return; } stepper.Session = stepper.CreateSession(tag); CurrentThread = await StepOutCoreAsync(frame); StepCompleted(null, tag); } catch (ForciblyCanceledException fce) { StepCompleted(fce.Message, tag); } catch (StepErrorException see) { StepError(see.Message, tag); } catch (Exception ex) { if (stepper.IgnoreException(ex)) return; StepFailed(ex, tag); } } async Task StepOutCoreAsync(DbgDotNetEngineStepperFrameInfo frame) { DbgDotNetEngineStepperFrameInfo? f = frame; DbgThread thread; for (;;) { thread = await StepOutCore2Async(f); bool keepLooping = false; if (IgnoreBaseWrapperMethods) { f = stepper.TryGetFrameInfo(thread); Debug2.Assert(f is not null); if (f is null) return thread; if (!f.TryGetLocation(out var module, out var token, out _)) throw new InvalidOperationException(); keepLooping = CompilerUtils.IsBaseWrapperMethod(module, token); } if (!keepLooping) break; } return thread; } async Task StepOutCore2Async(DbgDotNetEngineStepperFrameInfo frame) { runtime.Dispatcher.VerifyAccess(); Debug2.Assert(stepper.Session is not null); if (debuggerSettings.AsyncDebugging) { var result = await GetStepRangesAsync(frame, returnValues: false); if (result.DebugInfo?.AsyncInfo is not null && result.DebugInfo.AsyncInfo.SetResultOffset != uint.MaxValue) { if (!frame.TryGetLocation(out var module, out var token, out _)) throw new InvalidOperationException(); // When the BP gets hit, we could be on a different thread, so pass in null var returnToAwaiterTask = TryCreateReturnToAwaiterTask(null, module, token, result.DebugInfo.AsyncInfo.SetResultOffset, result.DebugInfo.AsyncInfo.BuilderField?.MDToken.Raw ?? 0); if (returnToAwaiterTask is not null) { stepper.Continue(); return await returnToAwaiterTask; } } } return await stepper.StepOutAsync(frame); } readonly struct GetStepRangesAsyncResult { public DbgMethodDebugInfo? DebugInfo { get; } public DbgMethodDebugInfo? StateMachineDebugInfo { get; } public DbgDotNetEngineStepperFrameInfo Frame { get; } public DbgCodeRange[] StatementRanges { get; } public DbgCodeRange[] ExactStatementRanges { get; } public DbgILInstruction[][] StatementInstructions { get; } public GetStepRangesAsyncResult(DbgMethodDebugInfo? debugInfo, DbgMethodDebugInfo? stateMachineDebugInfo, DbgDotNetEngineStepperFrameInfo frame, DbgCodeRange[] statementRanges, DbgCodeRange[] exactStatementRanges, DbgILInstruction[][] statementInstructions) { DebugInfo = debugInfo; StateMachineDebugInfo = stateMachineDebugInfo; Frame = frame ?? throw new ArgumentNullException(nameof(frame)); StatementRanges = statementRanges ?? throw new ArgumentNullException(nameof(statementRanges)); ExactStatementRanges = exactStatementRanges ?? throw new ArgumentNullException(nameof(exactStatementRanges)); StatementInstructions = statementInstructions ?? throw new ArgumentNullException(nameof(statementInstructions)); } } async Task GetStepRangesAsync(DbgDotNetEngineStepperFrameInfo frame, bool returnValues) { runtime.Dispatcher.VerifyAccess(); if (!frame.TryGetLocation(out var module, out uint token, out uint offset)) throw new StepErrorException("Internal error"); uint continueCounter = stepper.ContinueCounter; var info = await dbgDotNetDebugInfoService.GetMethodDebugInfoAsync(module, token); if (continueCounter != stepper.ContinueCounter) throw new StepErrorException("Internal error"); var codeRanges = Array.Empty(); var exactCodeRanges = Array.Empty(); var instructions = Array.Empty(); if (info.DebugInfo is not null) { var sourceStatement = info.DebugInfo.GetSourceStatementByCodeOffset(offset); DbgILSpan[] ranges; if (sourceStatement is null) ranges = info.DebugInfo.GetUnusedRanges(); else { var sourceStatements = info.DebugInfo.GetILSpansOfStatement(sourceStatement.Value.TextSpan); Debug.Assert(sourceStatements.Any(a => a == sourceStatement.Value.ILSpan)); exactCodeRanges = CreateStepRanges(sourceStatements); ranges = info.DebugInfo.GetRanges(sourceStatements); } codeRanges = CreateStepRanges(ranges); if (returnValues && debuggerSettings.ShowReturnValues && frame.SupportsReturnValues) instructions = GetInstructions(info.DebugInfo.Method, exactCodeRanges) ?? Array.Empty(); } if (codeRanges.Length == 0 || exactCodeRanges.Length == 0) { DbgCodeRange defCodeRange; if (offset == DbgDotNetInstructionOffsetConstants.PROLOG) defCodeRange = new DbgCodeRange(0, 1); else if (offset == DbgDotNetInstructionOffsetConstants.EPILOG) defCodeRange = new DbgCodeRange(0xFFFFFFFE, 0xFFFFFFFF); else defCodeRange = new DbgCodeRange(offset, offset + 1); if (codeRanges.Length == 0) codeRanges = new[] { defCodeRange }; if (exactCodeRanges.Length == 0) exactCodeRanges = new[] { defCodeRange }; } return new GetStepRangesAsyncResult(info.DebugInfo, info.StateMachineDebugInfo, frame, codeRanges, exactCodeRanges, instructions); } static DbgILInstruction[][]? GetInstructions(MethodDef method, DbgCodeRange[] ranges) { var body = method.Body; if (body is null) return null; var instrs = body.Instructions; int instrsIndex = 0; var res = new DbgILInstruction[ranges.Length][]; var list = new List(); for (int i = 0; i < res.Length; i++) { list.Clear(); ref readonly var span = ref ranges[i]; uint start = span.Start; uint end = span.End; while (instrsIndex < instrs.Count && instrs[instrsIndex].Offset < start) instrsIndex++; while (instrsIndex < instrs.Count && instrs[instrsIndex].Offset < end) { var instr = instrs[instrsIndex]; list.Add(new DbgILInstruction(instr.Offset, (ushort)instr.OpCode.Code, (instr.Operand as IMDTokenProvider)?.MDToken.Raw ?? 0)); instrsIndex++; } res[i] = list.ToArray(); } return res; } static DbgCodeRange[] CreateStepRanges(DbgILSpan[] ilSpans) { if (ilSpans.Length == 0) return Array.Empty(); var stepRanges = new DbgCodeRange[ilSpans.Length]; for (int i = 0; i < stepRanges.Length; i++) { ref readonly var span = ref ilSpans[i]; stepRanges[i] = new DbgCodeRange(span.Start, span.End); } return stepRanges; } void StepCompleted(string? forciblyCanceledErrorMessage, object? tag) { runtime.Dispatcher.VerifyAccess(); if (stepper.Session is null || stepper.Session.Tag != tag) return; if (forciblyCanceledErrorMessage is null) stepper.OnStepComplete(); stepper.Session = null; RaiseStepComplete(tag, forciblyCanceledErrorMessage, forciblyCanceled: forciblyCanceledErrorMessage is not null); } void StepError(string errorMessage, object? tag) { runtime.Dispatcher.VerifyAccess(); if (stepper.Session is null || stepper.Session.Tag != tag) return; stepper.Session = null; RaiseStepComplete(tag, errorMessage); } void StepFailed(Exception exception, object? tag) { runtime.Dispatcher.VerifyAccess(); StepError("Internal error: " + exception.Message, tag); } public override void Cancel(object? tag) { if (!runtime.Dispatcher.TryBeginInvoke(() => Cancel_EngineThread(tag))) { // process has exited } } void Cancel_EngineThread(object? tag) { runtime.Dispatcher.VerifyAccess(); var oldStepperData = stepper.Session; if (oldStepperData is null) return; if (oldStepperData.Tag != tag) return; ForceCancel_EngineThread(); } void CleanUp() { ClearReturnToAwaiterState(); ClearStepIntoState(); SetAsyncStepOverState(null); } void ForceCancel_EngineThread() { runtime.Dispatcher.VerifyAccess(); CleanUp(); var oldSession = stepper.Session; stepper.Session = null; if (oldSession is not null) stepper.OnCanceled(oldSession); } protected override void CloseCore(DbgDispatcher dispatcher) { if (stepper.Session is not null) { if (!runtime.Dispatcher.TryBeginInvoke(() => ForceCancel_EngineThread())) { // process has exited } } stepper.Close(dispatcher); } } }