mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-30 22:49:53 +08:00
Add live debugger frontend and mutex stall recovery (#383)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using SharpEmu.Core.Cpu.Debugging;
|
||||
using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
@@ -272,7 +273,23 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
entryFrameDiagnostic,
|
||||
Environment.NewLine,
|
||||
"CpuEngine: native-only");
|
||||
// Frame boundaries an attached debugger observes; null hook = a branch.
|
||||
var debugHook = executionOptions.DebugHook;
|
||||
var debugFrame = debugHook is null
|
||||
? null
|
||||
: new CpuContextDebugFrame(
|
||||
frameKind == EntryFrameKind.ProcessEntry
|
||||
? CpuDebugFrameKind.ProcessEntry
|
||||
: CpuDebugFrameKind.ModuleInitializer,
|
||||
entryPoint,
|
||||
processImageName,
|
||||
context,
|
||||
effectiveImportStubs);
|
||||
debugHook?.OnFrameEnter(debugFrame!);
|
||||
|
||||
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
|
||||
// Let backend stall reports reference the same frame as entry.
|
||||
(_nativeCpuBackend as DirectExecutionBackend)?.SetActiveDebugFrame(debugFrame);
|
||||
if (_nativeCpuBackend.TryExecute(
|
||||
context,
|
||||
entryPoint,
|
||||
@@ -282,6 +299,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
executionOptions,
|
||||
out var nativeResult))
|
||||
{
|
||||
debugHook?.OnFrameExit(debugFrame!, nativeResult);
|
||||
LastSessionSummary = new CpuSessionSummary(
|
||||
nativeResult,
|
||||
nativeResult == OrbisGen2Result.ORBIS_GEN2_OK
|
||||
@@ -296,6 +314,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
return nativeResult;
|
||||
}
|
||||
|
||||
debugHook?.OnFrameExit(debugFrame!, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
|
||||
var backendName = string.IsNullOrWhiteSpace(_nativeCpuBackend.BackendName)
|
||||
? "native-backend"
|
||||
: _nativeCpuBackend.BackendName;
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu;
|
||||
|
||||
public readonly struct CpuExecutionOptions
|
||||
{
|
||||
public bool EnableDisasmDiagnostics { get; init; }
|
||||
|
||||
|
||||
public CpuExecutionEngine CpuEngine { get; init; }
|
||||
|
||||
public bool StrictDynlibResolution { get; init; }
|
||||
|
||||
public int ImportTraceLimit { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional debugger attached to this execution session. When set, the
|
||||
/// dispatcher notifies it at each frame boundary via
|
||||
/// <see cref="ICpuDebugHook.OnFrameEnter"/> / <see cref="ICpuDebugHook.OnFrameExit"/>.
|
||||
/// Null when no debugger is attached, which is the default and imposes no
|
||||
/// runtime cost.
|
||||
/// </summary>
|
||||
public ICpuDebugHook? DebugHook { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// Adapts a live <see cref="CpuContext"/> to <see cref="ICpuDebugFrame"/>. The
|
||||
/// dispatcher creates one of these around the guest context it is about to run
|
||||
/// and passes it to the attached <see cref="ICpuDebugHook"/>; every accessor
|
||||
/// forwards directly to the underlying context.
|
||||
/// </summary>
|
||||
internal sealed class CpuContextDebugFrame : ICpuDebugFrame
|
||||
{
|
||||
private readonly CpuContext _context;
|
||||
|
||||
internal CpuContextDebugFrame(
|
||||
CpuDebugFrameKind kind,
|
||||
ulong entryPoint,
|
||||
string label,
|
||||
CpuContext context,
|
||||
IReadOnlyDictionary<ulong, string> importStubs)
|
||||
{
|
||||
Kind = kind;
|
||||
EntryPoint = entryPoint;
|
||||
Label = label ?? string.Empty;
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
ImportStubs = importStubs ?? new Dictionary<ulong, string>();
|
||||
}
|
||||
|
||||
public CpuDebugFrameKind Kind { get; }
|
||||
|
||||
public Generation Generation => _context.TargetGeneration;
|
||||
|
||||
public ulong EntryPoint { get; }
|
||||
|
||||
public string Label { get; }
|
||||
|
||||
public ICpuMemory Memory => _context.Memory;
|
||||
|
||||
public ulong GetRegister(CpuRegister register) => _context[register];
|
||||
|
||||
public void SetRegister(CpuRegister register, ulong value) => _context[register] = value;
|
||||
|
||||
public ulong Rip
|
||||
{
|
||||
get => _context.Rip;
|
||||
set => _context.Rip = value;
|
||||
}
|
||||
|
||||
public ulong Rflags
|
||||
{
|
||||
get => _context.Rflags;
|
||||
set => _context.Rflags = value;
|
||||
}
|
||||
|
||||
public ulong FsBase => _context.FsBase;
|
||||
|
||||
public ulong GsBase => _context.GsBase;
|
||||
|
||||
public void GetXmm(int registerIndex, out ulong low, out ulong high)
|
||||
=> _context.GetXmmRegister(registerIndex, out low, out high);
|
||||
|
||||
public IReadOnlyDictionary<ulong, string> ImportStubs { get; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the kind of guest entry frame a debugger is observing. The
|
||||
/// dispatcher enters a fresh frame for the process entry point and for every
|
||||
/// module initializer, so the debug layer can label stops accordingly.
|
||||
/// </summary>
|
||||
public enum CpuDebugFrameKind
|
||||
{
|
||||
/// <summary>The guest process entry point (<c>eboot.bin</c> start).</summary>
|
||||
ProcessEntry,
|
||||
|
||||
/// <summary>A module DT_INIT / initializer routine.</summary>
|
||||
ModuleInitializer,
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
/// <summary>The kind of execution stall the backend detected.</summary>
|
||||
public enum CpuStallKind
|
||||
{
|
||||
/// <summary>
|
||||
/// The guest is repeatedly re-dispatching the same import with no forward
|
||||
/// progress — most commonly a spin on a mutex lock/unlock pair.
|
||||
/// </summary>
|
||||
ImportLoop,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Details of a detected stall handed to <see cref="ICpuDebugHook.OnStall"/>.
|
||||
/// Reported from the emulation thread at the point the backend recognises the
|
||||
/// livelock, before it forces the guest out of the loop.
|
||||
/// </summary>
|
||||
public readonly struct CpuStallInfo
|
||||
{
|
||||
public CpuStallInfo(
|
||||
CpuStallKind kind,
|
||||
string? nid,
|
||||
ulong instructionPointer,
|
||||
long dispatchIndex,
|
||||
ulong argument0,
|
||||
ulong argument1,
|
||||
string detail,
|
||||
string? libraryName = null,
|
||||
string? functionName = null)
|
||||
{
|
||||
Kind = kind;
|
||||
Nid = nid;
|
||||
InstructionPointer = instructionPointer;
|
||||
DispatchIndex = dispatchIndex;
|
||||
Argument0 = argument0;
|
||||
Argument1 = argument1;
|
||||
Detail = detail ?? string.Empty;
|
||||
LibraryName = libraryName;
|
||||
FunctionName = functionName;
|
||||
}
|
||||
|
||||
public CpuStallKind Kind { get; }
|
||||
|
||||
/// <summary>The NID of the import being spun on, when known.</summary>
|
||||
public string? Nid { get; }
|
||||
|
||||
/// <summary>The guest return address of the looping import dispatch.</summary>
|
||||
public ulong InstructionPointer { get; }
|
||||
|
||||
/// <summary>The import dispatch counter at detection time.</summary>
|
||||
public long DispatchIndex { get; }
|
||||
|
||||
/// <summary>The first two guest ABI arguments at stall detection.</summary>
|
||||
public ulong Argument0 { get; }
|
||||
|
||||
public ulong Argument1 { get; }
|
||||
|
||||
/// <summary>The resolved HLE export, when the NID is registered.</summary>
|
||||
public string? LibraryName { get; }
|
||||
|
||||
public string? FunctionName { get; }
|
||||
|
||||
public bool IsResolved => !string.IsNullOrWhiteSpace(FunctionName);
|
||||
|
||||
/// <summary>A human-readable one-line summary of the stall.</summary>
|
||||
public string Detail { get; }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// A live view of the guest CPU state at a dispatch boundary, handed to an
|
||||
/// <see cref="ICpuDebugHook"/> so a debugger can read and mutate registers and
|
||||
/// guest memory without taking a dependency on the concrete
|
||||
/// <c>CpuContext</c>/<c>CpuDispatcher</c> types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The frame instance is only valid for the duration of the hook call that
|
||||
/// receives it (between <see cref="ICpuDebugHook.OnFrameEnter"/> and the
|
||||
/// matching <see cref="ICpuDebugHook.OnFrameExit"/>). Reads and writes are
|
||||
/// forwarded straight to the underlying guest context, so mutations made from
|
||||
/// a hook are observed by the CPU backend when it resumes the frame.
|
||||
/// </remarks>
|
||||
public interface ICpuDebugFrame
|
||||
{
|
||||
/// <summary>The kind of frame being executed.</summary>
|
||||
CpuDebugFrameKind Kind { get; }
|
||||
|
||||
/// <summary>The guest ABI generation this frame targets.</summary>
|
||||
Generation Generation { get; }
|
||||
|
||||
/// <summary>The guest virtual address the frame begins executing at.</summary>
|
||||
ulong EntryPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A human-readable label for the frame (process image name or module name).
|
||||
/// </summary>
|
||||
string Label { get; }
|
||||
|
||||
/// <summary>Guest-addressable memory for this frame.</summary>
|
||||
ICpuMemory Memory { get; }
|
||||
|
||||
/// <summary>Reads a general-purpose register.</summary>
|
||||
ulong GetRegister(CpuRegister register);
|
||||
|
||||
/// <summary>Overwrites a general-purpose register.</summary>
|
||||
void SetRegister(CpuRegister register, ulong value);
|
||||
|
||||
/// <summary>The instruction pointer.</summary>
|
||||
ulong Rip { get; set; }
|
||||
|
||||
/// <summary>The flags register.</summary>
|
||||
ulong Rflags { get; set; }
|
||||
|
||||
/// <summary>The FS segment base (guest TLS pointer).</summary>
|
||||
ulong FsBase { get; }
|
||||
|
||||
/// <summary>The GS segment base.</summary>
|
||||
ulong GsBase { get; }
|
||||
|
||||
/// <summary>Reads the 128-bit value of an XMM register.</summary>
|
||||
void GetXmm(int registerIndex, out ulong low, out ulong high);
|
||||
|
||||
/// <summary>
|
||||
/// The import stubs (guest address to NID) resolved for this frame, so a
|
||||
/// debugger can annotate calls into HLE exports.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<ulong, string> ImportStubs { get; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// The seam the CPU dispatcher uses to notify an attached debugger when guest
|
||||
/// execution crosses a frame boundary. Implemented outside of Core (for
|
||||
/// example by <c>SharpEmu.Debugger</c>) and supplied through
|
||||
/// <see cref="CpuExecutionOptions.DebugHook"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is intentionally coarse-grained: it exposes the entry and exit of each
|
||||
/// dispatched frame rather than per-instruction stepping. Per-instruction
|
||||
/// control requires cooperation from the native execution backend and is layered
|
||||
/// on top of this seam as the backend gains support; keeping the dispatcher-level
|
||||
/// contract stable lets the debugger infrastructure exist independently of that
|
||||
/// work. Implementations must be thread-safe: frames may be dispatched from the
|
||||
/// dedicated emulation thread while a debug server services clients on its own
|
||||
/// threads.
|
||||
/// </remarks>
|
||||
public interface ICpuDebugHook
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked immediately before the native backend begins executing a frame.
|
||||
/// The debugger may inspect or mutate <paramref name="frame"/> and may block
|
||||
/// the calling thread (for example, to honour a pause request) before
|
||||
/// returning to allow execution to proceed.
|
||||
/// </summary>
|
||||
void OnFrameEnter(ICpuDebugFrame frame);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked after a frame completes, whether it returned to the host or
|
||||
/// terminated with an error. <paramref name="frame"/> reflects the final
|
||||
/// guest state.
|
||||
/// </summary>
|
||||
void OnFrameExit(ICpuDebugFrame frame, OrbisGen2Result result);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked from the emulation thread when the backend detects an execution
|
||||
/// stall (for example a mutex spin loop) in the running frame, before it
|
||||
/// forces the guest out of the loop. As with <see cref="OnFrameEnter"/>, the
|
||||
/// implementation may inspect <paramref name="frame"/> and block to honour a
|
||||
/// break before returning to let the backend proceed.
|
||||
/// </summary>
|
||||
void OnStall(ICpuDebugFrame frame, CpuStallInfo info);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using SharpEmu.Core.Cpu.Debugging;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
@@ -316,11 +317,15 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
if (!isGuestWorker &&
|
||||
!ActiveForcedGuestExit &&
|
||||
ShouldForceGuestExitOnImportLoop(in importStubEntry, num7, num, value, value2) &&
|
||||
TryForceGuestExitToHostStub(argPackPtr, num, num7, importStubEntry.Nid))
|
||||
ShouldForceGuestExitOnImportLoop(in importStubEntry, num7, num, value, value2))
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 1uL;
|
||||
return 1uL;
|
||||
// Break before the forced exit so the loop state is still live.
|
||||
NotifyDebuggerStall(CpuStallKind.ImportLoop, in importStubEntry, num7, num, value, value2);
|
||||
if (TryForceGuestExitToHostStub(argPackPtr, num, num7, importStubEntry.Nid))
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 1uL;
|
||||
return 1uL;
|
||||
}
|
||||
}
|
||||
bool flag0 = importStubEntry.SuppressStrlenTrace;
|
||||
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.Core.Cpu.Debugging;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
@@ -235,6 +236,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private CpuContext? _cpuContext;
|
||||
|
||||
// Debugger seam; both null when no debugger is attached.
|
||||
private ICpuDebugHook? _debugHook;
|
||||
|
||||
private ICpuDebugFrame? _activeDebugFrame;
|
||||
|
||||
[ThreadStatic]
|
||||
private static DirectExecutionBackend? _activeExecutionBackend;
|
||||
|
||||
@@ -890,6 +896,49 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private bool HasActiveExecutionThread => ReferenceEquals(_activeExecutionBackend, this);
|
||||
|
||||
/// <summary>
|
||||
/// Binds the debug frame view the dispatcher created for the frame about to
|
||||
/// run, so stall notifications reference the same frame the debugger saw at
|
||||
/// entry. Set to null when no debugger is attached.
|
||||
/// </summary>
|
||||
internal void SetActiveDebugFrame(ICpuDebugFrame? frame) => _activeDebugFrame = frame;
|
||||
|
||||
/// <summary>
|
||||
/// Notifies an attached debugger of a detected execution stall. No-op when no
|
||||
/// debugger is attached or no frame is bound. The debugger may block here to
|
||||
/// present a break before the backend forces the guest out of the loop.
|
||||
/// </summary>
|
||||
private void NotifyDebuggerStall(
|
||||
CpuStallKind kind,
|
||||
in ImportStubEntry import,
|
||||
ulong instructionPointer,
|
||||
long dispatchIndex,
|
||||
ulong argument0,
|
||||
ulong argument1)
|
||||
{
|
||||
var hook = _debugHook;
|
||||
var frame = _activeDebugFrame;
|
||||
if (hook is null || frame is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var export = import.Export;
|
||||
var exportDescription = export is null ? "unresolved" : $"{export.LibraryName}:{export.Name}";
|
||||
var detail = $"kind={kind}, nid={import.Nid}, export={exportDescription}, dispatch#{dispatchIndex}, " +
|
||||
$"rip=0x{instructionPointer:X16}, arg0=0x{argument0:X16}, arg1=0x{argument1:X16}";
|
||||
hook.OnStall(frame, new CpuStallInfo(
|
||||
kind,
|
||||
import.Nid,
|
||||
instructionPointer,
|
||||
dispatchIndex,
|
||||
argument0,
|
||||
argument1,
|
||||
detail,
|
||||
export?.LibraryName,
|
||||
export?.Name));
|
||||
}
|
||||
|
||||
private CpuContext? ActiveCpuContext => HasActiveExecutionThread ? _activeCpuContext : _cpuContext;
|
||||
|
||||
private ulong ActiveEntryReturnSentinelRip
|
||||
@@ -1051,6 +1100,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
Console.Error.WriteLine(_moduleManager.TryGetExport("L-Q3LEjIbgA", out ExportedFunction export2) ? ("[LOADER][INFO] ExportCheck map_direct: " + export2.LibraryName + ":" + export2.Name) : "[LOADER][INFO] ExportCheck map_direct: MISSING");
|
||||
_entryPoint = entryPoint;
|
||||
_cpuContext = context;
|
||||
_debugHook = executionOptions.DebugHook;
|
||||
_returnFallbackTarget = context[CpuRegister.Rsi];
|
||||
Volatile.Write(ref _globalFallbackTarget, _returnFallbackTarget);
|
||||
Volatile.Write(ref _globalUnresolvedReturnStub, (ulong)_unresolvedReturnStub);
|
||||
|
||||
Reference in New Issue
Block a user