Merge branch 'gdb-stub' into 'master'

Add GDB Stub

See merge request [ryubing/ryujinx!71](https://git.ryujinx.app/ryubing/ryujinx/-/merge_requests/71)
This commit is contained in:
Coxxs 2025-06-20 16:32:29 -05:00
commit 3dd7739a23
53 changed files with 2464 additions and 21 deletions

View file

@ -25021,6 +25021,181 @@
"zh_CN": "动态 Rich Presence", "zh_CN": "动态 Rich Presence",
"zh_TW": "動態 Rich Presence" "zh_TW": "動態 Rich Presence"
} }
},
{
"ID": "SettingsTabDebug",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Debug",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SettingsTabDebugTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Debug (WARNING: For developer use only)",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "EnableGDBStub",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Enable GDB Stub",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "GDBStubToggleTooltip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Enables the GDB stub which makes it possible to debug the running application. For development use only!",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "GDBStubPort",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "GDB stub port:",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "DebuggerSuspendOnStart",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Suspend application on start",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "DebuggerSuspendOnStartTooltip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Suspends the application before executing the first instruction, allowing for debugging from the earliest point.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
} }
] ]
} }

View file

@ -3,6 +3,7 @@ using ARMeilleure.State;
using ARMeilleure.Translation; using ARMeilleure.Translation;
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using ExecutionContext = ARMeilleure.State.ExecutionContext;
namespace ARMeilleure.Instructions namespace ARMeilleure.Instructions
{ {
@ -200,7 +201,11 @@ namespace ARMeilleure.Instructions
ExecutionContext context = GetContext(); ExecutionContext context = GetContext();
context.CheckInterrupt(); // If debugging, we'll handle interrupts outside
if (!Optimizations.EnableDebugging)
{
context.CheckInterrupt();
}
Statistics.ResumeTimer(); Statistics.ResumeTimer();

View file

@ -12,6 +12,7 @@ namespace ARMeilleure
public static bool AllowLcqInFunctionTable { get; set; } = true; public static bool AllowLcqInFunctionTable { get; set; } = true;
public static bool UseUnmanagedDispatchLoop { get; set; } = true; public static bool UseUnmanagedDispatchLoop { get; set; } = true;
public static bool EnableDebugging { get; set; } = false;
public static bool UseAdvSimdIfAvailable { get; set; } = true; public static bool UseAdvSimdIfAvailable { get; set; } = true;
public static bool UseArm64AesIfAvailable { get; set; } = true; public static bool UseArm64AesIfAvailable { get; set; } = true;

View file

@ -1,4 +1,5 @@
using ARMeilleure.Memory; using ARMeilleure.Memory;
using System.Threading;
namespace ARMeilleure.State namespace ARMeilleure.State
{ {
@ -10,7 +11,7 @@ namespace ARMeilleure.State
internal nint NativeContextPtr => _nativeContext.BasePtr; internal nint NativeContextPtr => _nativeContext.BasePtr;
private bool _interrupted; internal bool Interrupted { get; private set; }
private readonly ICounter _counter; private readonly ICounter _counter;
@ -65,6 +66,8 @@ namespace ARMeilleure.State
public bool IsAarch32 { get; set; } public bool IsAarch32 { get; set; }
public ulong ThreadUid { get; set; }
internal ExecutionMode ExecutionMode internal ExecutionMode ExecutionMode
{ {
get get
@ -90,14 +93,19 @@ namespace ARMeilleure.State
private readonly ExceptionCallbackNoArgs _interruptCallback; private readonly ExceptionCallbackNoArgs _interruptCallback;
private readonly ExceptionCallback _breakCallback; private readonly ExceptionCallback _breakCallback;
private readonly ExceptionCallbackNoArgs _stepCallback;
private readonly ExceptionCallback _supervisorCallback; private readonly ExceptionCallback _supervisorCallback;
private readonly ExceptionCallback _undefinedCallback; private readonly ExceptionCallback _undefinedCallback;
internal int ShouldStep;
public ulong DebugPc { get; set; }
public ExecutionContext( public ExecutionContext(
IJitMemoryAllocator allocator, IJitMemoryAllocator allocator,
ICounter counter, ICounter counter,
ExceptionCallbackNoArgs interruptCallback = null, ExceptionCallbackNoArgs interruptCallback = null,
ExceptionCallback breakCallback = null, ExceptionCallback breakCallback = null,
ExceptionCallbackNoArgs stepCallback = null,
ExceptionCallback supervisorCallback = null, ExceptionCallback supervisorCallback = null,
ExceptionCallback undefinedCallback = null) ExceptionCallback undefinedCallback = null)
{ {
@ -105,6 +113,7 @@ namespace ARMeilleure.State
_counter = counter; _counter = counter;
_interruptCallback = interruptCallback; _interruptCallback = interruptCallback;
_breakCallback = breakCallback; _breakCallback = breakCallback;
_stepCallback = stepCallback;
_supervisorCallback = supervisorCallback; _supervisorCallback = supervisorCallback;
_undefinedCallback = undefinedCallback; _undefinedCallback = undefinedCallback;
@ -127,9 +136,9 @@ namespace ARMeilleure.State
internal void CheckInterrupt() internal void CheckInterrupt()
{ {
if (_interrupted) if (Interrupted)
{ {
_interrupted = false; Interrupted = false;
_interruptCallback?.Invoke(this); _interruptCallback?.Invoke(this);
} }
@ -139,7 +148,18 @@ namespace ARMeilleure.State
public void RequestInterrupt() public void RequestInterrupt()
{ {
_interrupted = true; Interrupted = true;
}
public void StepHandler()
{
_stepCallback.Invoke(this);
}
public void RequestDebugStep()
{
Interlocked.Exchange(ref ShouldStep, 1);
RequestInterrupt();
} }
internal void OnBreak(ulong address, int imm) internal void OnBreak(ulong address, int imm)
@ -149,6 +169,11 @@ namespace ARMeilleure.State
internal void OnSupervisorCall(ulong address, int imm) internal void OnSupervisorCall(ulong address, int imm)
{ {
if (Optimizations.EnableDebugging)
{
DebugPc = Pc; // TODO: Is this the best place to update DebugPc?
}
_supervisorCallback?.Invoke(this, address, imm); _supervisorCallback?.Invoke(this, address, imm);
} }

View file

@ -119,7 +119,25 @@ namespace ARMeilleure.Translation
NativeInterface.RegisterThread(context, Memory, this); NativeInterface.RegisterThread(context, Memory, this);
if (Optimizations.UseUnmanagedDispatchLoop) if (Optimizations.EnableDebugging)
{
context.DebugPc = address;
do
{
if (Interlocked.CompareExchange(ref context.ShouldStep, 0, 1) == 1)
{
context.DebugPc = Step(context, context.DebugPc);
context.StepHandler();
}
else
{
context.DebugPc = ExecuteSingle(context, context.DebugPc);
}
context.CheckInterrupt();
}
while (context.Running && context.DebugPc != 0);
}
else if (Optimizations.UseUnmanagedDispatchLoop)
{ {
Stubs.DispatchLoop(context.NativeContextPtr, address); Stubs.DispatchLoop(context.NativeContextPtr, address);
} }
@ -175,7 +193,7 @@ namespace ARMeilleure.Translation
return nextAddr; return nextAddr;
} }
public ulong Step(State.ExecutionContext context, ulong address) private ulong Step(State.ExecutionContext context, ulong address)
{ {
TranslatedFunction func = Translate(address, context.ExecutionMode, highCq: false, singleStep: true); TranslatedFunction func = Translate(address, context.ExecutionMode, highCq: false, singleStep: true);
@ -228,7 +246,7 @@ namespace ARMeilleure.Translation
Stubs, Stubs,
address, address,
highCq, highCq,
_ptc.State != PtcState.Disabled, _ptc.State != PtcState.Disabled && !Optimizations.EnableDebugging,
mode: Aarch32Mode.User); mode: Aarch32Mode.User);
Logger.StartPass(PassName.Decoding); Logger.StartPass(PassName.Decoding);
@ -367,9 +385,8 @@ namespace ARMeilleure.Translation
if (block.Exit) if (block.Exit)
{ {
// Left option here as it may be useful if we need to return to managed rather than tail call in // Return to managed rather than tail call.
// future. (eg. for debug) bool useReturns = Optimizations.EnableDebugging;
bool useReturns = false;
InstEmitFlowHelper.EmitVirtualJump(context, Const(block.Address), isReturn: useReturns); InstEmitFlowHelper.EmitVirtualJump(context, Const(block.Address), isReturn: useReturns);
} }

View file

@ -13,6 +13,7 @@ namespace Ryujinx.Common.Logging
Cpu, Cpu,
Emulation, Emulation,
FFmpeg, FFmpeg,
GdbStub,
Font, Font,
Gpu, Gpu,
Hid, Hid,

View file

@ -0,0 +1,10 @@
namespace Ryujinx.Cpu.AppleHv.Arm
{
enum ExceptionLevel : uint
{
PstateMask = 0xfffffff0,
EL1h = 0b0101,
El1t = 0b0100,
EL0 = 0b0000,
}
}

View file

@ -11,7 +11,18 @@ namespace Ryujinx.Cpu.AppleHv
class HvExecutionContext : IExecutionContext class HvExecutionContext : IExecutionContext
{ {
/// <inheritdoc/> /// <inheritdoc/>
public ulong Pc => _impl.ElrEl1; public ulong Pc
{
get
{
uint currentEl = Pstate & ~((uint)ExceptionLevel.PstateMask);
if (currentEl == (uint)ExceptionLevel.EL1h)
{
return _impl.ElrEl1;
}
return _impl.Pc;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public long TpidrEl0 public long TpidrEl0
@ -48,6 +59,9 @@ namespace Ryujinx.Cpu.AppleHv
set => _impl.Fpsr = value; set => _impl.Fpsr = value;
} }
/// <inheritdoc/>
public ulong ThreadUid { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public bool IsAarch32 public bool IsAarch32
{ {
@ -67,6 +81,7 @@ namespace Ryujinx.Cpu.AppleHv
private readonly ICounter _counter; private readonly ICounter _counter;
private readonly IHvExecutionContext _shadowContext; private readonly IHvExecutionContext _shadowContext;
private IHvExecutionContext _impl; private IHvExecutionContext _impl;
private int _shouldStep;
private readonly ExceptionCallbacks _exceptionCallbacks; private readonly ExceptionCallbacks _exceptionCallbacks;
@ -103,6 +118,11 @@ namespace Ryujinx.Cpu.AppleHv
_exceptionCallbacks.BreakCallback?.Invoke(this, address, imm); _exceptionCallbacks.BreakCallback?.Invoke(this, address, imm);
} }
private void StepHandler()
{
_exceptionCallbacks.StepCallback?.Invoke(this);
}
private void SupervisorCallHandler(ulong address, int imm) private void SupervisorCallHandler(ulong address, int imm)
{ {
_exceptionCallbacks.SupervisorCallback?.Invoke(this, address, imm); _exceptionCallbacks.SupervisorCallback?.Invoke(this, address, imm);
@ -127,6 +147,30 @@ namespace Ryujinx.Cpu.AppleHv
return Interlocked.Exchange(ref _interruptRequested, 0) != 0; return Interlocked.Exchange(ref _interruptRequested, 0) != 0;
} }
/// <inheritdoc/>
public void RequestDebugStep()
{
Interlocked.Exchange(ref _shouldStep, 1);
}
/// <inheritdoc/>
public ulong DebugPc
{
get => Pc;
set
{
uint currentEl = Pstate & ~((uint)ExceptionLevel.PstateMask);
if (currentEl == (uint)ExceptionLevel.EL1h)
{
_impl.ElrEl1 = value;
}
else
{
_impl.Pc = value;
}
}
}
/// <inheritdoc/> /// <inheritdoc/>
public void StopRunning() public void StopRunning()
{ {
@ -142,6 +186,22 @@ namespace Ryujinx.Cpu.AppleHv
while (Running) while (Running)
{ {
if (Interlocked.CompareExchange(ref _shouldStep, 0, 1) == 1)
{
uint currentEl = Pstate & ~((uint)ExceptionLevel.PstateMask);
if (currentEl == (uint)ExceptionLevel.EL1h)
{
HvApi.hv_vcpu_get_sys_reg(vcpu.Handle, HvSysReg.SPSR_EL1, out ulong spsr).ThrowOnError();
spsr |= (1 << 21);
HvApi.hv_vcpu_set_sys_reg(vcpu.Handle, HvSysReg.SPSR_EL1, spsr);
}
else
{
Pstate |= (1 << 21);
}
HvApi.hv_vcpu_set_sys_reg(vcpu.Handle, HvSysReg.MDSCR_EL1, 1);
}
HvApi.hv_vcpu_run(vcpu.Handle).ThrowOnError(); HvApi.hv_vcpu_run(vcpu.Handle).ThrowOnError();
HvExitReason reason = vcpu.ExitInfo->Reason; HvExitReason reason = vcpu.ExitInfo->Reason;
@ -209,6 +269,20 @@ namespace Ryujinx.Cpu.AppleHv
SupervisorCallHandler(elr - 4UL, id); SupervisorCallHandler(elr - 4UL, id);
vcpu = RentFromPool(memoryManager.AddressSpace, vcpu); vcpu = RentFromPool(memoryManager.AddressSpace, vcpu);
break; break;
case ExceptionClass.SoftwareStepLowerEl:
HvApi.hv_vcpu_get_sys_reg(vcpuHandle, HvSysReg.SPSR_EL1, out ulong spsr).ThrowOnError();
spsr &= ~((ulong)(1 << 21));
HvApi.hv_vcpu_set_sys_reg(vcpuHandle, HvSysReg.SPSR_EL1, spsr).ThrowOnError();
HvApi.hv_vcpu_set_sys_reg(vcpuHandle, HvSysReg.MDSCR_EL1, 0);
ReturnToPool(vcpu);
StepHandler();
vcpu = RentFromPool(memoryManager.AddressSpace, vcpu);
break;
case ExceptionClass.BrkAarch64:
ReturnToPool(vcpu);
BreakHandler(elr, (ushort)esr);
vcpu = RentFromPool(memoryManager.AddressSpace, vcpu);
break;
default: default:
throw new Exception($"Unhandled guest exception {ec}."); throw new Exception($"Unhandled guest exception {ec}.");
} }
@ -219,10 +293,7 @@ namespace Ryujinx.Cpu.AppleHv
// TODO: Invalidate only the range that was modified? // TODO: Invalidate only the range that was modified?
return HvAddressSpace.KernelRegionTlbiEretAddress; return HvAddressSpace.KernelRegionTlbiEretAddress;
} }
else return HvAddressSpace.KernelRegionEretAddress;
{
return HvAddressSpace.KernelRegionEretAddress;
}
} }
private static void DataAbort(MemoryTracking tracking, ulong vcpu, uint esr) private static void DataAbort(MemoryTracking tracking, ulong vcpu, uint esr)

View file

@ -18,6 +18,8 @@ namespace Ryujinx.Cpu.AppleHv
public bool IsAarch32 { get; set; } public bool IsAarch32 { get; set; }
public ulong ThreadUid { get; set; }
private readonly ulong[] _x; private readonly ulong[] _x;
private readonly V128[] _v; private readonly V128[] _v;
@ -46,5 +48,14 @@ namespace Ryujinx.Cpu.AppleHv
{ {
_v[index] = value; _v[index] = value;
} }
public void RequestInterrupt()
{
}
public bool GetAndClearInterruptRequested()
{
return false;
}
} }
} }

View file

@ -2,6 +2,7 @@ using ARMeilleure.State;
using Ryujinx.Memory; using Ryujinx.Memory;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Threading;
namespace Ryujinx.Cpu.AppleHv namespace Ryujinx.Cpu.AppleHv
{ {
@ -13,6 +14,8 @@ namespace Ryujinx.Cpu.AppleHv
private static readonly SetSimdFpReg _setSimdFpReg; private static readonly SetSimdFpReg _setSimdFpReg;
private static readonly nint _setSimdFpRegNativePtr; private static readonly nint _setSimdFpRegNativePtr;
public ulong ThreadUid { get; set; }
static HvExecutionContextVcpu() static HvExecutionContextVcpu()
{ {
// .NET does not support passing vectors by value, so we need to pass a pointer and use a native // .NET does not support passing vectors by value, so we need to pass a pointer and use a native
@ -135,6 +138,7 @@ namespace Ryujinx.Cpu.AppleHv
} }
private readonly ulong _vcpu; private readonly ulong _vcpu;
private int _interruptRequested;
public HvExecutionContextVcpu(ulong vcpu) public HvExecutionContextVcpu(ulong vcpu)
{ {
@ -180,8 +184,16 @@ namespace Ryujinx.Cpu.AppleHv
public void RequestInterrupt() public void RequestInterrupt()
{ {
ulong vcpu = _vcpu; if (Interlocked.Exchange(ref _interruptRequested, 1) == 0)
HvApi.hv_vcpus_exit(ref vcpu, 1); {
ulong vcpu = _vcpu;
HvApi.hv_vcpus_exit(ref vcpu, 1);
}
}
public bool GetAndClearInterruptRequested()
{
return Interlocked.Exchange(ref _interruptRequested, 0) != 0;
} }
} }
} }

View file

@ -15,6 +15,7 @@ namespace Ryujinx.Cpu.AppleHv
uint Fpcr { get; set; } uint Fpcr { get; set; }
uint Fpsr { get; set; } uint Fpsr { get; set; }
ulong ThreadUid { get; set; }
ulong GetX(int index); ulong GetX(int index);
void SetX(int index, ulong value); void SetX(int index, ulong value);
@ -39,5 +40,8 @@ namespace Ryujinx.Cpu.AppleHv
SetV(i, context.GetV(i)); SetV(i, context.GetV(i));
} }
} }
void RequestInterrupt();
bool GetAndClearInterruptRequested();
} }
} }

