Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public partial class ManagedDebugger : IDisposable
private bool _justMyCode;
private AsyncStepper? _asyncStepper;
private CompiledExpressionInterpreter _expressionInterpreter = null!;
private string? _launchTargetPath;
private CorDebugFunctionBreakpoint? _stopAtEntryBreakpoint;
private bool _stopAtEntryPending;

public event Action<int, string>? OnStopped;
// ThreadId, FilePath, Line, Column, Reason
Expand Down Expand Up @@ -198,7 +201,8 @@ private bool TryBindBreakpoint(BreakpointManager.BreakpointInfo bp)

// Create a breakpoint at the resolved IL offset
var corBreakpoint = ilCode.CreateBreakpoint(resolved.ILOffset);
corBreakpoint.Activate(true);
var activateBreakpoint = !_stopAtEntryPending;
corBreakpoint.Activate(activateBreakpoint);

// Update breakpoint info
bp.CorBreakpoint = corBreakpoint;
Expand All @@ -210,7 +214,7 @@ private bool TryBindBreakpoint(BreakpointManager.BreakpointInfo bp)
bp.ModuleBaseAddress = targetModule.BaseAddress;
bp.Message = null;

_logger?.Invoke($"Breakpoint bound at {bp.FilePath}:{bp.Line} -> resolved to line {resolved.StartLine}, IL offset {resolved.ILOffset} in method 0x{resolved.MethodToken:X}");
_logger?.Invoke($"Breakpoint bound at {bp.FilePath}:{bp.Line} -> resolved to line {resolved.StartLine}, IL offset {resolved.ILOffset} in method 0x{resolved.MethodToken:X}{(activateBreakpoint ? string.Empty : " (inactive until stopAtEntry completes)")}");
return true;
}
catch (Exception ex)
Expand Down Expand Up @@ -266,6 +270,34 @@ private void Cleanup()
IsRunning = false;
}

private void ActivateUserBreakpoints(bool active)
{
foreach (var breakpoint in _breakpointManager.GetAllBreakpoints())
{
if (breakpoint.CorBreakpoint == null)
{
continue;
}

try
{
breakpoint.CorBreakpoint.Activate(active);
}
catch (Exception ex)
{
_logger?.Invoke($"Error {(active ? "activating" : "deactivating")} breakpoint {breakpoint.FilePath}:{breakpoint.Line}: {ex.Message}");
}
}
}

private void CompleteStopAtEntry(CorDebugThread thread, string sourceFilePath, int line)
{
_stopAtEntryPending = false;
ActivateUserBreakpoints(true);
_logger?.Invoke($"stopAtEntry satisfied at {sourceFilePath}:{line}");
OnStopped2?.Invoke(thread.Id, sourceFilePath, line, 0, "entry", null);
}

