diff --git a/assets/locales.json b/assets/locales.json index 2f52ee71b..eeb882e9c 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..d43e20d83 100644 --- a/src/ARMeilleure/Instructions/NativeInterface.cs +++ b/src/ARMeilleure/Instructions/NativeInterface.cs @@ -3,6 +3,7 @@ using ARMeilleure.State; using ARMeilleure.Translation; using System; using System.Runtime.InteropServices; +using ExecutionContext = ARMeilleure.State.ExecutionContext; namespace ARMeilleure.Instructions { @@ -200,7 +201,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..5d8144d4e 100644 --- a/src/ARMeilleure/State/ExecutionContext.cs +++ b/src/ARMeilleure/State/ExecutionContext.cs @@ -1,4 +1,5 @@ using ARMeilleure.Memory; +using System.Threading; namespace ARMeilleure.State { @@ -10,7 +11,7 @@ namespace ARMeilleure.State internal nint NativeContextPtr => _nativeContext.BasePtr; - private bool _interrupted; + internal bool Interrupted { get; private set; } private readonly ICounter _counter; @@ -65,6 +66,8 @@ namespace ARMeilleure.State public bool IsAarch32 { get; set; } + public ulong ThreadUid { get; set; } + internal ExecutionMode ExecutionMode { get @@ -90,14 +93,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 +113,7 @@ namespace ARMeilleure.State _counter = counter; _interruptCallback = interruptCallback; _breakCallback = breakCallback; + _stepCallback = stepCallback; _supervisorCallback = supervisorCallback; _undefinedCallback = undefinedCallback; @@ -127,9 +136,9 @@ namespace ARMeilleure.State internal void CheckInterrupt() { - if (_interrupted) + if (Interrupted) { - _interrupted = false; + Interrupted = false; _interruptCallback?.Invoke(this); } @@ -139,7 +148,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) @@ -149,6 +169,11 @@ namespace ARMeilleure.State 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); } 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..f00acc1d7 100644 --- a/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs +++ b/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs @@ -1,5 +1,6 @@ using ARMeilleure.Memory; using ARMeilleure.State; +using ExecutionContext = ARMeilleure.State.ExecutionContext; namespace Ryujinx.Cpu.Jit { @@ -53,6 +54,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 +73,7 @@ namespace Ryujinx.Cpu.Jit counter, InterruptHandler, BreakHandler, + StepHandler, SupervisorCallHandler, UndefinedHandler); @@ -93,6 +102,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 +123,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..608ab977a --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -0,0 +1,930 @@ +using ARMeilleure.State; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Kernel; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Memory; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using IExecutionContext = Ryujinx.Cpu.IExecutionContext; + +namespace Ryujinx.HLE.Debugger +{ + public class Debugger : IDisposable + { + internal Switch Device { get; private set; } + + public ushort GdbStubPort { get; private set; } + + private TcpListener ListenerSocket; + private Socket ClientSocket = null; + private NetworkStream ReadStream = null; + private NetworkStream WriteStream = null; + private BlockingCollection 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.ReadLengthAsLEHex(16); + state.SetX(gdbRegId, value); + return true; + } + case 32: + { + ulong value = ss.ReadLengthAsLEHex(16); + state.DebugPc = value; + return true; + } + case 33: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.Pstate = (uint)value; + return true; + } + case >= 34 and <= 65: + { + ulong value0 = ss.ReadLengthAsLEHex(16); + ulong value1 = ss.ReadLengthAsLEHex(16); + state.SetV(gdbRegId - 34, new V128(value0, value1)); + return true; + } + case 66: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.Fpsr = (uint)value; + return true; + } + case 67: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.Fpcr = (uint)value; + return true; + } + default: + return false; + } + } + + private string GdbReadRegister32(IExecutionContext state, int gdbRegId) + { + switch (gdbRegId) + { + case >= 0 and <= 14: + return ToHex(BitConverter.GetBytes((uint)state.GetX(gdbRegId))); + case 15: + return ToHex(BitConverter.GetBytes((uint)state.DebugPc)); + case 16: + return ToHex(BitConverter.GetBytes((uint)state.Pstate)); + case >= 17 and <= 32: + return ToHex(state.GetV(gdbRegId - 17).ToArray()); + case >= 33 and <= 64: + int reg = (gdbRegId - 33); + int n = reg / 2; + int shift = reg % 2; + ulong value = state.GetV(n).Extract(shift); + return ToHex(BitConverter.GetBytes(value)); + case 65: + uint fpscr = (uint)state.Fpsr | (uint)state.Fpcr; + return ToHex(BitConverter.GetBytes(fpscr)); + default: + return null; + } + } + + private bool GdbWriteRegister32(IExecutionContext state, int gdbRegId, StringStream ss) + { + switch (gdbRegId) + { + case >= 0 and <= 14: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.SetX(gdbRegId, value); + return true; + } + case 15: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.DebugPc = value; + return true; + } + case 16: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.Pstate = (uint)value; + return true; + } + case >= 17 and <= 32: + { + ulong value0 = ss.ReadLengthAsLEHex(16); + ulong value1 = ss.ReadLengthAsLEHex(16); + state.SetV(gdbRegId - 17, new V128(value0, value1)); + return true; + } + case >= 33 and <= 64: + { + ulong value = ss.ReadLengthAsLEHex(16); + int regId = (gdbRegId - 33); + int regNum = regId / 2; + int shift = regId % 2; + V128 reg = state.GetV(regNum); + reg.Insert(shift, value); + return true; + } + case 65: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.Fpsr = (uint)value & FpcrMask; + state.Fpcr = (uint)value & ~FpcrMask; + return true; + } + default: + return false; + } + } + + private void MessageHandlerMain() + { + while (!_shuttingDown) + { + IMessage msg = Messages.Take(); + try { + switch (msg) + { + case BreakInMessage: + Logger.Notice.Print(LogClass.GdbStub, "Break-in requested"); + CommandQuery(); + break; + + case SendNackMessage: + WriteStream.WriteByte((byte)'-'); + break; + + case CommandMessage { Command: var cmd }: + Logger.Debug?.Print(LogClass.GdbStub, $"Received Command: {cmd}"); + WriteStream.WriteByte((byte)'+'); + ProcessCommand(cmd); + break; + + case ThreadBreakMessage { Context: var ctx }: + DebugProcess.DebugStop(); + Reply($"T05thread:{ctx.ThreadUid:x};"); + break; + + case KillMessage: + return; + } + } + catch (IOException e) + { + Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e); + } + catch (NullReferenceException e) + { + Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e); + } + } + } + + private void ProcessCommand(string cmd) + { + StringStream ss = new StringStream(cmd); + + switch (ss.ReadChar()) + { + case '!': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + // Enable extended mode + ReplyOK(); + break; + case '?': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + CommandQuery(); + break; + case 'c': + CommandContinue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + break; + case 'D': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + CommandDetach(); + break; + case 'g': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + CommandReadRegisters(); + break; + case 'G': + CommandWriteRegisters(ss); + break; + case 'H': + { + char op = ss.ReadChar(); + ulong? threadId = ss.ReadRemainingAsThreadUid(); + CommandSetThread(op, threadId); + break; + } + case 'k': + Logger.Notice.Print(LogClass.GdbStub, "Kill request received, detach instead"); + Reply(""); + CommandDetach(); + break; + case 'm': + { + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadRemainingAsHex(); + CommandReadMemory(addr, len); + break; + } + case 'M': + { + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadUntilAsHex(':'); + CommandWriteMemory(addr, len, ss); + break; + } + case 'p': + { + ulong gdbRegId = ss.ReadRemainingAsHex(); + CommandReadRegister((int)gdbRegId); + break; + } + case 'P': + { + ulong gdbRegId = ss.ReadUntilAsHex('='); + CommandWriteRegister((int)gdbRegId, ss); + break; + } + case 'q': + if (ss.ConsumeRemaining("GDBServerVersion")) + { + Reply($"name:Ryujinx;version:{ReleaseInformation.Version};"); + break; + } + + if (ss.ConsumeRemaining("HostInfo")) + { + if (IsProcessAarch32) + { + Reply( + $"triple:{ToHex("arm-unknown-linux-android")};endian:little;ptrsize:4;hostname:{ToHex("Ryujinx")};"); + } + else + { + Reply( + $"triple:{ToHex("aarch64-unknown-linux-android")};endian:little;ptrsize:8;hostname:{ToHex("Ryujinx")};"); + } + break; + } + + if (ss.ConsumeRemaining("ProcessInfo")) + { + if (IsProcessAarch32) + { + Reply( + $"pid:1;cputype:12;cpusubtype:0;triple:{ToHex("arm-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:4;"); + } + else + { + Reply( + $"pid:1;cputype:100000c;cpusubtype:0;triple:{ToHex("aarch64-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:8;"); + } + break; + } + + if (ss.ConsumePrefix("Supported:") || ss.ConsumeRemaining("Supported")) + { + Reply("PacketSize=10000;qXfer:features:read+"); + break; + } + + if (ss.ConsumeRemaining("fThreadInfo")) + { + Reply($"m{string.Join(",", DebugProcess.GetThreadUids().Select(x => $"{x:x}"))}"); + break; + } + + if (ss.ConsumeRemaining("sThreadInfo")) + { + Reply("l"); + break; + } + + if (ss.ConsumePrefix("ThreadExtraInfo,")) + { + ulong? threadId = ss.ReadRemainingAsThreadUid(); + if (threadId == null) + { + ReplyError(); + break; + } + + if (DebugProcess.GetDebugState() == DebugState.Stopped) + { + Reply(ToHex("Stopped")); + } + else + { + Reply(ToHex("Not stopped")); + } + break; + } + + if (ss.ConsumePrefix("Xfer:features:read:")) + { + string feature = ss.ReadUntil(':'); + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadRemainingAsHex(); + + if (feature == "target.xml") + { + feature = IsProcessAarch32 ? "target32.xml" : "target64.xml"; + } + + string data; + if (RegisterInformation.Features.TryGetValue(feature, out data)) + { + if (addr >= (ulong)data.Length) + { + Reply("l"); + break; + } + + if (len >= (ulong)data.Length - addr) + { + Reply("l" + ToBinaryFormat(data.Substring((int)addr))); + break; + } + else + { + Reply("m" + ToBinaryFormat(data.Substring((int)addr, (int)len))); + break; + } + } + else + { + Reply("E00"); // Invalid annex + break; + } + } + + goto unknownCommand; + case 'Q': + goto unknownCommand; + case 's': + CommandStep(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + break; + case 'T': + { + ulong? threadId = ss.ReadRemainingAsThreadUid(); + CommandIsAlive(threadId); + break; + } + case 'v': + if (ss.ConsumeRemaining("MustReplyEmpty")) + { + Reply(""); + break; + } + goto unknownCommand; + default: + unknownCommand: + Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}"); + Reply(""); + break; + } + } + + void CommandQuery() + { + // GDB is performing initial contact. Stop everything. + DebugProcess.DebugStop(); + gThread = cThread = DebugProcess.GetThreadUids().First(); + Reply($"T05thread:{cThread:x};"); + } + + void CommandContinue(ulong? newPc) + { + if (newPc.HasValue) + { + if (cThread == null) + { + ReplyError(); + return; + } + + DebugProcess.GetThread(cThread.Value).Context.DebugPc = newPc.Value; + } + + DebugProcess.DebugContinue(); + } + + void CommandDetach() + { + // TODO: Remove all breakpoints + CommandContinue(null); + } + + void CommandReadRegisters() + { + if (gThread == null) + { + ReplyError(); + return; + } + + var ctx = DebugProcess.GetThread(gThread.Value).Context; + string registers = ""; + if (IsProcessAarch32) + { + for (int i = 0; i < GdbRegisterCount32; i++) + { + registers += GdbReadRegister32(ctx, i); + } + } + else + { + for (int i = 0; i < GdbRegisterCount64; i++) + { + registers += GdbReadRegister64(ctx, i); + } + } + + Reply(registers); + } + + void CommandWriteRegisters(StringStream ss) + { + if (gThread == null) + { + ReplyError(); + return; + } + + var ctx = DebugProcess.GetThread(gThread.Value).Context; + if (IsProcessAarch32) + { + for (int i = 0; i < GdbRegisterCount32; i++) + { + if (!GdbWriteRegister32(ctx, i, ss)) + { + ReplyError(); + return; + } + } + } + else + { + for (int i = 0; i < GdbRegisterCount64; i++) + { + if (!GdbWriteRegister64(ctx, i, ss)) + { + ReplyError(); + return; + } + } + } + + if (ss.IsEmpty()) + { + ReplyOK(); + } + else + { + ReplyError(); + } + } + + void CommandSetThread(char op, ulong? threadId) + { + if (threadId == 0 || threadId == null) + { + threadId = GetThreads().First().ThreadUid; + } + + if (DebugProcess.GetThread(threadId.Value) == null) + { + ReplyError(); + return; + } + + switch (op) + { + case 'c': + cThread = threadId; + ReplyOK(); + return; + case 'g': + gThread = threadId; + ReplyOK(); + return; + default: + ReplyError(); + return; + } + } + + void CommandReadMemory(ulong addr, ulong len) + { + try + { + var data = new byte[len]; + DebugProcess.CpuMemory.Read(addr, data); + Reply(ToHex(data)); + } + catch (InvalidMemoryRegionException) + { + ReplyError(); + } + } + + void CommandWriteMemory(ulong addr, ulong len, StringStream ss) + { + try + { + var data = new byte[len]; + for (ulong i = 0; i < len; i++) + { + data[i] = (byte)ss.ReadLengthAsHex(2); + } + + DebugProcess.CpuMemory.Write(addr, data); + DebugProcess.InvalidateCacheRegion(addr, len); + ReplyOK(); + } + catch (InvalidMemoryRegionException) + { + ReplyError(); + } + } + + void CommandReadRegister(int gdbRegId) + { + if (gThread == null) + { + ReplyError(); + return; + } + + var ctx = DebugProcess.GetThread(gThread.Value).Context; + string result; + if (IsProcessAarch32) + { + result = GdbReadRegister32(ctx, gdbRegId); + if (result != null) + { + Reply(result); + } + else + { + ReplyError(); + } + } + else + { + result = GdbReadRegister64(ctx, gdbRegId); + if (result != null) + { + Reply(result); + } + else + { + ReplyError(); + } + } + } + + void CommandWriteRegister(int gdbRegId, StringStream ss) + { + if (gThread == null) + { + ReplyError(); + return; + } + + var ctx = DebugProcess.GetThread(gThread.Value).Context; + if (IsProcessAarch32) + { + if (GdbWriteRegister32(ctx, gdbRegId, ss) && ss.IsEmpty()) + { + ReplyOK(); + } + else + { + ReplyError(); + } + } + else + { + if (GdbWriteRegister64(ctx, gdbRegId, ss) && ss.IsEmpty()) + { + ReplyOK(); + } + else + { + ReplyError(); + } + } + } + + private void CommandStep(ulong? newPc) + { + if (cThread == null) + { + ReplyError(); + return; + } + + var thread = DebugProcess.GetThread(cThread.Value); + + if (newPc.HasValue) + { + thread.Context.DebugPc = newPc.Value; + } + + if (!DebugProcess.DebugStep(thread)) + { + ReplyError(); + } + else + { + Reply($"T05thread:{thread.ThreadUid:x};"); + } + } + + private void CommandIsAlive(ulong? threadId) + { + if (GetThreads().Any(x => x.ThreadUid == threadId)) + { + ReplyOK(); + } + else + { + Reply("E00"); + } + } + + private void Reply(string cmd) + { + Logger.Debug?.Print(LogClass.GdbStub, $"Reply: {cmd}"); + WriteStream.Write(Encoding.ASCII.GetBytes($"${cmd}#{CalculateChecksum(cmd):x2}")); + } + + private void ReplyOK() + { + Reply("OK"); + } + + private void ReplyError() + { + Reply("E01"); + } + + private void DebuggerThreadMain() + { + var endpoint = new IPEndPoint(IPAddress.Any, GdbStubPort); + ListenerSocket = new TcpListener(endpoint); + ListenerSocket.Start(); + Logger.Notice.Print(LogClass.GdbStub, $"Currently waiting on {endpoint} for GDB client"); + + while (!_shuttingDown) + { + try + { + ClientSocket = ListenerSocket.AcceptSocket(); + } + catch (SocketException) + { + return; + } + + // If the user connects before the application is running, wait for the application to start. + int retries = 10; + while (DebugProcess == null && retries-- > 0) + { + Thread.Sleep(200); + } + if (DebugProcess == null) + { + Logger.Warning?.Print(LogClass.GdbStub, "Application is not running, cannot accept GDB client connection"); + ClientSocket.Close(); + continue; + } + + ClientSocket.NoDelay = true; + ReadStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Read); + WriteStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Write); + Logger.Notice.Print(LogClass.GdbStub, "GDB client connected"); + + while (true) + { + try + { + switch (ReadStream.ReadByte()) + { + case -1: + goto eof; + case '+': + continue; + case '-': + Logger.Notice.Print(LogClass.GdbStub, "NACK received!"); + continue; + case '\x03': + Messages.Add(new BreakInMessage()); + break; + case '$': + string cmd = ""; + while (true) + { + int x = ReadStream.ReadByte(); + if (x == -1) + goto eof; + if (x == '#') + break; + cmd += (char)x; + } + + string checksum = $"{(char)ReadStream.ReadByte()}{(char)ReadStream.ReadByte()}"; + if (checksum == $"{CalculateChecksum(cmd):x2}") + { + Messages.Add(new CommandMessage(cmd)); + } + else + { + Messages.Add(new SendNackMessage()); + } + + break; + } + } + catch (IOException) + { + goto eof; + } + } + + eof: + Logger.Notice.Print(LogClass.GdbStub, "GDB client lost connection"); + ReadStream.Close(); + ReadStream = null; + WriteStream.Close(); + WriteStream = null; + ClientSocket.Close(); + ClientSocket = null; + } + } + + private byte CalculateChecksum(string cmd) + { + byte checksum = 0; + foreach (char x in cmd) + { + unchecked + { + checksum += (byte)x; + } + } + + return checksum; + } + + private string ToHex(byte[] bytes) + { + return string.Join("", bytes.Select(x => $"{x:x2}")); + } + + private string ToHex(string str) + { + return ToHex(Encoding.ASCII.GetBytes(str)); + } + + private string ToBinaryFormat(byte[] bytes) + { + return string.Join("", bytes.Select(x => + x switch + { + (byte)'#' => "}\x03", + (byte)'$' => "}\x04", + (byte)'*' => "}\x0a", + (byte)'}' => "}\x5d", + _ => Convert.ToChar(x).ToString(), + } + )); + } + + private string ToBinaryFormat(string str) + { + return ToBinaryFormat(Encoding.ASCII.GetBytes(str)); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _shuttingDown = true; + + ListenerSocket.Stop(); + ClientSocket?.Shutdown(SocketShutdown.Both); + ClientSocket?.Close(); + ReadStream?.Close(); + WriteStream?.Close(); + DebuggerThread.Join(); + Messages.Add(new KillMessage()); + MessageHandlerThread.Join(); + Messages.Dispose(); + } + } + + public void BreakHandler(IExecutionContext ctx, ulong address, int imm) + { + Logger.Notice.Print(LogClass.GdbStub, $"Break hit on thread {ctx.ThreadUid} at pc {address:x016}"); + + Messages.Add(new ThreadBreakMessage(ctx, address, imm)); + DebugProcess.DebugInterruptHandler(ctx); + } + + public void StepHandler(IExecutionContext ctx) + { + DebugProcess.DebugInterruptHandler(ctx); + } + } +} 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..c24d5c3cc 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,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Process SetState(newState); + if (KernelContext.Device.Configuration.DebuggerSuspendOnStart && IsApplication) + { + mainThread.Suspend(ThreadSchedState.ThreadPauseFlag); + debugState = (int)DebugState.Stopped; + Logger.Notice.Print(LogClass.Kernel, $"Application is suspended on start for debugging."); + } + result = mainThread.Start(); if (result != Result.Success) @@ -727,9 +740,19 @@ namespace Ryujinx.HLE.HOS.Kernel.Process public IExecutionContext CreateExecutionContext() { + ExceptionCallback breakCallback = null; + ExceptionCallbackNoArgs stepCallback = null; + + if (KernelContext.Device.Configuration.EnableGdbStub && KernelContext.Device.Debugger != null) + { + breakCallback = KernelContext.Device.Debugger.BreakHandler; + stepCallback = KernelContext.Device.Debugger.StepHandler; + } + return Context?.CreateExecutionContext(new ExceptionCallbacks( InterruptHandler, - null, + breakCallback, + stepCallback, KernelContext.SyscallHandler.SvcCall, UndefinedInstructionHandler)); } @@ -1174,5 +1197,168 @@ 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(); + + bool stepTimedOut = false; + if (!StepBarrier.SignalAndWait(TimeSpan.FromMilliseconds(2000))) + { + Logger.Warning?.Print(LogClass.Kernel, $"Failed to step thread {target.ThreadUid} in time."); + stepTimedOut = true; + } + + _kernelContext.CriticalSection.Enter(); + steppingThread = null; + if (waiting) + { + lock (_parent._threadingLock) + { + foreach (KThread thread in _parent._threads) + { + thread.Suspend(ThreadSchedState.ThreadPauseFlag); + } + } + } + else + { + target.Suspend(ThreadSchedState.ThreadPauseFlag); + } + _kernelContext.CriticalSection.Leave(); + + if (stepTimedOut) + { + return false; + } + + StepBarrier.SignalAndWait(); + return true; + } + + public DebugState GetDebugState() + { + return (DebugState)_parent.debugState; + } + + public ulong[] GetThreadUids() + { + lock (_parent._threadingLock) + { + var threads = _parent._threads.Where(x => !x.TerminationRequested).ToArray(); + return threads.Select(x => x.ThreadUid).ToArray(); + } + } + + public KThread GetThread(ulong threadUid) + { + lock (_parent._threadingLock) + { + var threads = _parent._threads.Where(x => !x.TerminationRequested).ToArray(); + return threads.FirstOrDefault(x => x.ThreadUid == threadUid); + } + } + + public void DebugInterruptHandler(IExecutionContext ctx) + { + _kernelContext.CriticalSection.Enter(); + bool stepping = steppingThread != null; + _kernelContext.CriticalSection.Leave(); + if (stepping) + { + StepBarrier.SignalAndWait(); + StepBarrier.SignalAndWait(); + } + _parent.InterruptHandler(ctx); + } + + public IVirtualMemoryManager CpuMemory { get { return _parent.CpuMemory; } } + + public void InvalidateCacheRegion(ulong address, ulong size) + { + _parent.Context.InvalidateCacheRegion(address, size); + } + } } } 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..e1aa8e0e4 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 @@ + +