View file

@ -29,6 +29,11 @@ namespace Ryujinx.Cpu
/// </summary> /// </summary>
public readonly ExceptionCallback BreakCallback; public readonly ExceptionCallback BreakCallback;
/// <summary>
/// Handler for CPU software interrupts caused by single-stepping.
/// </summary>
public readonly ExceptionCallbackNoArgs StepCallback;
/// <summary> /// <summary>
/// Handler for CPU software interrupts caused by the Arm SVC instruction. /// Handler for CPU software interrupts caused by the Arm SVC instruction.
/// </summary> /// </summary>
@ -47,16 +52,19 @@ namespace Ryujinx.Cpu
/// </remarks> /// </remarks>
/// <param name="interruptCallback">Handler for CPU interrupts triggered using <see cref="IExecutionContext.RequestInterrupt"/></param> /// <param name="interruptCallback">Handler for CPU interrupts triggered using <see cref="IExecutionContext.RequestInterrupt"/></param>
/// <param name="breakCallback">Handler for CPU software interrupts caused by the Arm BRK instruction</param> /// <param name="breakCallback">Handler for CPU software interrupts caused by the Arm BRK instruction</param>
/// <param name="stepCallback">Handler for CPU software interrupts caused by single-stepping</param>
/// <param name="supervisorCallback">Handler for CPU software interrupts caused by the Arm SVC instruction</param> /// <param name="supervisorCallback">Handler for CPU software interrupts caused by the Arm SVC instruction</param>
/// <param name="undefinedCallback">Handler for CPU software interrupts caused by any undefined Arm instruction</param> /// <param name="undefinedCallback">Handler for CPU software interrupts caused by any undefined Arm instruction</param>
public ExceptionCallbacks( public ExceptionCallbacks(
ExceptionCallbackNoArgs interruptCallback = null, ExceptionCallbackNoArgs interruptCallback = null,
ExceptionCallback breakCallback = null, ExceptionCallback breakCallback = null,
ExceptionCallbackNoArgs stepCallback = null,
ExceptionCallback supervisorCallback = null, ExceptionCallback supervisorCallback = null,
ExceptionCallback undefinedCallback = null) ExceptionCallback undefinedCallback = null)
{ {
InterruptCallback = interruptCallback; InterruptCallback = interruptCallback;
BreakCallback = breakCallback; BreakCallback = breakCallback;
StepCallback = stepCallback;
SupervisorCallback = supervisorCallback; SupervisorCallback = supervisorCallback;
UndefinedCallback = undefinedCallback; UndefinedCallback = undefinedCallback;
} }

View file

@ -1,5 +1,6 @@
using ARMeilleure.State; using ARMeilleure.State;
using System; using System;
using System.Threading;
namespace Ryujinx.Cpu namespace Ryujinx.Cpu
{ {
@ -46,6 +47,11 @@ namespace Ryujinx.Cpu
/// </summary> /// </summary>
bool IsAarch32 { get; set; } bool IsAarch32 { get; set; }
/// <summary>
/// Thread UID.
/// </summary>
public ulong ThreadUid { get; set; }
/// <summary> /// <summary>
/// Indicates whenever the CPU is still running code. /// Indicates whenever the CPU is still running code.
/// </summary> /// </summary>
@ -108,5 +114,23 @@ namespace Ryujinx.Cpu
/// If you only need to pause the thread temporarily, use <see cref="RequestInterrupt"/> instead. /// If you only need to pause the thread temporarily, use <see cref="RequestInterrupt"/> instead.
/// </remarks> /// </remarks>
void StopRunning(); void StopRunning();
/// <summary>
/// Requests the thread to stop running temporarily and call <see cref="ExceptionCallbacks.InterruptCallback"/>.
/// </summary>
/// <remarks>
/// The thread might not pause immediately.
/// One must not assume that guest code is no longer being executed by the thread after calling this function.
/// After single stepping, the thread should call call <see cref="ExceptionCallbacks.StepCallback"/>.
/// </remarks>
void RequestDebugStep();
/// <summary>
/// Current Program Counter (for debugging).
/// </summary>
/// <remarks>
/// PC register for the debugger. Must not be accessed while the thread isn't stopped for debugging.
/// </remarks>
ulong DebugPc { get; set; }
} }
} }

View file

