From 890165707a4f68371f1df131a402c1bd32b1b297 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Fri, 20 Jun 2025 16:14:12 +0800
Subject: [PATCH 01/28] Add GDB Stub
Author: merry, svc64
---
assets/locales.json | 175 ++++
.../Instructions/NativeInterface.cs | 8 +-
src/ARMeilleure/Optimizations.cs | 1 +
src/ARMeilleure/State/ExecutionContext.cs | 30 +-
src/ARMeilleure/Translation/Translator.cs | 29 +-
src/Ryujinx.Common/Logging/LogClass.cs | 1 +
src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs | 10 +
src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs | 81 +-
.../AppleHv/HvExecutionContextShadow.cs | 11 +
.../AppleHv/HvExecutionContextVcpu.cs | 16 +-
.../AppleHv/IHvExecutionContext.cs | 4 +
src/Ryujinx.Cpu/ExceptionCallbacks.cs | 8 +
src/Ryujinx.Cpu/IExecutionContext.cs | 24 +
src/Ryujinx.Cpu/Jit/JitExecutionContext.cs | 25 +
.../LightningJit/State/ExecutionContext.cs | 19 +
src/Ryujinx.HLE/Debugger/DebugState.cs | 9 +
src/Ryujinx.HLE/Debugger/Debugger.cs | 907 ++++++++++++++++++
src/Ryujinx.HLE/Debugger/GdbSignal.cs | 15 +
.../Debugger/GdbXml/aarch64-core.xml | 93 ++
.../Debugger/GdbXml/aarch64-fpu.xml | 159 +++
src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml | 27 +
src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml | 86 ++
src/Ryujinx.HLE/Debugger/GdbXml/target32.xml | 14 +
src/Ryujinx.HLE/Debugger/GdbXml/target64.xml | 14 +
.../Debugger/IDebuggableProcess.cs | 19 +
.../Debugger/Message/BreakInMessage.cs | 6 +
.../Debugger/Message/CommandMessage.cs | 12 +
src/Ryujinx.HLE/Debugger/Message/IMessage.cs | 6 +
.../Debugger/Message/KillMessage.cs | 6 +
.../Debugger/Message/SendNackMessage.cs | 6 +
.../Debugger/Message/ThreadBreakMessage.cs | 18 +
.../Debugger/RegisterInformation.cs | 28 +
src/Ryujinx.HLE/Debugger/StringStream.cs | 109 +++
src/Ryujinx.HLE/HOS/Horizon.cs | 9 +
.../HOS/Kernel/Process/KProcess.cs | 176 +++-
.../Kernel/Process/ProcessExecutionContext.cs | 9 +
.../HOS/Kernel/Threading/KScheduler.cs | 1 +
.../HOS/Kernel/Threading/KThread.cs | 12 +-
src/Ryujinx.HLE/HleConfiguration.cs | 21 +
src/Ryujinx.HLE/Ryujinx.HLE.csproj | 12 +
src/Ryujinx.HLE/Switch.cs | 4 +
src/Ryujinx/Headless/HeadlessRyujinx.Init.cs | 3 +
src/Ryujinx/Headless/Options.cs | 11 +
src/Ryujinx/Systems/AppHost.cs | 19 +
.../Configuration/ConfigurationFileFormat.cs | 15 +
.../ConfigurationState.Migration.cs | 4 +
.../Configuration/ConfigurationState.Model.cs | 40 +
.../Configuration/ConfigurationState.cs | 6 +
.../UI/ViewModels/SettingsViewModel.cs | 46 +
.../UI/Views/Settings/SettingsDebugView.axaml | 64 ++
.../Views/Settings/SettingsDebugView.axaml.cs | 13 +
src/Ryujinx/UI/Windows/SettingsWindow.axaml | 5 +
.../UI/Windows/SettingsWindow.axaml.cs | 3 +
53 files changed, 2428 insertions(+), 21 deletions(-)
create mode 100644 src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs
create mode 100644 src/Ryujinx.HLE/Debugger/DebugState.cs
create mode 100644 src/Ryujinx.HLE/Debugger/Debugger.cs
create mode 100644 src/Ryujinx.HLE/Debugger/GdbSignal.cs
create mode 100644 src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml
create mode 100644 src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml
create mode 100644 src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml
create mode 100644 src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml
create mode 100644 src/Ryujinx.HLE/Debugger/GdbXml/target32.xml
create mode 100644 src/Ryujinx.HLE/Debugger/GdbXml/target64.xml
create mode 100644 src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs
create mode 100644 src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs
create mode 100644 src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs
create mode 100644 src/Ryujinx.HLE/Debugger/Message/IMessage.cs
create mode 100644 src/Ryujinx.HLE/Debugger/Message/KillMessage.cs
create mode 100644 src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs
create mode 100644 src/Ryujinx.HLE/Debugger/Message/ThreadBreakMessage.cs
create mode 100644 src/Ryujinx.HLE/Debugger/RegisterInformation.cs
create mode 100644 src/Ryujinx.HLE/Debugger/StringStream.cs
create mode 100644 src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml
create mode 100644 src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml.cs
diff --git a/assets/locales.json b/assets/locales.json
index d931017aa..6269cb5dd 100644
--- a/assets/locales.json
+++ b/assets/locales.json
@@ -25021,6 +25021,181 @@
"zh_CN": "动态 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": ""
+ }
}
]
}
diff --git a/src/ARMeilleure/Instructions/NativeInterface.cs b/src/ARMeilleure/Instructions/NativeInterface.cs
index b4922629b..c44f24b7c 100644
--- a/src/ARMeilleure/Instructions/NativeInterface.cs
+++ b/src/ARMeilleure/Instructions/NativeInterface.cs
@@ -3,6 +3,8 @@ using ARMeilleure.State;
using ARMeilleure.Translation;
using System;
using System.Runtime.InteropServices;
+using System.Threading;
+using ExecutionContext = ARMeilleure.State.ExecutionContext;
namespace ARMeilleure.Instructions
{
@@ -200,7 +202,11 @@ namespace ARMeilleure.Instructions
ExecutionContext context = GetContext();
- context.CheckInterrupt();
+ // If debugging, we'll handle interrupts outside
+ if (!Optimizations.EnableDebugging)
+ {
+ context.CheckInterrupt();
+ }
Statistics.ResumeTimer();
diff --git a/src/ARMeilleure/Optimizations.cs b/src/ARMeilleure/Optimizations.cs
index 18390de31..6dd7befe7 100644
--- a/src/ARMeilleure/Optimizations.cs
+++ b/src/ARMeilleure/Optimizations.cs
@@ -12,6 +12,7 @@ namespace ARMeilleure
public static bool AllowLcqInFunctionTable { 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 UseArm64AesIfAvailable { get; set; } = true;
diff --git a/src/ARMeilleure/State/ExecutionContext.cs b/src/ARMeilleure/State/ExecutionContext.cs
index 223e59d79..2978bf27a 100644
--- a/src/ARMeilleure/State/ExecutionContext.cs
+++ b/src/ARMeilleure/State/ExecutionContext.cs
@@ -1,4 +1,7 @@
using ARMeilleure.Memory;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Threading;
namespace ARMeilleure.State
{
@@ -10,7 +13,7 @@ namespace ARMeilleure.State
internal nint NativeContextPtr => _nativeContext.BasePtr;
- private bool _interrupted;
+ internal bool Interrupted { get; private set; }
private readonly ICounter _counter;
@@ -65,6 +68,8 @@ namespace ARMeilleure.State
public bool IsAarch32 { get; set; }
+ public ulong ThreadUid { get; set; }
+
internal ExecutionMode ExecutionMode
{
get
@@ -90,14 +95,19 @@ namespace ARMeilleure.State
private readonly ExceptionCallbackNoArgs _interruptCallback;
private readonly ExceptionCallback _breakCallback;
+ private readonly ExceptionCallbackNoArgs _stepCallback;
private readonly ExceptionCallback _supervisorCallback;
private readonly ExceptionCallback _undefinedCallback;
+ internal int ShouldStep;
+ public ulong DebugPc { get; set; }
+
public ExecutionContext(
IJitMemoryAllocator allocator,
ICounter counter,
ExceptionCallbackNoArgs interruptCallback = null,
ExceptionCallback breakCallback = null,
+ ExceptionCallbackNoArgs stepCallback = null,
ExceptionCallback supervisorCallback = null,
ExceptionCallback undefinedCallback = null)
{
@@ -105,6 +115,7 @@ namespace ARMeilleure.State
_counter = counter;
_interruptCallback = interruptCallback;
_breakCallback = breakCallback;
+ _stepCallback = stepCallback;
_supervisorCallback = supervisorCallback;
_undefinedCallback = undefinedCallback;
@@ -127,9 +138,9 @@ namespace ARMeilleure.State
internal void CheckInterrupt()
{
- if (_interrupted)
+ if (Interrupted)
{
- _interrupted = false;
+ Interrupted = false;
_interruptCallback?.Invoke(this);
}
@@ -139,7 +150,18 @@ namespace ARMeilleure.State
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)
diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs
index d8528cfd6..343e361a5 100644
--- a/src/ARMeilleure/Translation/Translator.cs
+++ b/src/ARMeilleure/Translation/Translator.cs
@@ -119,7 +119,25 @@ namespace ARMeilleure.Translation
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);
}
@@ -175,7 +193,7 @@ namespace ARMeilleure.Translation
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);
@@ -228,7 +246,7 @@ namespace ARMeilleure.Translation
Stubs,
address,
highCq,
- _ptc.State != PtcState.Disabled,
+ _ptc.State != PtcState.Disabled && !Optimizations.EnableDebugging,
mode: Aarch32Mode.User);
Logger.StartPass(PassName.Decoding);
@@ -367,9 +385,8 @@ namespace ARMeilleure.Translation
if (block.Exit)
{
- // Left option here as it may be useful if we need to return to managed rather than tail call in
- // future. (eg. for debug)
- bool useReturns = false;
+ // Return to managed rather than tail call.
+ bool useReturns = Optimizations.EnableDebugging;
InstEmitFlowHelper.EmitVirtualJump(context, Const(block.Address), isReturn: useReturns);
}
diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs
index a4117580e..89f0336dc 100644
--- a/src/Ryujinx.Common/Logging/LogClass.cs
+++ b/src/Ryujinx.Common/Logging/LogClass.cs
@@ -13,6 +13,7 @@ namespace Ryujinx.Common.Logging
Cpu,
Emulation,
FFmpeg,
+ GdbStub,
Font,
Gpu,
Hid,
diff --git a/src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs b/src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs
new file mode 100644
index 000000000..08114e12a
--- /dev/null
+++ b/src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs
@@ -0,0 +1,10 @@
+namespace Ryujinx.Cpu.AppleHv.Arm
+{
+ enum ExceptionLevel : uint
+ {
+ PstateMask = 0xfffffff0,
+ EL1h = 0b0101,
+ El1t = 0b0100,
+ EL0 = 0b0000,
+ }
+}
diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs
index 53cea5385..f13662e44 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs
@@ -11,7 +11,18 @@ namespace Ryujinx.Cpu.AppleHv
class HvExecutionContext : IExecutionContext
{
///
- 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;
+ }
+ }
///
public long TpidrEl0
@@ -48,6 +59,9 @@ namespace Ryujinx.Cpu.AppleHv
set => _impl.Fpsr = value;
}
+ ///
+ public ulong ThreadUid { get; set; }
+
///
public bool IsAarch32
{
@@ -67,6 +81,7 @@ namespace Ryujinx.Cpu.AppleHv
private readonly ICounter _counter;
private readonly IHvExecutionContext _shadowContext;
private IHvExecutionContext _impl;
+ private int _shouldStep;
private readonly ExceptionCallbacks _exceptionCallbacks;
@@ -103,6 +118,11 @@ namespace Ryujinx.Cpu.AppleHv
_exceptionCallbacks.BreakCallback?.Invoke(this, address, imm);
}
+ private void StepHandler()
+ {
+ _exceptionCallbacks.StepCallback?.Invoke(this);
+ }
+
private void SupervisorCallHandler(ulong address, int imm)
{
_exceptionCallbacks.SupervisorCallback?.Invoke(this, address, imm);
@@ -127,6 +147,30 @@ namespace Ryujinx.Cpu.AppleHv
return Interlocked.Exchange(ref _interruptRequested, 0) != 0;
}
+ ///
+ public void RequestDebugStep()
+ {
+ Interlocked.Exchange(ref _shouldStep, 1);
+ }
+
+ ///
+ public ulong DebugPc
+ {
+ get => Pc;
+ set
+ {
+ uint currentEl = Pstate & ~((uint)ExceptionLevel.PstateMask);
+ if (currentEl == (uint)ExceptionLevel.EL1h)
+ {
+ _impl.ElrEl1 = value;
+ }
+ else
+ {
+ _impl.Pc = value;
+ }
+ }
+ }
+
///
public void StopRunning()
{
@@ -142,6 +186,22 @@ namespace Ryujinx.Cpu.AppleHv
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();
HvExitReason reason = vcpu.ExitInfo->Reason;
@@ -209,6 +269,20 @@ namespace Ryujinx.Cpu.AppleHv
SupervisorCallHandler(elr - 4UL, id);
vcpu = RentFromPool(memoryManager.AddressSpace, vcpu);
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:
throw new Exception($"Unhandled guest exception {ec}.");
}
@@ -219,10 +293,7 @@ namespace Ryujinx.Cpu.AppleHv
// TODO: Invalidate only the range that was modified?
return HvAddressSpace.KernelRegionTlbiEretAddress;
}
- else
- {
- return HvAddressSpace.KernelRegionEretAddress;
- }
+ return HvAddressSpace.KernelRegionEretAddress;
}
private static void DataAbort(MemoryTracking tracking, ulong vcpu, uint esr)
diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs
index 6ce8e1800..4ea5f276d 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs
@@ -18,6 +18,8 @@ namespace Ryujinx.Cpu.AppleHv
public bool IsAarch32 { get; set; }
+ public ulong ThreadUid { get; set; }
+
private readonly ulong[] _x;
private readonly V128[] _v;
@@ -46,5 +48,14 @@ namespace Ryujinx.Cpu.AppleHv
{
_v[index] = value;
}
+
+ public void RequestInterrupt()
+ {
+ }
+
+ public bool GetAndClearInterruptRequested()
+ {
+ return false;
+ }
}
}
diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs
index 1949cabdf..9ef03e61e 100644
--- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs
+++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs
@@ -2,6 +2,7 @@ using ARMeilleure.State;
using Ryujinx.Memory;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
+using System.Threading;
namespace Ryujinx.Cpu.AppleHv
{
@@ -13,6 +14,8 @@ namespace Ryujinx.Cpu.AppleHv
private static readonly SetSimdFpReg _setSimdFpReg;
private static readonly nint _setSimdFpRegNativePtr;
+ public ulong ThreadUid { get; set; }
+
static HvExecutionContextVcpu()
{
// .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 int _interruptRequested;
public HvExecutionContextVcpu(ulong vcpu)
{
@@ -180,8 +184,16 @@ namespace Ryujinx.Cpu.AppleHv
public void RequestInterrupt()
{
- ulong vcpu = _vcpu;
- HvApi.hv_vcpus_exit(ref vcpu, 1);
+ if (Interlocked.Exchange(ref _interruptRequested, 1) == 0)
+ {
+ ulong vcpu = _vcpu;
+ HvApi.hv_vcpus_exit(ref vcpu, 1);
+ }
+ }
+
+ public bool GetAndClearInterruptRequested()
+ {
+ return Interlocked.Exchange(ref _interruptRequested, 0) != 0;
}
}
}
diff --git a/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs b/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs
index 54b73acc6..134405b5c 100644
--- a/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs
+++ b/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs
@@ -15,6 +15,7 @@ namespace Ryujinx.Cpu.AppleHv
uint Fpcr { get; set; }
uint Fpsr { get; set; }
+ ulong ThreadUid { get; set; }
ulong GetX(int index);
void SetX(int index, ulong value);
@@ -39,5 +40,8 @@ namespace Ryujinx.Cpu.AppleHv
SetV(i, context.GetV(i));
}
}
+
+ void RequestInterrupt();
+ bool GetAndClearInterruptRequested();
}
}
diff --git a/src/Ryujinx.Cpu/ExceptionCallbacks.cs b/src/Ryujinx.Cpu/ExceptionCallbacks.cs
index d9293302b..6e50b4d70 100644
--- a/src/Ryujinx.Cpu/ExceptionCallbacks.cs
+++ b/src/Ryujinx.Cpu/ExceptionCallbacks.cs
@@ -29,6 +29,11 @@ namespace Ryujinx.Cpu
///
public readonly ExceptionCallback BreakCallback;
+ ///
+ /// Handler for CPU software interrupts caused by single-stepping.
+ ///
+ public readonly ExceptionCallbackNoArgs StepCallback;
+
///
/// Handler for CPU software interrupts caused by the Arm SVC instruction.
///
@@ -47,16 +52,19 @@ namespace Ryujinx.Cpu
///
/// Handler for CPU interrupts triggered using
/// Handler for CPU software interrupts caused by the Arm BRK instruction
+ /// Handler for CPU software interrupts caused by single-stepping
/// Handler for CPU software interrupts caused by the Arm SVC instruction
/// Handler for CPU software interrupts caused by any undefined Arm instruction
public ExceptionCallbacks(
ExceptionCallbackNoArgs interruptCallback = null,
ExceptionCallback breakCallback = null,
+ ExceptionCallbackNoArgs stepCallback = null,
ExceptionCallback supervisorCallback = null,
ExceptionCallback undefinedCallback = null)
{
InterruptCallback = interruptCallback;
BreakCallback = breakCallback;
+ StepCallback = stepCallback;
SupervisorCallback = supervisorCallback;
UndefinedCallback = undefinedCallback;
}
diff --git a/src/Ryujinx.Cpu/IExecutionContext.cs b/src/Ryujinx.Cpu/IExecutionContext.cs
index c38210800..df0c94278 100644
--- a/src/Ryujinx.Cpu/IExecutionContext.cs
+++ b/src/Ryujinx.Cpu/IExecutionContext.cs
@@ -1,5 +1,6 @@
using ARMeilleure.State;
using System;
+using System.Threading;
namespace Ryujinx.Cpu
{
@@ -46,6 +47,11 @@ namespace Ryujinx.Cpu
///
bool IsAarch32 { get; set; }
+ ///
+ /// Thread UID.
+ ///
+ public ulong ThreadUid { get; set; }
+
///
/// Indicates whenever the CPU is still running code.
///
@@ -108,5 +114,23 @@ namespace Ryujinx.Cpu
/// If you only need to pause the thread temporarily, use instead.
///
void StopRunning();
+
+ ///
+ /// Requests the thread to stop running temporarily and call .
+ ///
+ ///
+ /// 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 .
+ ///
+ void RequestDebugStep();
+
+ ///
+ /// Current Program Counter (for debugging).
+ ///
+ ///
+ /// PC register for the debugger. Must not be accessed while the thread isn't stopped for debugging.
+ ///
+ ulong DebugPc { get; set; }
}
}
diff --git a/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs b/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs
index f15486e68..d4775f3ed 100644
--- a/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs
+++ b/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs
@@ -1,5 +1,7 @@
using ARMeilleure.Memory;
using ARMeilleure.State;
+using System.Threading;
+using ExecutionContext = ARMeilleure.State.ExecutionContext;
namespace Ryujinx.Cpu.Jit
{
@@ -53,6 +55,13 @@ namespace Ryujinx.Cpu.Jit
set => _impl.IsAarch32 = value;
}
+ ///
+ public ulong ThreadUid
+ {
+ get => _impl.ThreadUid;
+ set => _impl.ThreadUid = value;
+ }
+
///
public bool Running => _impl.Running;
@@ -65,6 +74,7 @@ namespace Ryujinx.Cpu.Jit
counter,
InterruptHandler,
BreakHandler,
+ StepHandler,
SupervisorCallHandler,
UndefinedHandler);
@@ -93,6 +103,11 @@ namespace Ryujinx.Cpu.Jit
_exceptionCallbacks.BreakCallback?.Invoke(this, address, imm);
}
+ private void StepHandler(ExecutionContext context)
+ {
+ _exceptionCallbacks.StepCallback?.Invoke(this);
+ }
+
private void SupervisorCallHandler(ExecutionContext context, ulong address, int imm)
{
_exceptionCallbacks.SupervisorCallback?.Invoke(this, address, imm);
@@ -109,6 +124,16 @@ namespace Ryujinx.Cpu.Jit
_impl.RequestInterrupt();
}
+ ///
+ public void RequestDebugStep() => _impl.RequestDebugStep();
+
+ ///
+ public ulong DebugPc
+ {
+ get => _impl.DebugPc;
+ set => _impl.DebugPc = value;
+ }
+
///
public void StopRunning()
{
diff --git a/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs b/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs
index a366dcca6..fc75e5185 100644
--- a/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs
+++ b/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs
@@ -1,6 +1,7 @@
using ARMeilleure.Memory;
using ARMeilleure.State;
using System;
+using System.Threading;
namespace Ryujinx.Cpu.LightningJit.State
{
@@ -51,6 +52,8 @@ namespace Ryujinx.Cpu.LightningJit.State
}
public bool IsAarch32 { get; set; }
+
+ public ulong ThreadUid { get; set; }
internal ExecutionMode ExecutionMode
{
@@ -77,15 +80,20 @@ namespace Ryujinx.Cpu.LightningJit.State
private readonly ExceptionCallbackNoArgs _interruptCallback;
private readonly ExceptionCallback _breakCallback;
+ private readonly ExceptionCallbackNoArgs _stepCallback;
private readonly ExceptionCallback _supervisorCallback;
private readonly ExceptionCallback _undefinedCallback;
+ internal int ShouldStep;
+ public ulong DebugPc { get; set; }
+
public ExecutionContext(IJitMemoryAllocator allocator, ICounter counter, ExceptionCallbacks exceptionCallbacks)
{
_nativeContext = new NativeContext(allocator);
_counter = counter;
_interruptCallback = exceptionCallbacks.InterruptCallback;
_breakCallback = exceptionCallbacks.BreakCallback;
+ _stepCallback = exceptionCallbacks.StepCallback;
_supervisorCallback = exceptionCallbacks.SupervisorCallback;
_undefinedCallback = exceptionCallbacks.UndefinedCallback;
@@ -117,6 +125,17 @@ namespace Ryujinx.Cpu.LightningJit.State
_interrupted = true;
}
+ public void StepHandler()
+ {
+ _stepCallback.Invoke(this);
+ }
+
+ public void RequestDebugStep()
+ {
+ Interlocked.Exchange(ref ShouldStep, 1);
+ RequestInterrupt();
+ }
+
internal void OnBreak(ulong address, int imm)
{
_breakCallback?.Invoke(this, address, imm);
diff --git a/src/Ryujinx.HLE/Debugger/DebugState.cs b/src/Ryujinx.HLE/Debugger/DebugState.cs
new file mode 100644
index 000000000..d2efa2bff
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/DebugState.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.HLE.Debugger
+{
+ public enum DebugState
+ {
+ Running,
+ Stopping,
+ Stopped,
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
new file mode 100644
index 000000000..33c380175
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -0,0 +1,907 @@
+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 Messages = new BlockingCollection(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.ReadLengthAsHex(16);
+ state.SetX(gdbRegId, value);
+ return true;
+ }
+ case 32:
+ {
+ ulong value = ss.ReadLengthAsHex(16);
+ state.DebugPc = value;
+ return true;
+ }
+ case 33:
+ {
+ ulong value = ss.ReadLengthAsHex(8);
+ state.Pstate = (uint)value;
+ return true;
+ }
+ case >= 34 and <= 65:
+ {
+ ulong value0 = ss.ReadLengthAsHex(16);
+ ulong value1 = ss.ReadLengthAsHex(16);
+ state.SetV(gdbRegId - 34, new V128(value0, value1));
+ return true;
+ }
+ case 66:
+ {
+ ulong value = ss.ReadLengthAsHex(8);
+ state.Fpsr = (uint)value;
+ return true;
+ }
+ case 67:
+ {
+ ulong value = ss.ReadLengthAsHex(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(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.ReadLengthAsHex(8);
+ state.SetX(gdbRegId, value);
+ return true;
+ }
+ case 15:
+ {
+ ulong value = ss.ReadLengthAsHex(8);
+ state.DebugPc = value;
+ return true;
+ }
+ case 16:
+ {
+ ulong value = ss.ReadLengthAsHex(8);
+ state.Pstate = (uint)value;
+ return true;
+ }
+ case >= 17 and <= 32:
+ {
+ ulong value0 = ss.ReadLengthAsHex(16);
+ ulong value1 = ss.ReadLengthAsHex(16);
+ state.SetV(gdbRegId - 17, new V128(value0, value1));
+ return true;
+ }
+ case >= 33 and <= 64:
+ {
+ ulong value = ss.ReadLengthAsHex(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.ReadLengthAsHex(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();
+ 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;
+ }
+ }
+ }
+
+ private void ProcessCommand(string cmd)
+ {
+ Logger.Debug?.Print(LogClass.GdbStub, $"Receive: {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;
+ }
+ 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);
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/GdbSignal.cs b/src/Ryujinx.HLE/Debugger/GdbSignal.cs
new file mode 100644
index 000000000..ee4efbda4
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/GdbSignal.cs
@@ -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
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml b/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml
new file mode 100644
index 000000000..9899a0e4a
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml b/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml
new file mode 100644
index 000000000..a09120bc4
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml b/src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml
new file mode 100644
index 000000000..2307d65f9
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml b/src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml
new file mode 100644
index 000000000..d61f6b854
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/target32.xml b/src/Ryujinx.HLE/Debugger/GdbXml/target32.xml
new file mode 100644
index 000000000..890679858
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/GdbXml/target32.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ arm
+
+
+
diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/target64.xml b/src/Ryujinx.HLE/Debugger/GdbXml/target64.xml
new file mode 100644
index 000000000..cfd5bf780
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/GdbXml/target64.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ aarch64
+
+
+
diff --git a/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs b/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs
new file mode 100644
index 000000000..273a1147f
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs
@@ -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);
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs b/src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs
new file mode 100644
index 000000000..81d8784ae
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs
@@ -0,0 +1,6 @@
+namespace Ryujinx.HLE.Debugger
+{
+ struct BreakInMessage : IMessage
+ {
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs b/src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs
new file mode 100644
index 000000000..ad265d432
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs
@@ -0,0 +1,12 @@
+namespace Ryujinx.HLE.Debugger
+{
+ struct CommandMessage : IMessage
+ {
+ public string Command;
+
+ public CommandMessage(string cmd)
+ {
+ Command = cmd;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/Message/IMessage.cs b/src/Ryujinx.HLE/Debugger/Message/IMessage.cs
new file mode 100644
index 000000000..4b03183c5
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Message/IMessage.cs
@@ -0,0 +1,6 @@
+namespace Ryujinx.HLE.Debugger
+{
+ interface IMessage
+ {
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/Message/KillMessage.cs b/src/Ryujinx.HLE/Debugger/Message/KillMessage.cs
new file mode 100644
index 000000000..43ae0f21e
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Message/KillMessage.cs
@@ -0,0 +1,6 @@
+namespace Ryujinx.HLE.Debugger
+{
+ struct KillMessage : IMessage
+ {
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs b/src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs
new file mode 100644
index 000000000..ce804c46e
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs
@@ -0,0 +1,6 @@
+namespace Ryujinx.HLE.Debugger
+{
+ struct SendNackMessage : IMessage
+ {
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/Message/ThreadBreakMessage.cs b/src/Ryujinx.HLE/Debugger/Message/ThreadBreakMessage.cs
new file mode 100644
index 000000000..027096eeb
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/Message/ThreadBreakMessage.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/RegisterInformation.cs b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs
new file mode 100644
index 000000000..b5fd88ea5
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace Ryujinx.HLE.Debugger
+{
+ class RegisterInformation
+ {
+ public static readonly Dictionary 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;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/StringStream.cs b/src/Ryujinx.HLE/Debugger/StringStream.cs
new file mode 100644
index 000000000..d8148a9c2
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/StringStream.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs
index 5063b4329..44adb9674 100644
--- a/src/Ryujinx.HLE/HOS/Horizon.cs
+++ b/src/Ryujinx.HLE/HOS/Horizon.cs
@@ -5,6 +5,7 @@ using LibHac.Fs.Shim;
using LibHac.FsSystem;
using LibHac.Tools.FsSystem;
using Ryujinx.Cpu;
+using Ryujinx.HLE.Debugger;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Memory;
@@ -500,5 +501,13 @@ namespace Ryujinx.HLE.HOS
IsPaused = pause;
}
+
+ internal IDebuggableProcess DebugGetApplicationProcess()
+ {
+ lock (KernelContext.Processes)
+ {
+ return KernelContext.Processes.Values.FirstOrDefault(x => x.IsApplication)?.DebugInterface;
+ }
+ }
}
}
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
index 478b4e864..0c400b425 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
@@ -1,6 +1,7 @@
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Cpu;
+using Ryujinx.HLE.Debugger;
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Memory;
@@ -11,6 +12,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using ExceptionCallback = Ryujinx.Cpu.ExceptionCallback;
+using ExceptionCallbackNoArgs = Ryujinx.Cpu.ExceptionCallbackNoArgs;
namespace Ryujinx.HLE.HOS.Kernel.Process
{
@@ -89,6 +92,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
public IVirtualMemoryManager CpuMemory => Context.AddressSpace;
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)
{
@@ -110,6 +115,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
_threads = [];
Debugger = new HleProcessDebugger(this);
+ DebugInterface = new DebuggerInterface(this);
}
public Result InitializeKip(
@@ -679,6 +685,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
SetState(newState);
+ if (KernelContext.Device.Configuration.DebuggerSuspendOnStart && IsApplication)
+ {
+ mainThread.Suspend(ThreadSchedState.ThreadPauseFlag);
+ debugState = (int)DebugState.Stopped;
+ }
+
result = mainThread.Start();
if (result != Result.Success)
@@ -727,9 +739,19 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
public IExecutionContext CreateExecutionContext()
{
+ ExceptionCallback breakCallback = null;
+ ExceptionCallbackNoArgs stepCallback = null;
+
+ if (KernelContext.Device.Configuration.EnableGdbStub)
+ {
+ breakCallback = KernelContext.Device.Debugger.BreakHandler;
+ stepCallback = KernelContext.Device.Debugger.StepHandler;
+ }
+
return Context?.CreateExecutionContext(new ExceptionCallbacks(
InterruptHandler,
- null,
+ breakCallback,
+ stepCallback,
KernelContext.SyscallHandler.SvcCall,
UndefinedInstructionHandler));
}
@@ -1174,5 +1196,157 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
{
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();
+
+ StepBarrier.SignalAndWait();
+
+ _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();
+ 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);
+ }
+ }
}
}
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs
index b8118fbb4..f0e44c4b7 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs
@@ -1,5 +1,6 @@
using ARMeilleure.State;
using Ryujinx.Cpu;
+using System.Threading;
namespace Ryujinx.HLE.HOS.Kernel.Process
{
@@ -17,10 +18,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
public bool IsAarch32 { get => false; set { } }
+ public ulong ThreadUid { get; set; }
+
public bool Running { get; private set; } = true;
private readonly ulong[] _x = new ulong[32];
+ public ulong DebugPc { get; set; }
+
public ulong GetX(int index) => _x[index];
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()
{
Running = false;
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs
index 7471702c3..54b20ff99 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs
@@ -301,6 +301,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
currentThread.SchedulerWaitEvent.Reset();
currentThread.ThreadContext.Unlock();
+ currentThread.DebugHalt.Set();
// Wake all the threads that might be waiting until this thread context is unlocked.
for (int core = 0; core < CpuCoresCount; core++)
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
index b5a14ad5b..2b5d11244 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
@@ -1,5 +1,6 @@
using Ryujinx.Common.Logging;
using Ryujinx.Cpu;
+using Ryujinx.HLE.Debugger;
using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.HOS.Kernel.SupervisorCall;
@@ -114,6 +115,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
private readonly Lock _activityOperationLock = new();
+ internal readonly ManualResetEvent DebugHalt = new(false);
+
public KThread(KernelContext context) : base(context)
{
WaitSyncObjects = new KSynchronizationObject[MaxWaitSyncObjects];
@@ -202,8 +205,10 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
}
Context.TpidrroEl0 = (long)_tlsAddress;
+ Context.DebugPc = _entrypoint;
ThreadUid = KernelContext.NewThreadUid();
+ Context.ThreadUid = ThreadUid;
HostThread.Name = customThreadStart != null ? $"HLE.OsThread.{ThreadUid}" : $"HLE.GuestThread.{ThreadUid}";
@@ -307,7 +312,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
{
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);
}
@@ -362,7 +369,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
{
ThreadSchedState state = PrepareForTermination();
- if (state != ThreadSchedState.TerminationPending)
+ if (KernelStatic.GetCurrentThread() == this && state != ThreadSchedState.TerminationPending)
{
KernelContext.Synchronization.WaitFor(new KSynchronizationObject[] { this }, -1, out _);
}
@@ -1248,6 +1255,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
private void ThreadStart()
{
_schedulerWaitEvent.WaitOne();
+ DebugHalt.Reset();
KernelStatic.SetKernelContext(KernelContext, this);
if (_customThreadStart != null)
diff --git a/src/Ryujinx.HLE/HleConfiguration.cs b/src/Ryujinx.HLE/HleConfiguration.cs
index 10c2a1f30..e2f95ede7 100644
--- a/src/Ryujinx.HLE/HleConfiguration.cs
+++ b/src/Ryujinx.HLE/HleConfiguration.cs
@@ -194,6 +194,21 @@ namespace Ryujinx.HLE
///
public Action RefreshInputConfig { internal get; set; }
+ ///
+ /// Enables gdbstub to allow for debugging of the guest .
+ ///
+ public bool EnableGdbStub { internal get; set; }
+
+ ///
+ /// A TCP port to use to expose a gdbstub for a debugger to connect to.
+ ///
+ public ushort GdbStubPort { internal get; set; }
+
+ ///
+ /// Suspend execution when starting an application
+ ///
+ public bool DebuggerSuspendOnStart { internal get; set; }
+
///
/// The desired hacky workarounds.
///
@@ -222,6 +237,9 @@ namespace Ryujinx.HLE
bool multiplayerDisableP2p,
string multiplayerLdnPassphrase,
string multiplayerLdnServer,
+ bool enableGdbStub,
+ ushort gdbStubPort,
+ bool debuggerSuspendOnStart,
int customVSyncInterval,
EnabledDirtyHack[] dirtyHacks = null)
{
@@ -248,6 +266,9 @@ namespace Ryujinx.HLE
MultiplayerDisableP2p = multiplayerDisableP2p;
MultiplayerLdnPassphrase = multiplayerLdnPassphrase;
MultiplayerLdnServer = multiplayerLdnServer;
+ EnableGdbStub = enableGdbStub;
+ GdbStubPort = gdbStubPort;
+ DebuggerSuspendOnStart = debuggerSuspendOnStart;
Hacks = dirtyHacks ?? [];
}
diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
index 5139d9276..1938796e8 100644
--- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj
+++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
@@ -33,6 +33,12 @@
+
+
+
+
+
+
@@ -42,6 +48,12 @@
+
+
+
+
+
+
diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs
index bdcbe82c7..ed6d57b2d 100644
--- a/src/Ryujinx.HLE/Switch.cs
+++ b/src/Ryujinx.HLE/Switch.cs
@@ -14,6 +14,7 @@ using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.HLE.UI;
using Ryujinx.Memory;
using System;
+using System.Threading;
namespace Ryujinx.HLE
{
@@ -41,6 +42,7 @@ namespace Ryujinx.HLE
public Hid Hid { get; }
public TamperMachine TamperMachine { get; }
public IHostUIHandler UIHandler { get; }
+ public Debugger.Debugger Debugger { get; }
public int CpuCoresCount = 4; // Switch has a quad-core Tegra X1 SoC
@@ -72,6 +74,7 @@ namespace Ryujinx.HLE
AudioDeviceDriver = new CompatLayerHardwareDeviceDriver(Configuration.AudioDeviceDriver);
Memory = new MemoryBlock(Configuration.MemoryConfiguration.ToDramSize(), memoryAllocationFlags);
Gpu = new GpuContext(Configuration.GpuRenderer, DirtyHacks);
+ Debugger = Configuration.EnableGdbStub ? new Debugger.Debugger(this, Configuration.GdbStubPort) : null;
System = new HOS.Horizon(this);
Statistics = new PerformanceStatistics(this);
Hid = new Hid(this, System.HidStorage);
@@ -173,6 +176,7 @@ namespace Ryujinx.HLE
AudioDeviceDriver.Dispose();
FileSystem.Dispose();
Memory.Dispose();
+ Debugger.Dispose();
TitleIDs.CurrentApplication.Value = null;
Shared = null;
diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs
index f15d24e8a..dc7b8625e 100644
--- a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs
+++ b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs
@@ -338,6 +338,9 @@ namespace Ryujinx.Headless
false,
string.Empty,
string.Empty,
+ options.EnableGdbStub,
+ options.GdbStubPort,
+ options.DebuggerSuspendOnStart,
options.CustomVSyncInterval
)
.Configure(
diff --git a/src/Ryujinx/Headless/Options.cs b/src/Ryujinx/Headless/Options.cs
index 49050005c..876d0c936 100644
--- a/src/Ryujinx/Headless/Options.cs
+++ b/src/Ryujinx/Headless/Options.cs
@@ -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")]
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
[Value(0, MetaName = "input", HelpText = "Input to load.", Required = true)]
diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs
index 1c5f64309..f11280d62 100644
--- a/src/Ryujinx/Systems/AppHost.cs
+++ b/src/Ryujinx/Systems/AppHost.cs
@@ -218,6 +218,10 @@ namespace Ryujinx.Ava.Systems
ConfigurationState.Instance.Multiplayer.LdnServer.Event += UpdateLdnServerState;
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();
_gpuDoneEvent = new ManualResetEvent(false);
}
@@ -564,6 +568,21 @@ namespace Ryujinx.Ava.Systems
Device.Configuration.MultiplayerDisableP2p = e.NewValue;
}
+ private void UpdateEnableGdbStubState(object sender, ReactiveEventArgs e)
+ {
+ Device.Configuration.EnableGdbStub = e.NewValue;
+ }
+
+ private void UpdateGdbStubPortState(object sender, ReactiveEventArgs e)
+ {
+ Device.Configuration.GdbStubPort = e.NewValue;
+ }
+
+ private void UpdateDebuggerSuspendOnStartState(object sender, ReactiveEventArgs e)
+ {
+ Device.Configuration.DebuggerSuspendOnStart = e.NewValue;
+ }
+
public void Stop()
{
_isActive = false;
diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs
index c21383349..26ea73f73 100644
--- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs
+++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs
@@ -464,6 +464,21 @@ namespace Ryujinx.Ava.Systems.Configuration
///
public bool UseHypervisor { get; set; }
+ ///
+ /// Enables or disables the GDB stub
+ ///
+ public bool EnableGdbStub { get; set; }
+
+ ///
+ /// Which TCP port should the GDB stub listen on
+ ///
+ public ushort GdbStubPort { get; set; }
+
+ ///
+ /// Suspend execution when starting an application
+ ///
+ public bool DebuggerSuspendOnStart { get; set; }
+
///
/// Show toggles for dirty hacks in the UI.
///
diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs
index afabdb4e3..58f25b783 100644
--- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs
+++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs
@@ -156,6 +156,10 @@ namespace Ryujinx.Ava.Systems.Configuration
Multiplayer.LdnPassphrase.Value = cff.MultiplayerLdnPassphrase;
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
diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs
index 29a390b26..bc8fdb40a 100644
--- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs
+++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs
@@ -703,6 +703,37 @@ namespace Ryujinx.Ava.Systems.Configuration
}
}
+ ///
+ /// Debug configuration section
+ ///
+ public class DebugSection
+ {
+ ///
+ /// Enables or disables the GDB stub
+ ///
+ public ReactiveObject EnableGdbStub { get; private set; }
+
+ ///
+ /// Which TCP port should the GDB stub listen on
+ ///
+ public ReactiveObject GdbStubPort { get; private set; }
+
+ ///
+ /// Suspend execution when starting an application
+ ///
+ public ReactiveObject DebuggerSuspendOnStart { get; private set; }
+
+ public DebugSection()
+ {
+ EnableGdbStub = new ReactiveObject();
+ EnableGdbStub.LogChangesToValue(nameof(EnableGdbStub));
+ GdbStubPort = new ReactiveObject();
+ GdbStubPort.LogChangesToValue(nameof(GdbStubPort));
+ DebuggerSuspendOnStart = new ReactiveObject();
+ DebuggerSuspendOnStart.LogChangesToValue(nameof(DebuggerSuspendOnStart));
+ }
+ }
+
public class HacksSection
{
///
@@ -801,6 +832,11 @@ namespace Ryujinx.Ava.Systems.Configuration
///
public MultiplayerSection Multiplayer { get; private set; }
+ ///
+ /// The Debug
+ ///
+ public DebugSection Debug { get; private set; }
+
///
/// The Dirty Hacks section
///
@@ -854,6 +890,7 @@ namespace Ryujinx.Ava.Systems.Configuration
Graphics = new GraphicsSection();
Hid = new HidSection();
Multiplayer = new MultiplayerSection();
+ Debug = new DebugSection();
Hacks = new HacksSection();
UpdateCheckerType = new ReactiveObject();
FocusLostActionType = new ReactiveObject();
@@ -893,6 +930,9 @@ namespace Ryujinx.Ava.Systems.Configuration
Multiplayer.DisableP2p,
Multiplayer.LdnPassphrase,
Multiplayer.GetLdnServer(),
+ Debug.EnableGdbStub,
+ Debug.GdbStubPort,
+ Debug.DebuggerSuspendOnStart,
Graphics.CustomVSyncInterval,
Hacks.ShowDirtyHacks ? Hacks.EnabledHacks : null);
}
diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs
index 4a565d5d3..185aedf64 100644
--- a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs
+++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs
@@ -147,6 +147,9 @@ namespace Ryujinx.Ava.Systems.Configuration
MultiplayerDisableP2p = Multiplayer.DisableP2p,
MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase,
LdnServer = Multiplayer.LdnServer,
+ EnableGdbStub = Debug.EnableGdbStub,
+ GdbStubPort = Debug.GdbStubPort,
+ DebuggerSuspendOnStart = Debug.DebuggerSuspendOnStart,
ShowDirtyHacks = Hacks.ShowDirtyHacks,
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()
diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
index 654eb0c43..54fd951fb 100644
--- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
@@ -71,6 +71,10 @@ namespace Ryujinx.Ava.UI.ViewModels
private string _ldnPassphrase;
[ObservableProperty] private string _ldnServer;
+ private bool _enableGDBStub;
+ private ushort _gdbStubPort;
+ private bool _debuggerSuspendOnStart;
+
public SettingsHacksViewModel DirtyHacks { get; }
private readonly bool _isGameRunning;
@@ -387,6 +391,36 @@ namespace Ryujinx.Ava.UI.ViewModels
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()
{
_virtualFileSystem = virtualFileSystem;
@@ -680,10 +714,16 @@ namespace Ryujinx.Ava.UI.ViewModels
FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
+ // Multiplayer
MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value;
DisableP2P = config.Multiplayer.DisableP2p;
LdnPassphrase = config.Multiplayer.LdnPassphrase;
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)
@@ -800,12 +840,18 @@ namespace Ryujinx.Ava.UI.ViewModels
config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode;
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;
+ // Multiplayer
config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]];
config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex;
config.Multiplayer.DisableP2p.Value = DisableP2P;
config.Multiplayer.LdnPassphrase.Value = LdnPassphrase;
config.Multiplayer.LdnServer.Value = LdnServer;
+ // Debug
+ config.Debug.EnableGdbStub.Value = EnableGdbStub;
+ config.Debug.GdbStubPort.Value = GDBStubPort;
+ config.Debug.DebuggerSuspendOnStart.Value = DebuggerSuspendOnStart;
+
// Dirty Hacks
config.Hacks.Xc2MenuSoftlockFix.Value = DirtyHacks.Xc2MenuSoftlockFix;
config.Hacks.DisableNifmIsAnyInternetRequestAccepted.Value =
diff --git a/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml
new file mode 100644
index 000000000..036471059
--- /dev/null
+++ b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml.cs
new file mode 100644
index 000000000..14a65b8b2
--- /dev/null
+++ b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml.cs
@@ -0,0 +1,13 @@
+using Avalonia.Controls;
+
+namespace Ryujinx.Ava.UI.Views.Settings
+{
+ public partial class SettingsDebugView : UserControl
+ {
+ public SettingsDebugView()
+ {
+ InitializeComponent();
+ }
+ }
+}
+
diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml b/src/Ryujinx/UI/Windows/SettingsWindow.axaml
index 15d174123..9bfe0a9db 100644
--- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml
+++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml
@@ -46,6 +46,7 @@
+
+
Date: Sat, 21 Jun 2025 00:02:31 +0800
Subject: [PATCH 02/28] gdb: Remove redundant log
---
src/Ryujinx.HLE/Debugger/Debugger.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index 33c380175..3eebb0b38 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -238,7 +238,6 @@ namespace Ryujinx.HLE.Debugger
private void ProcessCommand(string cmd)
{
- Logger.Debug?.Print(LogClass.GdbStub, $"Receive: {cmd}");
StringStream ss = new StringStream(cmd);
switch (ss.ReadChar())
From 8682c51ef794f0b2643874dd88ff9d1749566bc9 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 01:04:44 +0800
Subject: [PATCH 03/28] gdb: Fix crash on exit when not using Debugger
---
src/Ryujinx.HLE/Switch.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs
index ed6d57b2d..e1aa8e0e4 100644
--- a/src/Ryujinx.HLE/Switch.cs
+++ b/src/Ryujinx.HLE/Switch.cs
@@ -176,7 +176,7 @@ namespace Ryujinx.HLE
AudioDeviceDriver.Dispose();
FileSystem.Dispose();
Memory.Dispose();
- Debugger.Dispose();
+ Debugger?.Dispose();
TitleIDs.CurrentApplication.Value = null;
Shared = null;
From 737afbfa2fce2b0440c7bedaab89019b8ffab8b7 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 01:05:25 +0800
Subject: [PATCH 04/28] gdb: Wait for the application to start if user connect
gdb too early
---
src/Ryujinx.HLE/Debugger/Debugger.cs | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index 3eebb0b38..3ec4dbb93 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -47,7 +47,7 @@ namespace Ryujinx.HLE.Debugger
MessageHandlerThread.Start();
}
- private IDebuggableProcess DebugProcess => Device.System.DebugGetApplicationProcess();
+ 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;
@@ -761,6 +761,20 @@ namespace Ryujinx.HLE.Debugger
{
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);
From d81dca0dcc65349aac2e9dc8063f6337b29d8f59 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 01:05:43 +0800
Subject: [PATCH 05/28] gdb: Add notice when application is suspended on start
---
src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
index 0c400b425..249e9720a 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
@@ -689,6 +689,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
{
mainThread.Suspend(ThreadSchedState.ThreadPauseFlag);
debugState = (int)DebugState.Stopped;
+ Logger.Notice.Print(LogClass.Kernel, $"Application is suspended on start for debugging.");
}
result = mainThread.Start();
From 785641a40262d1d14ce8fe6574231cf7f30c5b7f Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 01:24:06 +0800
Subject: [PATCH 06/28] gdb: Remove unused using
---
src/ARMeilleure/Instructions/NativeInterface.cs | 1 -
src/ARMeilleure/State/ExecutionContext.cs | 2 --
src/Ryujinx.Cpu/Jit/JitExecutionContext.cs | 1 -
3 files changed, 4 deletions(-)
diff --git a/src/ARMeilleure/Instructions/NativeInterface.cs b/src/ARMeilleure/Instructions/NativeInterface.cs
index c44f24b7c..d43e20d83 100644
--- a/src/ARMeilleure/Instructions/NativeInterface.cs
+++ b/src/ARMeilleure/Instructions/NativeInterface.cs
@@ -3,7 +3,6 @@ using ARMeilleure.State;
using ARMeilleure.Translation;
using System;
using System.Runtime.InteropServices;
-using System.Threading;
using ExecutionContext = ARMeilleure.State.ExecutionContext;
namespace ARMeilleure.Instructions
diff --git a/src/ARMeilleure/State/ExecutionContext.cs b/src/ARMeilleure/State/ExecutionContext.cs
index 2978bf27a..0b0a83c9f 100644
--- a/src/ARMeilleure/State/ExecutionContext.cs
+++ b/src/ARMeilleure/State/ExecutionContext.cs
@@ -1,6 +1,4 @@
using ARMeilleure.Memory;
-using System.Collections.Concurrent;
-using System.Diagnostics;
using System.Threading;
namespace ARMeilleure.State
diff --git a/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs b/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs
index d4775f3ed..f00acc1d7 100644
--- a/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs
+++ b/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs
@@ -1,6 +1,5 @@
using ARMeilleure.Memory;
using ARMeilleure.State;
-using System.Threading;
using ExecutionContext = ARMeilleure.State.ExecutionContext;
namespace Ryujinx.Cpu.Jit
From 36bb910e67e5cb5826a312b5771fe2fd3d1d9469 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 01:47:19 +0800
Subject: [PATCH 07/28] gdb: Fix crash on stop emulation if gdb stub is enabled
with app running
---
src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
index 249e9720a..7edf263e2 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
@@ -743,7 +743,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
ExceptionCallback breakCallback = null;
ExceptionCallbackNoArgs stepCallback = null;
- if (KernelContext.Device.Configuration.EnableGdbStub)
+ if (KernelContext.Device.Configuration.EnableGdbStub && KernelContext.Device.Debugger != null)
{
breakCallback = KernelContext.Device.Debugger.BreakHandler;
stepCallback = KernelContext.Device.Debugger.StepHandler;
From e547c4f5f4b92b1a3e6af3b80adeb37582f78eef Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 05:25:22 +0800
Subject: [PATCH 08/28] gdb: Fix GdbWriteRegister endianness
---
src/Ryujinx.HLE/Debugger/Debugger.cs | 28 ++++++++++++++--------------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index 3ec4dbb93..4350aab53 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -86,38 +86,38 @@ namespace Ryujinx.HLE.Debugger
{
case >= 0 and <= 31:
{
- ulong value = ss.ReadLengthAsHex(16);
+ ulong value = ss.ReadLengthAsLEHex(16);
state.SetX(gdbRegId, value);
return true;
}
case 32:
{
- ulong value = ss.ReadLengthAsHex(16);
+ ulong value = ss.ReadLengthAsLEHex(16);
state.DebugPc = value;
return true;
}
case 33:
{
- ulong value = ss.ReadLengthAsHex(8);
+ ulong value = ss.ReadLengthAsLEHex(8);
state.Pstate = (uint)value;
return true;
}
case >= 34 and <= 65:
{
- ulong value0 = ss.ReadLengthAsHex(16);
- ulong value1 = ss.ReadLengthAsHex(16);
+ 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.ReadLengthAsHex(8);
+ ulong value = ss.ReadLengthAsLEHex(8);
state.Fpsr = (uint)value;
return true;
}
case 67:
{
- ulong value = ss.ReadLengthAsHex(8);
+ ulong value = ss.ReadLengthAsLEHex(8);
state.Fpcr = (uint)value;
return true;
}
@@ -158,32 +158,32 @@ namespace Ryujinx.HLE.Debugger
{
case >= 0 and <= 14:
{
- ulong value = ss.ReadLengthAsHex(8);
+ ulong value = ss.ReadLengthAsLEHex(8);
state.SetX(gdbRegId, value);
return true;
}
case 15:
{
- ulong value = ss.ReadLengthAsHex(8);
+ ulong value = ss.ReadLengthAsLEHex(8);
state.DebugPc = value;
return true;
}
case 16:
{
- ulong value = ss.ReadLengthAsHex(8);
+ ulong value = ss.ReadLengthAsLEHex(8);
state.Pstate = (uint)value;
return true;
}
case >= 17 and <= 32:
{
- ulong value0 = ss.ReadLengthAsHex(16);
- ulong value1 = ss.ReadLengthAsHex(16);
+ 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.ReadLengthAsHex(16);
+ ulong value = ss.ReadLengthAsLEHex(16);
int regId = (gdbRegId - 33);
int regNum = regId / 2;
int shift = regId % 2;
@@ -193,7 +193,7 @@ namespace Ryujinx.HLE.Debugger
}
case 65:
{
- ulong value = ss.ReadLengthAsHex(8);
+ ulong value = ss.ReadLengthAsLEHex(8);
state.Fpsr = (uint)value & FpcrMask;
state.Fpcr = (uint)value & ~FpcrMask;
return true;
From bc68502179111a000c062a356422027583148127 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 05:26:48 +0800
Subject: [PATCH 09/28] gdb: Add timeout to prevent deadlock in DebugStep
Deadlock can happen when step at some svc instructions.
---
src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
index 7edf263e2..c24d5c3cc 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
@@ -1283,7 +1283,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
}
_kernelContext.CriticalSection.Leave();
- StepBarrier.SignalAndWait();
+ 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;
@@ -1302,6 +1307,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
target.Suspend(ThreadSchedState.ThreadPauseFlag);
}
_kernelContext.CriticalSection.Leave();
+
+ if (stepTimedOut)
+ {
+ return false;
+ }
+
StepBarrier.SignalAndWait();
return true;
}
From f630d5ba99e312435fa4868d0066679641f52d08 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 05:27:39 +0800
Subject: [PATCH 10/28] gdb: Fix crash when gdb client disconnected in some
cases
---
src/Ryujinx.HLE/Debugger/Debugger.cs | 56 ++++++++++++++++------------
1 file changed, 33 insertions(+), 23 deletions(-)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index 4350aab53..608ab977a 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -208,30 +208,40 @@ namespace Ryujinx.HLE.Debugger
while (!_shuttingDown)
{
IMessage msg = Messages.Take();
- switch (msg)
+ 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)
{
- 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;
+ 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);
}
}
}
From fb1655f1ad33ee610fdf60e52aacb7a132b1e18b Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 09:37:36 +0800
Subject: [PATCH 11/28] gdb: Update DebugPc during SVC call and break
---
src/ARMeilleure/State/ExecutionContext.cs | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/src/ARMeilleure/State/ExecutionContext.cs b/src/ARMeilleure/State/ExecutionContext.cs
index 0b0a83c9f..c44c6e062 100644
--- a/src/ARMeilleure/State/ExecutionContext.cs
+++ b/src/ARMeilleure/State/ExecutionContext.cs
@@ -164,11 +164,21 @@ namespace ARMeilleure.State
internal void OnBreak(ulong address, int imm)
{
+ if (Optimizations.EnableDebugging)
+ {
+ DebugPc = Pc; // TODO: Is this the best place to update DebugPc?
+ }
+
_breakCallback?.Invoke(this, address, 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);
}
From 5cad23f793403838bcada8e4c192fec2aee4378b Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 09:43:26 +0800
Subject: [PATCH 12/28] gdb: Set correct gThread and cThread when break
---
src/Ryujinx.HLE/Debugger/Debugger.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index 608ab977a..dd6394e60 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -916,6 +916,7 @@ namespace Ryujinx.HLE.Debugger
public void BreakHandler(IExecutionContext ctx, ulong address, int imm)
{
+ gThread = cThread = ctx.ThreadUid;
Logger.Notice.Print(LogClass.GdbStub, $"Break hit on thread {ctx.ThreadUid} at pc {address:x016}");
Messages.Add(new ThreadBreakMessage(ctx, address, imm));
From 9506eba98251a029e0e8f147c5dc41983be2734c Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 10:22:08 +0800
Subject: [PATCH 13/28] gdb: Show thread names
Reference: https://github.com/Atmosphere-NX/Atmosphere/blob/d8a37b4b7184b80ba979bcceb98365b8365a1c3a/libraries/libstratosphere/source/osdbg/impl/osdbg_thread_type.os.horizon.hpp
---
src/Ryujinx.HLE/Debugger/Debugger.cs | 62 ++++++++++--
.../HOS/Kernel/Threading/KThread.cs | 95 +++++++++++++++++++
2 files changed, 151 insertions(+), 6 deletions(-)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index dd6394e60..9b9850e58 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -34,6 +34,8 @@ namespace Ryujinx.HLE.Debugger
private ulong? cThread;
private ulong? gThread;
+ private string previousThreadListXml = "";
+
public Debugger(Switch device, ushort port)
{
Device = device;
@@ -368,7 +370,7 @@ namespace Ryujinx.HLE.Debugger
if (ss.ConsumePrefix("Supported:") || ss.ConsumeRemaining("Supported"))
{
- Reply("PacketSize=10000;qXfer:features:read+");
+ Reply("PacketSize=10000;qXfer:features:read+;qXfer:threads:read+");
break;
}
@@ -404,10 +406,43 @@ namespace Ryujinx.HLE.Debugger
break;
}
+ if (ss.ConsumePrefix("Xfer:threads:read:"))
+ {
+ ss.ReadUntil(':');
+ ulong offset = ss.ReadUntilAsHex(',');
+ ulong len = ss.ReadRemainingAsHex();
+
+ var data = "";
+ if (offset > 0)
+ {
+ data = previousThreadListXml;
+ } else
+ {
+ previousThreadListXml = data = GetThreadListXml();
+ }
+
+ if (offset >= (ulong)data.Length)
+ {
+ Reply("l");
+ break;
+ }
+
+ if (len >= (ulong)data.Length - offset)
+ {
+ Reply("l" + ToBinaryFormat(data.Substring((int)offset)));
+ break;
+ }
+ else
+ {
+ Reply("m" + ToBinaryFormat(data.Substring((int)offset, (int)len)));
+ break;
+ }
+ }
+
if (ss.ConsumePrefix("Xfer:features:read:"))
{
string feature = ss.ReadUntil(':');
- ulong addr = ss.ReadUntilAsHex(',');
+ ulong offset = ss.ReadUntilAsHex(',');
ulong len = ss.ReadRemainingAsHex();
if (feature == "target.xml")
@@ -418,20 +453,20 @@ namespace Ryujinx.HLE.Debugger
string data;
if (RegisterInformation.Features.TryGetValue(feature, out data))
{
- if (addr >= (ulong)data.Length)
+ if (offset >= (ulong)data.Length)
{
Reply("l");
break;
}
- if (len >= (ulong)data.Length - addr)
+ if (len >= (ulong)data.Length - offset)
{
- Reply("l" + ToBinaryFormat(data.Substring((int)addr)));
+ Reply("l" + ToBinaryFormat(data.Substring((int)offset)));
break;
}
else
{
- Reply("m" + ToBinaryFormat(data.Substring((int)addr, (int)len)));
+ Reply("m" + ToBinaryFormat(data.Substring((int)offset, (int)len)));
break;
}
}
@@ -469,6 +504,21 @@ namespace Ryujinx.HLE.Debugger
}
}
+ private string GetThreadListXml()
+ {
+ var sb = new StringBuilder();
+ sb.Append("\n");
+
+ foreach (var thread in GetThreads())
+ {
+ string threadName = System.Security.SecurityElement.Escape(thread.GetThreadName());
+ sb.Append($"\n");
+ }
+
+ sb.Append("");
+ return sb.ToString();
+ }
+
void CommandQuery()
{
// GDB is performing initial contact. Stop everything.
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
index 2b5d11244..bb0548d19 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
@@ -5,9 +5,11 @@ using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.HOS.Kernel.SupervisorCall;
using Ryujinx.Horizon.Common;
+using Ryujinx.Memory;
using System;
using System.Collections.Generic;
using System.Numerics;
+using System.Text;
using System.Threading;
namespace Ryujinx.HLE.HOS.Kernel.Threading
@@ -17,6 +19,23 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
private const int TlsUserDisableCountOffset = 0x100;
private const int TlsUserInterruptFlagOffset = 0x102;
+ // Tls -> ThreadType
+ private const int TlsThreadTypeOffsetAArch64 = 0x1F8;
+ private const int TlsThreadTypeOffsetAArch32 = 0x1FC;
+
+ // Tls -> ThreadType -> Version
+ private const int TlsThreadTypeVersionOffsetAArch64 = 0x46;
+ private const int TlsThreadTypeVersionOffsetAArch32 = 0x26;
+
+ // Tls -> ThreadType (Version 0) -> ThreadNamePointer
+ private const int TlsThreadTypeVersion0ThreadNamePointerOffsetAArch64 = 0x1A8;
+ private const int TlsThreadTypeVersion0ThreadNamePointerOffsetAArch32 = 0xE8;
+
+ // Tls -> ThreadType (Version 1) -> ThreadNamePointer
+ private const int TlsThreadTypeThreadNamePointerOffsetAArch64 = 0x1A0;
+ private const int TlsThreadTypeThreadNamePointerOffsetAArch32 = 0xE4;
+
+
public const int MaxWaitSyncObjects = 64;
private ManualResetEvent _schedulerWaitEvent;
@@ -1439,5 +1458,81 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
{
Owner.CpuMemory.Write(_tlsAddress + TlsUserInterruptFlagOffset, 0);
}
+
+ public string GetThreadName()
+ {
+ try
+ {
+ ulong threadNamePtr = 0;
+ if (Context.IsAarch32)
+ {
+ uint threadTypePtr32 = Owner.CpuMemory.Read(_tlsAddress + TlsThreadTypeOffsetAArch32);
+ if (threadTypePtr32 == 0)
+ {
+ return "";
+ }
+
+ ushort version = Owner.CpuMemory.Read(threadTypePtr32 + TlsThreadTypeVersionOffsetAArch32);
+ switch (version)
+ {
+ case 0x0000:
+ case 0xFFFF:
+ threadNamePtr = Owner.CpuMemory.Read(threadTypePtr32 + TlsThreadTypeVersion0ThreadNamePointerOffsetAArch32);
+ break;
+ case 0x0001:
+ threadNamePtr = Owner.CpuMemory.Read(threadTypePtr32 + TlsThreadTypeThreadNamePointerOffsetAArch32);
+ break;
+ default:
+ Logger.Warning?.Print(LogClass.Kernel, $"Unknown ThreadType struct version: {version}");
+ break;
+ }
+ }
+ else
+ {
+ ulong threadTypePtr64 = Owner.CpuMemory.Read(_tlsAddress + TlsThreadTypeOffsetAArch64);
+ if (threadTypePtr64 == 0)
+ {
+ return "";
+ }
+
+ ushort version = Owner.CpuMemory.Read(threadTypePtr64 + TlsThreadTypeVersionOffsetAArch64);
+ switch (version)
+ {
+ case 0x0000:
+ case 0xFFFF:
+ threadNamePtr = Owner.CpuMemory.Read(threadTypePtr64 + TlsThreadTypeVersion0ThreadNamePointerOffsetAArch64);
+ break;
+ case 0x0001:
+ threadNamePtr = Owner.CpuMemory.Read(threadTypePtr64 + TlsThreadTypeThreadNamePointerOffsetAArch64);
+ break;
+ default:
+ Logger.Warning?.Print(LogClass.Kernel, $"Unknown ThreadType struct version: {version}");
+ break;
+ }
+ }
+
+ if (threadNamePtr == 0)
+ {
+ return "";
+ }
+
+ List nameBytes = new();
+ for (int i = 0; i < 0x20; i++)
+ {
+ byte b = Owner.CpuMemory.Read(threadNamePtr + (ulong)i);
+ if (b == 0)
+ {
+ break;
+ }
+ nameBytes.Add(b);
+ }
+ return Encoding.UTF8.GetString(nameBytes.ToArray());
+
+ } catch (InvalidMemoryRegionException)
+ {
+ Logger.Warning?.Print(LogClass.Kernel, "Failed to get thread name.");
+ return "";
+ }
+ }
}
}
From fe02ff3a3abc61d2e6ad9aa9c37b2f6bed7938cc Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 14:31:00 +0800
Subject: [PATCH 14/28] gdb: Fix ExecutionContext
---
src/ARMeilleure/State/ExecutionContext.cs | 6 +++---
.../LightningJit/State/ExecutionContext.cs | 13 ++++++++++++-
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/src/ARMeilleure/State/ExecutionContext.cs b/src/ARMeilleure/State/ExecutionContext.cs
index c44c6e062..fa1a4a032 100644
--- a/src/ARMeilleure/State/ExecutionContext.cs
+++ b/src/ARMeilleure/State/ExecutionContext.cs
@@ -153,7 +153,7 @@ namespace ARMeilleure.State
public void StepHandler()
{
- _stepCallback.Invoke(this);
+ _stepCallback?.Invoke(this);
}
public void RequestDebugStep()
@@ -166,7 +166,7 @@ namespace ARMeilleure.State
{
if (Optimizations.EnableDebugging)
{
- DebugPc = Pc; // TODO: Is this the best place to update DebugPc?
+ DebugPc = Pc;
}
_breakCallback?.Invoke(this, address, imm);
@@ -176,7 +176,7 @@ namespace ARMeilleure.State
{
if (Optimizations.EnableDebugging)
{
- DebugPc = Pc; // TODO: Is this the best place to update DebugPc?
+ DebugPc = Pc;
}
_supervisorCallback?.Invoke(this, address, imm);
diff --git a/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs b/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs
index fc75e5185..cb3c6c2af 100644
--- a/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs
+++ b/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs
@@ -1,3 +1,4 @@
+using ARMeilleure;
using ARMeilleure.Memory;
using ARMeilleure.State;
using System;
@@ -127,7 +128,7 @@ namespace Ryujinx.Cpu.LightningJit.State
public void StepHandler()
{
- _stepCallback.Invoke(this);
+ _stepCallback?.Invoke(this);
}
public void RequestDebugStep()
@@ -138,11 +139,21 @@ namespace Ryujinx.Cpu.LightningJit.State
internal void OnBreak(ulong address, int imm)
{
+ if (Optimizations.EnableDebugging)
+ {
+ DebugPc = Pc;
+ }
+
_breakCallback?.Invoke(this, address, imm);
}
internal void OnSupervisorCall(ulong address, int imm)
{
+ if (Optimizations.EnableDebugging)
+ {
+ DebugPc = Pc;
+ }
+
_supervisorCallback?.Invoke(this, address, imm);
}
From 44f4e9af51c1180154369e5950816b21effb214c Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sat, 21 Jun 2025 15:38:32 +0800
Subject: [PATCH 15/28] gdb: Do not use LightningJitEngine when GDB Stub is
enabled
---
src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs b/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs
index 759780c42..28f7ef25f 100644
--- a/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs
+++ b/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs
@@ -69,7 +69,7 @@ namespace Ryujinx.HLE.HOS
mode = MemoryManagerMode.SoftwarePageTable;
}
- ICpuEngine cpuEngine = isArm64Host && (mode == MemoryManagerMode.HostMapped || mode == MemoryManagerMode.HostMappedUnsafe)
+ ICpuEngine cpuEngine = isArm64Host && (mode == MemoryManagerMode.HostMapped || mode == MemoryManagerMode.HostMappedUnsafe) && !context.Device.Configuration.EnableGdbStub
? new LightningJitEngine(_tickSource)
: new JitEngine(_tickSource);
From 7d5f7bc47968095a9b14b7264f36dc64c5cfa89b Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sun, 22 Jun 2025 02:31:04 +0800
Subject: [PATCH 16/28] gdb: Implement vCont to support step on AArch32
---
src/Ryujinx.HLE/Debugger/Debugger.cs | 125 ++++++++++++++++++++++++++-
1 file changed, 123 insertions(+), 2 deletions(-)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index 9b9850e58..d1b41c7f7 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -6,6 +6,7 @@ using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.Memory;
using System;
using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
@@ -230,6 +231,7 @@ namespace Ryujinx.HLE.Debugger
case ThreadBreakMessage { Context: var ctx }:
DebugProcess.DebugStop();
+ gThread = cThread = ctx.ThreadUid;
Reply($"T05thread:{ctx.ThreadUid:x};");
break;
@@ -370,7 +372,7 @@ namespace Ryujinx.HLE.Debugger
if (ss.ConsumePrefix("Supported:") || ss.ConsumeRemaining("Supported"))
{
- Reply("PacketSize=10000;qXfer:features:read+;qXfer:threads:read+");
+ Reply("PacketSize=10000;qXfer:features:read+;qXfer:threads:read+;vContSupported+");
break;
}
@@ -490,6 +492,22 @@ namespace Ryujinx.HLE.Debugger
break;
}
case 'v':
+ if (ss.ConsumePrefix("Cont"))
+ {
+ if (ss.ConsumeRemaining("?"))
+ {
+ Reply("vCont;c;C;s;S");
+ break;
+ }
+
+ if (ss.ConsumePrefix(";"))
+ {
+ HandleVContCommand(ss);
+ break;
+ }
+
+ goto unknownCommand;
+ }
if (ss.ConsumeRemaining("MustReplyEmpty"))
{
Reply("");
@@ -504,6 +522,109 @@ namespace Ryujinx.HLE.Debugger
}
}
+ enum VContAction
+ {
+ None,
+ Continue,
+ Stop,
+ Step
+ }
+
+ record VContPendingAction(VContAction Action, ushort? Signal = null);
+
+ private void HandleVContCommand(StringStream ss)
+ {
+ string[] rawActions = ss.ReadRemaining().Split(';', StringSplitOptions.RemoveEmptyEntries);
+
+ var threadActionMap = new Dictionary();
+ foreach (var thread in GetThreads())
+ {
+ threadActionMap[thread.ThreadUid] = new VContPendingAction(VContAction.None);
+ }
+
+ // For each inferior thread, the *leftmost* action with a matching thread-id is applied.
+ for (int i = rawActions.Length - 1; i >= 0; i--)
+ {
+ var rawAction = rawActions[i];
+ var stream = new StringStream(rawAction);
+
+ char cmd = stream.ReadChar();
+ VContAction action = cmd switch
+ {
+ 'c' => VContAction.Continue,
+ 'C' => VContAction.Continue,
+ 's' => VContAction.Step,
+ 'S' => VContAction.Step,
+ 't' => VContAction.Stop,
+ _ => VContAction.None
+ };
+
+ ushort? signal = null;
+ if (cmd == 'C' || cmd == 'S')
+ {
+ signal = (ushort)stream.ReadLengthAsHex(2);
+ }
+
+ ulong? threadId = null;
+ if (stream.ConsumePrefix(":"))
+ {
+ threadId = stream.ReadRemainingAsThreadUid();
+ }
+
+ if (threadId.HasValue)
+ {
+ if (threadActionMap.ContainsKey(threadId.Value)) {
+ threadActionMap[threadId.Value] = new VContPendingAction(action, signal);
+ }
+ }
+ else
+ {
+ foreach (var row in threadActionMap.ToList())
+ {
+ threadActionMap[row.Key] = new VContPendingAction(action, signal);
+ }
+ }
+ }
+
+ bool hasError = false;
+
+ // TODO: We don't support stop or continue yet, and we don't support signals.
+ foreach (var (threadUid, action) in threadActionMap)
+ {
+ if (action.Action == VContAction.Step)
+ {
+ var thread = DebugProcess.GetThread(threadUid);
+ if (!DebugProcess.DebugStep(thread)) {
+ hasError = true;
+ }
+ }
+ }
+
+ // If all threads are set to continue, continue the process.
+ if (threadActionMap.Values.All(a => a.Action == VContAction.Continue))
+ {
+ DebugProcess.DebugContinue();
+ }
+
+ if (hasError)
+ {
+ ReplyError();
+ }
+ else
+ {
+ ReplyOK();
+ }
+
+ foreach (var (threadUid, action) in threadActionMap)
+ {
+ if (action.Action == VContAction.Step)
+ {
+ gThread = cThread = threadUid;
+ Reply($"T05thread:{threadUid:x};");
+ }
+ }
+ }
+
private string GetThreadListXml()
{
var sb = new StringBuilder();
@@ -772,6 +893,7 @@ namespace Ryujinx.HLE.Debugger
}
else
{
+ gThread = cThread = thread.ThreadUid;
Reply($"T05thread:{thread.ThreadUid:x};");
}
}
@@ -966,7 +1088,6 @@ namespace Ryujinx.HLE.Debugger
public void BreakHandler(IExecutionContext ctx, ulong address, int imm)
{
- gThread = cThread = ctx.ThreadUid;
Logger.Notice.Print(LogClass.GdbStub, $"Break hit on thread {ctx.ThreadUid} at pc {address:x016}");
Messages.Add(new ThreadBreakMessage(ctx, address, imm));
From bad1dd88993edd667cad30881bf3c2b74bbf3ba2 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sun, 22 Jun 2025 05:28:31 +0800
Subject: [PATCH 17/28] gdb: Implement z0/Z0 software breakpoints
---
src/Ryujinx.HLE/Debugger/BreakpointManager.cs | 203 ++++++++++++++++++
src/Ryujinx.HLE/Debugger/Debugger.cs | 80 ++++++-
2 files changed, 280 insertions(+), 3 deletions(-)
create mode 100644 src/Ryujinx.HLE/Debugger/BreakpointManager.cs
diff --git a/src/Ryujinx.HLE/Debugger/BreakpointManager.cs b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs
new file mode 100644
index 000000000..bf462a781
--- /dev/null
+++ b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs
@@ -0,0 +1,203 @@
+using Ryujinx.Common;
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Kernel.Threading;
+using Ryujinx.Memory;
+using System.Collections.Concurrent;
+using System.Linq;
+
+namespace Ryujinx.HLE.Debugger
+{
+ internal class Breakpoint
+ {
+ public byte[] OriginalData { get; }
+
+ public bool IsStep { get; }
+
+ public Breakpoint(byte[] originalData, bool isStep)
+ {
+ OriginalData = originalData;
+ IsStep = isStep;
+ }
+ }
+
+ ///
+ /// Manages software breakpoints for the debugger.
+ ///
+ public class BreakpointManager
+ {
+ private readonly Debugger _debugger;
+ private readonly ConcurrentDictionary _breakpoints = new();
+
+ private static readonly byte[] _aarch64BreakInstruction = { 0x00, 0x00, 0x20, 0xD4 }; // BRK #0
+ private static readonly byte[] _aarch32BreakInstruction = { 0xFE, 0xDE, 0xFF, 0xE7 }; // TRAP
+ private static readonly byte[] _aarch32ThumbBreakInstruction = { 0x80, 0xB6 };
+
+ public BreakpointManager(Debugger debugger)
+ {
+ _debugger = debugger;
+ }
+
+ ///
+ /// Sets a software breakpoint at a specified address.
+ ///
+ /// The memory address to set the breakpoint at.
+ /// The length of the instruction to replace.
+ /// Indicates if this is a single-step breakpoint.
+ /// True if the breakpoint was set successfully; otherwise, false.
+ public bool SetBreakPoint(ulong address, ulong length, bool isStep = false)
+ {
+ if (_breakpoints.ContainsKey(address))
+ {
+ return false;
+ }
+
+ byte[] breakInstruction = GetBreakInstruction(length);
+ if (breakInstruction == null)
+ {
+ Logger.Error?.Print(LogClass.GdbStub, $"Unsupported instruction length for breakpoint: {length}");
+ return false;
+ }
+
+ var originalInstruction = new byte[length];
+ if (!ReadMemory(address, originalInstruction))
+ {
+ Logger.Error?.Print(LogClass.GdbStub, $"Failed to read memory at 0x{address:X16} to set breakpoint.");
+ return false;
+ }
+
+ if (!WriteMemory(address, breakInstruction))
+ {
+ Logger.Error?.Print(LogClass.GdbStub, $"Failed to write breakpoint at 0x{address:X16}.");
+ return false;
+ }
+
+ var breakpoint = new Breakpoint(originalInstruction, isStep);
+ if (_breakpoints.TryAdd(address, breakpoint))
+ {
+ Logger.Debug?.Print(LogClass.GdbStub, $"Breakpoint set at 0x{address:X16}");
+ return true;
+ }
+
+ Logger.Error?.Print(LogClass.GdbStub, $"Failed to add breakpoint at 0x{address:X16}.");
+ return false;
+ }
+
+ ///
+ /// Clears a software breakpoint at a specified address.
+ ///
+ /// The memory address of the breakpoint to clear.
+ /// The length of the instruction (unused).
+ /// True if the breakpoint was cleared successfully; otherwise, false.
+ public bool ClearBreakPoint(ulong address, ulong length)
+ {
+ if (_breakpoints.TryGetValue(address, out Breakpoint breakpoint))
+ {
+ if (!WriteMemory(address, breakpoint.OriginalData))
+ {
+ Logger.Error?.Print(LogClass.GdbStub, $"Failed to restore original instruction at 0x{address:X16} to clear breakpoint.");
+ return false;
+ }
+
+ _breakpoints.TryRemove(address, out _);
+ Logger.Debug?.Print(LogClass.GdbStub, $"Breakpoint cleared at 0x{address:X16}");
+ return true;
+ }
+
+ Logger.Warning?.Print(LogClass.GdbStub, $"No breakpoint found at address 0x{address:X16}");
+ return false;
+ }
+
+ ///
+ /// Clears all currently set software breakpoints.
+ ///
+ public void ClearAll()
+ {
+ foreach (var bp in _breakpoints)
+ {
+ if (!WriteMemory(bp.Key, bp.Value.OriginalData))
+ {
+ Logger.Error?.Print(LogClass.GdbStub, $"Failed to restore original instruction at 0x{bp.Key:X16} while clearing all breakpoints.");
+ }
+
+ }
+ _breakpoints.Clear();
+ Logger.Debug?.Print(LogClass.GdbStub, "All breakpoints cleared.");
+ }
+
+ ///
+ /// Clears all currently set single-step software breakpoints.
+ ///
+ public void ClearAllStepBreakpoints()
+ {
+ var stepBreakpoints = _breakpoints.Where(p => p.Value.IsStep).ToList();
+
+ if (stepBreakpoints.Count == 0)
+ {
+ return;
+ }
+
+ foreach (var bp in stepBreakpoints)
+ {
+ if (_breakpoints.TryRemove(bp.Key, out Breakpoint removedBreakpoint))
+ {
+ WriteMemory(bp.Key, removedBreakpoint.OriginalData);
+ }
+ }
+
+ Logger.Debug?.Print(LogClass.GdbStub, "All step breakpoints cleared.");
+ }
+
+
+ private byte[] GetBreakInstruction(ulong length)
+ {
+ if (_debugger.IsProcessAarch32)
+ {
+ if (length == 2)
+ {
+ return _aarch32ThumbBreakInstruction;
+ }
+
+ if (length == 4)
+ {
+ return _aarch32BreakInstruction;
+ }
+ }
+ else
+ {
+ if (length == 4)
+ {
+ return _aarch64BreakInstruction;
+ }
+ }
+
+ return null;
+ }
+
+ private bool ReadMemory(ulong address, byte[] data)
+ {
+ try
+ {
+ _debugger.DebugProcess.CpuMemory.Read(address, data);
+ return true;
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ return false;
+ }
+ }
+
+ private bool WriteMemory(ulong address, byte[] data)
+ {
+ try
+ {
+ _debugger.DebugProcess.CpuMemory.Write(address, data);
+ _debugger.DebugProcess.InvalidateCacheRegion(address, (ulong)data.Length);
+ return true;
+ }
+ catch (InvalidMemoryRegionException)
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index d1b41c7f7..3c051639d 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -35,6 +35,8 @@ namespace Ryujinx.HLE.Debugger
private ulong? cThread;
private ulong? gThread;
+ private BreakpointManager BreakpointManager;
+
private string previousThreadListXml = "";
public Debugger(Switch device, ushort port)
@@ -48,11 +50,12 @@ namespace Ryujinx.HLE.Debugger
DebuggerThread.Start();
MessageHandlerThread = new Thread(MessageHandlerMain);
MessageHandlerThread.Start();
+ BreakpointManager = new BreakpointManager(this);
}
- private IDebuggableProcess DebugProcess => Device.System?.DebugGetApplicationProcess();
+ internal 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;
+ internal bool IsProcessAarch32 => DebugProcess.GetThread(gThread.Value).Context.IsAarch32;
private KernelContext KernelContext => Device.System.KernelContext;
const int GdbRegisterCount64 = 68;
@@ -514,6 +517,75 @@ namespace Ryujinx.HLE.Debugger
break;
}
goto unknownCommand;
+ case 'Z':
+ {
+ string type = ss.ReadUntil(',');
+ ulong addr = ss.ReadUntilAsHex(',');
+ ulong len = ss.ReadLengthAsHex(1);
+ string extra = ss.ReadRemaining();
+
+ if (extra.Length > 0)
+ {
+ Logger.Notice.Print(LogClass.GdbStub, $"Unsupported Z command extra data: {extra}");
+ ReplyError();
+ return;
+ }
+
+ switch (type)
+ {
+ case "0": // Software breakpoint
+ if (!BreakpointManager.SetBreakPoint(addr, len, false))
+ {
+ ReplyError();
+ }
+ ReplyOK();
+ return;
+ case "1": // Hardware breakpoint
+ case "2": // Write watchpoint
+ case "3": // Read watchpoint
+ case "4": // Access watchpoint
+ ReplyError();
+ return;
+ default:
+ ReplyError();
+ return;
+ }
+ }
+ case 'z':
+ {
+ string type = ss.ReadUntil(',');
+ ss.ConsumePrefix(",");
+ ulong addr = ss.ReadUntilAsHex(',');
+ ulong len = ss.ReadLengthAsHex(1);
+ string extra = ss.ReadRemaining();
+
+ if (extra.Length > 0)
+ {
+ Logger.Notice.Print(LogClass.GdbStub, $"Unsupported z command extra data: {extra}");
+ ReplyError();
+ return;
+ }
+
+ switch (type)
+ {
+ case "0": // Software breakpoint
+ if (!BreakpointManager.ClearBreakPoint(addr, len))
+ {
+ ReplyError();
+ }
+ ReplyOK();
+ return;
+ case "1": // Hardware breakpoint
+ case "2": // Write watchpoint
+ case "3": // Read watchpoint
+ case "4": // Access watchpoint
+ ReplyError();
+ return;
+ default:
+ ReplyError();
+ return;
+ }
+ }
default:
unknownCommand:
Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}");
@@ -666,7 +738,7 @@ namespace Ryujinx.HLE.Debugger
void CommandDetach()
{
- // TODO: Remove all breakpoints
+ BreakpointManager.ClearAll();
CommandContinue(null);
}
@@ -1017,6 +1089,8 @@ namespace Ryujinx.HLE.Debugger
WriteStream = null;
ClientSocket.Close();
ClientSocket = null;
+
+ BreakpointManager.ClearAll();
}
}
From 7d189ab2c0fac2915c8870f613eea3f87d812a98 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Sun, 22 Jun 2025 05:28:40 +0800
Subject: [PATCH 18/28] gdb: Revert ExecutionContext for now
Pc isn't reliable either
---
src/ARMeilleure/State/ExecutionContext.cs | 10 ----------
src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs | 10 ----------
2 files changed, 20 deletions(-)
diff --git a/src/ARMeilleure/State/ExecutionContext.cs b/src/ARMeilleure/State/ExecutionContext.cs
index fa1a4a032..cdf6f56c5 100644
--- a/src/ARMeilleure/State/ExecutionContext.cs
+++ b/src/ARMeilleure/State/ExecutionContext.cs
@@ -164,21 +164,11 @@ namespace ARMeilleure.State
internal void OnBreak(ulong address, int imm)
{
- if (Optimizations.EnableDebugging)
- {
- DebugPc = Pc;
- }
-
_breakCallback?.Invoke(this, address, imm);
}
internal void OnSupervisorCall(ulong address, int imm)
{
- if (Optimizations.EnableDebugging)
- {
- DebugPc = Pc;
- }
-
_supervisorCallback?.Invoke(this, address, imm);
}
diff --git a/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs b/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs
index cb3c6c2af..a1ba0002e 100644
--- a/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs
+++ b/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs
@@ -139,21 +139,11 @@ namespace Ryujinx.Cpu.LightningJit.State
internal void OnBreak(ulong address, int imm)
{
- if (Optimizations.EnableDebugging)
- {
- DebugPc = Pc;
- }
-
_breakCallback?.Invoke(this, address, imm);
}
internal void OnSupervisorCall(ulong address, int imm)
{
- if (Optimizations.EnableDebugging)
- {
- DebugPc = Pc;
- }
-
_supervisorCallback?.Invoke(this, address, imm);
}
From 009d319bc24b5ae7b260da286d8b81f54fbc42ea Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Mon, 23 Jun 2025 06:18:34 +0800
Subject: [PATCH 19/28] gdb: Implement QRcmd (monitor) commands
monitor backtrace (mo bt)
monitor registers (mo reg)
monitor get info
---
src/Ryujinx.HLE/Debugger/Debugger.cs | 111 +++++++++++++++++-
src/Ryujinx.HLE/HOS/Horizon.cs | 10 +-
.../HOS/Kernel/Process/HleProcessDebugger.cs | 24 +++-
.../HOS/Kernel/Threading/KThread.cs | 3 +
4 files changed, 144 insertions(+), 4 deletions(-)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index 3c051639d..5bde92aed 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -2,6 +2,7 @@ using ARMeilleure.State;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Kernel;
+using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.Memory;
using System;
@@ -53,7 +54,8 @@ namespace Ryujinx.HLE.Debugger
BreakpointManager = new BreakpointManager(this);
}
- internal IDebuggableProcess DebugProcess => Device.System?.DebugGetApplicationProcess();
+ internal KProcess Process => Device.System?.DebugGetApplicationProcess();
+ internal IDebuggableProcess DebugProcess => Device.System?.DebugGetApplicationProcessDebugInterface();
private KThread[] GetThreads() => DebugProcess.GetThreadUids().Select(x => DebugProcess.GetThread(x)).ToArray();
internal bool IsProcessAarch32 => DebugProcess.GetThread(gThread.Value).Context.IsAarch32;
private KernelContext KernelContext => Device.System.KernelContext;
@@ -379,6 +381,13 @@ namespace Ryujinx.HLE.Debugger
break;
}
+ if (ss.ConsumePrefix("Rcmd,"))
+ {
+ string hexCommand = ss.ReadRemaining();
+ HandleQRcmdCommand(hexCommand);
+ break;
+ }
+
if (ss.ConsumeRemaining("fThreadInfo"))
{
Reply($"m{string.Join(",", DebugProcess.GetThreadUids().Select(x => $"{x:x}"))}");
@@ -982,6 +991,97 @@ namespace Ryujinx.HLE.Debugger
}
}
+ private void HandleQRcmdCommand(string hexCommand)
+ {
+ try
+ {
+ string command = FromHex(hexCommand);
+ Logger.Debug?.Print(LogClass.GdbStub, $"Received Rcmd: {command}");
+
+ string response = command.Trim().ToLowerInvariant() switch
+ {
+ "help" => "backtrace\nbt\nregisters\nreg\nget info\n",
+ "get info" => GetProcessInfo(),
+ "backtrace" => GetStackTrace(),
+ "bt" => GetStackTrace(),
+ "registers" => GetRegisters(),
+ "reg" => GetRegisters(),
+ _ => $"Unknown command: {command}\n"
+ };
+
+ Reply(ToHex(response));
+ }
+ catch (Exception e)
+ {
+ Logger.Error?.Print(LogClass.GdbStub, $"Error processing Rcmd: {e.Message}");
+ ReplyError();
+ }
+ }
+
+ private string GetStackTrace()
+ {
+ if (gThread == null)
+ return "No thread selected\n";
+
+ if (Process == null)
+ return "No application process found\n";
+
+ return Process.Debugger.GetGuestStackTrace(DebugProcess.GetThread(gThread.Value));
+ }
+
+ private string GetRegisters()
+ {
+ if (gThread == null)
+ return "No thread selected\n";
+
+ if (Process == null)
+ return "No application process found\n";
+
+ return Process.Debugger.GetCpuRegisterPrintout(DebugProcess.GetThread(gThread.Value));
+ }
+
+ private string GetProcessInfo()
+ {
+ try
+ {
+ if (Process == null)
+ return "No application process found\n";
+
+ KProcess kProcess = Process;
+
+ var sb = new StringBuilder();
+
+ sb.AppendLine($"Program Id: 0x{kProcess.TitleId:x16}");
+ sb.AppendLine($"Application: {(kProcess.IsApplication ? 1 : 0)}");
+ sb.AppendLine("Layout:");
+ sb.AppendLine($" Alias: 0x{kProcess.MemoryManager.AliasRegionStart:x10} - 0x{kProcess.MemoryManager.AliasRegionEnd - 1:x10}");
+ sb.AppendLine($" Heap: 0x{kProcess.MemoryManager.HeapRegionStart:x10} - 0x{kProcess.MemoryManager.HeapRegionEnd - 1:x10}");
+ sb.AppendLine($" Aslr: 0x{kProcess.MemoryManager.AslrRegionStart:x10} - 0x{kProcess.MemoryManager.AslrRegionEnd - 1:x10}");
+ sb.AppendLine($" Stack: 0x{kProcess.MemoryManager.StackRegionStart:x10} - 0x{kProcess.MemoryManager.StackRegionEnd - 1:x10}");
+
+ sb.AppendLine("Modules:");
+ var debugger = kProcess.Debugger;
+ if (debugger != null)
+ {
+ var images = debugger.GetLoadedImages();
+ for (int i = 0; i < images.Count; i++)
+ {
+ var image = images[i];
+ ulong endAddress = image.BaseAddress + image.Size - 1;
+ string name = debugger.GetGuessedNsoNameFromIndex(i);
+ sb.AppendLine($" 0x{image.BaseAddress:x10} - 0x{endAddress:x10} {name}");
+ }
+ }
+
+ return sb.ToString();
+ }
+ catch (Exception e)
+ {
+ Logger.Error?.Print(LogClass.GdbStub, $"Error getting process info: {e.Message}");
+ return $"Error getting process info: {e.Message}\n";
+ }
+ }
+
private void Reply(string cmd)
{
Logger.Debug?.Print(LogClass.GdbStub, $"Reply: {cmd}");
@@ -1108,6 +1208,15 @@ namespace Ryujinx.HLE.Debugger
return checksum;
}
+ private string FromHex(string hexString)
+ {
+ if (string.IsNullOrEmpty(hexString))
+ return string.Empty;
+
+ byte[] bytes = Convert.FromHexString(hexString);
+ return Encoding.ASCII.GetString(bytes);
+ }
+
private string ToHex(byte[] bytes)
{
return string.Join("", bytes.Select(x => $"{x:x2}"));
diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs
index 44adb9674..517f8ef16 100644
--- a/src/Ryujinx.HLE/HOS/Horizon.cs
+++ b/src/Ryujinx.HLE/HOS/Horizon.cs
@@ -502,12 +502,20 @@ namespace Ryujinx.HLE.HOS
IsPaused = pause;
}
- internal IDebuggableProcess DebugGetApplicationProcess()
+ internal IDebuggableProcess DebugGetApplicationProcessDebugInterface()
{
lock (KernelContext.Processes)
{
return KernelContext.Processes.Values.FirstOrDefault(x => x.IsApplication)?.DebugInterface;
}
}
+
+ internal KProcess DebugGetApplicationProcess()
+ {
+ lock (KernelContext.Processes)
+ {
+ return KernelContext.Processes.Values.FirstOrDefault(x => x.IsApplication);
+ }
+ }
}
}
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs
index c4a9835cc..87da9f7a6 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs
@@ -4,6 +4,7 @@ using Ryujinx.HLE.HOS.Kernel.Memory;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.Loaders.Elf;
using Ryujinx.Memory;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -17,7 +18,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
private readonly KProcess _owner;
- private class Image
+ public class Image
{
public ulong BaseAddress { get; }
public ulong Size { get; }
@@ -54,6 +55,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
trace.AppendLine($"Process: {_owner.Name}, PID: {_owner.Pid}");
+ string ThreadName = thread.GetThreadName();
+
+ if (!String.IsNullOrEmpty(ThreadName))
+ {
+ trace.AppendLine($"Thread ID: {thread.ThreadUid} ({ThreadName})");
+ } else {
+ trace.AppendLine($"Thread ID: {thread.ThreadUid}");
+ }
+
void AppendTrace(ulong address)
{
if (AnalyzePointer(out PointerInfo info, address, thread))
@@ -283,7 +293,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
return null;
}
- private string GetGuessedNsoNameFromIndex(int index)
+ public string GetGuessedNsoNameFromIndex(int index)
{
if ((uint)index > 11)
{
@@ -316,6 +326,16 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
}
}
+ public List GetLoadedImages()
+ {
+ EnsureLoaded();
+
+ lock (_images)
+ {
+ return [.. _images];
+ }
+ }
+
private void EnsureLoaded()
{
if (Interlocked.CompareExchange(ref _loaded, 1, 0) == 0)
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
index bb0548d19..20fb426ba 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
@@ -1532,6 +1532,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
{
Logger.Warning?.Print(LogClass.Kernel, "Failed to get thread name.");
return "";
+ } catch (Exception e) {
+ Logger.Error?.Print(LogClass.Kernel, $"Error getting thread name: {e.Message}");
+ return "";
}
}
}
From 838296ccb63bb76b1d2d672f0794bab8e5678cd7 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Mon, 23 Jun 2025 07:00:48 +0800
Subject: [PATCH 20/28] gdb: Support precise tracking of PC value when GDB Stub
is enabled
---
src/ARMeilleure/State/ExecutionContext.cs | 10 ++++++++++
src/ARMeilleure/State/NativeContext.cs | 15 +++++++++++++++
src/ARMeilleure/Translation/Translator.cs | 18 ++++++++++++++++++
3 files changed, 43 insertions(+)
diff --git a/src/ARMeilleure/State/ExecutionContext.cs b/src/ARMeilleure/State/ExecutionContext.cs
index cdf6f56c5..fa1a4a032 100644
--- a/src/ARMeilleure/State/ExecutionContext.cs
+++ b/src/ARMeilleure/State/ExecutionContext.cs
@@ -164,11 +164,21 @@ namespace ARMeilleure.State
internal void OnBreak(ulong address, int imm)
{
+ if (Optimizations.EnableDebugging)
+ {
+ DebugPc = Pc;
+ }
+
_breakCallback?.Invoke(this, address, imm);
}
internal void OnSupervisorCall(ulong address, int imm)
{
+ if (Optimizations.EnableDebugging)
+ {
+ DebugPc = Pc;
+ }
+
_supervisorCallback?.Invoke(this, address, imm);
}
diff --git a/src/ARMeilleure/State/NativeContext.cs b/src/ARMeilleure/State/NativeContext.cs
index c90e522a9..25b5e51c3 100644
--- a/src/ARMeilleure/State/NativeContext.cs
+++ b/src/ARMeilleure/State/NativeContext.cs
@@ -22,6 +22,11 @@ namespace ARMeilleure.State
public ulong ExclusiveValueHigh;
public int Running;
public long Tpidr2El0;
+
+ ///
+ /// This is only set when Optimizations.EnableDebugging is true.
+ ///
+ public ulong CurrentPc;
}
private static NativeCtxStorage _dummyStorage = new();
@@ -39,6 +44,11 @@ namespace ARMeilleure.State
public ulong GetPc()
{
+ if (Optimizations.EnableDebugging)
+ {
+ return GetStorage().CurrentPc;
+ }
+
// TODO: More precise tracking of PC value.
return GetStorage().DispatchAddress;
}
@@ -268,6 +278,11 @@ namespace ARMeilleure.State
return StorageOffset(ref _dummyStorage, ref _dummyStorage.Running);
}
+ public static int GetCurrentPcOffset()
+ {
+ return StorageOffset(ref _dummyStorage, ref _dummyStorage.CurrentPc);
+ }
+
private static int StorageOffset(ref NativeCtxStorage storage, ref T target)
{
return (int)Unsafe.ByteOffset(ref Unsafe.As(ref storage), ref target);
diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs
index 343e361a5..351ee34f1 100644
--- a/src/ARMeilleure/Translation/Translator.cs
+++ b/src/ARMeilleure/Translation/Translator.cs
@@ -388,6 +388,11 @@ namespace ARMeilleure.Translation
// Return to managed rather than tail call.
bool useReturns = Optimizations.EnableDebugging;
+ if (Optimizations.EnableDebugging)
+ {
+ EmitPcUpdate(context, block.Address);
+ }
+
InstEmitFlowHelper.EmitVirtualJump(context, Const(block.Address), isReturn: useReturns);
}
else
@@ -410,6 +415,11 @@ namespace ARMeilleure.Translation
}
}
+ if (Optimizations.EnableDebugging)
+ {
+ EmitPcUpdate(context, opCode.Address);
+ }
+
Operand lblPredicateSkip = default;
if (context.IsInIfThenBlock && context.CurrentIfThenBlockCond != Condition.Al)
@@ -506,6 +516,14 @@ namespace ARMeilleure.Translation
context.MarkLabel(lblExit);
}
+ internal static void EmitPcUpdate(EmitterContext context, ulong address)
+ {
+ long currentPcOffs = NativeContext.GetCurrentPcOffset();
+
+ Operand currentPcAddr = context.Add(context.LoadArgument(OperandType.I64, 0), Const(currentPcOffs));
+ context.Store(currentPcAddr, Const(address));
+ }
+
public void InvalidateJitCacheRegion(ulong address, ulong size)
{
ulong[] overlapAddresses = [];
From c5c8647de756ef45337a7ed784af2d14bad6c45c Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Mon, 23 Jun 2025 07:03:40 +0800
Subject: [PATCH 21/28] gdb: Invalidate PTC cache when GDB Stub is
enabled/disabled
---
src/ARMeilleure/Translation/PTC/Ptc.cs | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs
index f36d4256d..c69ebcadb 100644
--- a/src/ARMeilleure/Translation/PTC/Ptc.cs
+++ b/src/ARMeilleure/Translation/PTC/Ptc.cs
@@ -33,7 +33,7 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
- private const uint InternalVersion = 7008; //! To be incremented manually for each change to the ARMeilleure project.
+ private const uint InternalVersion = 7009; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";
@@ -303,6 +303,13 @@ namespace ARMeilleure.Translation.PTC
return false;
}
+ if (outerHeader.DebuggerMode != Optimizations.EnableDebugging)
+ {
+ InvalidateCompressedStream(compressedStream);
+
+ return false;
+ }
+
nint intPtr = nint.Zero;
try
@@ -479,6 +486,7 @@ namespace ARMeilleure.Translation.PTC
MemoryManagerMode = GetMemoryManagerMode(),
OSPlatform = GetOSPlatform(),
Architecture = (uint)RuntimeInformation.ProcessArchitecture,
+ DebuggerMode = Optimizations.EnableDebugging,
UncompressedStreamSize =
(long)Unsafe.SizeOf() +
@@ -1068,7 +1076,7 @@ namespace ARMeilleure.Translation.PTC
return osPlatform;
}
- [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 86*/)]
+ [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 87*/)]
private struct OuterHeader
{
public ulong Magic;
@@ -1080,6 +1088,7 @@ namespace ARMeilleure.Translation.PTC
public byte MemoryManagerMode;
public uint OSPlatform;
public uint Architecture;
+ public bool DebuggerMode;
public long UncompressedStreamSize;
From c4abaa6cf27c25865a967fdfe0e3ce374cbf0c2e Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Mon, 23 Jun 2025 07:04:20 +0800
Subject: [PATCH 22/28] gdb: Allow PTC cache when GDB Stub is enabled
---
src/ARMeilleure/Translation/Translator.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs
index 351ee34f1..a5678be13 100644
--- a/src/ARMeilleure/Translation/Translator.cs
+++ b/src/ARMeilleure/Translation/Translator.cs
@@ -246,7 +246,7 @@ namespace ARMeilleure.Translation
Stubs,
address,
highCq,
- _ptc.State != PtcState.Disabled && !Optimizations.EnableDebugging,
+ _ptc.State != PtcState.Disabled,
mode: Aarch32Mode.User);
Logger.StartPass(PassName.Decoding);
From b1c1ad54e8ec7a3c0f9e0658a469f51f7334b424 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Mon, 23 Jun 2025 07:05:45 +0800
Subject: [PATCH 23/28] gdb: Fix single-stepping of branch instructions
---
src/ARMeilleure/Translation/Translator.cs | 104 ++++++++++++++++++++++
1 file changed, 104 insertions(+)
diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs
index a5678be13..0300d51e1 100644
--- a/src/ARMeilleure/Translation/Translator.cs
+++ b/src/ARMeilleure/Translation/Translator.cs
@@ -195,6 +195,22 @@ namespace ARMeilleure.Translation
private ulong Step(State.ExecutionContext context, ulong address)
{
+ try
+ {
+ OpCode opCode = Decoder.DecodeOpCode(Memory, address, context.ExecutionMode);
+
+ // For branch instructions during single-stepping, we handle them manually
+ // func.Execute() will sometimes execute the entire function call, which is not what we want
+ if (opCode.Instruction.Name is InstName.Bl or InstName.Blr or InstName.Blx or InstName.Br)
+ {
+ return ExecuteBranchInstructionForStepping(context, address, opCode);
+ }
+ }
+ catch
+ {
+ // ignore
+ }
+
TranslatedFunction func = Translate(address, context.ExecutionMode, highCq: false, singleStep: true);
address = func.Execute(Stubs.ContextWrapper, context);
@@ -204,6 +220,94 @@ namespace ARMeilleure.Translation
return address;
}
+ private static ulong ExecuteBranchInstructionForStepping(State.ExecutionContext context, ulong address, OpCode opCode)
+ {
+ switch (opCode.Instruction.Name)
+ {
+ case InstName.Bl:
+ if (opCode is IOpCodeBImm opBImm)
+ {
+ // Set link register
+ if (context.ExecutionMode == ExecutionMode.Aarch64)
+ {
+ context.SetX(30, address + (ulong)opCode.OpCodeSizeInBytes); // LR = X30
+ }
+ else
+ {
+ // For ARM32, need to set the appropriate return address
+ uint returnAddr = opCode is OpCode32 op32 && op32.IsThumb
+ ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u // Thumb bit set
+ : (uint)address + (uint)opCode.OpCodeSizeInBytes;
+ context.SetX(14, returnAddr); // LR = R14
+ }
+ return (ulong)opBImm.Immediate;
+ }
+ break;
+
+ case InstName.Blr:
+ if (opCode is OpCodeBReg opBReg)
+ {
+ // Set link register
+ if (context.ExecutionMode == ExecutionMode.Aarch64)
+ {
+ context.SetX(30, address + (ulong)opCode.OpCodeSizeInBytes); // LR = X30
+ }
+ else
+ {
+ uint returnAddr = opCode is OpCode32 op32 && op32.IsThumb
+ ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u // Thumb bit set
+ : (uint)address + (uint)opCode.OpCodeSizeInBytes;
+ context.SetX(14, returnAddr); // LR = R14
+ }
+ return context.GetX(opBReg.Rn);
+ }
+ break;
+
+ case InstName.Blx:
+ if (opCode is IOpCodeBImm opBlxImm)
+ {
+ // Handle mode switching for BLX
+ if (opCode is OpCode32 op32)
+ {
+ uint returnAddr = op32.IsThumb
+ ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u
+ : (uint)address + (uint)opCode.OpCodeSizeInBytes;
+ context.SetX(14, returnAddr);
+
+ // BLX switches between ARM and Thumb modes
+ context.SetPstateFlag(PState.TFlag, !op32.IsThumb);
+ }
+ return (ulong)opBlxImm.Immediate;
+ }
+ else if (opCode is IOpCode32BReg opBlxReg)
+ {
+ if (opCode is OpCode32 op32)
+ {
+ uint returnAddr = op32.IsThumb
+ ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u
+ : (uint)address + (uint)opCode.OpCodeSizeInBytes;
+ context.SetX(14, returnAddr);
+
+ // For BLX register, the target address determines the mode
+ ulong targetAddr = context.GetX(opBlxReg.Rm);
+ context.SetPstateFlag(PState.TFlag, (targetAddr & 1) != 0);
+ return targetAddr & ~1UL; // Clear the Thumb bit for the actual address
+ }
+ }
+ break;
+
+ case InstName.Br:
+ if (opCode is OpCodeBReg opBr)
+ {
+ // BR doesn't set link register, just branches to the target
+ return context.GetX(opBr.Rn);
+ }
+ break;
+ }
+
+ throw new InvalidOperationException($"Unhandled branch instruction: {opCode.Instruction.Name}");
+ }
+
internal TranslatedFunction GetOrTranslate(ulong address, ExecutionMode mode)
{
if (!Functions.TryGetValue(address, out TranslatedFunction func))
From 669179ca2e19d6ab9ebead3bad741ea0c300d2a1 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Mon, 23 Jun 2025 17:59:53 +0800
Subject: [PATCH 24/28] gdb: Adjust Settings UI
---
assets/locales.json | 41 +++++++++++++++----
.../UI/Views/Settings/SettingsDebugView.axaml | 12 +++---
2 files changed, 40 insertions(+), 13 deletions(-)
diff --git a/assets/locales.json b/assets/locales.json
index 6269cb5dd..915fb42e5 100644
--- a/assets/locales.json
+++ b/assets/locales.json
@@ -25053,7 +25053,7 @@
"ar_SA": "",
"de_DE": "",
"el_GR": "",
- "en_US": "Debug (WARNING: For developer use only)",
+ "en_US": "Debug",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
@@ -25073,7 +25073,32 @@
}
},
{
- "ID": "EnableGDBStub",
+ "ID": "SettingsTabDebugNote",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "WARNING: For developer use only, will reduce performance",
+ "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": "SettingsTabDebugEnableGDBStub",
"Translations": {
"ar_SA": "",
"de_DE": "",
@@ -25098,7 +25123,7 @@
}
},
{
- "ID": "GDBStubToggleTooltip",
+ "ID": "SettingsTabDebugGDBStubToggleTooltip",
"Translations": {
"ar_SA": "",
"de_DE": "",
@@ -25123,12 +25148,12 @@
}
},
{
- "ID": "GDBStubPort",
+ "ID": "SettingsTabDebugGDBStubPort",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
- "en_US": "GDB stub port:",
+ "en_US": "GDB Stub Port:",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
@@ -25148,12 +25173,12 @@
}
},
{
- "ID": "DebuggerSuspendOnStart",
+ "ID": "SettingsTabDebugSuspendOnStart",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
- "en_US": "Suspend application on start",
+ "en_US": "Suspend Application on Start",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
@@ -25173,7 +25198,7 @@
}
},
{
- "ID": "DebuggerSuspendOnStartTooltip",
+ "ID": "SettingsTabDebugSuspendOnStartTooltip",
"Translations": {
"ar_SA": "",
"de_DE": "",
diff --git a/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml
index 036471059..f491dda24 100644
--- a/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml
+++ b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml
@@ -26,19 +26,21 @@
Orientation="Vertical"
Spacing="10">
+
-
+
-
+
From cd2a7c99166c544973ddfa6513f68e663f611870 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Tue, 24 Jun 2025 07:25:47 +0800
Subject: [PATCH 25/28] gdb: Prevent BreakHandler being called multiple times
from the same breakpoint
---
src/Ryujinx.HLE/Debugger/Debugger.cs | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index 5bde92aed..21bb32ec2 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -32,6 +32,7 @@ namespace Ryujinx.HLE.Debugger
private Thread DebuggerThread;
private Thread MessageHandlerThread;
private bool _shuttingDown = false;
+ private ManualResetEventSlim _breakHandlerEvent = new ManualResetEventSlim(false);
private ulong? cThread;
private ulong? gThread;
@@ -237,6 +238,7 @@ namespace Ryujinx.HLE.Debugger
case ThreadBreakMessage { Context: var ctx }:
DebugProcess.DebugStop();
gThread = cThread = ctx.ThreadUid;
+ _breakHandlerEvent.Set();
Reply($"T05thread:{ctx.ThreadUid:x};");
break;
@@ -1266,15 +1268,20 @@ namespace Ryujinx.HLE.Debugger
Messages.Add(new KillMessage());
MessageHandlerThread.Join();
Messages.Dispose();
+ _breakHandlerEvent.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);
+
+ _breakHandlerEvent.Reset();
+ Messages.Add(new ThreadBreakMessage(ctx, address, imm));
+ // Messages.Add can block, so we log it after adding the message to make sure user can see the log at the same time GDB receives the break message
+ Logger.Notice.Print(LogClass.GdbStub, $"Break hit on thread {ctx.ThreadUid} at pc {address:x016}");
+ // Wait for the process to stop before returning to avoid BreakHander being called multiple times from the same breakpoint
+ _breakHandlerEvent.Wait(5000);
}
public void StepHandler(IExecutionContext ctx)
From 1b37038d5a04d9ebd84431b36e9742bdb048b5a3 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Tue, 24 Jun 2025 07:26:20 +0800
Subject: [PATCH 26/28] gdb: Log CommandReadMemory failure
---
src/Ryujinx.HLE/Debugger/Debugger.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index 21bb32ec2..d32fa2c99 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -862,6 +862,9 @@ namespace Ryujinx.HLE.Debugger
}
catch (InvalidMemoryRegionException)
{
+ // InvalidAccessHandler will show an error message, we log it again to tell user the error is from GDB (which can be ignored)
+ // TODO: Do not let InvalidAccessHandler show the error message
+ Logger.Notice.Print(LogClass.GdbStub, $"GDB failed to read memory at 0x{addr:X16}");
ReplyError();
}
}
From b932905053994109b2fcbd4e4749219de78ea31d Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Tue, 24 Jun 2025 15:40:10 +0800
Subject: [PATCH 27/28] gdb: Interrupt at currently selected thread
---
src/Ryujinx.HLE/Debugger/Debugger.cs | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index d32fa2c99..c94f142cd 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -222,7 +222,7 @@ namespace Ryujinx.HLE.Debugger
{
case BreakInMessage:
Logger.Notice.Print(LogClass.GdbStub, "Break-in requested");
- CommandQuery();
+ CommandInterrupt();
break;
case SendNackMessage:
@@ -731,6 +731,18 @@ namespace Ryujinx.HLE.Debugger
Reply($"T05thread:{cThread:x};");
}
+ void CommandInterrupt()
+ {
+ // GDB is requesting an interrupt. Stop everything.
+ DebugProcess.DebugStop();
+ if (gThread == null || !GetThreads().Any(x => x.ThreadUid == gThread.Value))
+ {
+ gThread = cThread = DebugProcess.GetThreadUids().First();
+ }
+
+ Reply($"T02thread:{gThread:x};");
+ }
+
void CommandContinue(ulong? newPc)
{
if (newPc.HasValue)
From 625ca3f934ab231bfbc8467bdae064f0f93cb6d5 Mon Sep 17 00:00:00 2001
From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app>
Date: Tue, 24 Jun 2025 15:45:18 +0800
Subject: [PATCH 28/28] gdb: Support continue specific threads
---
src/Ryujinx.HLE/Debugger/Debugger.cs | 30 +++++++++++++++----
.../Debugger/IDebuggableProcess.cs | 2 ++
.../HOS/Kernel/Process/KProcess.cs | 20 ++++++++++++-
3 files changed, 45 insertions(+), 7 deletions(-)
diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs
index c94f142cd..015fbf8fb 100644
--- a/src/Ryujinx.HLE/Debugger/Debugger.cs
+++ b/src/Ryujinx.HLE/Debugger/Debugger.cs
@@ -411,13 +411,13 @@ namespace Ryujinx.HLE.Debugger
break;
}
- if (DebugProcess.GetDebugState() == DebugState.Stopped)
+ if (DebugProcess.IsThreadPaused(DebugProcess.GetThread(threadId.Value)))
{
- Reply(ToHex("Stopped"));
+ Reply(ToHex("Paused"));
}
else
{
- Reply(ToHex("Not stopped"));
+ Reply(ToHex("Running"));
}
break;
}
@@ -625,6 +625,8 @@ namespace Ryujinx.HLE.Debugger
threadActionMap[thread.ThreadUid] = new VContPendingAction(VContAction.None);
}
+ VContAction defaultAction = VContAction.None;
+
// For each inferior thread, the *leftmost* action with a matching thread-id is applied.
for (int i = rawActions.Length - 1; i >= 0; i--)
{
@@ -642,6 +644,7 @@ namespace Ryujinx.HLE.Debugger
_ => VContAction.None
};
+ // Note: We don't support signals yet.
ushort? signal = null;
if (cmd == 'C' || cmd == 'S')
{
@@ -666,12 +669,17 @@ namespace Ryujinx.HLE.Debugger
{
threadActionMap[row.Key] = new VContPendingAction(action, signal);
}
+
+ if (action == VContAction.Continue) {
+ defaultAction = action;
+ } else {
+ Logger.Warning?.Print(LogClass.GdbStub, $"Received vCont command with unsupported default action: {rawAction}");
+ }
}
}
bool hasError = false;
- // TODO: We don't support stop or continue yet, and we don't support signals.
foreach (var (threadUid, action) in threadActionMap)
{
if (action.Action == VContAction.Step)
@@ -683,10 +691,20 @@ namespace Ryujinx.HLE.Debugger
}
}
- // If all threads are set to continue, continue the process.
+ // If we receive "vCont;c", just continue the process.
+ // If we receive something like "vCont;c:2e;c:2f" (IDA Pro will send commands like this), continue these threads.
+ // For "vCont;s:2f;c", we only step thread 2f, and do not continue other threads. (Is this correct?)
if (threadActionMap.Values.All(a => a.Action == VContAction.Continue))
{
DebugProcess.DebugContinue();
+ } else if (defaultAction == VContAction.None) {
+ foreach (var (threadUid, action) in threadActionMap)
+ {
+ if (action.Action == VContAction.Continue)
+ {
+ DebugProcess.DebugContinue(DebugProcess.GetThread(threadUid));
+ }
+ }
}
if (hasError)
@@ -716,7 +734,7 @@ namespace Ryujinx.HLE.Debugger
foreach (var thread in GetThreads())
{
string threadName = System.Security.SecurityElement.Escape(thread.GetThreadName());
- sb.Append($"\n");
+ sb.Append($"{(DebugProcess.IsThreadPaused(thread) ? "Paused" : "Running")}\n");
}
sb.Append("");
diff --git a/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs b/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs
index 273a1147f..0896f25d2 100644
--- a/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs
+++ b/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs
@@ -8,9 +8,11 @@ namespace Ryujinx.HLE.Debugger
{
void DebugStop();
void DebugContinue();
+ void DebugContinue(KThread thread);
bool DebugStep(KThread thread);
KThread GetThread(ulong threadUid);
DebugState GetDebugState();
+ bool IsThreadPaused(KThread thread);
ulong[] GetThreadUids();
public void DebugInterruptHandler(IExecutionContext ctx);
IVirtualMemoryManager CpuMemory { get; }
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
index c24d5c3cc..0a57f5bc6 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs
@@ -1257,12 +1257,25 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
_kernelContext.CriticalSection.Leave();
}
+ public void DebugContinue(KThread target)
+ {
+ Interlocked.Exchange(ref _parent.debugState, (int)DebugState.Running);
+
+ _kernelContext.CriticalSection.Enter();
+ lock (_parent._threadingLock)
+ {
+ target.Resume(ThreadSchedState.ThreadPauseFlag);
+ }
+ _kernelContext.CriticalSection.Leave();
+ }
+
public bool DebugStep(KThread target)
{
- if (_parent.debugState != (int)DebugState.Stopped)
+ if (!IsThreadPaused(target))
{
return false;
}
+
_kernelContext.CriticalSection.Enter();
steppingThread = target;
bool waiting = target.MutexOwner != null || target.WaitingSync || target.WaitingInArbitration;
@@ -1322,6 +1335,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
return (DebugState)_parent.debugState;
}
+ public bool IsThreadPaused(KThread target)
+ {
+ return (target.SchedFlags & ThreadSchedState.ThreadPauseFlag) != 0;
+ }
+
public ulong[] GetThreadUids()
{
lock (_parent._threadingLock)