471 lines
17 KiB
C#
471 lines
17 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.Threading.Tasks;
|
|
using dndbg.Engine;
|
|
using dnSpy.Contracts.Debugger;
|
|
using dnSpy.Contracts.Debugger.DotNet.Code;
|
|
using dnSpy.Contracts.Debugger.DotNet.Evaluation;
|
|
using dnSpy.Contracts.Debugger.DotNet.Steppers.Engine;
|
|
using dnSpy.Debugger.DotNet.CorDebug.Impl;
|
|
using dnSpy.Debugger.DotNet.Metadata;
|
|
using DNE = dnlib.DotNet.Emit;
|
|
|
|
namespace dnSpy.Debugger.DotNet.CorDebug.Steppers {
|
|
sealed class DbgDotNetEngineStepperImpl : DbgDotNetEngineStepper {
|
|
public override SessionBase? Session {
|
|
get => session;
|
|
set {
|
|
if (session != value) {
|
|
session?.Dispose();
|
|
session = (SessionImpl?)value;
|
|
}
|
|
}
|
|
}
|
|
SessionImpl? session;
|
|
|
|
sealed class DbgDotNetEngineStepperFrameInfoImpl : DbgDotNetEngineStepperFrameInfo {
|
|
// Return values are available since .NET Framework 4.5.1 / .NET Core 1.0
|
|
public override bool SupportsReturnValues => CorFrame.Code?.SupportsReturnValues == true;
|
|
public override DbgThread Thread { get; }
|
|
|
|
internal CorFrame CorFrame { get; }
|
|
|
|
readonly DbgEngineImpl engine;
|
|
|
|
public DbgDotNetEngineStepperFrameInfoImpl(DbgEngineImpl engine, DbgThread thread, CorFrame frame) {
|
|
this.engine = engine ?? throw new ArgumentNullException(nameof(engine));
|
|
Thread = thread;
|
|
CorFrame = frame ?? throw new ArgumentNullException(nameof(frame));
|
|
}
|
|
|
|
public override bool TryGetLocation([NotNullWhen(true)] out DbgModule? module, out uint token, out uint offset) {
|
|
engine.VerifyCorDebugThread();
|
|
var func = CorFrame.Function;
|
|
module = engine.TryGetModule(func?.Module);
|
|
token = func?.Token ?? 0;
|
|
var offs = GetILOffset(CorFrame);
|
|
offset = offs ?? 0;
|
|
return module is not null && token != 0 && offs is not null;
|
|
}
|
|
|
|
public override bool Equals(DbgDotNetEngineStepperFrameInfo other) {
|
|
var otherImpl = (DbgDotNetEngineStepperFrameInfoImpl)other;
|
|
return otherImpl.CorFrame.StackStart == CorFrame.StackStart &&
|
|
otherImpl.CorFrame.StackEnd == CorFrame.StackEnd;
|
|
}
|
|
|
|
static uint? GetILOffset(CorFrame frame) {
|
|
var ip = frame.ILFrameIP;
|
|
if (ip.IsExact || ip.IsApproximate)
|
|
return ip.Offset;
|
|
if (ip.IsProlog)
|
|
return DbgDotNetInstructionOffsetConstants.PROLOG;
|
|
if (ip.IsEpilog)
|
|
return DbgDotNetInstructionOffsetConstants.EPILOG;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
sealed class CallSiteInfo {
|
|
public DmdMethodBase Method { get; }
|
|
public DnNativeCodeBreakpoint[] Breakpoints { get; }
|
|
public CallSiteInfo(DmdMethodBase method, DnNativeCodeBreakpoint[] breakpoints) {
|
|
Method = method ?? throw new ArgumentNullException(nameof(method));
|
|
Breakpoints = breakpoints ?? throw new ArgumentNullException(nameof(breakpoints));
|
|
}
|
|
}
|
|
|
|
sealed class ReturnValuesCollection {
|
|
public List<DbgDotNetReturnValueInfo> ReturnValues { get; }
|
|
public bool TooManyReturnValues { get; private set; }
|
|
|
|
readonly DbgEngineImpl engine;
|
|
readonly int maxReturnValues;
|
|
readonly List<ReturnValueState> rvStates;
|
|
|
|
public ReturnValuesCollection(DbgEngineImpl engine, int maxReturnValues) {
|
|
this.engine = engine ?? throw new ArgumentNullException(nameof(engine));
|
|
this.maxReturnValues = maxReturnValues;
|
|
ReturnValues = new List<DbgDotNetReturnValueInfo>();
|
|
rvStates = new List<ReturnValueState>();
|
|
}
|
|
|
|
public ReturnValueState CreateReturnValueState(CorThread thread, CorFrame frame) {
|
|
var rvState = new ReturnValueState(thread, frame);
|
|
rvStates.Add(rvState);
|
|
return rvState;
|
|
}
|
|
|
|
public void Hit(ReturnValueState rvState, CorThread? corThread, uint offset) {
|
|
if (TooManyReturnValues)
|
|
return;
|
|
if (!rvState.CorThread.Equals(corThread))
|
|
return;
|
|
Debug2.Assert(corThread is not null);
|
|
var corFrame = corThread.ActiveFrame;
|
|
Debug2.Assert(corFrame is not null);
|
|
if (corFrame is null)
|
|
return;
|
|
if (rvState.StackStart != corFrame.StackStart || rvState.StackEnd != corFrame.StackEnd || rvState.Token != corFrame.Token)
|
|
return;
|
|
if (!rvState.CallSiteInfos.TryGetValue(offset, out var callSiteInfo))
|
|
return;
|
|
|
|
CorValue? corValue = null;
|
|
DbgDotNetValue? dnValue = null;
|
|
bool error = true;
|
|
try {
|
|
corValue = corFrame.GetReturnValueForILOffset(offset);
|
|
if (corValue is not null) {
|
|
var reflectionModule = engine.TryGetModule(corFrame.Function?.Module)?.GetReflectionModule();
|
|
Debug2.Assert(reflectionModule is not null);
|
|
if (reflectionModule is not null) {
|
|
if (ReturnValues.Count >= maxReturnValues) {
|
|
TooManyReturnValues = true;
|
|
RemoveAllBreakpointsCore();
|
|
return;
|
|
}
|
|
|
|
// Don't add it to the close on continue list since it will get closed when we continue the
|
|
// stepper. This is not what we want...
|
|
dnValue = engine.CreateDotNetValue_CorDebug(corValue, reflectionModule.AppDomain, tryCreateStrongHandle: true, closeOnContinue: false);
|
|
uint id = (uint)ReturnValues.Count + 1;
|
|
ReturnValues.Add(new DbgDotNetReturnValueInfo(id, callSiteInfo.Method, dnValue));
|
|
error = false;
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
if (error) {
|
|
engine.DisposeHandle_CorDebug(corValue);
|
|
dnValue?.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
public DbgDotNetReturnValueInfo[] TakeOwnershipOfReturnValues() {
|
|
if (ReturnValues.Count == 0)
|
|
return Array.Empty<DbgDotNetReturnValueInfo>();
|
|
var res = ReturnValues.ToArray();
|
|
ReturnValues.Clear();
|
|
return res;
|
|
}
|
|
|
|
public void ClearReturnValues() {
|
|
RemoveAllBreakpointsCore();
|
|
foreach (var info in TakeOwnershipOfReturnValues())
|
|
info.Value.Dispose();
|
|
}
|
|
|
|
void RemoveAllBreakpointsCore() {
|
|
foreach (var rvState in rvStates) {
|
|
foreach (var kv in rvState.CallSiteInfos) {
|
|
foreach (var bp in kv.Value.Breakpoints)
|
|
engine.RemoveNativeBreakpointForGetReturnValue(bp);
|
|
}
|
|
rvState.CallSiteInfos.Clear();
|
|
}
|
|
}
|
|
|
|
void ClearReturnValuesCore() {
|
|
foreach (var info in ReturnValues)
|
|
info.Value.Dispose();
|
|
ReturnValues.Clear();
|
|
}
|
|
|
|
public void Dispose() {
|
|
RemoveAllBreakpointsCore();
|
|
ClearReturnValuesCore();
|
|
}
|
|
}
|
|
|
|
sealed class ReturnValueState {
|
|
public Dictionary<uint, CallSiteInfo> CallSiteInfos { get; }
|
|
public readonly CorThread CorThread;
|
|
public readonly ulong StackStart;
|
|
public readonly ulong StackEnd;
|
|
public readonly uint Token;
|
|
|
|
public ReturnValueState(CorThread corThread, CorFrame corFrame) {
|
|
CorThread = corThread;
|
|
StackStart = corFrame.StackStart;
|
|
StackEnd = corFrame.StackEnd;
|
|
Token = corFrame.Token;
|
|
CallSiteInfos = new Dictionary<uint, CallSiteInfo>();
|
|
}
|
|
}
|
|
|
|
sealed class SessionImpl : SessionBase {
|
|
public CorStepper? CorStepper { get; set; }
|
|
ReturnValuesCollection? returnValuesCollection;
|
|
|
|
public SessionImpl(object? tag) : base(tag) { }
|
|
public ReturnValuesCollection GetOrCreateReturnValuesCollection(DbgEngineImpl engine, int maxReturnValues) =>
|
|
returnValuesCollection ??= new ReturnValuesCollection(engine, maxReturnValues);
|
|
public void ClearReturnValues() => returnValuesCollection?.ClearReturnValues();
|
|
public DbgDotNetReturnValueInfo[] TakeOwnershipOfReturnValues() => returnValuesCollection?.TakeOwnershipOfReturnValues() ?? Array.Empty<DbgDotNetReturnValueInfo>();
|
|
public void Dispose() => returnValuesCollection?.Dispose();
|
|
}
|
|
|
|
readonly DbgEngineImpl engine;
|
|
readonly DnDebugger dnDebugger;
|
|
|
|
public DbgDotNetEngineStepperImpl(DbgEngineImpl engine, DnDebugger dnDebugger) {
|
|
this.engine = engine ?? throw new ArgumentNullException(nameof(engine));
|
|
this.dnDebugger = dnDebugger ?? throw new ArgumentNullException(nameof(dnDebugger));
|
|
}
|
|
|
|
public override SessionBase CreateSession(object? tag) => new SessionImpl(tag);
|
|
|
|
public override bool IsRuntimePaused => dnDebugger.ProcessState == DebuggerProcessState.Paused;
|
|
public override uint ContinueCounter => dnDebugger.ContinueCounter;
|
|
|
|
public override DbgDotNetEngineStepperFrameInfo? TryGetFrameInfo(DbgThread thread) {
|
|
var frame = GetILFrame(thread);
|
|
if (frame is null)
|
|
return null;
|
|
return new DbgDotNetEngineStepperFrameInfoImpl(engine, thread, frame);
|
|
}
|
|
|
|
public override void Continue() => engine.Continue_CorDebug();
|
|
|
|
public override Task<DbgThread> StepOutAsync(DbgDotNetEngineStepperFrameInfo frame) {
|
|
engine.VerifyCorDebugThread();
|
|
Debug2.Assert(session is not null);
|
|
var frameImpl = (DbgDotNetEngineStepperFrameInfoImpl)frame;
|
|
Debug.Assert(dnDebugger.ProcessState == DebuggerProcessState.Paused);
|
|
CorStepper? newCorStepper = null;
|
|
var tcs = new TaskCompletionSource<DbgThread>();
|
|
newCorStepper = dnDebugger.StepOut(frameImpl.CorFrame, (_, e, canceled) => {
|
|
if (canceled)
|
|
tcs.SetCanceled();
|
|
else {
|
|
Debug2.Assert(e is not null);
|
|
e.AddPauseReason(DebuggerPauseReason.Other);
|
|
var thread = engine.TryGetThread(e.CorThread);
|
|
if (thread is not null)
|
|
tcs.SetResult(thread);
|
|
else
|
|
tcs.SetException(new InvalidOperationException());
|
|
}
|
|
});
|
|
session.CorStepper = newCorStepper;
|
|
engine.Continue_CorDebug();
|
|
return tcs.Task;
|
|
}
|
|
|
|
public override Task<DbgThread> StepIntoAsync(DbgDotNetEngineStepperFrameInfo frame, DbgCodeRange[] ranges) {
|
|
engine.VerifyCorDebugThread();
|
|
Debug2.Assert(session is not null);
|
|
var frameImpl = (DbgDotNetEngineStepperFrameInfoImpl)frame;
|
|
Debug.Assert(dnDebugger.ProcessState == DebuggerProcessState.Paused);
|
|
CorStepper? newCorStepper = null;
|
|
var tcs = new TaskCompletionSource<DbgThread>();
|
|
var stepRanges = ToStepRanges(ranges);
|
|
newCorStepper = dnDebugger.StepInto(frameImpl.CorFrame, stepRanges, (_, e, canceled) => {
|
|
if (canceled)
|
|
tcs.SetCanceled();
|
|
else {
|
|
Debug2.Assert(e is not null);
|
|
e.AddPauseReason(DebuggerPauseReason.Other);
|
|
var thread = engine.TryGetThread(e.CorThread);
|
|
if (thread is not null)
|
|
tcs.SetResult(thread);
|
|
else
|
|
tcs.SetException(new InvalidOperationException());
|
|
}
|
|
});
|
|
session.CorStepper = newCorStepper;
|
|
engine.Continue_CorDebug();
|
|
return tcs.Task;
|
|
}
|
|
|
|
public override Task<DbgThread> StepOverAsync(DbgDotNetEngineStepperFrameInfo frame, DbgCodeRange[] ranges) {
|
|
engine.VerifyCorDebugThread();
|
|
Debug2.Assert(session is not null);
|
|
var frameImpl = (DbgDotNetEngineStepperFrameInfoImpl)frame;
|
|
Debug.Assert(dnDebugger.ProcessState == DebuggerProcessState.Paused);
|
|
CorStepper? newCorStepper = null;
|
|
var tcs = new TaskCompletionSource<DbgThread>();
|
|
var stepRanges = ToStepRanges(ranges);
|
|
newCorStepper = dnDebugger.StepOver(frameImpl.CorFrame, stepRanges, (_, e, canceled) => {
|
|
if (canceled)
|
|
tcs.SetCanceled();
|
|
else {
|
|
Debug2.Assert(e is not null);
|
|
e.AddPauseReason(DebuggerPauseReason.Other);
|
|
var thread = engine.TryGetThread(e.CorThread);
|
|
if (thread is not null)
|
|
tcs.SetResult(thread);
|
|
else
|
|
tcs.SetException(new InvalidOperationException());
|
|
}
|
|
});
|
|
session.CorStepper = newCorStepper;
|
|
engine.Continue_CorDebug();
|
|
return tcs.Task;
|
|
}
|
|
|
|
public override void OnStepComplete() {
|
|
engine.VerifyCorDebugThread();
|
|
Debug2.Assert(session is not null);
|
|
var returnValues = session.TakeOwnershipOfReturnValues() ?? Array.Empty<DbgDotNetReturnValueInfo>();
|
|
engine.SetReturnValues(returnValues);
|
|
}
|
|
|
|
public override void CollectReturnValues(DbgDotNetEngineStepperFrameInfo frame, DbgILInstruction[][] statementInstructions) {
|
|
engine.VerifyCorDebugThread();
|
|
Debug2.Assert(session is not null);
|
|
var frameImpl = (DbgDotNetEngineStepperFrameInfoImpl)frame;
|
|
if (statementInstructions.Length == 0)
|
|
return;
|
|
var code = frameImpl.CorFrame.Code;
|
|
if (code is null)
|
|
return;
|
|
var rvColl = session.GetOrCreateReturnValuesCollection(engine, maxReturnValues);
|
|
var rvState = rvColl.CreateReturnValueState(engine.GetThread(frameImpl.Thread).CorThread, frameImpl.CorFrame);
|
|
DmdModule? reflectionModule = null;
|
|
IList<DmdType>? genericTypeArguments = null;
|
|
IList<DmdType>? genericMethodArguments = null;
|
|
var bps = new List<DnNativeCodeBreakpoint>();
|
|
foreach (var instrs in statementInstructions) {
|
|
for (int i = 0; i < instrs.Length; i++) {
|
|
var instr = instrs[i];
|
|
uint instrOffs = instr.Offset;
|
|
if (instr.OpCode == (ushort)DNE.Code.Tailcall) {
|
|
if (i + 1 < instrs.Length)
|
|
instr = instrs[++i];
|
|
}
|
|
// Newobj isn't supported by the CorDebug API
|
|
bool isCall = instr.OpCode == (ushort)DNE.Code.Call ||
|
|
instr.OpCode == (ushort)DNE.Code.Callvirt;
|
|
if (!isCall)
|
|
continue;
|
|
var liveOffsets = code.GetReturnValueLiveOffset(instrOffs);
|
|
if (liveOffsets.Length == 0)
|
|
continue;
|
|
var method = GetMethod(frameImpl.CorFrame, (int)instr.Operand, ref reflectionModule, ref genericTypeArguments, ref genericMethodArguments);
|
|
Debug2.Assert(method is not null);
|
|
if (method is null)
|
|
continue;
|
|
bps.Clear();
|
|
Action<CorThread?> bpHitCallback = bpThread => rvColl.Hit(rvState, bpThread, instrOffs);
|
|
foreach (var liveOffset in liveOffsets)
|
|
bps.Add(engine.CreateNativeBreakpointForGetReturnValue(code, liveOffset, bpHitCallback));
|
|
var callSiteInfo = new CallSiteInfo(method, bps.ToArray());
|
|
rvState.CallSiteInfos.Add(instrOffs, callSiteInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void ClearReturnValues() {
|
|
engine.VerifyCorDebugThread();
|
|
session?.ClearReturnValues();
|
|
}
|
|
|
|
DmdMethodBase? GetMethod(CorFrame frame, int methodMetadataToken, ref DmdModule? reflectionModule, ref IList<DmdType>? genericTypeArguments, ref IList<DmdType>? genericMethodArguments) {
|
|
if (reflectionModule is null) {
|
|
reflectionModule = engine.TryGetModule(frame.Function?.Module)?.GetReflectionModule();
|
|
if (reflectionModule is null)
|
|
return null;
|
|
}
|
|
|
|
if (genericTypeArguments is null) {
|
|
if (!frame.GetTypeAndMethodGenericParameters(out var typeGenArgs, out var methGenArgs))
|
|
return null;
|
|
var reflectionAppDomain = reflectionModule.AppDomain;
|
|
genericTypeArguments = Convert(reflectionAppDomain, typeGenArgs);
|
|
genericMethodArguments = Convert(reflectionAppDomain, methGenArgs);
|
|
}
|
|
|
|
return reflectionModule.ResolveMethod(methodMetadataToken, genericTypeArguments, genericMethodArguments, DmdResolveOptions.None);
|
|
}
|
|
|
|
IList<DmdType> Convert(DmdAppDomain reflectionAppDomain, CorType[] typeArgs) {
|
|
if (typeArgs.Length == 0)
|
|
return Array.Empty<DmdType>();
|
|
var types = new DmdType[typeArgs.Length];
|
|
var reflectionTypeCreator = new ReflectionTypeCreator(engine, reflectionAppDomain);
|
|
for (int i = 0; i < types.Length; i++)
|
|
types[i] = reflectionTypeCreator.Create(typeArgs[i]);
|
|
return types;
|
|
}
|
|
|
|
static StepRange[] ToStepRanges(DbgCodeRange[] ranges) {
|
|
var result = new StepRange[ranges.Length];
|
|
for (int i = 0; i < result.Length; i++) {
|
|
var r = ranges[i];
|
|
result[i] = new StepRange(r.Start, r.End);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
CorFrame? GetILFrame(DbgThread thread) {
|
|
engine.VerifyCorDebugThread();
|
|
var dnThread = engine.GetThread(thread);
|
|
foreach (var frame in dnThread.AllFrames) {
|
|
if (frame.IsILFrame)
|
|
return frame;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public override DbgDotNetStepperBreakpoint CreateBreakpoint(DbgThread? thread, DbgModule module, uint token, uint offset) {
|
|
engine.VerifyCorDebugThread();
|
|
return new DbgDotNetStepperBreakpointImpl(engine, thread, module, token, offset);
|
|
}
|
|
|
|
public override void RemoveBreakpoints(DbgDotNetStepperBreakpoint[] breakpoints) {
|
|
engine.VerifyCorDebugThread();
|
|
foreach (DbgDotNetStepperBreakpointImpl bp in breakpoints)
|
|
bp.Dispose();
|
|
}
|
|
|
|
public override bool IgnoreException(Exception exception) => false;
|
|
|
|
public override void OnCanceled(SessionBase session) {
|
|
engine.VerifyCorDebugThread();
|
|
CancelStepper(session);
|
|
}
|
|
|
|
public override void CancelLastStep() {
|
|
engine.VerifyCorDebugThread();
|
|
CancelStepper(session);
|
|
}
|
|
|
|
void CancelStepper(SessionBase? session) {
|
|
engine.VerifyCorDebugThread();
|
|
if (session is null)
|
|
return;
|
|
var sessionImpl = (SessionImpl)session;
|
|
var stepper = sessionImpl.CorStepper;
|
|
sessionImpl.CorStepper = null;
|
|
if (stepper is not null)
|
|
dnDebugger.CancelStep(stepper);
|
|
}
|
|
|
|
public override void Close(DbgDispatcher dispatcher) { }
|
|
}
|
|
}
|