@ -1,5 +1,6 @@
using ARMeilleure.Memory; using ARMeilleure.Memory;
using ARMeilleure.State; using ARMeilleure.State;
using ExecutionContext = ARMeilleure.State.ExecutionContext;
namespace Ryujinx.Cpu.Jit namespace Ryujinx.Cpu.Jit
{ {
@ -53,6 +54,13 @@ namespace Ryujinx.Cpu.Jit
set => _impl.IsAarch32 = value; set => _impl.IsAarch32 = value;
} }
/// <inheritdoc/>
public ulong ThreadUid
{
get => _impl.ThreadUid;
set => _impl.ThreadUid = value;
}
/// <inheritdoc/> /// <inheritdoc/>
public bool Running => _impl.Running; public bool Running => _impl.Running;
@ -65,6 +73,7 @@ namespace Ryujinx.Cpu.Jit
counter, counter,
InterruptHandler, InterruptHandler,
BreakHandler, BreakHandler,
StepHandler,
SupervisorCallHandler, SupervisorCallHandler,
UndefinedHandler); UndefinedHandler);
@ -93,6 +102,11 @@ namespace Ryujinx.Cpu.Jit
_exceptionCallbacks.BreakCallback?.Invoke(this, address, imm); _exceptionCallbacks.BreakCallback?.Invoke(this, address, imm);
} }
private void StepHandler(ExecutionContext context)
{
_exceptionCallbacks.StepCallback?.Invoke(this);
}
private void SupervisorCallHandler(ExecutionContext context, ulong address, int imm) private void SupervisorCallHandler(ExecutionContext context, ulong address, int imm)
{ {
_exceptionCallbacks.SupervisorCallback?.Invoke(this, address, imm); _exceptionCallbacks.SupervisorCallback?.Invoke(this, address, imm);
@ -109,6 +123,16 @@ namespace Ryujinx.Cpu.Jit
_impl.RequestInterrupt(); _impl.RequestInterrupt();
} }
/// <inheritdoc/>
public void RequestDebugStep() => _impl.RequestDebugStep();
/// <inheritdoc/>
public ulong DebugPc
{
get => _impl.DebugPc;
set => _impl.DebugPc = value;
}
/// <inheritdoc/> /// <inheritdoc/>
public void StopRunning() public void StopRunning()
{ {

View file

@ -1,6 +1,7 @@
using ARMeilleure.Memory; using ARMeilleure.Memory;
using ARMeilleure.State; using ARMeilleure.State;
using System; using System;
using System.Threading;
namespace Ryujinx.Cpu.LightningJit.State namespace Ryujinx.Cpu.LightningJit.State
{ {
@ -51,6 +52,8 @@ namespace Ryujinx.Cpu.LightningJit.State
} }
public bool IsAarch32 { get; set; } public bool IsAarch32 { get; set; }
public ulong ThreadUid { get; set; }
internal ExecutionMode ExecutionMode internal ExecutionMode ExecutionMode
{ {
@ -77,15 +80,20 @@ namespace Ryujinx.Cpu.LightningJit.State
private readonly ExceptionCallbackNoArgs _interruptCallback; private readonly ExceptionCallbackNoArgs _interruptCallback;
private readonly ExceptionCallback _breakCallback; private readonly ExceptionCallback _breakCallback;
private readonly ExceptionCallbackNoArgs _stepCallback;
private readonly ExceptionCallback _supervisorCallback; private readonly ExceptionCallback _supervisorCallback;
private readonly ExceptionCallback _undefinedCallback; private readonly ExceptionCallback _undefinedCallback;
internal int ShouldStep;
public ulong DebugPc { get; set; }
public ExecutionContext(IJitMemoryAllocator allocator, ICounter counter, ExceptionCallbacks exceptionCallbacks) public ExecutionContext(IJitMemoryAllocator allocator, ICounter counter, ExceptionCallbacks exceptionCallbacks)
{ {
_nativeContext = new NativeContext(allocator); _nativeContext = new NativeContext(allocator);
_counter = counter; _counter = counter;
_interruptCallback = exceptionCallbacks.InterruptCallback; _interruptCallback = exceptionCallbacks.InterruptCallback;
_breakCallback = exceptionCallbacks.BreakCallback; _breakCallback = exceptionCallbacks.BreakCallback;
_stepCallback = exceptionCallbacks.StepCallback;
_supervisorCallback = exceptionCallbacks.SupervisorCallback; _supervisorCallback = exceptionCallbacks.SupervisorCallback;
_undefinedCallback = exceptionCallbacks.UndefinedCallback; _undefinedCallback = exceptionCallbacks.UndefinedCallback;
@ -117,6 +125,17 @@ namespace Ryujinx.Cpu.LightningJit.State
_interrupted = true; _interrupted = true;
} }
public void StepHandler()
{
_stepCallback.Invoke(this);
}
public void RequestDebugStep()
{
Interlocked.Exchange(ref ShouldStep, 1);
RequestInterrupt();
}
internal void OnBreak(ulong address, int imm) internal void OnBreak(ulong address, int imm)
{ {
_breakCallback?.Invoke(this, address, imm); _breakCallback?.Invoke(this, address, imm);

View file

@ -0,0 +1,9 @@
namespace Ryujinx.HLE.Debugger
{
public enum DebugState
{
Running,
Stopping,
Stopped,
}
}

View file

@ -0,0 +1,930 @@
using ARMeilleure.State;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.Memory;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using IExecutionContext = Ryujinx.Cpu.IExecutionContext;
namespace Ryujinx.HLE.Debugger
{
public class Debugger : IDisposable
{
internal Switch Device { get; private set; }
public ushort GdbStubPort { get; private set; }
private TcpListener ListenerSocket;
private Socket ClientSocket = null;
private NetworkStream ReadStream = null;
private NetworkStream WriteStream = null;
private BlockingCollection<IMessage> Messages = new BlockingCollection<IMessage>(1);
private Thread DebuggerThread;
private Thread MessageHandlerThread;
private bool _shuttingDown = false;
private ulong? cThread;
private ulong? gThread;
public Debugger(Switch device, ushort port)
{
Device = device;
GdbStubPort = port;
ARMeilleure.Optimizations.EnableDebugging = true;
DebuggerThread = new Thread(DebuggerThreadMain);
DebuggerThread.Start();
MessageHandlerThread = new Thread(MessageHandlerMain);
MessageHandlerThread.Start();
}
private IDebuggableProcess DebugProcess => Device.System?.DebugGetApplicationProcess();
private KThread[] GetThreads() => DebugProcess.GetThreadUids().Select(x => DebugProcess.GetThread(x)).ToArray();
private bool IsProcessAarch32 => DebugProcess.GetThread(gThread.Value).Context.IsAarch32;
private KernelContext KernelContext => Device.System.KernelContext;
const int GdbRegisterCount64 = 68;
const int GdbRegisterCount32 = 66;
/* FPCR = FPSR & ~FpcrMask
All of FPCR's bits are reserved in FPCR and vice versa,
see ARM's documentation. */
private const uint FpcrMask = 0xfc1fffff;
private string GdbReadRegister64(IExecutionContext state, int gdbRegId)
{
switch (gdbRegId)
{
case >= 0 and <= 31:
return ToHex(BitConverter.GetBytes(state.GetX(gdbRegId)));
case 32:
return ToHex(BitConverter.GetBytes(state.DebugPc));
case 33:
return ToHex(BitConverter.GetBytes(state.Pstate));
case >= 34 and <= 65:
return ToHex(state.GetV(gdbRegId - 34).ToArray());
case 66:
return ToHex(BitConverter.GetBytes((uint)state.Fpsr));
case 67:
return ToHex(BitConverter.GetBytes((uint)state.Fpcr));
default:
return null;
}
}
private bool GdbWriteRegister64(IExecutionContext state, int gdbRegId, StringStream ss)
{
switch (gdbRegId)
{
case >= 0 and <= 31:
{
ulong value = ss.ReadLengthAsLEHex(16);
state.SetX(gdbRegId, value);
return true;
}
case 32:
{
ulong value = ss.ReadLengthAsLEHex(16);
state.DebugPc = value;
return true;
}
case 33:
{
ulong value = ss.ReadLengthAsLEHex(8);
state.Pstate = (uint)value;
return true;
}
case >= 34 and <= 65:
{
ulong value0 = ss.ReadLengthAsLEHex(16);
ulong value1 = ss.ReadLengthAsLEHex(16);
state.SetV(gdbRegId - 34, new V128(value0, value1));
return true;
}
case 66:
{
ulong value = ss.ReadLengthAsLEHex(8);
state.Fpsr = (uint)value;
return true;
}
case 67:
{
ulong value = ss.ReadLengthAsLEHex(8);
state.Fpcr = (uint)value;
return true;
}
default:
return false;
}
}
private string GdbReadRegister32(IExecutionContext state, int gdbRegId)
{
switch (gdbRegId)
{
case >= 0 and <= 14:
return ToHex(BitConverter.GetBytes((uint)state.GetX(gdbRegId)));
case 15:
return ToHex(BitConverter.GetBytes((uint)state.DebugPc));
case 16:
return ToHex(BitConverter.GetBytes((uint)state.Pstate));
case >= 17 and <= 32:
return ToHex(state.GetV(gdbRegId - 17).ToArray());
case >= 33 and <= 64:
int reg = (gdbRegId - 33);
int n = reg / 2;
int shift = reg % 2;
ulong value = state.GetV(n).Extract<ulong>(shift);
return ToHex(BitConverter.GetBytes(value));
case 65:
uint fpscr = (uint)state.Fpsr | (uint)state.Fpcr;
return ToHex(BitConverter.GetBytes(fpscr));
default:
return null;
}
}
private bool GdbWriteRegister32(IExecutionContext state, int gdbRegId, StringStream ss)
{
switch (gdbRegId)
{
case >= 0 and <= 14:
{
ulong value = ss.ReadLengthAsLEHex(8);
state.SetX(gdbRegId, value);
return true;
}
case 15:
{
ulong value = ss.ReadLengthAsLEHex(8);
state.DebugPc = value;
return true;
}
case 16:
{
ulong value = ss.ReadLengthAsLEHex(8);
state.Pstate = (uint)value;
return true;
}
case >= 17 and <= 32:
{
ulong value0 = ss.ReadLengthAsLEHex(16);
ulong value1 = ss.ReadLengthAsLEHex(16);
state.SetV(gdbRegId - 17, new V128(value0, value1));
return true;
}
case >= 33 and <= 64:
{
ulong value = ss.ReadLengthAsLEHex(16);
int regId = (gdbRegId - 33);
int regNum = regId / 2;
int shift = regId % 2;
V128 reg = state.GetV(regNum);
reg.Insert(shift, value);
return true;
}
case 65:
{
ulong value = ss.ReadLengthAsLEHex(8);
state.Fpsr = (uint)value & FpcrMask;
state.Fpcr = (uint)value & ~FpcrMask;
return true;
}
default:
return false;
}
}
private void MessageHandlerMain()
{
while (!_shuttingDown)
{
IMessage msg = Messages.Take();
try {
switch (msg)
{
case BreakInMessage:
Logger.Notice.Print(LogClass.GdbStub, "Break-in requested");
CommandQuery();
break;
case SendNackMessage:
WriteStream.WriteByte((byte)'-');
break;
case CommandMessage { Command: var cmd }:
Logger.Debug?.Print(LogClass.GdbStub, $"Received Command: {cmd}");
WriteStream.WriteByte((byte)'+');
ProcessCommand(cmd);
break;
case ThreadBreakMessage { Context: var ctx }:
DebugProcess.DebugStop();
Reply($"T05thread:{ctx.ThreadUid:x};");
break;
case KillMessage:
return;
}
}
catch (IOException e)
{
Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e);
}
catch (NullReferenceException e)
{
Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e);
}
}
}
private void ProcessCommand(string cmd)
{
StringStream ss = new StringStream(cmd);
switch (ss.ReadChar())
{
case '!':
if (!ss.IsEmpty())
{
goto unknownCommand;
}
// Enable extended mode
ReplyOK();
break;
case '?':
if (!ss.IsEmpty())
{
goto unknownCommand;
}
CommandQuery();
break;
case 'c':
CommandContinue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex());
break;
case 'D':
if (!ss.IsEmpty())
{
goto unknownCommand;
}
CommandDetach();
break;
case 'g':
if (!ss.IsEmpty())
{
goto unknownCommand;
}
CommandReadRegisters();
break;
case 'G':
CommandWriteRegisters(ss);
break;
case 'H':
{
char op = ss.ReadChar();
ulong? threadId = ss.ReadRemainingAsThreadUid();
CommandSetThread(op, threadId);
break;
}
case 'k':
Logger.Notice.Print(LogClass.GdbStub, "Kill request received, detach instead");
Reply("");
CommandDetach();
break;
case 'm':
{
ulong addr = ss.ReadUntilAsHex(',');
ulong len = ss.ReadRemainingAsHex();
CommandReadMemory(addr, len);
break;
}
case 'M':
{
ulong addr = ss.ReadUntilAsHex(',');
ulong len = ss.ReadUntilAsHex(':');
CommandWriteMemory(addr, len, ss);
break;
}
case 'p':
{
ulong gdbRegId = ss.ReadRemainingAsHex();
CommandReadRegister((int)gdbRegId);
break;
}
case 'P':
{
ulong gdbRegId = ss.ReadUntilAsHex('=');
CommandWriteRegister((int)gdbRegId, ss);
break;
}
case 'q':
if (ss.ConsumeRemaining("GDBServerVersion"))
{
Reply($"name:Ryujinx;version:{ReleaseInformation.Version};");
break;
}
if (ss.ConsumeRemaining("HostInfo"))
{
if (IsProcessAarch32)
{
Reply(
$"triple:{ToHex("arm-unknown-linux-android")};endian:little;ptrsize:4;hostname:{ToHex("Ryujinx")};");
}
else
{
Reply(
$"triple:{ToHex("aarch64-unknown-linux-android")};endian:little;ptrsize:8;hostname:{ToHex("Ryujinx")};");
}
break;
}
if (ss.ConsumeRemaining("ProcessInfo"))
{
if (IsProcessAarch32)
{
Reply(
$"pid:1;cputype:12;cpusubtype:0;triple:{ToHex("arm-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:4;");
}
else
{
Reply(
$"pid:1;cputype:100000c;cpusubtype:0;triple:{ToHex("aarch64-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:8;");
}
break;
}
if (ss.ConsumePrefix("Supported:") || ss.ConsumeRemaining("Supported"))
{
Reply("PacketSize=10000;qXfer:features:read+");
break;
}
if (ss.ConsumeRemaining("fThreadInfo"))
{
Reply($"m{string.Join(",", DebugProcess.GetThreadUids().Select(x => $"{x:x}"))}");
break;
}
if (ss.ConsumeRemaining("sThreadInfo"))
{
Reply("l");
break;
}
if (ss.ConsumePrefix("ThreadExtraInfo,"))
{
ulong? threadId = ss.ReadRemainingAsThreadUid();
if (threadId == null)
{
ReplyError();
break;
}
if (DebugProcess.GetDebugState() == DebugState.Stopped)
{
Reply(ToHex("Stopped"));
}
else
{
Reply(ToHex("Not stopped"));
}
break;
}
if (ss.ConsumePrefix("Xfer:features:read:"))
{
string feature = ss.ReadUntil(':');
ulong addr = ss.ReadUntilAsHex(',');
ulong len = ss.ReadRemainingAsHex();
if (feature == "target.xml")
{
feature = IsProcessAarch32 ? "target32.xml" : "target64.xml";
}
string data;
if (RegisterInformation.Features.TryGetValue(feature, out data))
{
if (addr >= (ulong)data.Length)
{
Reply("l");
break;
}
if (len >= (ulong)data.Length - addr)
{
Reply("l" + ToBinaryFormat(data.Substring((int)addr)));
break;
}
else
{
Reply("m" + ToBinaryFormat(data.Substring((int)addr, (int)len)));
break;
}
}
else
{
Reply("E00"); // Invalid annex
break;
}
}
goto unknownCommand;
case 'Q':
goto unknownCommand;
case 's':
CommandStep(ss.IsEmpty() ? null : ss.ReadRemainingAsHex());
break;
case 'T':
{
ulong? threadId = ss.ReadRemainingAsThreadUid();
CommandIsAlive(threadId);
break;
}
case 'v':
if (ss.ConsumeRemaining("MustReplyEmpty"))
{
Reply("");
break;
}
goto unknownCommand;
default:
unknownCommand:
Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}");
Reply("");
break;
}
}
void CommandQuery()
{
// GDB is performing initial contact. Stop everything.
DebugProcess.DebugStop();
gThread = cThread = DebugProcess.GetThreadUids().First();
Reply($"T05thread:{cThread:x};");
}
void CommandContinue(ulong? newPc)
{
if (newPc.HasValue)
{
if (cThread == null)
{
ReplyError();
return;
}
DebugProcess.GetThread(cThread.Value).Context.DebugPc = newPc.Value;
}
DebugProcess.DebugContinue();
}
void CommandDetach()
{
// TODO: Remove all breakpoints
CommandContinue(null);
}
void CommandReadRegisters()
{
if (gThread == null)
{
ReplyError();
return;
}
var ctx = DebugProcess.GetThread(gThread.Value).Context;
string registers = "";
if (IsProcessAarch32)
{
for (int i = 0; i < GdbRegisterCount32; i++)
{
registers += GdbReadRegister32(ctx, i);
}
}
else
{
for (int i = 0; i < GdbRegisterCount64; i++)
{
registers += GdbReadRegister64(ctx, i);
}
}
Reply(registers);
}
void CommandWriteRegisters(StringStream ss)
{
if (gThread == null)
{
ReplyError();
return;
}
var ctx = DebugProcess.GetThread(gThread.Value).Context;
if (IsProcessAarch32)
{
for (int i = 0; i < GdbRegisterCount32; i++)
{
if (!GdbWriteRegister32(ctx, i, ss))
{
ReplyError();
return;
}
}
}
else
{
for (int i = 0; i < GdbRegisterCount64; i++)
{
if (!GdbWriteRegister64(ctx, i, ss))
{
ReplyError();
return;
}
}
}
if (ss.IsEmpty())
{
ReplyOK();
}
else
{
ReplyError();
}
}
void CommandSetThread(char op, ulong? threadId)
{
if (threadId == 0 || threadId == null)
{
threadId = GetThreads().First().ThreadUid;
}
if (DebugProcess.GetThread(threadId.Value) == null)
{
ReplyError();
return;
}
switch (op)
{
case 'c':
cThread = threadId;
ReplyOK();
return;
case 'g':
gThread = threadId;
ReplyOK();
return;
default:
ReplyError();
return;
}
}
void CommandReadMemory(ulong addr, ulong len)
{
try
{
var data = new byte[len];
DebugProcess.CpuMemory.Read(addr, data);
Reply(ToHex(data));
}
catch (InvalidMemoryRegionException)
{
ReplyError();
}
}
void CommandWriteMemory(ulong addr, ulong len, StringStream ss)
{
try
{
var data = new byte[len];
for (ulong i = 0; i < len; i++)
{
data[i] = (byte)ss.ReadLengthAsHex(2);
}
DebugProcess.CpuMemory.Write(addr, data);
DebugProcess.InvalidateCacheRegion(addr, len);
ReplyOK();
}
catch (InvalidMemoryRegionException)
{
ReplyError();
}
}
void CommandReadRegister(int gdbRegId)
{
if (gThread == null)
{
ReplyError();
return;
}
var ctx = DebugProcess.GetThread(gThread.Value).Context;
string result;
if (IsProcessAarch32)
{
result = GdbReadRegister32(ctx, gdbRegId);
if (result != null)
{
Reply(result);
}
else
{
ReplyError();
}
}
else
{
result = GdbReadRegister64(ctx, gdbRegId);
if (result != null)
{
Reply(result);
}
else
{
ReplyError();
}
}
}
void CommandWriteRegister(int gdbRegId, StringStream ss)
{
if (gThread == null)
{
ReplyError();
return;
}
var ctx = DebugProcess.GetThread(gThread.Value).Context;
if (IsProcessAarch32)
{
if (GdbWriteRegister32(ctx, gdbRegId, ss) && ss.IsEmpty())
{
ReplyOK();
}
else
{
ReplyError();
}
}
else
{
if (GdbWriteRegister64(ctx, gdbRegId, ss) && ss.IsEmpty())
{
ReplyOK();
}
else
{
ReplyError();
}
}
}
private void CommandStep(ulong? newPc)
{
if (cThread == null)
{
ReplyError();
return;
}
var thread = DebugProcess.GetThread(cThread.Value);
if (newPc.HasValue)
{
thread.Context.DebugPc = newPc.Value;
}
if (!DebugProcess.DebugStep(thread))
{
ReplyError();
}
else
{
Reply($"T05thread:{thread.ThreadUid:x};");
}
}
private void CommandIsAlive(ulong? threadId)
{
if (GetThreads().Any(x => x.ThreadUid == threadId))
{
ReplyOK();
}
else
{
Reply("E00");
}
}
private void Reply(string cmd)
{
Logger.Debug?.Print(LogClass.GdbStub, $"Reply: {cmd}");
WriteStream.Write(Encoding.ASCII.GetBytes($"${cmd}#{CalculateChecksum(cmd):x2}"));
}
private void ReplyOK()
{
Reply("OK");
}
private void ReplyError()
{
Reply("E01");
}
private void DebuggerThreadMain()
{
var endpoint = new IPEndPoint(IPAddress.Any, GdbStubPort);
ListenerSocket = new TcpListener(endpoint);
ListenerSocket.Start();
Logger.Notice.Print(LogClass.GdbStub, $"Currently waiting on {endpoint} for GDB client");
while (!_shuttingDown)
{
try
{
ClientSocket = ListenerSocket.AcceptSocket();
}
catch (SocketException)
{
return;
}
// If the user connects before the application is running, wait for the application to start.
int retries = 10;
while (DebugProcess == null && retries-- > 0)
{
Thread.Sleep(200);
}
if (DebugProcess == null)
{
Logger.Warning?.Print(LogClass.GdbStub, "Application is not running, cannot accept GDB client connection");
ClientSocket.Close();
continue;
}
ClientSocket.NoDelay = true;
ReadStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Read);
WriteStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Write);
Logger.Notice.Print(LogClass.GdbStub, "GDB client connected");
while (true)
{
try
{
switch (ReadStream.ReadByte())
{
case -1:
goto eof;
case '+':
continue;
case '-':
Logger.Notice.Print(LogClass.GdbStub, "NACK received!");
continue;
case '\x03':
Messages.Add(new BreakInMessage());
break;
case '$':
string cmd = "";
while (true)
{
int x = ReadStream.ReadByte();
if (x == -1)
goto eof;
if (x == '#')
break;
cmd += (char)x;
}
string checksum = $"{(char)ReadStream.ReadByte()}{(char)ReadStream.ReadByte()}";
if (checksum == $"{CalculateChecksum(cmd):x2}")
{
Messages.Add(new CommandMessage(cmd));
}
else
{
Messages.Add(new SendNackMessage());
}
break;
}
}
catch (IOException)
{
goto eof;
}
}
eof:
Logger.Notice.Print(LogClass.GdbStub, "GDB client lost connection");
ReadStream.Close();
ReadStream = null;
WriteStream.Close();
WriteStream = null;
ClientSocket.Close();
ClientSocket = null;
}
}
private byte CalculateChecksum(string cmd)
{
byte checksum = 0;
foreach (char x in cmd)
{
unchecked
{
checksum += (byte)x;
}
}
return checksum;
}
private string ToHex(byte[] bytes)
{
return string.Join("", bytes.Select(x => $"{x:x2}"));
}
private string ToHex(string str)
{
return ToHex(Encoding.ASCII.GetBytes(str));
}
private string ToBinaryFormat(byte[] bytes)
{
return string.Join("", bytes.Select(x =>
x switch
{
(byte)'#' => "}\x03",
(byte)'$' => "}\x04",
(byte)'*' => "}\x0a",
(byte)'}' => "}\x5d",
_ => Convert.ToChar(x).ToString(),
}
));
}
private string ToBinaryFormat(string str)
{
return ToBinaryFormat(Encoding.ASCII.GetBytes(str));
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_shuttingDown = true;
ListenerSocket.Stop();
ClientSocket?.Shutdown(SocketShutdown.Both);
ClientSocket?.Close();
ReadStream?.Close();
WriteStream?.Close();
DebuggerThread.Join();
Messages.Add(new KillMessage());
MessageHandlerThread.Join();
Messages.Dispose();
}
}
public void BreakHandler(IExecutionContext ctx, ulong address, int imm)
{
Logger.Notice.Print(LogClass.GdbStub, $"Break hit on thread {ctx.ThreadUid} at pc {address:x016}");
Messages.Add(new ThreadBreakMessage(ctx, address, imm));
DebugProcess.DebugInterruptHandler(ctx);
}
public void StepHandler(IExecutionContext ctx)
{
DebugProcess.DebugInterruptHandler(ctx);
}
}
}

View file

@ -0,0 +1,15 @@
namespace Ryujinx.HLE.Debugger
{
enum GdbSignal
{
Zero = 0,
Int = 2,
Quit = 3,
Trap = 5,
Abort = 6,
Alarm = 14,
IO = 23,
XCPU = 24,
Unknown = 143
}
}

View file

@ -0,0 +1,93 @@
<?xml version="1.0"?>
<!-- Copyright (C) 2009-2022 Free Software Foundation, Inc.
Contributed by ARM Ltd.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. -->
<!DOCTYPE feature SYSTEM "gdb-target.dtd">
<feature name="org.gnu.gdb.aarch64.core">
<reg name="x0" bitsize="64"/>
<reg name="x1" bitsize="64"/>
<reg name="x2" bitsize="64"/>
<reg name="x3" bitsize="64"/>
<reg name="x4" bitsize="64"/>
<reg name="x5" bitsize="64"/>
<reg name="x6" bitsize="64"/>
<reg name="x7" bitsize="64"/>
<reg name="x8" bitsize="64"/>
<reg name="x9" bitsize="64"/>
<reg name="x10" bitsize="64"/>
<reg name="x11" bitsize="64"/>
<reg name="x12" bitsize="64"/>
<reg name="x13" bitsize="64"/>
<reg name="x14" bitsize="64"/>
<reg name="x15" bitsize="64"/>
<reg name="x16" bitsize="64"/>
<reg name="x17" bitsize="64"/>
<reg name="x18" bitsize="64"/>
<reg name="x19" bitsize="64"/>
<reg name="x20" bitsize="64"/>
<reg name="x21" bitsize="64"/>
<reg name="x22" bitsize="64"/>
<reg name="x23" bitsize="64"/>
<reg name="x24" bitsize="64"/>
<reg name="x25" bitsize="64"/>
<reg name="x26" bitsize="64"/>
<reg name="x27" bitsize="64"/>
<reg name="x28" bitsize="64"/>
<reg name="x29" bitsize="64"/>
<reg name="x30" bitsize="64"/>
<reg name="sp" bitsize="64" type="data_ptr"/>
<reg name="pc" bitsize="64" type="code_ptr"/>
<flags id="cpsr_flags" size="4">
<!-- Stack Pointer. -->
<field name="SP" start="0" end="0"/>
<!-- Exception Level. -->
<field name="EL" start="2" end="3"/>
<!-- Execution state. -->
<field name="nRW" start="4" end="4"/>
<!-- FIQ interrupt mask. -->
<field name="F" start="6" end="6"/>
<!-- IRQ interrupt mask. -->
<field name="I" start="7" end="7"/>
<!-- SError interrupt mask. -->
<field name="A" start="8" end="8"/>
<!-- Debug exception mask. -->
<field name="D" start="9" end="9"/>
<!-- ARMv8.5-A: Branch Target Identification BTYPE. -->
<field name="BTYPE" start="10" end="11"/>
<!-- ARMv8.0-A: Speculative Store Bypass. -->
<field name="SSBS" start="12" end="12"/>
<!-- Illegal Execution state. -->
<field name="IL" start="20" end="20"/>
<!-- Software Step. -->
<field name="SS" start="21" end="21"/>
<!-- ARMv8.1-A: Privileged Access Never. -->
<field name="PAN" start="22" end="22"/>
<!-- ARMv8.2-A: User Access Override. -->
<field name="UAO" start="23" end="23"/>
<!-- ARMv8.4-A: Data Independent Timing. -->
<field name="DIT" start="24" end="24"/>
<!-- ARMv8.5-A: Tag Check Override. -->
<field name="TCO" start="25" end="25"/>
<!-- Overflow Condition flag. -->
<field name="V" start="28" end="28"/>
<!-- Carry Condition flag. -->
<field name="C" start="29" end="29"/>
<!-- Zero Condition flag. -->
<field name="Z" start="30" end="30"/>
<!-- Negative Condition flag. -->
<field name="N" start="31" end="31"/>
</flags>
<reg name="cpsr" bitsize="32" type="cpsr_flags"/>
</feature>

View file

@ -0,0 +1,159 @@
<?xml version="1.0"?>
<!-- Copyright (C) 2009-2022 Free Software Foundation, Inc.
Contributed by ARM Ltd.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. -->
<!DOCTYPE feature SYSTEM "gdb-target.dtd">
<feature name="org.gnu.gdb.aarch64.fpu">
<vector id="v2d" type="ieee_double" count="2"/>
<vector id="v2u" type="uint64" count="2"/>
<vector id="v2i" type="int64" count="2"/>
<vector id="v4f" type="ieee_single" count="4"/>
<vector id="v4u" type="uint32" count="4"/>
<vector id="v4i" type="int32" count="4"/>
<vector id="v8f" type="ieee_half" count="8"/>
<vector id="v8u" type="uint16" count="8"/>
<vector id="v8i" type="int16" count="8"/>
<vector id="v8bf16" type="bfloat16" count="8"/>
<vector id="v16u" type="uint8" count="16"/>
<vector id="v16i" type="int8" count="16"/>
<vector id="v1u" type="uint128" count="1"/>
<vector id="v1i" type="int128" count="1"/>
<union id="vnd">
<field name="f" type="v2d"/>
<field name="u" type="v2u"/>
<field name="s" type="v2i"/>
</union>
<union id="vns">
<field name="f" type="v4f"/>
<field name="u" type="v4u"/>
<field name="s" type="v4i"/>
</union>
<union id="vnh">
<field name="bf" type="v8bf16"/>
<field name="f" type="v8f"/>
<field name="u" type="v8u"/>
<field name="s" type="v8i"/>
</union>
<union id="vnb">
<field name="u" type="v16u"/>
<field name="s" type="v16i"/>
</union>
<union id="vnq">
<field name="u" type="v1u"/>
<field name="s" type="v1i"/>
</union>
<union id="aarch64v">
<field name="d" type="vnd"/>
<field name="s" type="vns"/>
<field name="h" type="vnh"/>
<field name="b" type="vnb"/>
<field name="q" type="vnq"/>
</union>
<reg name="v0" bitsize="128" type="aarch64v" regnum="34"/>
<reg name="v1" bitsize="128" type="aarch64v" />
<reg name="v2" bitsize="128" type="aarch64v" />
<reg name="v3" bitsize="128" type="aarch64v" />
<reg name="v4" bitsize="128" type="aarch64v" />
<reg name="v5" bitsize="128" type="aarch64v" />
<reg name="v6" bitsize="128" type="aarch64v" />
<reg name="v7" bitsize="128" type="aarch64v" />
<reg name="v8" bitsize="128" type="aarch64v" />
<reg name="v9" bitsize="128" type="aarch64v" />
<reg name="v10" bitsize="128" type="aarch64v"/>
<reg name="v11" bitsize="128" type="aarch64v"/>
<reg name="v12" bitsize="128" type="aarch64v"/>
<reg name="v13" bitsize="128" type="aarch64v"/>
<reg name="v14" bitsize="128" type="aarch64v"/>
<reg name="v15" bitsize="128" type="aarch64v"/>
<reg name="v16" bitsize="128" type="aarch64v"/>
<reg name="v17" bitsize="128" type="aarch64v"/>
<reg name="v18" bitsize="128" type="aarch64v"/>
<reg name="v19" bitsize="128" type="aarch64v"/>
<reg name="v20" bitsize="128" type="aarch64v"/>
<reg name="v21" bitsize="128" type="aarch64v"/>
<reg name="v22" bitsize="128" type="aarch64v"/>
<reg name="v23" bitsize="128" type="aarch64v"/>
<reg name="v24" bitsize="128" type="aarch64v"/>
<reg name="v25" bitsize="128" type="aarch64v"/>
<reg name="v26" bitsize="128" type="aarch64v"/>
<reg name="v27" bitsize="128" type="aarch64v"/>
<reg name="v28" bitsize="128" type="aarch64v"/>
<reg name="v29" bitsize="128" type="aarch64v"/>
<reg name="v30" bitsize="128" type="aarch64v"/>
<reg name="v31" bitsize="128" type="aarch64v"/>
<flags id="fpsr_flags" size="4">
<!-- Invalid Operation cumulative floating-point exception bit. -->
<field name="IOC" start="0" end="0"/>
<!-- Divide by Zero cumulative floating-point exception bit. -->
<field name="DZC" start="1" end="1"/>
<!-- Overflow cumulative floating-point exception bit. -->
<field name="OFC" start="2" end="2"/>
<!-- Underflow cumulative floating-point exception bit. -->
<field name="UFC" start="3" end="3"/>
<!-- Inexact cumulative floating-point exception bit.. -->
<field name="IXC" start="4" end="4"/>
<!-- Input Denormal cumulative floating-point exception bit. -->
<field name="IDC" start="7" end="7"/>
<!-- Cumulative saturation bit, Advanced SIMD only. -->
<field name="QC" start="27" end="27"/>
<!-- When AArch32 is supported at any Exception level and AArch32
floating-point is implemented: Overflow condition flag for AArch32
floating-point comparison operations. -->
<field name="V" start="28" end="28"/>
<!-- When AArch32 is supported at any Exception level and AArch32
floating-point is implemented:
Carry condition flag for AArch32 floating-point comparison operations.
-->
<field name="C" start="29" end="29"/>
<!-- When AArch32 is supported at any Exception level and AArch32
floating-point is implemented:
Zero condition flag for AArch32 floating-point comparison operations.
-->
<field name="Z" start="30" end="30"/>
<!-- When AArch32 is supported at any Exception level and AArch32
floating-point is implemented:
Negative condition flag for AArch32 floating-point comparison
operations. -->
<field name="N" start="31" end="31"/>
</flags>
<reg name="fpsr" bitsize="32" type="fpsr_flags"/>
<flags id="fpcr_flags" size="4">
<!-- Flush Inputs to Zero (part of Armv8.7). -->
<field name="FIZ" start="0" end="0"/>
<!-- Alternate Handling (part of Armv8.7). -->
<field name="AH" start="1" end="1"/>
<!-- Controls how the output elements other than the lowest element of the
vector are determined for Advanced SIMD scalar instructions (part of
Armv8.7). -->
<field name="NEP" start="2" end="2"/>
<!-- Invalid Operation floating-point exception trap enable. -->
<field name="IOE" start="8" end="8"/>
<!-- Divide by Zero floating-point exception trap enable. -->
<field name="DZE" start="9" end="9"/>
<!-- Overflow floating-point exception trap enable. -->
<field name="OFE" start="10" end="10"/>
<!-- Underflow floating-point exception trap enable. -->
<field name="UFE" start="11" end="11"/>
<!-- Inexact floating-point exception trap enable. -->
<field name="IXE" start="12" end="12"/>
<!-- Input Denormal floating-point exception trap enable. -->
<field name="IDE" start="15" end="15"/>
<!-- Flush-to-zero mode control bit on half-precision data-processing
instructions. -->
<field name="FZ16" start="19" end="19"/>
<!-- Rounding Mode control field. -->
<field name="RMode" start="22" end="23"/>
<!-- Flush-to-zero mode control bit. -->
<field name="FZ" start="24" end="24"/>
<!-- Default NaN mode control bit. -->
<field name="DN" start="25" end="25"/>
<!-- Alternative half-precision control bit. -->
<field name="AHP" start="26" end="26"/>
</flags>
<reg name="fpcr" bitsize="32" type="fpcr_flags"/>
</feature>

View file

@ -0,0 +1,27 @@
<?xml version="1.0"?>
<!-- Copyright (C) 2008 Free Software Foundation, Inc.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. -->
<!DOCTYPE feature SYSTEM "gdb-target.dtd">
<feature name="org.gnu.gdb.arm.core">
<reg name="r0" bitsize="32"/>
<reg name="r1" bitsize="32"/>
<reg name="r2" bitsize="32"/>
<reg name="r3" bitsize="32"/>
<reg name="r4" bitsize="32"/>
<reg name="r5" bitsize="32"/>
<reg name="r6" bitsize="32"/>
<reg name="r7" bitsize="32"/>
<reg name="r8" bitsize="32"/>
<reg name="r9" bitsize="32"/>
<reg name="r10" bitsize="32"/>
<reg name="r11" bitsize="32"/>
<reg name="r12" bitsize="32"/>
<reg name="sp" bitsize="32" type="data_ptr"/>
<reg name="lr" bitsize="32"/>
<reg name="pc" bitsize="32" type="code_ptr"/>
<reg name="cpsr" bitsize="32" />
</feature>

View file

@ -0,0 +1,86 @@
<?xml version="1.0"?>
<!-- Copyright (C) 2008 Free Software Foundation, Inc.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. -->
<!DOCTYPE feature SYSTEM "gdb-target.dtd">
<feature name="org.gnu.gdb.arm.vfp">
<vector id="neon_uint8x8" type="uint8" count="8"/>
<vector id="neon_uint16x4" type="uint16" count="4"/>
<vector id="neon_uint32x2" type="uint32" count="2"/>
<vector id="neon_float32x2" type="ieee_single" count="2"/>
<union id="neon_d">
<field name="u8" type="neon_uint8x8"/>
<field name="u16" type="neon_uint16x4"/>
<field name="u32" type="neon_uint32x2"/>
<field name="u64" type="uint64"/>
<field name="f32" type="neon_float32x2"/>
<field name="f64" type="ieee_double"/>
</union>
<vector id="neon_uint8x16" type="uint8" count="16"/>
<vector id="neon_uint16x8" type="uint16" count="8"/>
<vector id="neon_uint32x4" type="uint32" count="4"/>
<vector id="neon_uint64x2" type="uint64" count="2"/>
<vector id="neon_float32x4" type="ieee_single" count="4"/>
<vector id="neon_float64x2" type="ieee_double" count="2"/>
<union id="neon_q">
<field name="u8" type="neon_uint8x16"/>
<field name="u16" type="neon_uint16x8"/>
<field name="u32" type="neon_uint32x4"/>
<field name="u64" type="neon_uint64x2"/>
<field name="f32" type="neon_float32x4"/>
<field name="f64" type="neon_float64x2"/>
</union>
<reg name="d0" bitsize="64" type="neon_d"/>
<reg name="d1" bitsize="64" type="neon_d"/>
<reg name="d2" bitsize="64" type="neon_d"/>
<reg name="d3" bitsize="64" type="neon_d"/>
<reg name="d4" bitsize="64" type="neon_d"/>
<reg name="d5" bitsize="64" type="neon_d"/>
<reg name="d6" bitsize="64" type="neon_d"/>
<reg name="d7" bitsize="64" type="neon_d"/>
<reg name="d8" bitsize="64" type="neon_d"/>
<reg name="d9" bitsize="64" type="neon_d"/>
<reg name="d10" bitsize="64" type="neon_d"/>
<reg name="d11" bitsize="64" type="neon_d"/>
<reg name="d12" bitsize="64" type="neon_d"/>
<reg name="d13" bitsize="64" type="neon_d"/>
<reg name="d14" bitsize="64" type="neon_d"/>
<reg name="d15" bitsize="64" type="neon_d"/>
<reg name="d16" bitsize="64" type="neon_d"/>
<reg name="d17" bitsize="64" type="neon_d"/>
<reg name="d18" bitsize="64" type="neon_d"/>
<reg name="d19" bitsize="64" type="neon_d"/>
<reg name="d20" bitsize="64" type="neon_d"/>
<reg name="d21" bitsize="64" type="neon_d"/>
<reg name="d22" bitsize="64" type="neon_d"/>
<reg name="d23" bitsize="64" type="neon_d"/>
<reg name="d24" bitsize="64" type="neon_d"/>
<reg name="d25" bitsize="64" type="neon_d"/>
<reg name="d26" bitsize="64" type="neon_d"/>
<reg name="d27" bitsize="64" type="neon_d"/>
<reg name="d28" bitsize="64" type="neon_d"/>
<reg name="d29" bitsize="64" type="neon_d"/>
<reg name="d30" bitsize="64" type="neon_d"/>
<reg name="d31" bitsize="64" type="neon_d"/>
<reg name="q0" bitsize="128" type="neon_q"/>
<reg name="q1" bitsize="128" type="neon_q"/>
<reg name="q2" bitsize="128" type="neon_q"/>
<reg name="q3" bitsize="128" type="neon_q"/>
<reg name="q4" bitsize="128" type="neon_q"/>
<reg name="q5" bitsize="128" type="neon_q"/>
<reg name="q6" bitsize="128" type="neon_q"/>
<reg name="q7" bitsize="128" type="neon_q"/>
<reg name="q8" bitsize="128" type="neon_q"/>
<reg name="q9" bitsize="128" type="neon_q"/>
<reg name="q10" bitsize="128" type="neon_q"/>
<reg name="q11" bitsize="128" type="neon_q"/>
<reg name="q12" bitsize="128" type="neon_q"/>
<reg name="q13" bitsize="128" type="neon_q"/>
<reg name="q14" bitsize="128" type="neon_q"/>
<reg name="q15" bitsize="128" type="neon_q"/>
<reg name="fpscr" bitsize="32" type="int" group="float"/>
</feature>

View file

@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!-- Copyright (C) 2009-2013 Free Software Foundation, Inc.
Contributed by ARM Ltd.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. -->
<!DOCTYPE target SYSTEM "gdb-target.dtd">
<target>
<architecture>arm</architecture>
<xi:include href="arm-core.xml"/>
<xi:include href="arm-neon.xml"/>
</target>

View file

@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!-- Copyright (C) 2009-2013 Free Software Foundation, Inc.
Contributed by ARM Ltd.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. -->
<!DOCTYPE target SYSTEM "gdb-target.dtd">
<target>
<architecture>aarch64</architecture>
<xi:include href="aarch64-core.xml"/>
<xi:include href="aarch64-fpu.xml"/>
</target>

View file

@ -0,0 +1,19 @@
using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.Memory;
namespace Ryujinx.HLE.Debugger
{
internal interface IDebuggableProcess
{
void DebugStop();
void DebugContinue();
bool DebugStep(KThread thread);
KThread GetThread(ulong threadUid);
DebugState GetDebugState();
ulong[] GetThreadUids();
public void DebugInterruptHandler(IExecutionContext ctx);
IVirtualMemoryManager CpuMemory { get; }
void InvalidateCacheRegion(ulong address, ulong size);
}
}

View file

@ -0,0 +1,6 @@
namespace Ryujinx.HLE.Debugger
{
struct BreakInMessage : IMessage
{
}
}

View file

@ -0,0 +1,12 @@
namespace Ryujinx.HLE.Debugger
{
struct CommandMessage : IMessage
{
public string Command;
public CommandMessage(string cmd)
{
Command = cmd;
}
}
}

View file

@ -0,0 +1,6 @@
namespace Ryujinx.HLE.Debugger
{
interface IMessage
{
}
}

View file

@ -0,0 +1,6 @@
namespace Ryujinx.HLE.Debugger
{
struct KillMessage : IMessage
{
}
}

View file

@ -0,0 +1,6 @@
namespace Ryujinx.HLE.Debugger
{
struct SendNackMessage : IMessage
{
}
}

View file

@ -0,0 +1,18 @@
using IExecutionContext = Ryujinx.Cpu.IExecutionContext;
namespace Ryujinx.HLE.Debugger
{
public class ThreadBreakMessage : IMessage
{
public IExecutionContext Context { get; }
public ulong Address { get; }
public int Opcode { get; }
public ThreadBreakMessage(IExecutionContext context, ulong address, int opcode)
{
Context = context;
Address = address;
Opcode = opcode;
}
}
}

View file

@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.IO;
namespace Ryujinx.HLE.Debugger
{
class RegisterInformation
{
public static readonly Dictionary<string, string> Features = new()
{
{ "target64.xml", GetEmbeddedResourceContent("target64.xml") },
{ "target32.xml", GetEmbeddedResourceContent("target32.xml") },
{ "aarch64-core.xml", GetEmbeddedResourceContent("aarch64-core.xml") },
{ "aarch64-fpu.xml", GetEmbeddedResourceContent("aarch64-fpu.xml") },
{ "arm-core.xml", GetEmbeddedResourceContent("arm-core.xml") },
{ "arm-neon.xml", GetEmbeddedResourceContent("arm-neon.xml") },
};
private static string GetEmbeddedResourceContent(string resourceName)
{
Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.HLE.Debugger.GdbXml." + resourceName);
StreamReader reader = new StreamReader(stream);
string result = reader.ReadToEnd();
reader.Dispose();
stream.Dispose();
return result;
}
}
}

View file

@ -0,0 +1,109 @@
using System.Diagnostics;
using System.Globalization;
namespace Ryujinx.HLE.Debugger
{
class StringStream
{
private readonly string Data;
private int Position;
public StringStream(string s)
{
Data = s;
}
public char ReadChar()
{
return Data[Position++];
}
public string ReadUntil(char needle)
{
int needlePos = Data.IndexOf(needle, Position);
if (needlePos == -1)
{
needlePos = Data.Length;
}
string result = Data.Substring(Position, needlePos - Position);
Position = needlePos + 1;
return result;
}
public string ReadLength(int len)
{
string result = Data.Substring(Position, len);
Position += len;
return result;
}
public string ReadRemaining()
{
string result = Data.Substring(Position);
Position = Data.Length;
return result;
}
public ulong ReadRemainingAsHex()
{
return ulong.Parse(ReadRemaining(), NumberStyles.HexNumber);
}
public ulong ReadUntilAsHex(char needle)
{
return ulong.Parse(ReadUntil(needle), NumberStyles.HexNumber);
}
public ulong ReadLengthAsHex(int len)
{
return ulong.Parse(ReadLength(len), NumberStyles.HexNumber);
}
public ulong ReadLengthAsLEHex(int len)
{
Debug.Assert(len % 2 == 0);
ulong result = 0;
int pos = 0;
while (pos < len)
{
result += ReadLengthAsHex(2) << (4 * pos);
pos += 2;
}
return result;
}
public ulong? ReadRemainingAsThreadUid()
{
string s = ReadRemaining();
return s == "-1" ? null : ulong.Parse(s, NumberStyles.HexNumber);
}
public bool ConsumePrefix(string prefix)
{
if (Data.Substring(Position).StartsWith(prefix))
{
Position += prefix.Length;
return true;
}
return false;
}
public bool ConsumeRemaining(string match)
{
if (Data.Substring(Position) == match)
{
Position += match.Length;
return true;
}
return false;
}
public bool IsEmpty()
{
return Position >= Data.Length;
}
}
}

View file

@ -5,6 +5,7 @@ using LibHac.Fs.Shim;
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using Ryujinx.HLE.Debugger;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Kernel; using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Memory; using Ryujinx.HLE.HOS.Kernel.Memory;
@ -500,5 +501,13 @@ namespace Ryujinx.HLE.HOS
IsPaused = pause; IsPaused = pause;
} }
internal IDebuggableProcess DebugGetApplicationProcess()
{
lock (KernelContext.Processes)
{
return KernelContext.Processes.Values.FirstOrDefault(x => x.IsApplication)?.DebugInterface;
}
}
} }
} }

