2021-09-20 18:20:01 +02:00

1227 lines
44 KiB
C#

/*
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 <http://www.gnu.org/licenses/>.
*/
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<DbgEngineStepCompleteEventArgs>? 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<DbgThread>? 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<DbgThread>? 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<DbgThread>();
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<DbgThread> CreateReturnToAwaiterTaskCoreAsync(Task<DbgThread> 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<DbgThread> 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<DbgThread>();
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<DbgThread> SetNotifyDebuggerOfWaitCompletionBreakpointCoreAsync(Task<DbgThread> 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<T>
"System.Threading.Tasks",
// eg. TaskAwaiter, TaskAwaiter<T>
"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<DbgThread> StepOverHiddenInstructionsAsync(DbgDotNetEngineStepperFrameInfo frame) {
runtime.Dispatcher.VerifyAccess();
var result = await GetStepRangesAsync(frame, returnValues: false);
return await StepOverHiddenInstructionsAsync(frame, result);
}
Task<DbgThread> 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<MethodILSpanState>();
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<Task<T>> WhenAny<T>(IEnumerable<Task<T>> tasks) {
var list = new List<Task<T>>(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<DbgThread> ?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<DbgThread> SetStepIntoBreakpoint(DbgThread thread, DbgModule module, uint token, uint offset) {
runtime.Dispatcher.VerifyAccess();
ClearStepIntoBreakpoint();
if (stepIntoState is null)
stepIntoState = new StepIntoState();
stepIntoState.taskCompletionSource = new TaskCompletionSource<DbgThread>();
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<DbgThread> 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<DbgThread> 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<DbgThread> 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<DbgThread> 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<Task<DbgThread>>(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<DbgAsyncStepInfo>? GetAsyncStepInfos(in GetStepRangesAsyncResult result) {
runtime.Dispatcher.VerifyAccess();
if (!debuggerSettings.AsyncDebugging)
return null;
if (result.DebugInfo?.AsyncInfo is null)
return null;
List<DbgAsyncStepInfo>? 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<AsyncBreakpointState> yieldBreakpoints;
readonly TaskCompletionSource<AsyncBreakpointState> 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<AsyncBreakpointState>();
yieldTaskCompletionSource = new TaskCompletionSource<AsyncBreakpointState>();
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<DbgThread> 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<DbgThread>();
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<AsyncBreakpointState>? 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<DbgCodeRange[]> 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<DbgAsyncStepInfo>? 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<DbgAsyncStepInfo>();
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<DbgThread> 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<DbgThread> 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<GetStepRangesAsyncResult> 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<DbgCodeRange>();
var exactCodeRanges = Array.Empty<DbgCodeRange>();
var instructions = Array.Empty<DbgILInstruction[]>();
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<DbgILInstruction[]>();
}
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<DbgILInstruction>();
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<DbgCodeRange>();
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);
}
}
}