private static string GetFunctionFormattedName(CorDebugFunction function)
{
try
Expand Down
135 changes: 109 additions & 26 deletions src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using ClrDebug;
using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator;
using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator.Interpreter;
using System.Reflection.PortableExecutable;

namespace SharpDbg.Infrastructure.Debugger;

Expand All @@ -17,15 +18,15 @@ private void HandleProcessCreated(object? sender, CreateProcessCorDebugManagedCa

private void HandleProcessExited(object? sender, ExitProcessCorDebugManagedCallbackEventArgs exitProcessCorDebugManagedCallbackEventArgs)
{
_logger?.Invoke($"Process exited");
_logger?.Invoke("Process exited");
IsRunning = false;
OnExited?.Invoke();
OnTerminated?.Invoke();
}

private void HandleThreadCreated(object? sender, CreateThreadCorDebugManagedCallbackEventArgs createThreadCorDebugManagedCallbackEventArgs)
private void HandleThreadCreated(object? sender, CreateThreadCorDebugManagedCallbackEventArgs createProcessCorDebugManagedCallbackEventArgs)
{
var corThread = createThreadCorDebugManagedCallbackEventArgs.Thread;
var corThread = createProcessCorDebugManagedCallbackEventArgs.Thread;
_threads[corThread.Id] = corThread;
OnThreadStarted?.Invoke(corThread.Id, $"Thread {corThread.Id}");
ContinueProcess();
Expand All @@ -44,11 +45,10 @@ private void HandleModuleLoaded(object? sender, LoadModuleCorDebugManagedCallbac
var corModule = loadModuleCorDebugManagedCallbackEventArgs.Module;
var modulePath = corModule.Name;
var moduleName = Path.GetFileName(modulePath);
var baseAddress = (long) corModule.BaseAddress;
var baseAddress = (long)corModule.BaseAddress;

_logger?.Invoke($"Module loaded: {modulePath} at 0x{baseAddress:X}");

// Try to load symbols for this module
SymbolReader? symbolReader = null;
try
{
Expand All @@ -75,30 +75,30 @@ private void HandleModuleLoaded(object? sender, LoadModuleCorDebugManagedCallbac

if (moduleName is "System.Private.CoreLib.dll")
{
// we need to map value classes to primitive types to allow evaluation to invoke methods on them
MapRuntimePrimitiveTypesToCorDebugClass(corModule);
// We can now initialize the expression interpreter, and assume that modules will be loaded before any stop event is allowed to be returned
var runtimeAssemblyPrimitiveTypeClasses = new RuntimeAssemblyPrimitiveTypeClasses(CorElementToValueClassMap, CorVoidClass, CorDecimalClass);
_expressionInterpreter = new CompiledExpressionInterpreter(runtimeAssemblyPrimitiveTypeClasses, _callbacks, this);
}

// Fire the module loaded event
OnModuleLoaded?.Invoke(modulePath, Path.GetFileName(modulePath), modulePath);

// Try to bind any pending breakpoints now that we have a new module with symbols
if (symbolReader != null)
{
TryBindPendingBreakpoints();
}

if (_stopAtEntryPending && _stopAtEntryBreakpoint == null && IsLaunchTargetModule(modulePath))
{
TryArmStopAtEntryBreakpoint(moduleInfo);
}

ContinueProcess();
}

private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCallbackEventArgs breakpointCorDebugManagedCallbackEventArgs)
{
try
{
//System.Diagnostics.Debugger.Launch();
var breakpoint = breakpointCorDebugManagedCallbackEventArgs.Breakpoint;
ArgumentNullException.ThrowIfNull(breakpoint);

Expand All @@ -111,13 +111,41 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal
if (breakpoint is not CorDebugFunctionBreakpoint functionBreakpoint)
{
_logger?.Invoke("Unknown breakpoint type hit");
ContinueProcess(); // may be incorrect
ContinueProcess();
return;
}

var corThread = breakpointCorDebugManagedCallbackEventArgs.Thread;

// Check if async stepper handles this breakpoint
if (_stopAtEntryBreakpoint != null && breakpoint.Raw == _stopAtEntryBreakpoint.Raw)
{
_stopAtEntryBreakpoint.Activate(false);
_stopAtEntryBreakpoint = null;
IsRunning = false;

var sourceInfoAtEntry = GetSourceInfoAtFrame(corThread.ActiveFrame);
if (sourceInfoAtEntry is not null)
{
var (sourceFilePath, line, _, _) = sourceInfoAtEntry.Value;
CompleteStopAtEntry(corThread, sourceFilePath, line);
return;
}

try
{
SetupStepper(corThread, AsyncStepper.StepType.StepIn);
_logger?.Invoke($"stopAtEntry hit managed entry point; stepping to first user source on thread {corThread.Id}");
Continue();
return;
}
catch (Exception ex)
{
_stopAtEntryPending = false;
ActivateUserBreakpoints(true);
_logger?.Invoke($"stopAtEntry could not step from managed entry point: {ex.Message}");
}
}

if (_asyncStepper != null)
{
var (asyncHandled, shouldStop) = await _asyncStepper.TryHandleBreakpoint(corThread, functionBreakpoint);
Expand Down Expand Up @@ -166,25 +194,29 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal
}

IsRunning = false;
if (_stopAtEntryPending)
{
_logger?.Invoke($"Ignoring user breakpoint at {managedBreakpoint.FilePath}:{managedBreakpoint.Line} until stopAtEntry completes");
Continue();
return;
}

OnStopped2?.Invoke(corThread.Id, managedBreakpoint.FilePath, managedBreakpoint.Line, 0, "breakpoint", null);
}
catch (Exception e)
catch
{
throw; // TODO handle exception
throw;
}
}

private void HandleStepComplete(object? sender, StepCompleteCorDebugManagedCallbackEventArgs stepCompleteEventArgs)
{
var corThread = stepCompleteEventArgs.Thread;
IsRunning = false;
var ilFrame = (CorDebugILFrame) corThread.ActiveFrame;
// If we have an active async stepper, it means we would have a breakpoint set up for either yield or resume for the next await statement
// We would then have done a regular step over/in/out to get to that breakpoint
// Since the step has completed, it means we did not hit the breakpoint, so we can clear the active async step
var ilFrame = (CorDebugILFrame)corThread.ActiveFrame;
_asyncStepper?.ClearActiveAsyncStep();
var stepper = _stepper ?? throw new InvalidOperationException("No stepper found for step complete");
stepper.Deactivate(); // I really don't know if its necessary to deactivate the steppers once done
stepper.Deactivate();
_stepper = null;
var module = _modules[ilFrame.Function.Module.BaseAddress];
var sourceInfo = GetSourceInfoAtFrame(ilFrame);
Expand All @@ -211,11 +243,9 @@ private void HandleStepComplete(object? sender, StepCompleteCorDebugManagedCallb

if (nextUserCodeIlOffset is null)
{
// Check attributes
var metadataImport = ilFrame.Function.Module.GetMetaDataInterface().MetaDataImport;
var mdMethodDef = ilFrame.Function.Token;
var methodIsNotDebuggable =
metadataImport.HasAnyAttribute(mdMethodDef, JmcConstants.JmcMethodAttributeNames);
var methodIsNotDebuggable = metadataImport.HasAnyAttribute(mdMethodDef, JmcConstants.JmcMethodAttributeNames);
if (methodIsNotDebuggable)
{
SetupStepper(corThread, AsyncStepper.StepType.StepIn);
Expand All @@ -224,12 +254,25 @@ private void HandleStepComplete(object? sender, StepCompleteCorDebugManagedCallb
}
}

var (sourceFilePath, line, column, decompiledSourceInfo) = sourceInfo.Value;
OnStopped2?.Invoke(corThread.Id, sourceFilePath, line, column, "step", decompiledSourceInfo);
var sourceInfoAtStep = GetSourceInfoAtFrame(ilFrame);
if (sourceInfoAtStep is null)
{
SetupStepper(corThread, AsyncStepper.StepType.StepOver);
Continue();
return;
}

var (sourceFilePathAtStep, lineAtStep, columnAtStep, decompiledSourceInfoAtStep) = sourceInfoAtStep.Value;
if (_stopAtEntryPending)
{
CompleteStopAtEntry(corThread, sourceFilePathAtStep, lineAtStep);
return;
}

OnStopped2?.Invoke(corThread.Id, sourceFilePathAtStep, lineAtStep, columnAtStep, "step", decompiledSourceInfoAtStep);
}

private void HandleBreak(object? sender,
BreakCorDebugManagedCallbackEventArgs breakCorDebugManagedCallbackEventArgs)
private void HandleBreak(object? sender, BreakCorDebugManagedCallbackEventArgs breakCorDebugManagedCallbackEventArgs)
{
var corThread = breakCorDebugManagedCallbackEventArgs.Thread;
IsRunning = false;
Expand All @@ -256,4 +299,44 @@ private void HandleException(object? sender, ExceptionCorDebugManagedCallbackEve

OnStopped?.Invoke(corThread.Id, "exception");
}

private void TryArmStopAtEntryBreakpoint(ModuleInfo moduleInfo)
{
if (!_stopAtEntryPending || _stopAtEntryBreakpoint != null)
{
return;
}

try
{
using var stream = File.OpenRead(moduleInfo.ModulePath);
using var peReader = new PEReader(stream);
var corHeader = peReader.PEHeaders.CorHeader;
if (corHeader == null || corHeader.EntryPointTokenOrRelativeVirtualAddress == 0)
{
_logger?.Invoke($"No managed entry point found for {moduleInfo.ModulePath}");
return;
}

var function = moduleInfo.Module.GetFunctionFromToken(corHeader.EntryPointTokenOrRelativeVirtualAddress);
var breakpoint = function.ILCode.CreateBreakpoint(0);
breakpoint.Activate(true);
_stopAtEntryBreakpoint = breakpoint;
_logger?.Invoke($"Armed stopAtEntry breakpoint in {moduleInfo.ModuleName} at token 0x{corHeader.EntryPointTokenOrRelativeVirtualAddress:X}");
}
catch (Exception ex)
{
_logger?.Invoke($"Could not arm stopAtEntry breakpoint for {moduleInfo.ModulePath}: {ex.Message}");
}
}

private bool IsLaunchTargetModule(string modulePath)
{
if (string.IsNullOrEmpty(_launchTargetPath))
{
return false;
}

return string.Equals(Path.GetFullPath(modulePath), Path.GetFullPath(_launchTargetPath), StringComparison.OrdinalIgnoreCase);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public void Launch(string program, string[] args, string? workingDirectory, Dict
_pendingLaunchArgs = args;
_pendingLaunchWorkingDirectory = workingDirectory;
_pendingLaunchStopAtEntry = stopAtEntry;
_launchTargetPath = ResolveLaunchTargetPath(program, args ?? Array.Empty<string>());
_stopAtEntryPending = stopAtEntry;
}

/// <summary>
Expand All @@ -55,6 +57,7 @@ private void PerformLaunch()
_pendingLaunchProgram = null;
_pendingLaunchArgs = null;
_pendingLaunchWorkingDirectory = null;
_pendingLaunchStopAtEntry = false;

// Build command line: "program" "arg1" "arg2" ...
var commandLine = new StringBuilder();
Expand Down Expand Up @@ -132,10 +135,28 @@ private void PerformLaunch()
_process = _corDebug.DebugActiveProcess(processId, false);
_isAttached = true;
IsRunning = true;
_stopAtEntryPending = stopAtEntry;
_stopAtEntryBreakpoint = null;
if (_stopAtEntryPending)
{
_logger?.Invoke("stopAtEntry enabled; user breakpoints will remain inactive until the entry stop is reached");
}

_logger?.Invoke($"Successfully attached to process: {processId}");
}

private static string? ResolveLaunchTargetPath(string program, string[] args)
{
if (string.Equals(Path.GetFileNameWithoutExtension(program), "dotnet", StringComparison.OrdinalIgnoreCase) &&
args.Length > 0 &&
(args[0].EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || args[0].EndsWith(".exe", StringComparison.OrdinalIgnoreCase)))
{
return args[0];
}

return program;
}

public bool RemoveBreakpoint(int id)
{
_logger?.Invoke($"RemoveBreakpoint: {id}");
Expand Down
20 changes: 18 additions & 2 deletions tests/SharpDbg.Cli.Tests/Helpers/DebugAdapterProcessHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,22 @@ public static InitializeRequest GetInitializeRequest()
};
}

public static AttachRequest GetAttachRequest(int processId, bool justMyCode = true)
public static LaunchRequest GetLaunchRequest(string program, string[] args, bool stopAtEntry = false)
{
return new LaunchRequest
{
ConfigurationProperties = new Dictionary<string, JToken>
{
["name"] = "LaunchRequestName",
["type"] = "coreclr",
["program"] = program,
["args"] = JToken.FromObject(args),
["stopAtEntry"] = stopAtEntry
}
};
}

public static AttachRequest GetAttachRequest(int processId, bool justMyCode = true, bool stopAtEntry = false)
{
return new AttachRequest
{
Expand All @@ -83,7 +98,8 @@ public static AttachRequest GetAttachRequest(int processId, bool justMyCode = tr
["type"] = "coreclr",
["processId"] = processId,
["console"] = "internalConsole", // integratedTerminal, externalTerminal, internalConsole
["justMyCode"] = justMyCode
["justMyCode"] = justMyCode,
["stopAtEntry"] = stopAtEntry
}
};
}
Expand Down
2 changes: 1 addition & 1 deletion tests/SharpDbg.Cli.Tests/Helpers/GitRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public static string GetGitRootPath()
if (_gitRoot is not null) return _gitRoot;
var currentDirectory = Directory.GetCurrentDirectory();
var gitRoot = currentDirectory;
while (!Directory.Exists(Path.Combine(gitRoot, ".git")))
while (!Directory.Exists(Path.Combine(gitRoot, ".git")) && !File.Exists(Path.Combine(gitRoot, ".git")))
{
gitRoot = Path.GetDirectoryName(gitRoot); // parent directory
if (string.IsNullOrWhiteSpace(gitRoot))
Expand Down
Loading