View file

@ -1,6 +1,7 @@
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using Ryujinx.HLE.Debugger;
using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Memory; using Ryujinx.HLE.HOS.Kernel.Memory;
@ -11,6 +12,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using ExceptionCallback = Ryujinx.Cpu.ExceptionCallback;
using ExceptionCallbackNoArgs = Ryujinx.Cpu.ExceptionCallbackNoArgs;
namespace Ryujinx.HLE.HOS.Kernel.Process namespace Ryujinx.HLE.HOS.Kernel.Process
{ {
@ -89,6 +92,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
public IVirtualMemoryManager CpuMemory => Context.AddressSpace; public IVirtualMemoryManager CpuMemory => Context.AddressSpace;
public HleProcessDebugger Debugger { get; private set; } public HleProcessDebugger Debugger { get; private set; }
public IDebuggableProcess DebugInterface { get; private set; }
protected int debugState = (int)DebugState.Running;
public KProcess(KernelContext context, bool allowCodeMemoryForJit = false) : base(context) public KProcess(KernelContext context, bool allowCodeMemoryForJit = false) : base(context)
{ {
@ -110,6 +115,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
_threads = []; _threads = [];
Debugger = new HleProcessDebugger(this); Debugger = new HleProcessDebugger(this);
DebugInterface = new DebuggerInterface(this);
} }
public Result InitializeKip( public Result InitializeKip(
@ -679,6 +685,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
SetState(newState); SetState(newState);
if (KernelContext.Device.Configuration.DebuggerSuspendOnStart && IsApplication)
{
mainThread.Suspend(ThreadSchedState.ThreadPauseFlag);
debugState = (int)DebugState.Stopped;
Logger.Notice.Print(LogClass.Kernel, $"Application is suspended on start for debugging.");
}
result = mainThread.Start(); result = mainThread.Start();
if (result != Result.Success) if (result != Result.Success)
@ -727,9 +740,19 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
public IExecutionContext CreateExecutionContext() public IExecutionContext CreateExecutionContext()
{ {
ExceptionCallback breakCallback = null;
ExceptionCallbackNoArgs stepCallback = null;
if (KernelContext.Device.Configuration.EnableGdbStub && KernelContext.Device.Debugger != null)
{
breakCallback = KernelContext.Device.Debugger.BreakHandler;
stepCallback = KernelContext.Device.Debugger.StepHandler;
}
return Context?.CreateExecutionContext(new ExceptionCallbacks( return Context?.CreateExecutionContext(new ExceptionCallbacks(
InterruptHandler, InterruptHandler,
null, breakCallback,
stepCallback,
KernelContext.SyscallHandler.SvcCall, KernelContext.SyscallHandler.SvcCall,
UndefinedInstructionHandler)); UndefinedInstructionHandler));
} }
@ -1174,5 +1197,168 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
{ {
return Capabilities.IsSvcPermitted(svcId); return Capabilities.IsSvcPermitted(svcId);
} }
private class DebuggerInterface : IDebuggableProcess
{
private Barrier StepBarrier;
private readonly KProcess _parent;
private readonly KernelContext _kernelContext;
private KThread steppingThread;
public DebuggerInterface(KProcess p)
{
_parent = p;
_kernelContext = p.KernelContext;
StepBarrier = new(2);
}
public void DebugStop()
{
if (Interlocked.CompareExchange(ref _parent.debugState, (int)DebugState.Stopping,
(int)DebugState.Running) != (int)DebugState.Running)
{
return;
}
_kernelContext.CriticalSection.Enter();
lock (_parent._threadingLock)
{
foreach (KThread thread in _parent._threads)
{
thread.Suspend(ThreadSchedState.ThreadPauseFlag);
thread.Context.RequestInterrupt();
if (!thread.DebugHalt.WaitOne(TimeSpan.FromMilliseconds(50)))
{
Logger.Warning?.Print(LogClass.Kernel, $"Failed to suspend thread {thread.ThreadUid} in time.");
}
}
}
_parent.debugState = (int)DebugState.Stopped;
_kernelContext.CriticalSection.Leave();
}
public void DebugContinue()
{
if (Interlocked.CompareExchange(ref _parent.debugState, (int)DebugState.Running,
(int)DebugState.Stopped) != (int)DebugState.Stopped)
{
return;
}
_kernelContext.CriticalSection.Enter();
lock (_parent._threadingLock)
{
foreach (KThread thread in _parent._threads)
{
thread.Resume(ThreadSchedState.ThreadPauseFlag);
}
}
_kernelContext.CriticalSection.Leave();
}
public bool DebugStep(KThread target)
{
if (_parent.debugState != (int)DebugState.Stopped)
{
return false;
}
_kernelContext.CriticalSection.Enter();
steppingThread = target;
bool waiting = target.MutexOwner != null || target.WaitingSync || target.WaitingInArbitration;
target.Context.RequestDebugStep();
if (waiting)
{
lock (_parent._threadingLock)
{
foreach (KThread thread in _parent._threads)
{
thread.Resume(ThreadSchedState.ThreadPauseFlag);
}
}
}
else
{
target.Resume(ThreadSchedState.ThreadPauseFlag);
}
_kernelContext.CriticalSection.Leave();
bool stepTimedOut = false;
if (!StepBarrier.SignalAndWait(TimeSpan.FromMilliseconds(2000)))
{
Logger.Warning?.Print(LogClass.Kernel, $"Failed to step thread {target.ThreadUid} in time.");
stepTimedOut = true;
}
_kernelContext.CriticalSection.Enter();
steppingThread = null;
if (waiting)
{
lock (_parent._threadingLock)
{
foreach (KThread thread in _parent._threads)
{
thread.Suspend(ThreadSchedState.ThreadPauseFlag);
}
}
}
else
{
target.Suspend(ThreadSchedState.ThreadPauseFlag);
}
_kernelContext.CriticalSection.Leave();
if (stepTimedOut)
{
return false;
}
StepBarrier.SignalAndWait();
return true;
}
public DebugState GetDebugState()
{
return (DebugState)_parent.debugState;
}
public ulong[] GetThreadUids()
{
lock (_parent._threadingLock)
{
var threads = _parent._threads.Where(x => !x.TerminationRequested).ToArray();
return threads.Select(x => x.ThreadUid).ToArray();
}
}
public KThread GetThread(ulong threadUid)
{
lock (_parent._threadingLock)
{
var threads = _parent._threads.Where(x => !x.TerminationRequested).ToArray();
return threads.FirstOrDefault(x => x.ThreadUid == threadUid);
}
}
public void DebugInterruptHandler(IExecutionContext ctx)
{
_kernelContext.CriticalSection.Enter();
bool stepping = steppingThread != null;
_kernelContext.CriticalSection.Leave();
if (stepping)
{
StepBarrier.SignalAndWait();
StepBarrier.SignalAndWait();
}
_parent.InterruptHandler(ctx);
}
public IVirtualMemoryManager CpuMemory { get { return _parent.CpuMemory; } }
public void InvalidateCacheRegion(ulong address, ulong size)
{
_parent.Context.InvalidateCacheRegion(address, size);
}
}
} }
} }

