mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
694 lines
19 KiB
694 lines
19 KiB
// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) |
|
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) |
|
|
|
using System; |
|
using System.Collections.Generic; |
|
using System.Runtime.InteropServices; |
|
|
|
using Debugger.Interop.CorDebug; |
|
using Debugger.Interop.CorSym; |
|
using ICSharpCode.NRefactory.Ast; |
|
using ICSharpCode.NRefactory.Visitors; |
|
|
|
namespace Debugger |
|
{ |
|
internal enum DebuggeeStateAction { Keep, Clear } |
|
|
|
/// <summary> |
|
/// Debug Mode Flags. |
|
/// </summary> |
|
public enum DebugModeFlag |
|
{ |
|
/// <summary> |
|
/// Run in the same mode as without debugger. |
|
/// </summary> |
|
Default, |
|
/// <summary> |
|
/// Run in forced optimized mode. |
|
/// </summary> |
|
Optimized, |
|
/// <summary> |
|
/// Run in debug mode (easy inspection) but slower. |
|
/// </summary> |
|
Debug, |
|
/// <summary> |
|
/// Run in ENC mode (ENC possible) but even slower than debug |
|
/// </summary> |
|
Enc |
|
} |
|
|
|
public class Process: DebuggerObject |
|
{ |
|
NDebugger debugger; |
|
|
|
ICorDebugProcess corProcess; |
|
ManagedCallback callbackInterface; |
|
|
|
EvalCollection activeEvals; |
|
ModuleCollection modules; |
|
ThreadCollection threads; |
|
AppDomainCollection appDomains; |
|
|
|
string workingDirectory; |
|
|
|
|
|
public NDebugger Debugger { |
|
get { return debugger; } |
|
} |
|
|
|
internal ICorDebugProcess CorProcess { |
|
get { return corProcess; } |
|
} |
|
|
|
public Options Options { |
|
get { return debugger.Options; } |
|
} |
|
|
|
public string DebuggeeVersion { |
|
get { return debugger.DebuggeeVersion; } |
|
} |
|
|
|
internal ManagedCallback CallbackInterface { |
|
get { return callbackInterface; } |
|
} |
|
|
|
public EvalCollection ActiveEvals { |
|
get { return activeEvals; } |
|
} |
|
|
|
internal bool Evaluating { |
|
get { return activeEvals.Count > 0; } |
|
} |
|
|
|
public ModuleCollection Modules { |
|
get { return modules; } |
|
} |
|
|
|
public ThreadCollection Threads { |
|
get { return threads; } |
|
} |
|
|
|
public Thread SelectedThread { |
|
get { return this.Threads.Selected; } |
|
set { this.Threads.Selected = value; } |
|
} |
|
|
|
public StackFrame SelectedStackFrame { |
|
get { |
|
if (SelectedThread == null) { |
|
return null; |
|
} else { |
|
return SelectedThread.SelectedStackFrame; |
|
} |
|
} |
|
} |
|
|
|
public SourcecodeSegment NextStatement { |
|
get { |
|
if (SelectedStackFrame == null || IsRunning) { |
|
return null; |
|
} else { |
|
return SelectedStackFrame.NextStatement; |
|
} |
|
} |
|
} |
|
|
|
public bool BreakAtBeginning { |
|
get; |
|
set; |
|
} |
|
|
|
public AppDomainCollection AppDomains { |
|
get { return appDomains; } |
|
} |
|
|
|
List<Stepper> steppers = new List<Stepper>(); |
|
|
|
internal List<Stepper> Steppers { |
|
get { return steppers; } |
|
} |
|
|
|
public string WorkingDirectory { |
|
get { return workingDirectory; } |
|
} |
|
|
|
public static DebugModeFlag DebugMode { get; set; } |
|
|
|
internal Process(NDebugger debugger, ICorDebugProcess corProcess, string workingDirectory) |
|
{ |
|
this.debugger = debugger; |
|
this.corProcess = corProcess; |
|
this.workingDirectory = workingDirectory; |
|
|
|
this.callbackInterface = new ManagedCallback(this); |
|
|
|
activeEvals = new EvalCollection(debugger); |
|
modules = new ModuleCollection(debugger); |
|
modules.Added += OnModulesAdded; |
|
threads = new ThreadCollection(debugger); |
|
appDomains = new AppDomainCollection(debugger); |
|
} |
|
|
|
static unsafe public Process CreateProcess(NDebugger debugger, string filename, string workingDirectory, string arguments) |
|
{ |
|
debugger.TraceMessage("Executing " + filename + " " + arguments); |
|
|
|
uint[] processStartupInfo = new uint[17]; |
|
processStartupInfo[0] = sizeof(uint) * 17; |
|
uint[] processInfo = new uint[4]; |
|
|
|
ICorDebugProcess outProcess; |
|
|
|
if (workingDirectory == null || workingDirectory == "") { |
|
workingDirectory = System.IO.Path.GetDirectoryName(filename); |
|
} |
|
|
|
_SECURITY_ATTRIBUTES secAttr = new _SECURITY_ATTRIBUTES(); |
|
secAttr.bInheritHandle = 0; |
|
secAttr.lpSecurityDescriptor = IntPtr.Zero; |
|
secAttr.nLength = (uint)sizeof(_SECURITY_ATTRIBUTES); |
|
|
|
fixed (uint* pprocessStartupInfo = processStartupInfo) |
|
fixed (uint* pprocessInfo = processInfo) |
|
outProcess = |
|
debugger.CorDebug.CreateProcess( |
|
filename, // lpApplicationName |
|
// If we do not prepend " ", the first argument migh just get lost |
|
" " + arguments, // lpCommandLine |
|
ref secAttr, // lpProcessAttributes |
|
ref secAttr, // lpThreadAttributes |
|
1,//TRUE // bInheritHandles |
|
0x00000010 /*CREATE_NEW_CONSOLE*/, // dwCreationFlags |
|
IntPtr.Zero, // lpEnvironment |
|
workingDirectory, // lpCurrentDirectory |
|
(uint)pprocessStartupInfo, // lpStartupInfo |
|
(uint)pprocessInfo, // lpProcessInformation, |
|
CorDebugCreateProcessFlags.DEBUG_NO_SPECIAL_OPTIONS // debuggingFlags |
|
); |
|
|
|
return new Process(debugger, outProcess, workingDirectory); |
|
} |
|
|
|
/// <summary> Fired when System.Diagnostics.Trace.WriteLine() is called in debuged process </summary> |
|
public event EventHandler<MessageEventArgs> LogMessage; |
|
|
|
protected internal virtual void OnLogMessage(MessageEventArgs arg) |
|
{ |
|
TraceMessage ("Debugger event: OnLogMessage"); |
|
if (LogMessage != null) { |
|
LogMessage(this, arg); |
|
} |
|
} |
|
|
|
public void TraceMessage(string message, params object[] args) |
|
{ |
|
if (args.Length > 0) |
|
message = string.Format(message, args); |
|
System.Diagnostics.Debug.WriteLine("Debugger:" + message); |
|
debugger.OnDebuggerTraceMessage(new MessageEventArgs(this, message)); |
|
} |
|
|
|
/// <summary> Read the specified amount of memory at the given memory address </summary> |
|
/// <returns> The content of the memory. The amount of the read memory may be less then requested. </returns> |
|
public unsafe byte[] ReadMemory(ulong address, int size) |
|
{ |
|
byte[] buffer = new byte[size]; |
|
int readCount; |
|
fixed(byte* pBuffer = buffer) { |
|
readCount = (int)corProcess.ReadMemory(address, (uint)size, new IntPtr(pBuffer)); |
|
} |
|
if (readCount != size) Array.Resize(ref buffer, readCount); |
|
return buffer; |
|
} |
|
|
|
/// <summary> Writes the given buffer at the specified memory address </summary> |
|
/// <returns> The number of bytes written </returns> |
|
public unsafe int WriteMemory(ulong address, byte[] buffer) |
|
{ |
|
if (buffer.Length == 0) return 0; |
|
int written; |
|
fixed(byte* pBuffer = buffer) { |
|
written = (int)corProcess.WriteMemory(address, (uint)buffer.Length, new IntPtr(pBuffer)); |
|
} |
|
return written; |
|
} |
|
|
|
#region Exceptions |
|
|
|
bool pauseOnHandledException = false; |
|
|
|
public event EventHandler<ExceptionEventArgs> ExceptionThrown; |
|
|
|
public bool PauseOnHandledException { |
|
get { return pauseOnHandledException; } |
|
set { pauseOnHandledException = value; } |
|
} |
|
|
|
protected internal virtual void OnExceptionThrown(ExceptionEventArgs e) |
|
{ |
|
TraceMessage ("Debugger event: OnExceptionThrown()"); |
|
if (ExceptionThrown != null) { |
|
ExceptionThrown(this, e); |
|
} |
|
} |
|
|
|
#endregion |
|
|
|
// State control for the process |
|
|
|
internal bool TerminateCommandIssued = false; |
|
internal Queue<Breakpoint> BreakpointHitEventQueue = new Queue<Breakpoint>(); |
|
internal Dictionary<INode, TypedValue> ExpressionsCache = new Dictionary<INode, TypedValue>(); |
|
|
|
#region Events |
|
|
|
public event EventHandler<ProcessEventArgs> Paused; |
|
public event EventHandler<ProcessEventArgs> Resumed; |
|
|
|
// HACK: public |
|
public virtual void OnPaused() |
|
{ |
|
AssertPaused(); |
|
// No real purpose - just additional check |
|
if (callbackInterface.IsInCallback) throw new DebuggerException("Can not raise event within callback."); |
|
TraceMessage ("Debugger event: OnPaused()"); |
|
if (Paused != null) { |
|
foreach(Delegate d in Paused.GetInvocationList()) { |
|
if (IsRunning) { |
|
TraceMessage ("Skipping OnPaused delegate because process has resumed"); |
|
break; |
|
} |
|
if (this.TerminateCommandIssued || this.HasExited) { |
|
TraceMessage ("Skipping OnPaused delegate because process has exited"); |
|
break; |
|
} |
|
d.DynamicInvoke(this, new ProcessEventArgs(this)); |
|
} |
|
} |
|
} |
|
|
|
protected virtual void OnResumed() |
|
{ |
|
AssertRunning(); |
|
if (callbackInterface.IsInCallback) |
|
throw new DebuggerException("Can not raise event within callback."); |
|
TraceMessage ("Debugger event: OnResumed()"); |
|
if (Resumed != null) { |
|
Resumed(this, new ProcessEventArgs(this)); |
|
} |
|
} |
|
|
|
#endregion |
|
|
|
#region PauseSession & DebugeeState |
|
|
|
PauseSession pauseSession; |
|
DebuggeeState debuggeeState; |
|
|
|
/// <summary> |
|
/// Indentification of the current debugger session. This value changes whenever debugger is continued |
|
/// </summary> |
|
public PauseSession PauseSession { |
|
get { return pauseSession; } |
|
} |
|
|
|
/// <summary> |
|
/// Indentification of the state of the debugee. This value changes whenever the state of the debugee significatntly changes |
|
/// </summary> |
|
public DebuggeeState DebuggeeState { |
|
get { return debuggeeState; } |
|
} |
|
|
|
/// <summary> Puts the process into a paused state </summary> |
|
internal void NotifyPaused(PausedReason pauseReason) |
|
{ |
|
AssertRunning(); |
|
pauseSession = new PauseSession(this, pauseReason); |
|
if (debuggeeState == null) { |
|
debuggeeState = new DebuggeeState(this); |
|
} |
|
} |
|
|
|
/// <summary> Puts the process into a resumed state </summary> |
|
internal void NotifyResumed(DebuggeeStateAction action) |
|
{ |
|
AssertPaused(); |
|
pauseSession = null; |
|
if (action == DebuggeeStateAction.Clear) { |
|
if (debuggeeState == null) throw new DebuggerException("Debugee state already cleared"); |
|
debuggeeState = null; |
|
this.ExpressionsCache.Clear(); |
|
} |
|
} |
|
|
|
/// <summary> Sets up the eviroment and raises user events </summary> |
|
internal void RaisePausedEvents() |
|
{ |
|
AssertPaused(); |
|
DisableAllSteppers(); |
|
CheckSelectedStackFrames(); |
|
SelectMostRecentStackFrameWithLoadedSymbols(); |
|
|
|
if (this.PauseSession.PausedReason == PausedReason.Exception) { |
|
ExceptionEventArgs args = new ExceptionEventArgs(this, this.SelectedThread.CurrentException, this.SelectedThread.CurrentExceptionType, this.SelectedThread.CurrentExceptionIsUnhandled); |
|
OnExceptionThrown(args); |
|
// The event could have resumed or killed the process |
|
if (this.IsRunning || this.TerminateCommandIssued || this.HasExited) return; |
|
} |
|
|
|
while(BreakpointHitEventQueue.Count > 0) { |
|
Breakpoint breakpoint = BreakpointHitEventQueue.Dequeue(); |
|
breakpoint.NotifyHit(); |
|
// The event could have resumed or killed the process |
|
if (this.IsRunning || this.TerminateCommandIssued || this.HasExited) return; |
|
} |
|
|
|
OnPaused(); |
|
// The event could have resumed the process |
|
if (this.IsRunning || this.TerminateCommandIssued || this.HasExited) return; |
|
} |
|
|
|
#endregion |
|
|
|
internal void AssertPaused() |
|
{ |
|
if (IsRunning) { |
|
throw new DebuggerException("Process is not paused."); |
|
} |
|
} |
|
|
|
internal void AssertRunning() |
|
{ |
|
if (IsPaused) { |
|
throw new DebuggerException("Process is not running."); |
|
} |
|
} |
|
|
|
public bool IsRunning { |
|
get { return pauseSession == null; } |
|
} |
|
|
|
public bool IsPaused { |
|
get { return !IsRunning; } |
|
} |
|
|
|
bool hasExited = false; |
|
|
|
public event EventHandler Exited; |
|
|
|
public bool HasExited { |
|
get { |
|
return hasExited; |
|
} |
|
} |
|
|
|
internal void NotifyHasExited() |
|
{ |
|
if(!hasExited) { |
|
hasExited = true; |
|
if (Exited != null) { |
|
Exited(this, new ProcessEventArgs(this)); |
|
} |
|
// Expire pause seesion first |
|
if (IsPaused) { |
|
NotifyResumed(DebuggeeStateAction.Clear); |
|
} |
|
debugger.Processes.Remove(this); |
|
} |
|
} |
|
|
|
public void Break() |
|
{ |
|
AssertRunning(); |
|
|
|
corProcess.Stop(uint.MaxValue); // Infinite; ignored anyway |
|
|
|
NotifyPaused(PausedReason.ForcedBreak); |
|
RaisePausedEvents(); |
|
} |
|
|
|
public void Detach() |
|
{ |
|
if (IsRunning) { |
|
corProcess.Stop(uint.MaxValue); |
|
NotifyPaused(PausedReason.ForcedBreak); |
|
} |
|
|
|
// This is necessary for detach |
|
foreach(Stepper s in this.Steppers) { |
|
if (s.CorStepper.IsActive() == 1) { |
|
s.CorStepper.Deactivate(); |
|
} |
|
} |
|
this.Steppers.Clear(); |
|
|
|
corProcess.Detach(); |
|
|
|
// modules |
|
foreach(Module m in this.Modules) |
|
{ |
|
m.Dispose(); |
|
} |
|
|
|
this.modules.Clear(); |
|
|
|
// threads |
|
this.threads.Clear(); |
|
|
|
NotifyHasExited(); |
|
} |
|
|
|
public void Continue() |
|
{ |
|
AsyncContinue(); |
|
WaitForPause(); |
|
} |
|
|
|
internal Thread[] UnsuspendedThreads { |
|
get { |
|
List<Thread> unsuspendedThreads = new List<Thread>(this.Threads.Count); |
|
foreach(Thread t in this.Threads) { |
|
if (!t.Suspended) |
|
unsuspendedThreads.Add(t); |
|
} |
|
return unsuspendedThreads.ToArray(); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Resume execution and run all threads not marked by the user as susspended. |
|
/// </summary> |
|
public void AsyncContinue() |
|
{ |
|
AsyncContinue(DebuggeeStateAction.Clear, this.UnsuspendedThreads, CorDebugThreadState.THREAD_RUN); |
|
} |
|
|
|
internal CorDebugThreadState NewThreadState = CorDebugThreadState.THREAD_RUN; |
|
|
|
/// <param name="threadsToRun"> Null to keep current setting </param> |
|
/// <param name="newThreadState"> What happens to created threads. Null to keep current setting </param> |
|
internal void AsyncContinue(DebuggeeStateAction action, Thread[] threadsToRun, CorDebugThreadState? newThreadState) |
|
{ |
|
AssertPaused(); |
|
|
|
if (threadsToRun != null) { |
|
// corProcess.SetAllThreadsDebugState(CorDebugThreadState.THREAD_SUSPEND, null); |
|
// Note: There is unreported thread, stopping it prevents the debugee from exiting |
|
// It is not corProcess.GetHelperThreadID |
|
// ICorDebugThread[] ts = new ICorDebugThread[corProcess.EnumerateThreads().GetCount()]; |
|
// corProcess.EnumerateThreads().Next((uint)ts.Length, ts); |
|
foreach(Thread t in this.Threads) { |
|
CorDebugThreadState state = Array.IndexOf(threadsToRun, t) == -1 ? CorDebugThreadState.THREAD_SUSPEND : CorDebugThreadState.THREAD_RUN; |
|
try { |
|
t.CorThread.SetDebugState(state); |
|
} catch (COMException e) { |
|
// The state of the thread is invalid. (Exception from HRESULT: 0x8013132D) |
|
// It can happen for example when thread has not started yet |
|
if ((uint)e.ErrorCode == 0x8013132D) { |
|
// TraceMessage("Can not suspend thread - The state of the thread is invalid. Thread ID = " + t.CorThread.GetID()); |
|
} else { |
|
throw; |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (newThreadState != null) { |
|
this.NewThreadState = newThreadState.Value; |
|
} |
|
|
|
NotifyResumed(action); |
|
corProcess.Continue(0); |
|
if (this.Options.Verbose) { |
|
this.TraceMessage("Continue"); |
|
} |
|
|
|
if (action == DebuggeeStateAction.Clear) { |
|
OnResumed(); |
|
} |
|
} |
|
|
|
/// <summary> Terminates the execution of the process </summary> |
|
public void Terminate() |
|
{ |
|
AsyncTerminate(); |
|
// Wait until ExitProcess callback is received |
|
WaitForExit(); |
|
} |
|
|
|
/// <summary> Terminates the execution of the process </summary> |
|
public void AsyncTerminate() |
|
{ |
|
// Resume stoped tread |
|
if (this.IsPaused) { |
|
// We might get more callbacks so we should maintain consistent sate |
|
//AsyncContinue(); // Continue the process to get remaining callbacks |
|
} |
|
|
|
// Expose race condition - drain callback queue |
|
System.Threading.Thread.Sleep(0); |
|
|
|
// Stop&terminate - both must be called |
|
corProcess.Stop(uint.MaxValue); |
|
corProcess.Terminate(0); |
|
this.TerminateCommandIssued = true; |
|
|
|
// Do not mark the process as exited |
|
// This is done once ExitProcess callback is received |
|
} |
|
|
|
void SelectSomeThread() |
|
{ |
|
if (this.SelectedThread != null && !this.SelectedThread.IsInValidState) { |
|
this.SelectedThread = null; |
|
} |
|
if (this.SelectedThread == null) { |
|
foreach(Thread thread in this.Threads) { |
|
if (thread.IsInValidState) { |
|
this.SelectedThread = thread; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
internal void CheckSelectedStackFrames() |
|
{ |
|
foreach(Thread thread in this.Threads) { |
|
if (thread.IsInValidState) { |
|
if (thread.SelectedStackFrame != null && thread.SelectedStackFrame.IsInvalid) { |
|
thread.SelectedStackFrame = null; |
|
} |
|
} else { |
|
thread.SelectedStackFrame = null; |
|
} |
|
} |
|
} |
|
|
|
internal void SelectMostRecentStackFrameWithLoadedSymbols() |
|
{ |
|
SelectSomeThread(); |
|
if (this.SelectedThread != null) { |
|
this.SelectedThread.SelectedStackFrame = null; |
|
foreach (StackFrame stackFrame in this.SelectedThread.Callstack) { |
|
if (stackFrame.HasSymbols) { |
|
if (this.Options.StepOverDebuggerAttributes && stackFrame.MethodInfo.IsNonUserCode) |
|
continue; |
|
this.SelectedThread.SelectedStackFrame = stackFrame; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
internal Stepper GetStepper(ICorDebugStepper corStepper) |
|
{ |
|
foreach(Stepper stepper in this.Steppers) { |
|
if (stepper.IsCorStepper(corStepper)) { |
|
return stepper; |
|
} |
|
} |
|
throw new DebuggerException("Stepper is not in collection"); |
|
} |
|
|
|
internal void DisableAllSteppers() |
|
{ |
|
foreach(Thread thread in this.Threads) { |
|
thread.CurrentStepIn = null; |
|
} |
|
foreach(Stepper stepper in this.Steppers) { |
|
stepper.Ignore = true; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Waits until the debugger pauses unless it is already paused. |
|
/// Use PausedReason to find out why it paused. |
|
/// </summary> |
|
public void WaitForPause() |
|
{ |
|
while(this.IsRunning && !this.HasExited) { |
|
debugger.MTA2STA.WaitForCall(); |
|
debugger.MTA2STA.PerformAllCalls(); |
|
} |
|
if (this.HasExited) throw new ProcessExitedException(); |
|
} |
|
|
|
public void WaitForPause(TimeSpan timeout) |
|
{ |
|
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch(); |
|
watch.Start(); |
|
while(this.IsRunning && !this.HasExited) { |
|
TimeSpan timeLeft = timeout - watch.Elapsed; |
|
if (timeLeft <= TimeSpan.FromMilliseconds(10)) break; |
|
//this.TraceMessage("Time left: " + timeLeft.TotalMilliseconds); |
|
debugger.MTA2STA.WaitForCall(timeLeft); |
|
debugger.MTA2STA.PerformAllCalls(); |
|
} |
|
if (this.HasExited) throw new ProcessExitedException(); |
|
} |
|
|
|
/// <summary> |
|
/// Waits until the precesses exits. |
|
/// </summary> |
|
public void WaitForExit() |
|
{ |
|
while(!this.HasExited) { |
|
debugger.MTA2STA.WaitForCall(); |
|
debugger.MTA2STA.PerformAllCalls(); |
|
} |
|
} |
|
|
|
#region Break at begining |
|
|
|
private void OnModulesAdded(object sender, CollectionItemEventArgs<Module> e) |
|
{ |
|
if (BreakAtBeginning) { |
|
if (e.Item.SymReader == null) return; // No symbols |
|
|
|
try { |
|
// create a BP at entry point |
|
uint entryPoint = e.Item.SymReader.GetUserEntryPoint(); |
|
if (entryPoint == 0) return; // no EP |
|
var mainFunction = e.Item.CorModule.GetFunctionFromToken(entryPoint); |
|
var corBreakpoint = mainFunction.CreateBreakpoint(); |
|
corBreakpoint.Activate(1); |
|
|
|
// create a SD BP |
|
var breakpoint = new Breakpoint(this.debugger, corBreakpoint); |
|
this.debugger.Breakpoints.Add(breakpoint); |
|
breakpoint.Hit += delegate { |
|
if (breakpoint != null) |
|
breakpoint.Remove(); |
|
breakpoint = null; |
|
}; |
|
} catch { |
|
// the app does not have an entry point - COM exception |
|
} |
|
BreakAtBeginning = false; |
|
} |
|
} |
|
|
|
#endregion |
|
} |
|
}
|
|
|