View file

@ -1,5 +1,6 @@
using ARMeilleure.State; using ARMeilleure.State;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using System.Threading;
namespace Ryujinx.HLE.HOS.Kernel.Process namespace Ryujinx.HLE.HOS.Kernel.Process
{ {
@ -17,10 +18,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
public bool IsAarch32 { get => false; set { } } public bool IsAarch32 { get => false; set { } }
public ulong ThreadUid { get; set; }
public bool Running { get; private set; } = true; public bool Running { get; private set; } = true;
private readonly ulong[] _x = new ulong[32]; private readonly ulong[] _x = new ulong[32];
public ulong DebugPc { get; set; }
public ulong GetX(int index) => _x[index]; public ulong GetX(int index) => _x[index];
public void SetX(int index, ulong value) => _x[index] = value; public void SetX(int index, ulong value) => _x[index] = value;
@ -31,6 +36,10 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
{ {
} }
public void RequestDebugStep()
{
}
public void StopRunning() public void StopRunning()
{ {
Running = false; Running = false;

View file

@ -301,6 +301,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
currentThread.SchedulerWaitEvent.Reset(); currentThread.SchedulerWaitEvent.Reset();
currentThread.ThreadContext.Unlock(); currentThread.ThreadContext.Unlock();
currentThread.DebugHalt.Set();
// Wake all the threads that might be waiting until this thread context is unlocked. // Wake all the threads that might be waiting until this thread context is unlocked.
for (int core = 0; core < CpuCoresCount; core++) for (int core = 0; core < CpuCoresCount; core++)

View file

@ -1,5 +1,6 @@
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using Ryujinx.HLE.Debugger;
using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.HOS.Kernel.SupervisorCall; using Ryujinx.HLE.HOS.Kernel.SupervisorCall;
@ -114,6 +115,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
private readonly Lock _activityOperationLock = new(); private readonly Lock _activityOperationLock = new();
internal readonly ManualResetEvent DebugHalt = new(false);
public KThread(KernelContext context) : base(context) public KThread(KernelContext context) : base(context)
{ {
WaitSyncObjects = new KSynchronizationObject[MaxWaitSyncObjects]; WaitSyncObjects = new KSynchronizationObject[MaxWaitSyncObjects];
@ -202,8 +205,10 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
} }
Context.TpidrroEl0 = (long)_tlsAddress; Context.TpidrroEl0 = (long)_tlsAddress;
Context.DebugPc = _entrypoint;
ThreadUid = KernelContext.NewThreadUid(); ThreadUid = KernelContext.NewThreadUid();
Context.ThreadUid = ThreadUid;
HostThread.Name = customThreadStart != null ? $"HLE.OsThread.{ThreadUid}" : $"HLE.GuestThread.{ThreadUid}"; HostThread.Name = customThreadStart != null ? $"HLE.OsThread.{ThreadUid}" : $"HLE.GuestThread.{ThreadUid}";
@ -307,7 +312,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
{ {
KernelContext.CriticalSection.Enter(); KernelContext.CriticalSection.Enter();
if (Owner != null && Owner.PinnedThreads[KernelStatic.GetCurrentThread().CurrentCore] == this) KThread currentThread = KernelStatic.GetCurrentThread();
if (Owner != null && currentThread != null && Owner.PinnedThreads[currentThread.CurrentCore] == this)
{ {
Owner.UnpinThread(this); Owner.UnpinThread(this);
} }
@ -362,7 +369,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
{ {
ThreadSchedState state = PrepareForTermination(); ThreadSchedState state = PrepareForTermination();
if (state != ThreadSchedState.TerminationPending) if (KernelStatic.GetCurrentThread() == this && state != ThreadSchedState.TerminationPending)
{ {
KernelContext.Synchronization.WaitFor(new KSynchronizationObject[] { this }, -1, out _); KernelContext.Synchronization.WaitFor(new KSynchronizationObject[] { this }, -1, out _);
} }
@ -1248,6 +1255,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
private void ThreadStart() private void ThreadStart()
{ {
_schedulerWaitEvent.WaitOne(); _schedulerWaitEvent.WaitOne();
DebugHalt.Reset();
KernelStatic.SetKernelContext(KernelContext, this); KernelStatic.SetKernelContext(KernelContext, this);
if (_customThreadStart != null) if (_customThreadStart != null)

View file

@ -194,6 +194,21 @@ namespace Ryujinx.HLE
/// </summary> /// </summary>
public Action RefreshInputConfig { internal get; set; } public Action RefreshInputConfig { internal get; set; }
/// <summary>
/// Enables gdbstub to allow for debugging of the guest .
/// </summary>
public bool EnableGdbStub { internal get; set; }
/// <summary>
/// A TCP port to use to expose a gdbstub for a debugger to connect to.
/// </summary>
public ushort GdbStubPort { internal get; set; }
/// <summary>
/// Suspend execution when starting an application
/// </summary>
public bool DebuggerSuspendOnStart { internal get; set; }
/// <summary> /// <summary>
/// The desired hacky workarounds. /// The desired hacky workarounds.
/// </summary> /// </summary>
@ -222,6 +237,9 @@ namespace Ryujinx.HLE
bool multiplayerDisableP2p, bool multiplayerDisableP2p,
string multiplayerLdnPassphrase, string multiplayerLdnPassphrase,
string multiplayerLdnServer, string multiplayerLdnServer,
bool enableGdbStub,
ushort gdbStubPort,
bool debuggerSuspendOnStart,
int customVSyncInterval, int customVSyncInterval,
EnabledDirtyHack[] dirtyHacks = null) EnabledDirtyHack[] dirtyHacks = null)
{ {
@ -248,6 +266,9 @@ namespace Ryujinx.HLE
MultiplayerDisableP2p = multiplayerDisableP2p; MultiplayerDisableP2p = multiplayerDisableP2p;
MultiplayerLdnPassphrase = multiplayerLdnPassphrase; MultiplayerLdnPassphrase = multiplayerLdnPassphrase;
MultiplayerLdnServer = multiplayerLdnServer; MultiplayerLdnServer = multiplayerLdnServer;
EnableGdbStub = enableGdbStub;
GdbStubPort = gdbStubPort;
DebuggerSuspendOnStart = debuggerSuspendOnStart;
Hacks = dirtyHacks ?? []; Hacks = dirtyHacks ?? [];
} }

View file

@ -33,6 +33,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Debugger\GdbXml\aarch64-core.xml" />
<None Remove="Debugger\GdbXml\aarch64-fpu.xml" />
<None Remove="Debugger\GdbXml\arm-core.xml" />
<None Remove="Debugger\GdbXml\arm-neon.xml" />
<None Remove="Debugger\GdbXml\target64.xml" />
<None Remove="Debugger\GdbXml\target32.xml" />
<None Remove="Homebrew.npdm" /> <None Remove="Homebrew.npdm" />
<None Remove="HOS\Applets\SoftwareKeyboard\Resources\Logo_Ryujinx.png" /> <None Remove="HOS\Applets\SoftwareKeyboard\Resources\Logo_Ryujinx.png" />
<None Remove="HOS\Applets\SoftwareKeyboard\Resources\Icon_BtnA.png" /> <None Remove="HOS\Applets\SoftwareKeyboard\Resources\Icon_BtnA.png" />
@ -42,6 +48,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Debugger\GdbXml\aarch64-core.xml" />
<EmbeddedResource Include="Debugger\GdbXml\aarch64-fpu.xml" />
<EmbeddedResource Include="Debugger\GdbXml\arm-core.xml" />
<EmbeddedResource Include="Debugger\GdbXml\arm-neon.xml" />
<EmbeddedResource Include="Debugger\GdbXml\target64.xml" />
<EmbeddedResource Include="Debugger\GdbXml\target32.xml" />
<EmbeddedResource Include="Homebrew.npdm" /> <EmbeddedResource Include="Homebrew.npdm" />
<EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Logo_Ryujinx.png" /> <EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Logo_Ryujinx.png" />
<EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Icon_BtnA.png" /> <EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Icon_BtnA.png" />

View file

@ -14,6 +14,7 @@ using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.HLE.UI; using Ryujinx.HLE.UI;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Threading;
namespace Ryujinx.HLE namespace Ryujinx.HLE
{ {
@ -41,6 +42,7 @@ namespace Ryujinx.HLE
public Hid Hid { get; } public Hid Hid { get; }
public TamperMachine TamperMachine { get; } public TamperMachine TamperMachine { get; }
public IHostUIHandler UIHandler { get; } public IHostUIHandler UIHandler { get; }
public Debugger.Debugger Debugger { get; }
public int CpuCoresCount = 4; // Switch has a quad-core Tegra X1 SoC public int CpuCoresCount = 4; // Switch has a quad-core Tegra X1 SoC
@ -72,6 +74,7 @@ namespace Ryujinx.HLE
AudioDeviceDriver = new CompatLayerHardwareDeviceDriver(Configuration.AudioDeviceDriver); AudioDeviceDriver = new CompatLayerHardwareDeviceDriver(Configuration.AudioDeviceDriver);
Memory = new MemoryBlock(Configuration.MemoryConfiguration.ToDramSize(), memoryAllocationFlags); Memory = new MemoryBlock(Configuration.MemoryConfiguration.ToDramSize(), memoryAllocationFlags);
Gpu = new GpuContext(Configuration.GpuRenderer, DirtyHacks); Gpu = new GpuContext(Configuration.GpuRenderer, DirtyHacks);
Debugger = Configuration.EnableGdbStub ? new Debugger.Debugger(this, Configuration.GdbStubPort) : null;
System = new HOS.Horizon(this); System = new HOS.Horizon(this);
Statistics = new PerformanceStatistics(this); Statistics = new PerformanceStatistics(this);
Hid = new Hid(this, System.HidStorage); Hid = new Hid(this, System.HidStorage);
@ -173,6 +176,7 @@ namespace Ryujinx.HLE
AudioDeviceDriver.Dispose(); AudioDeviceDriver.Dispose();
FileSystem.Dispose(); FileSystem.Dispose();
Memory.Dispose(); Memory.Dispose();
Debugger?.Dispose();
TitleIDs.CurrentApplication.Value = null; TitleIDs.CurrentApplication.Value = null;
Shared = null; Shared = null;

View file

@ -338,6 +338,9 @@ namespace Ryujinx.Headless
false, false,
string.Empty, string.Empty,
string.Empty, string.Empty,
options.EnableGdbStub,
options.GdbStubPort,
options.DebuggerSuspendOnStart,
options.CustomVSyncInterval options.CustomVSyncInterval
) )
.Configure( .Configure(

View file

@ -423,6 +423,17 @@ namespace Ryujinx.Headless
[Option("skip-user-profiles-manager", Required = false, Default = false, HelpText = "Enable skips the Profiles Manager popup during gameplay. Select the desired profile before starting the game")] [Option("skip-user-profiles-manager", Required = false, Default = false, HelpText = "Enable skips the Profiles Manager popup during gameplay. Select the desired profile before starting the game")]
public bool SkipUserProfilesManager { get; set; } public bool SkipUserProfilesManager { get; set; }
// Debug
[Option("enable-gdb-stub", Required = false, Default = false, HelpText = "Enables the GDB stub so that a developer can attach a debugger to the emulated process.")]
public bool EnableGdbStub { get; set; }
[Option("gdb-stub-port", Required = false, Default = 55555, HelpText = "Specifies which TCP port the GDB stub listens on.")]
public ushort GdbStubPort { get; set; }
[Option("suspend-on-start", Required = false, Default = false, HelpText = "Suspend execution when starting an application.")]
public bool DebuggerSuspendOnStart { get; set; }
// Values // Values
[Value(0, MetaName = "input", HelpText = "Input to load.", Required = true)] [Value(0, MetaName = "input", HelpText = "Input to load.", Required = true)]

View file

@ -218,6 +218,10 @@ namespace Ryujinx.Ava.Systems
ConfigurationState.Instance.Multiplayer.LdnServer.Event += UpdateLdnServerState; ConfigurationState.Instance.Multiplayer.LdnServer.Event += UpdateLdnServerState;
ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateDisableP2pState; ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateDisableP2pState;
ConfigurationState.Instance.Debug.EnableGdbStub.Event += UpdateEnableGdbStubState;
ConfigurationState.Instance.Debug.GdbStubPort.Event += UpdateGdbStubPortState;
ConfigurationState.Instance.Debug.DebuggerSuspendOnStart.Event += UpdateDebuggerSuspendOnStartState;
_gpuCancellationTokenSource = new CancellationTokenSource(); _gpuCancellationTokenSource = new CancellationTokenSource();
_gpuDoneEvent = new ManualResetEvent(false); _gpuDoneEvent = new ManualResetEvent(false);
} }
@ -564,6 +568,21 @@ namespace Ryujinx.Ava.Systems
Device.Configuration.MultiplayerDisableP2p = e.NewValue; Device.Configuration.MultiplayerDisableP2p = e.NewValue;
} }
private void UpdateEnableGdbStubState(object sender, ReactiveEventArgs<bool> e)
{
Device.Configuration.EnableGdbStub = e.NewValue;
}
private void UpdateGdbStubPortState(object sender, ReactiveEventArgs<ushort> e)
{
Device.Configuration.GdbStubPort = e.NewValue;
}
private void UpdateDebuggerSuspendOnStartState(object sender, ReactiveEventArgs<bool> e)
{
Device.Configuration.DebuggerSuspendOnStart = e.NewValue;
}
public void Stop() public void Stop()
{ {
_isActive = false; _isActive = false;

View file

@ -464,6 +464,21 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary> /// </summary>
public bool UseHypervisor { get; set; } public bool UseHypervisor { get; set; }
/// <summary>
/// Enables or disables the GDB stub
/// </summary>
public bool EnableGdbStub { get; set; }
/// <summary>
/// Which TCP port should the GDB stub listen on
/// </summary>
public ushort GdbStubPort { get; set; }
/// <summary>
/// Suspend execution when starting an application
/// </summary>
public bool DebuggerSuspendOnStart { get; set; }
/// <summary> /// <summary>
/// Show toggles for dirty hacks in the UI. /// Show toggles for dirty hacks in the UI.
/// </summary> /// </summary>

View file

@ -156,6 +156,10 @@ namespace Ryujinx.Ava.Systems.Configuration
Multiplayer.LdnPassphrase.Value = cff.MultiplayerLdnPassphrase; Multiplayer.LdnPassphrase.Value = cff.MultiplayerLdnPassphrase;
Multiplayer.LdnServer.Value = cff.LdnServer; Multiplayer.LdnServer.Value = cff.LdnServer;
Debug.EnableGdbStub.Value = cff.EnableGdbStub;
Debug.GdbStubPort.Value = cff.GdbStubPort;
Debug.DebuggerSuspendOnStart.Value = cff.DebuggerSuspendOnStart;
{ {
Hacks.ShowDirtyHacks.Value = shouldLoadFromFile ? cff.ShowDirtyHacks : Hacks.ShowDirtyHacks.Value; // Get from global config only Hacks.ShowDirtyHacks.Value = shouldLoadFromFile ? cff.ShowDirtyHacks : Hacks.ShowDirtyHacks.Value; // Get from global config only

View file

@ -703,6 +703,37 @@ namespace Ryujinx.Ava.Systems.Configuration
} }
} }
/// <summary>
/// Debug configuration section
/// </summary>
public class DebugSection
{
/// <summary>
/// Enables or disables the GDB stub
/// </summary>
public ReactiveObject<bool> EnableGdbStub { get; private set; }
/// <summary>
/// Which TCP port should the GDB stub listen on
/// </summary>
public ReactiveObject<ushort> GdbStubPort { get; private set; }
/// <summary>
/// Suspend execution when starting an application
/// </summary>
public ReactiveObject<bool> DebuggerSuspendOnStart { get; private set; }
public DebugSection()
{
EnableGdbStub = new ReactiveObject<bool>();
EnableGdbStub.LogChangesToValue(nameof(EnableGdbStub));
GdbStubPort = new ReactiveObject<ushort>();
GdbStubPort.LogChangesToValue(nameof(GdbStubPort));
DebuggerSuspendOnStart = new ReactiveObject<bool>();
DebuggerSuspendOnStart.LogChangesToValue(nameof(DebuggerSuspendOnStart));
}
}
public class HacksSection public class HacksSection
{ {
/// <summary> /// <summary>
@ -801,6 +832,11 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary> /// </summary>
public MultiplayerSection Multiplayer { get; private set; } public MultiplayerSection Multiplayer { get; private set; }
/// <summary>
/// The Debug
/// </summary>
public DebugSection Debug { get; private set; }
/// <summary> /// <summary>
/// The Dirty Hacks section /// The Dirty Hacks section
/// </summary> /// </summary>
@ -854,6 +890,7 @@ namespace Ryujinx.Ava.Systems.Configuration
Graphics = new GraphicsSection(); Graphics = new GraphicsSection();
Hid = new HidSection(); Hid = new HidSection();
Multiplayer = new MultiplayerSection(); Multiplayer = new MultiplayerSection();
Debug = new DebugSection();
Hacks = new HacksSection(); Hacks = new HacksSection();
UpdateCheckerType = new ReactiveObject<UpdaterType>(); UpdateCheckerType = new ReactiveObject<UpdaterType>();
FocusLostActionType = new ReactiveObject<FocusLostType>(); FocusLostActionType = new ReactiveObject<FocusLostType>();
@ -893,6 +930,9 @@ namespace Ryujinx.Ava.Systems.Configuration
Multiplayer.DisableP2p, Multiplayer.DisableP2p,
Multiplayer.LdnPassphrase, Multiplayer.LdnPassphrase,
Multiplayer.GetLdnServer(), Multiplayer.GetLdnServer(),
Debug.EnableGdbStub,
Debug.GdbStubPort,
Debug.DebuggerSuspendOnStart,
Graphics.CustomVSyncInterval, Graphics.CustomVSyncInterval,
Hacks.ShowDirtyHacks ? Hacks.EnabledHacks : null); Hacks.ShowDirtyHacks ? Hacks.EnabledHacks : null);
} }

View file

@ -147,6 +147,9 @@ namespace Ryujinx.Ava.Systems.Configuration
MultiplayerDisableP2p = Multiplayer.DisableP2p, MultiplayerDisableP2p = Multiplayer.DisableP2p,
MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase, MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase,
LdnServer = Multiplayer.LdnServer, LdnServer = Multiplayer.LdnServer,
EnableGdbStub = Debug.EnableGdbStub,
GdbStubPort = Debug.GdbStubPort,
DebuggerSuspendOnStart = Debug.DebuggerSuspendOnStart,
ShowDirtyHacks = Hacks.ShowDirtyHacks, ShowDirtyHacks = Hacks.ShowDirtyHacks,
DirtyHacks = Hacks.EnabledHacks.Select(it => it.Pack()).ToArray(), DirtyHacks = Hacks.EnabledHacks.Select(it => it.Pack()).ToArray(),
}; };
@ -324,6 +327,9 @@ namespace Ryujinx.Ava.Systems.Configuration
}, },
} }
]; ];
Debug.EnableGdbStub.Value = false;
Debug.GdbStubPort.Value = 55555;
Debug.DebuggerSuspendOnStart.Value = false;
} }
private static GraphicsBackend DefaultGraphicsBackend() private static GraphicsBackend DefaultGraphicsBackend()

View file

@ -71,6 +71,10 @@ namespace Ryujinx.Ava.UI.ViewModels
private string _ldnPassphrase; private string _ldnPassphrase;
[ObservableProperty] private string _ldnServer; [ObservableProperty] private string _ldnServer;
private bool _enableGDBStub;
private ushort _gdbStubPort;
private bool _debuggerSuspendOnStart;
public SettingsHacksViewModel DirtyHacks { get; } public SettingsHacksViewModel DirtyHacks { get; }
private readonly bool _isGameRunning; private readonly bool _isGameRunning;
@ -387,6 +391,36 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool IsInvalidLdnPassphraseVisible { get; set; } public bool IsInvalidLdnPassphraseVisible { get; set; }
public bool EnableGdbStub
{
get => _enableGDBStub;
set
{
_enableGDBStub = value;
ConfigurationState.Instance.Debug.EnableGdbStub.Value = _enableGDBStub;
}
}
public ushort GDBStubPort
{
get => _gdbStubPort;
set
{
_gdbStubPort = value;
ConfigurationState.Instance.Debug.GdbStubPort.Value = _gdbStubPort;
}
}
public bool DebuggerSuspendOnStart
{
get => _debuggerSuspendOnStart;
set
{
_debuggerSuspendOnStart = value;
ConfigurationState.Instance.Debug.DebuggerSuspendOnStart.Value = _debuggerSuspendOnStart;
}
}
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this()
{ {
_virtualFileSystem = virtualFileSystem; _virtualFileSystem = virtualFileSystem;
@ -680,10 +714,16 @@ namespace Ryujinx.Ava.UI.ViewModels
FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
// Multiplayer
MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value; MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value;
DisableP2P = config.Multiplayer.DisableP2p; DisableP2P = config.Multiplayer.DisableP2p;
LdnPassphrase = config.Multiplayer.LdnPassphrase; LdnPassphrase = config.Multiplayer.LdnPassphrase;
LdnServer = config.Multiplayer.LdnServer; LdnServer = config.Multiplayer.LdnServer;
// Debug
EnableGdbStub = config.Debug.EnableGdbStub.Value;
GDBStubPort = config.Debug.GdbStubPort.Value;
DebuggerSuspendOnStart = config.Debug.DebuggerSuspendOnStart.Value;
} }
public void SaveSettings(bool global = false) public void SaveSettings(bool global = false)
@ -800,12 +840,18 @@ namespace Ryujinx.Ava.UI.ViewModels
config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode;
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;
// Multiplayer
config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]]; config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]];
config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex; config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex;
config.Multiplayer.DisableP2p.Value = DisableP2P; config.Multiplayer.DisableP2p.Value = DisableP2P;
config.Multiplayer.LdnPassphrase.Value = LdnPassphrase; config.Multiplayer.LdnPassphrase.Value = LdnPassphrase;
config.Multiplayer.LdnServer.Value = LdnServer; config.Multiplayer.LdnServer.Value = LdnServer;
// Debug
config.Debug.EnableGdbStub.Value = EnableGdbStub;
config.Debug.GdbStubPort.Value = GDBStubPort;
config.Debug.DebuggerSuspendOnStart.Value = DebuggerSuspendOnStart;
// Dirty Hacks // Dirty Hacks
config.Hacks.Xc2MenuSoftlockFix.Value = DirtyHacks.Xc2MenuSoftlockFix; config.Hacks.Xc2MenuSoftlockFix.Value = DirtyHacks.Xc2MenuSoftlockFix;
config.Hacks.DisableNifmIsAnyInternetRequestAccepted.Value = config.Hacks.DisableNifmIsAnyInternetRequestAccepted.Value =

View file

@ -0,0 +1,64 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsDebugView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:DataType="viewModels:SettingsViewModel">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<ScrollViewer
Name="DebugPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabDebugTitle}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<CheckBox IsChecked="{Binding EnableGdbStub}">
<TextBlock Text="{ext:Locale EnableGDBStub}"
ToolTip.Tip="{ext:Locale GDBStubToggleTooltip}" />
</CheckBox>
</StackPanel>
<StackPanel
Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{ext:Locale GDBStubPort}"
Width="250" />
<ui:NumberBox Value="{Binding GDBStubPort}"
Width="350"
SmallChange="1"
LargeChange="10"
SimpleNumberFormat="F0"
SpinButtonPlacementMode="Inline"
Minimum="1024"
Maximum="65535" />
</StackPanel>
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<CheckBox IsChecked="{Binding DebuggerSuspendOnStart}">
<TextBlock Text="{ext:Locale DebuggerSuspendOnStart}"
ToolTip.Tip="{ext:Locale DebuggerSuspendOnStartTooltip}" />
</CheckBox>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View file

@ -0,0 +1,13 @@
using Avalonia.Controls;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsDebugView : UserControl
{
public SettingsDebugView()
{
InitializeComponent();
}
}
}

View file

@ -46,6 +46,7 @@
<settings:SettingsAudioView Name="AudioPage" /> <settings:SettingsAudioView Name="AudioPage" />
<settings:SettingsNetworkView Name="NetworkPage" /> <settings:SettingsNetworkView Name="NetworkPage" />
<settings:SettingsLoggingView Name="LoggingPage" /> <settings:SettingsLoggingView Name="LoggingPage" />
<settings:SettingsDebugView Name="DebugPage" />
<settings:SettingsHacksView Name="HacksPage" /> <settings:SettingsHacksView Name="HacksPage" />
</Grid> </Grid>
<ui:NavigationView <ui:NavigationView
@ -100,6 +101,10 @@
Content="{ext:Locale SettingsTabLogging}" Content="{ext:Locale SettingsTabLogging}"
Tag="LoggingPage" Tag="LoggingPage"
IconSource="Document" /> IconSource="Document" />
<ui:NavigationViewItem
Content="{ext:Locale SettingsTabDebug}"
Tag="DebugPage"
IconSource="Star" />
<ui:NavigationViewItem <ui:NavigationViewItem
IsVisible="{Binding ShowDirtyHacks}" IsVisible="{Binding ShowDirtyHacks}"
Content="Dirty Hacks" Content="Dirty Hacks"

View file

@ -98,6 +98,9 @@ namespace Ryujinx.Ava.UI.Windows
case "LoggingPage": case "LoggingPage":
NavPanel.Content = LoggingPage; NavPanel.Content = LoggingPage;
break; break;
case "DebugPage":
NavPanel.Content = DebugPage;
break;
case nameof(HacksPage): case nameof(HacksPage):
HacksPage.DataContext = ViewModel; HacksPage.DataContext = ViewModel;
NavPanel.Content = HacksPage; NavPanel.Content = HacksPage;