From 1d409f712721b38327ff80ee0786465952a746d8 Mon Sep 17 00:00:00 2001 From: LotP <22-lotp@users.noreply.git.ryujinx.app> Date: Wed, 15 Oct 2025 15:37:13 -0500 Subject: [PATCH 1/9] 12 GiB heap support (ryubing/ryujinx!166) See merge request ryubing/ryujinx!166 --- src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs | 1 + src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs | 9 ++++++--- src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs | 4 ++++ src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs | 3 ++- src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs | 3 ++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs b/src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs index e05fc8397..e3c5865ae 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/KernelStatic.cs @@ -29,6 +29,7 @@ namespace Ryujinx.HLE.HOS.Kernel capabilities, context.ResourceLimit, MemoryRegion.Service, + context.Device.Configuration.MemoryConfiguration, null, customThreadStart); diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs index ee1b4a7be..bc59b0b4d 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs @@ -102,6 +102,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory ProcessCreationFlags flags, bool fromBack, MemoryRegion memRegion, + MemoryConfiguration memConfig, ulong address, ulong size, KMemoryBlockSlabManager slabManager) @@ -117,6 +118,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory addrSpaceBase, addrSpaceSize, memRegion, + memConfig, address, size, slabManager); @@ -159,6 +161,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory ulong addrSpaceStart, ulong addrSpaceEnd, MemoryRegion memRegion, + MemoryConfiguration memConfig, ulong address, ulong size, KMemoryBlockSlabManager slabManager) @@ -193,7 +196,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory case ProcessCreationFlags.AddressSpace64BitDeprecated: aliasRegion.Size = 0x180000000; - heapRegion.Size = 0x180000000; + heapRegion.Size = memConfig == MemoryConfiguration.MemoryConfiguration12GiB ? 0x300000000u : 0x180000000u; stackRegion.Size = 0; tlsIoRegion.Size = 0; CodeRegionStart = 0x8000000; @@ -223,7 +226,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory int addressSpaceWidth = (int)ulong.Log2(_reservedAddressSpaceSize); aliasRegion.Size = 1UL << (addressSpaceWidth - 3); - heapRegion.Size = 0x180000000; + heapRegion.Size = memConfig == MemoryConfiguration.MemoryConfiguration12GiB ? 0x300000000u : 0x180000000u; stackRegion.Size = 1UL << (addressSpaceWidth - 8); tlsIoRegion.Size = 1UL << (addressSpaceWidth - 3); CodeRegionStart = BitUtils.AlignDown(address, RegionAlignment); @@ -237,7 +240,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory else { aliasRegion.Size = 0x1000000000; - heapRegion.Size = 0x180000000; + heapRegion.Size = memConfig == MemoryConfiguration.MemoryConfiguration12GiB ? 0x300000000u : 0x180000000u; stackRegion.Size = 0x80000000; tlsIoRegion.Size = 0x1000000000; CodeRegionStart = BitUtils.AlignDown(address, RegionAlignment); diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs index bea1f0ef3..b823d14f7 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs @@ -124,6 +124,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process KPageList pageList, KResourceLimit resourceLimit, MemoryRegion memRegion, + MemoryConfiguration memConfig, IProcessContextFactory contextFactory, ThreadStart customThreadStart = null) { @@ -153,6 +154,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process creationInfo.Flags, !creationInfo.Flags.HasFlag(ProcessCreationFlags.EnableAslr), memRegion, + memConfig, codeAddress, codeSize, slabManager); @@ -189,6 +191,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process ReadOnlySpan capabilities, KResourceLimit resourceLimit, MemoryRegion memRegion, + MemoryConfiguration memConfig, IProcessContextFactory contextFactory, ThreadStart customThreadStart = null) { @@ -252,6 +255,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process creationInfo.Flags, !creationInfo.Flags.HasFlag(ProcessCreationFlags.EnableAslr), memRegion, + memConfig, codeAddress, codeSize, slabManager); diff --git a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs index 005ac1452..260ff8af3 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs @@ -137,6 +137,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall capabilities, resourceLimit, memRegion, + _context.Device.Configuration.MemoryConfiguration, contextFactory, customThreadStart); @@ -888,7 +889,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall [Svc(1)] public Result SetHeapSize([PointerSized] out ulong address, [PointerSized] ulong size) { - if ((size & 0xfffffffe001fffff) != 0) + if ((size & 0xfffffffd001fffff) != 0) { address = 0; diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs index 5729052e8..7f0c6b3f5 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs @@ -189,7 +189,7 @@ namespace Ryujinx.HLE.Loaders.Processes codeAddress, codeSize); - result = process.InitializeKip(creationInfo, kip.Capabilities, pageList, context.ResourceLimit, memoryRegion, processContextFactory); + result = process.InitializeKip(creationInfo, kip.Capabilities, pageList, context.ResourceLimit, memoryRegion, context.Device.Configuration.MemoryConfiguration, processContextFactory); if (result != Result.Success) { Logger.Error?.Print(LogClass.Loader, $"Process initialization returned error \"{result}\"."); @@ -389,6 +389,7 @@ namespace Ryujinx.HLE.Loaders.Processes MemoryMarshal.Cast(npdm.KernelCapabilityData), resourceLimit, memoryRegion, + context.Device.Configuration.MemoryConfiguration, processContextFactory); if (result != Result.Success) From d4107ac05f696827c51ff1d120560ca3536cd8be Mon Sep 17 00:00:00 2001 From: GreemDev Date: Wed, 15 Oct 2025 21:51:13 -0500 Subject: [PATCH 2/9] UI: Add a startup flag to ignore new Amiibo file updates, useful for testing changes you intend on committing to Ryubing/Nfc. Flag is `--local-only-amiibo` --- .../UI/ViewModels/AmiiboWindowViewModel.cs | 54 ++++++++++++++++++- src/Ryujinx/Utilities/CommandLineState.cs | 5 ++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs index 51541b615..0ba071475 100644 --- a/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/AmiiboWindowViewModel.cs @@ -6,6 +6,7 @@ using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Models.Amiibo; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Windows; +using Ryujinx.Ava.Utilities; using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; @@ -250,6 +251,7 @@ namespace Ryujinx.Ava.UI.ViewModels catch (Exception exception) { Logger.Warning?.Print(LogClass.Application, $"Unable to read data from '{_amiiboJsonPath}': {exception}"); + localIsValid = false; } if (!localIsValid || await NeedsUpdate(amiiboJson.LastUpdated)) @@ -280,11 +282,59 @@ namespace Ryujinx.Ava.UI.ViewModels return amiiboJson; } + private async Task ReadLocalJsonFileAsync() + { + bool isValid = false; + AmiiboJson amiiboJson = new(); + + try + { + try + { + if (File.Exists(_amiiboJsonPath)) + { + isValid = TryGetAmiiboJson(await File.ReadAllTextAsync(_amiiboJsonPath), out amiiboJson); + } + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"Unable to read data from '{_amiiboJsonPath}': {exception}"); + isValid = false; + } + + if (!isValid) + { + return null; + } + } + catch (Exception exception) + { + if (!isValid) + { + Logger.Error?.Print(LogClass.Application, $"Couldn't get valid amiibo data: {exception}"); + + // Neither local file is not valid JSON, close window. + await ShowInfoDialog(); + Close(); + } + } + + return amiiboJson; + } + private async Task LoadContentAsync() { - AmiiboJson amiiboJson = await GetMostRecentAmiiboListOrDefaultJson(); + AmiiboJson? amiiboJson; - _amiiboList = amiiboJson.Amiibo.OrderBy(amiibo => amiibo.AmiiboSeries).ToList(); + if (CommandLineState.OnlyLocalAmiibo) + amiiboJson = await ReadLocalJsonFileAsync(); + else + amiiboJson = await GetMostRecentAmiiboListOrDefaultJson(); + + if (!amiiboJson.HasValue) + return; + + _amiiboList = amiiboJson.Value.Amiibo.OrderBy(amiibo => amiibo.AmiiboSeries).ToList(); ParseAmiiboData(); } diff --git a/src/Ryujinx/Utilities/CommandLineState.cs b/src/Ryujinx/Utilities/CommandLineState.cs index d7d113ebe..f156792a1 100644 --- a/src/Ryujinx/Utilities/CommandLineState.cs +++ b/src/Ryujinx/Utilities/CommandLineState.cs @@ -25,6 +25,7 @@ namespace Ryujinx.Ava.Utilities public static string LaunchApplicationId { get; private set; } public static bool StartFullscreenArg { get; private set; } public static bool HideAvailableUpdates { get; private set; } + public static bool OnlyLocalAmiibo { get; private set; } public static void ParseArguments(string[] args) { @@ -130,6 +131,10 @@ namespace Ryujinx.Ava.Utilities OverridePPTC = args[++i]; break; + case "-la": + case "--local-only-amiibo": + OnlyLocalAmiibo = true; + break; case "-m": case "--memory-manager-mode": if (i + 1 >= args.Length) From 7268acbfb4630d87ccc0ea9ab9ecb1753e1615ea Mon Sep 17 00:00:00 2001 From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app> Date: Thu, 16 Oct 2025 07:49:41 -0500 Subject: [PATCH 3/9] gdb: Do not skip CheckInterrupt when gdb stub is enabled (ryubing/ryujinx!169) See merge request ryubing/ryujinx!169 --- src/ARMeilleure/Instructions/NativeInterface.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ARMeilleure/Instructions/NativeInterface.cs b/src/ARMeilleure/Instructions/NativeInterface.cs index d43e20d83..3c2d9bb33 100644 --- a/src/ARMeilleure/Instructions/NativeInterface.cs +++ b/src/ARMeilleure/Instructions/NativeInterface.cs @@ -201,11 +201,7 @@ namespace ARMeilleure.Instructions ExecutionContext context = GetContext(); - // If debugging, we'll handle interrupts outside - if (!Optimizations.EnableDebugging) - { - context.CheckInterrupt(); - } + context.CheckInterrupt(); Statistics.ResumeTimer(); From fdbdb05cb583a1604ada310d160791bd945f758e Mon Sep 17 00:00:00 2001 From: GreemDev Date: Thu, 16 Oct 2025 12:23:01 -0500 Subject: [PATCH 4/9] misc: Update Ryujinx.LibHac --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 78f9acd59..1389b58a6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -40,7 +40,7 @@ - + From c33a97f01c631941512140f8dd583ce3029a790f Mon Sep 17 00:00:00 2001 From: GreemDev Date: Thu, 16 Oct 2025 17:32:04 -0500 Subject: [PATCH 5/9] gdb: Cleanup Debugger.cs by moving the GDB command handlers and command processor out of the class and into their own --- src/Ryujinx.HLE/Debugger/Debugger.cs | 1107 +---------------- .../Debugger/Gdb/CommandProcessor.cs | 393 ++++++ src/Ryujinx.HLE/Debugger/Gdb/Commands.cs | 489 ++++++++ src/Ryujinx.HLE/Debugger/Gdb/Registers.cs | 160 +++ .../{GdbXml => Gdb/Xml}/aarch64-core.xml | 0 .../{GdbXml => Gdb/Xml}/aarch64-fpu.xml | 0 .../Debugger/{GdbXml => Gdb/Xml}/arm-core.xml | 0 .../Debugger/{GdbXml => Gdb/Xml}/arm-neon.xml | 0 .../Debugger/{GdbXml => Gdb/Xml}/target32.xml | 0 .../Debugger/{GdbXml => Gdb/Xml}/target64.xml | 0 src/Ryujinx.HLE/Debugger/Helpers.cs | 50 + src/Ryujinx.HLE/Debugger/StringStream.cs | 2 +- src/Ryujinx.HLE/Ryujinx.HLE.csproj | 24 +- 13 files changed, 1150 insertions(+), 1075 deletions(-) create mode 100644 src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs create mode 100644 src/Ryujinx.HLE/Debugger/Gdb/Commands.cs create mode 100644 src/Ryujinx.HLE/Debugger/Gdb/Registers.cs rename src/Ryujinx.HLE/Debugger/{GdbXml => Gdb/Xml}/aarch64-core.xml (100%) rename src/Ryujinx.HLE/Debugger/{GdbXml => Gdb/Xml}/aarch64-fpu.xml (100%) rename src/Ryujinx.HLE/Debugger/{GdbXml => Gdb/Xml}/arm-core.xml (100%) rename src/Ryujinx.HLE/Debugger/{GdbXml => Gdb/Xml}/arm-neon.xml (100%) rename src/Ryujinx.HLE/Debugger/{GdbXml => Gdb/Xml}/target32.xml (100%) rename src/Ryujinx.HLE/Debugger/{GdbXml => Gdb/Xml}/target64.xml (100%) create mode 100644 src/Ryujinx.HLE/Debugger/Helpers.cs diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs index 2c935ca7f..e03f05b7f 100644 --- a/src/Ryujinx.HLE/Debugger/Debugger.cs +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -1,13 +1,9 @@ -using ARMeilleure.State; -using Ryujinx.Common; using Ryujinx.Common.Logging; -using Ryujinx.HLE.HOS.Kernel; +using Ryujinx.HLE.Debugger.Gdb; using Ryujinx.HLE.HOS.Kernel.Process; 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; @@ -15,6 +11,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; using IExecutionContext = Ryujinx.Cpu.IExecutionContext; +using static Ryujinx.HLE.Debugger.Helpers; namespace Ryujinx.HLE.Debugger { @@ -28,18 +25,18 @@ namespace Ryujinx.HLE.Debugger private Socket ClientSocket = null; private NetworkStream ReadStream = null; private NetworkStream WriteStream = null; - private BlockingCollection Messages = new BlockingCollection(1); + private BlockingCollection Messages = new(1); private Thread DebuggerThread; private Thread MessageHandlerThread; private bool _shuttingDown = false; - private ManualResetEventSlim _breakHandlerEvent = new ManualResetEventSlim(false); + private ManualResetEventSlim _breakHandlerEvent = new(false); - private ulong? cThread; - private ulong? gThread; + private GdbCommandProcessor CommandProcessor = null; - private BreakpointManager BreakpointManager; + internal ulong? CThread; + internal ulong? GThread; - private string previousThreadListXml = ""; + internal BreakpointManager BreakpointManager; public Debugger(Switch device, ushort port) { @@ -57,172 +54,24 @@ namespace Ryujinx.HLE.Debugger 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; - 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; + internal KThread[] GetThreads() => + DebugProcess.GetThreadUids().Select(x => DebugProcess.GetThread(x)).ToArray(); - 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; - } - } + internal bool IsProcessAarch32 => DebugProcess.GetThread(GThread.Value).Context.IsAarch32; private void MessageHandlerMain() { while (!_shuttingDown) { IMessage msg = Messages.Take(); - try { + try + { switch (msg) { case BreakInMessage: Logger.Notice.Print(LogClass.GdbStub, "Break-in requested"); - CommandInterrupt(); + CommandProcessor.Commands.CommandInterrupt(); break; case SendNackMessage: @@ -232,14 +81,14 @@ namespace Ryujinx.HLE.Debugger case CommandMessage { Command: var cmd }: Logger.Debug?.Print(LogClass.GdbStub, $"Received Command: {cmd}"); WriteStream.WriteByte((byte)'+'); - ProcessCommand(cmd); + CommandProcessor.Process(cmd); break; case ThreadBreakMessage { Context: var ctx }: DebugProcess.DebugStop(); - gThread = cThread = ctx.ThreadUid; + GThread = CThread = ctx.ThreadUid; _breakHandlerEvent.Set(); - Reply($"T05thread:{ctx.ThreadUid:x};"); + CommandProcessor.Commands.Reply($"T05thread:{ctx.ThreadUid:x};"); break; case KillMessage: @@ -257,834 +106,29 @@ namespace Ryujinx.HLE.Debugger } } - private void ProcessCommand(string cmd) + public string GetStackTrace() { - 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+;qXfer:threads:read+;vContSupported+"); - 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}"))}"); - break; - } - - if (ss.ConsumeRemaining("sThreadInfo")) - { - Reply("l"); - break; - } - - if (ss.ConsumePrefix("ThreadExtraInfo,")) - { - ulong? threadId = ss.ReadRemainingAsThreadUid(); - if (threadId == null) - { - ReplyError(); - break; - } - - if (DebugProcess.IsThreadPaused(DebugProcess.GetThread(threadId.Value))) - { - Reply(ToHex("Paused")); - } - else - { - Reply(ToHex("Running")); - } - 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 offset = 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 (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; - } - } - 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.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(""); - 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(); - return; - } - 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(); - return; - } - 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}"); - Reply(""); - break; - } - } - - 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); - } - - 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--) - { - 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 - }; - - // Note: We don't support signals yet. - 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); - } - - if (action == VContAction.Continue) { - defaultAction = action; - } else { - Logger.Warning?.Print(LogClass.GdbStub, $"Received vCont command with unsupported default action: {rawAction}"); - } - } - } - - bool hasError = false; - - foreach (var (threadUid, action) in threadActionMap) - { - if (action.Action == VContAction.Step) - { - var thread = DebugProcess.GetThread(threadUid); - if (!DebugProcess.DebugStep(thread)) { - hasError = true; - } - } - } - - // 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", `DebugProcess.DebugStep()` will continue and suspend other threads if needed, so we don't do anything here. - 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) - { - 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(); - sb.Append("\n"); - - foreach (var thread in GetThreads()) - { - string threadName = System.Security.SecurityElement.Escape(thread.GetThreadName()); - sb.Append($"{(DebugProcess.IsThreadPaused(thread) ? "Paused" : "Running")}\n"); - } - - sb.Append(""); - return sb.ToString(); - } - - void CommandQuery() - { - // GDB is performing initial contact. Stop everything. - DebugProcess.DebugStop(); - gThread = cThread = DebugProcess.GetThreadUids().First(); - 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) - { - if (cThread == null) - { - ReplyError(); - return; - } - - DebugProcess.GetThread(cThread.Value).Context.DebugPc = newPc.Value; - } - - DebugProcess.DebugContinue(); - } - - void CommandDetach() - { - BreakpointManager.ClearAll(); - 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) - { - var threads = GetThreads(); - if (threads.Length == 0) - { - ReplyError(); - return; - } - threadId = threads.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) - { - // 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(); - } - } - - 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 - { - gThread = cThread = thread.ThreadUid; - Reply($"T05thread:{thread.ThreadUid:x};"); - } - } - - private void CommandIsAlive(ulong? threadId) - { - if (GetThreads().Any(x => x.ThreadUid == threadId)) - { - ReplyOK(); - } - else - { - Reply("E00"); - } - } - - 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\nminidump\n", - "get info" => GetProcessInfo(), - "backtrace" => GetStackTrace(), - "bt" => GetStackTrace(), - "registers" => GetRegisters(), - "reg" => GetRegisters(), - "minidump" => GetMinidump(), - _ => $"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) + 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)); + return Process.Debugger.GetGuestStackTrace(DebugProcess.GetThread(GThread.Value)); } - private string GetRegisters() + public string GetRegisters() { - if (gThread == null) + 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)); + return Process.Debugger.GetCpuRegisterPrintout(DebugProcess.GetThread(GThread.Value)); } - private string GetMinidump() + public string GetMinidump() { var response = new StringBuilder(); response.AppendLine("=== Begin Minidump ===\n"); @@ -1120,7 +164,7 @@ namespace Ryujinx.HLE.Debugger return response.ToString(); } - private string GetProcessInfo() + public string GetProcessInfo() { try { @@ -1130,15 +174,19 @@ namespace Ryujinx.HLE.Debugger 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( + $" 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) @@ -1152,7 +200,7 @@ namespace Ryujinx.HLE.Debugger sb.AppendLine($" 0x{image.BaseAddress:x10} - 0x{endAddress:x10} {name}"); } } - + return sb.ToString(); } catch (Exception e) @@ -1162,22 +210,6 @@ namespace Ryujinx.HLE.Debugger } } - 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); @@ -1202,9 +234,11 @@ namespace Ryujinx.HLE.Debugger { Thread.Sleep(200); } + if (DebugProcess == null || GetThreads().Length == 0) { - Logger.Warning?.Print(LogClass.GdbStub, "Application is not running, cannot accept GDB client connection"); + Logger.Warning?.Print(LogClass.GdbStub, + "Application is not running, cannot accept GDB client connection"); ClientSocket.Close(); continue; } @@ -1212,6 +246,7 @@ namespace Ryujinx.HLE.Debugger ClientSocket.NoDelay = true; ReadStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Read); WriteStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Write); + CommandProcessor = new GdbCommandProcessor(ListenerSocket, ClientSocket, ReadStream, WriteStream, this); Logger.Notice.Print(LogClass.GdbStub, "GDB client connected"); while (true) @@ -1221,7 +256,7 @@ namespace Ryujinx.HLE.Debugger switch (ReadStream.ReadByte()) { case -1: - goto eof; + goto EndOfLoop; case '+': continue; case '-': @@ -1236,7 +271,7 @@ namespace Ryujinx.HLE.Debugger { int x = ReadStream.ReadByte(); if (x == -1) - goto eof; + goto EndOfLoop; if (x == '#') break; cmd += (char)x; @@ -1257,11 +292,11 @@ namespace Ryujinx.HLE.Debugger } catch (IOException) { - goto eof; + goto EndOfLoop; } } - eof: + EndOfLoop: Logger.Notice.Print(LogClass.GdbStub, "GDB client lost connection"); ReadStream.Close(); ReadStream = null; @@ -1274,58 +309,6 @@ namespace Ryujinx.HLE.Debugger } } - private byte CalculateChecksum(string cmd) - { - byte checksum = 0; - foreach (char x in cmd) - { - unchecked - { - checksum += (byte)x; - } - } - - 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}")); - } - - 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); @@ -1358,7 +341,7 @@ namespace Ryujinx.HLE.Debugger 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 + // Wait for the process to stop before returning to avoid BreakHandler being called multiple times from the same breakpoint _breakHandlerEvent.Wait(5000); } diff --git a/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs b/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs new file mode 100644 index 000000000..19b3b7a2b --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs @@ -0,0 +1,393 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using System.Linq; +using System.Net.Sockets; +using System.Text; + +namespace Ryujinx.HLE.Debugger.Gdb +{ + class GdbCommandProcessor + { + public readonly GdbCommands Commands; + + public GdbCommandProcessor(TcpListener listenerSocket, Socket clientSocket, NetworkStream readStream, NetworkStream writeStream, Debugger debugger) + { + Commands = new GdbCommands(listenerSocket, clientSocket, readStream, writeStream, debugger); + } + + private string previousThreadListXml = ""; + + public void Process(string cmd) + { + StringStream ss = new(cmd); + + switch (ss.ReadChar()) + { + case '!': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + // Enable extended mode + Commands.ReplyOK(); + break; + case '?': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + Commands.CommandQuery(); + break; + case 'c': + Commands.CommandContinue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + break; + case 'D': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + Commands.CommandDetach(); + break; + case 'g': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + Commands.CommandReadRegisters(); + break; + case 'G': + Commands.CommandWriteRegisters(ss); + break; + case 'H': + { + char op = ss.ReadChar(); + ulong? threadId = ss.ReadRemainingAsThreadUid(); + Commands.CommandSetThread(op, threadId); + break; + } + case 'k': + Logger.Notice.Print(LogClass.GdbStub, "Kill request received, detach instead"); + Commands.Reply(""); + Commands.CommandDetach(); + break; + case 'm': + { + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadRemainingAsHex(); + Commands.CommandReadMemory(addr, len); + break; + } + case 'M': + { + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadUntilAsHex(':'); + Commands.CommandWriteMemory(addr, len, ss); + break; + } + case 'p': + { + ulong gdbRegId = ss.ReadRemainingAsHex(); + Commands.CommandReadRegister((int)gdbRegId); + break; + } + case 'P': + { + ulong gdbRegId = ss.ReadUntilAsHex('='); + Commands.CommandWriteRegister((int)gdbRegId, ss); + break; + } + case 'q': + if (ss.ConsumeRemaining("GDBServerVersion")) + { + Commands.Reply($"name:Ryujinx;version:{ReleaseInformation.Version};"); + break; + } + + if (ss.ConsumeRemaining("HostInfo")) + { + if (Commands.Debugger.IsProcessAarch32) + { + Commands.Reply( + $"triple:{Helpers.ToHex("arm-unknown-linux-android")};endian:little;ptrsize:4;hostname:{Helpers.ToHex("Ryujinx")};"); + } + else + { + Commands.Reply( + $"triple:{Helpers.ToHex("aarch64-unknown-linux-android")};endian:little;ptrsize:8;hostname:{Helpers.ToHex("Ryujinx")};"); + } + + break; + } + + if (ss.ConsumeRemaining("ProcessInfo")) + { + if (Commands.Debugger.IsProcessAarch32) + { + Commands.Reply( + $"pid:1;cputype:12;cpusubtype:0;triple:{Helpers.ToHex("arm-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:4;"); + } + else + { + Commands.Reply( + $"pid:1;cputype:100000c;cpusubtype:0;triple:{Helpers.ToHex("aarch64-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:8;"); + } + + break; + } + + if (ss.ConsumePrefix("Supported:") || ss.ConsumeRemaining("Supported")) + { + Commands.Reply("PacketSize=10000;qXfer:features:read+;qXfer:threads:read+;vContSupported+"); + break; + } + + if (ss.ConsumePrefix("Rcmd,")) + { + string hexCommand = ss.ReadRemaining(); + Commands.HandleQRcmdCommand(hexCommand); + break; + } + + if (ss.ConsumeRemaining("fThreadInfo")) + { + Commands. Reply($"m{string.Join(",", Commands.Debugger.DebugProcess.GetThreadUids().Select(x => $"{x:x}"))}"); + break; + } + + if (ss.ConsumeRemaining("sThreadInfo")) + { + Commands.Reply("l"); + break; + } + + if (ss.ConsumePrefix("ThreadExtraInfo,")) + { + ulong? threadId = ss.ReadRemainingAsThreadUid(); + if (threadId == null) + { + Commands.ReplyError(); + break; + } + + Commands.Reply(Helpers.ToHex( + Commands.Debugger.DebugProcess.IsThreadPaused( + Commands.Debugger.DebugProcess.GetThread(threadId.Value)) + ? "Paused" + : "Running" + ) + ); + + 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) + { + Commands.Reply("l"); + break; + } + + if (len >= (ulong)data.Length - offset) + { + Commands.Reply("l" + Helpers.ToBinaryFormat(data.Substring((int)offset))); + break; + } + else + { + Commands.Reply("m" + Helpers.ToBinaryFormat(data.Substring((int)offset, (int)len))); + break; + } + } + + if (ss.ConsumePrefix("Xfer:features:read:")) + { + string feature = ss.ReadUntil(':'); + ulong offset = ss.ReadUntilAsHex(','); + ulong len = ss.ReadRemainingAsHex(); + + if (feature == "target.xml") + { + feature = Commands.Debugger.IsProcessAarch32 ? "target32.xml" : "target64.xml"; + } + + string data; + if (RegisterInformation.Features.TryGetValue(feature, out data)) + { + if (offset >= (ulong)data.Length) + { + Commands.Reply("l"); + break; + } + + if (len >= (ulong)data.Length - offset) + { + Commands.Reply("l" + Helpers.ToBinaryFormat(data.Substring((int)offset))); + break; + } + else + { + Commands.Reply("m" + Helpers.ToBinaryFormat(data.Substring((int)offset, (int)len))); + break; + } + } + else + { + Commands.Reply("E00"); // Invalid annex + break; + } + } + + goto unknownCommand; + case 'Q': + goto unknownCommand; + case 's': + Commands.CommandStep(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + break; + case 'T': + { + ulong? threadId = ss.ReadRemainingAsThreadUid(); + Commands.CommandIsAlive(threadId); + break; + } + case 'v': + if (ss.ConsumePrefix("Cont")) + { + if (ss.ConsumeRemaining("?")) + { + Commands.Reply("vCont;c;C;s;S"); + break; + } + + if (ss.ConsumePrefix(";")) + { + Commands.HandleVContCommand(ss); + break; + } + + goto unknownCommand; + } + + if (ss.ConsumeRemaining("MustReplyEmpty")) + { + Commands.Reply(""); + 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}"); + Commands.ReplyError(); + return; + } + + switch (type) + { + case "0": // Software breakpoint + if (!Commands.Debugger.BreakpointManager.SetBreakPoint(addr, len, false)) + { + Commands.ReplyError(); + return; + } + + Commands.ReplyOK(); + return; + case "1": // Hardware breakpoint + case "2": // Write watchpoint + case "3": // Read watchpoint + case "4": // Access watchpoint + Commands.ReplyError(); + return; + default: + Commands. 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}"); + Commands.ReplyError(); + return; + } + + switch (type) + { + case "0": // Software breakpoint + if (!Commands.Debugger.BreakpointManager.ClearBreakPoint(addr, len)) + { + Commands.ReplyError(); + return; + } + + Commands.ReplyOK(); + return; + case "1": // Hardware breakpoint + case "2": // Write watchpoint + case "3": // Read watchpoint + case "4": // Access watchpoint + Commands.ReplyError(); + return; + default: + Commands.ReplyError(); + return; + } + } + default: + unknownCommand: + Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}"); + Commands.Reply(""); + break; + } + } + + private string GetThreadListXml() + { + var sb = new StringBuilder(); + sb.Append("\n"); + + foreach (var thread in Commands.Debugger.GetThreads()) + { + string threadName = System.Security.SecurityElement.Escape(thread.GetThreadName()); + sb.Append( + $"{(Commands.Debugger.DebugProcess.IsThreadPaused(thread) ? "Paused" : "Running")}\n"); + } + + sb.Append(""); + return sb.ToString(); + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs b/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs new file mode 100644 index 000000000..6c0a258a0 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs @@ -0,0 +1,489 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; + +namespace Ryujinx.HLE.Debugger.Gdb +{ + class GdbCommands + { + const int GdbRegisterCount64 = 68; + const int GdbRegisterCount32 = 66; + + public readonly Debugger Debugger; + + private readonly TcpListener _listenerSocket; + private readonly Socket _clientSocket; + private readonly NetworkStream _readStream; + private readonly NetworkStream _writeStream; + + + public GdbCommands(TcpListener listenerSocket, Socket clientSocket, NetworkStream readStream, + NetworkStream writeStream, Debugger debugger) + { + _listenerSocket = listenerSocket; + _clientSocket = clientSocket; + _readStream = readStream; + _writeStream = writeStream; + Debugger = debugger; + } + + public void Reply(string cmd) + { + Logger.Debug?.Print(LogClass.GdbStub, $"Reply: {cmd}"); + _writeStream.Write(Encoding.ASCII.GetBytes($"${cmd}#{Helpers.CalculateChecksum(cmd):x2}")); + } + + public void ReplyOK() => Reply("OK"); + + public void ReplyError() => Reply("E01"); + + internal void CommandQuery() + { + // GDB is performing initial contact. Stop everything. + Debugger.DebugProcess.DebugStop(); + Debugger.GThread = Debugger.CThread = Debugger.DebugProcess.GetThreadUids().First(); + Reply($"T05thread:{Debugger.CThread:x};"); + } + + internal void CommandInterrupt() + { + // GDB is requesting an interrupt. Stop everything. + Debugger.DebugProcess.DebugStop(); + if (Debugger.GThread == null || Debugger.GetThreads().All(x => x.ThreadUid != Debugger.GThread.Value)) + { + Debugger.GThread = Debugger.CThread = Debugger.DebugProcess.GetThreadUids().First(); + } + + Reply($"T02thread:{Debugger.GThread:x};"); + } + + internal void CommandContinue(ulong? newPc) + { + if (newPc.HasValue) + { + if (Debugger.CThread == null) + { + ReplyError(); + return; + } + + Debugger.DebugProcess.GetThread(Debugger.CThread.Value).Context.DebugPc = newPc.Value; + } + + Debugger.DebugProcess.DebugContinue(); + } + + internal void CommandDetach() + { + Debugger.BreakpointManager.ClearAll(); + CommandContinue(null); + } + + internal void CommandReadRegisters() + { + if (Debugger.GThread == null) + { + ReplyError(); + return; + } + + var ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; + string registers = ""; + if (Debugger.IsProcessAarch32) + { + for (int i = 0; i < GdbRegisterCount32; i++) + { + registers += GdbRegisters.Read32(ctx, i); + } + } + else + { + for (int i = 0; i < GdbRegisterCount64; i++) + { + registers += GdbRegisters.Read64(ctx, i); + } + } + + Reply(registers); + } + + internal void CommandWriteRegisters(StringStream ss) + { + if (Debugger.GThread == null) + { + ReplyError(); + return; + } + + var ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; + if (Debugger.IsProcessAarch32) + { + for (int i = 0; i < GdbRegisterCount32; i++) + { + if (!GdbRegisters.Write32(ctx, i, ss)) + { + ReplyError(); + return; + } + } + } + else + { + for (int i = 0; i < GdbRegisterCount64; i++) + { + if (!GdbRegisters.Write64(ctx, i, ss)) + { + ReplyError(); + return; + } + } + } + + if (ss.IsEmpty()) + { + ReplyOK(); + } + else + { + ReplyError(); + } + } + + internal void CommandSetThread(char op, ulong? threadId) + { + if (threadId is 0 or null) + { + var threads = Debugger.GetThreads(); + if (threads.Length == 0) + { + ReplyError(); + return; + } + + threadId = threads.First().ThreadUid; + } + + if (Debugger.DebugProcess.GetThread(threadId.Value) == null) + { + ReplyError(); + return; + } + + switch (op) + { + case 'c': + Debugger.CThread = threadId; + ReplyOK(); + return; + case 'g': + Debugger.GThread = threadId; + ReplyOK(); + return; + default: + ReplyError(); + return; + } + } + + internal void CommandReadMemory(ulong addr, ulong len) + { + try + { + var data = new byte[len]; + Debugger.DebugProcess.CpuMemory.Read(addr, data); + Reply(Helpers.ToHex(data)); + } + 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(); + } + } + + internal 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); + } + + Debugger.DebugProcess.CpuMemory.Write(addr, data); + Debugger.DebugProcess.InvalidateCacheRegion(addr, len); + ReplyOK(); + } + catch (InvalidMemoryRegionException) + { + ReplyError(); + } + } + + internal void CommandReadRegister(int gdbRegId) + { + if (Debugger.GThread == null) + { + ReplyError(); + return; + } + + var ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; + string result; + if (Debugger.IsProcessAarch32) + { + result = GdbRegisters.Read32(ctx, gdbRegId); + if (result != null) + { + Reply(result); + } + else + { + ReplyError(); + } + } + else + { + result = GdbRegisters.Read64(ctx, gdbRegId); + if (result != null) + { + Reply(result); + } + else + { + ReplyError(); + } + } + } + + internal void CommandWriteRegister(int gdbRegId, StringStream ss) + { + if (Debugger.GThread == null) + { + ReplyError(); + return; + } + + var ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; + if (Debugger.IsProcessAarch32) + { + if (GdbRegisters.Write32(ctx, gdbRegId, ss) && ss.IsEmpty()) + { + ReplyOK(); + } + else + { + ReplyError(); + } + } + else + { + if (GdbRegisters.Write64(ctx, gdbRegId, ss) && ss.IsEmpty()) + { + ReplyOK(); + } + else + { + ReplyError(); + } + } + } + + internal void CommandStep(ulong? newPc) + { + if (Debugger.CThread == null) + { + ReplyError(); + return; + } + + var thread = Debugger.DebugProcess.GetThread(Debugger.CThread.Value); + + if (newPc.HasValue) + { + thread.Context.DebugPc = newPc.Value; + } + + if (!Debugger.DebugProcess.DebugStep(thread)) + { + ReplyError(); + } + else + { + Debugger.GThread = Debugger.CThread = thread.ThreadUid; + Reply($"T05thread:{thread.ThreadUid:x};"); + } + } + + internal void CommandIsAlive(ulong? threadId) + { + if (Debugger.GetThreads().Any(x => x.ThreadUid == threadId)) + { + ReplyOK(); + } + else + { + Reply("E00"); + } + } + + enum VContAction + { + None, + Continue, + Stop, + Step + } + + record VContPendingAction(VContAction Action, ushort? Signal = null); + + internal void HandleVContCommand(StringStream ss) + { + string[] rawActions = ss.ReadRemaining().Split(';', StringSplitOptions.RemoveEmptyEntries); + + var threadActionMap = new Dictionary(); + foreach (var thread in Debugger.GetThreads()) + { + 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--) + { + var rawAction = rawActions[i]; + var stream = new StringStream(rawAction); + + char cmd = stream.ReadChar(); + VContAction action = cmd switch + { + 'c' or 'C' => VContAction.Continue, + 's' or 'S' => VContAction.Step, + 't' => VContAction.Stop, + _ => VContAction.None + }; + + // Note: We don't support signals yet. + ushort? signal = null; + if (cmd is 'C' or '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); + } + + if (action == VContAction.Continue) + { + defaultAction = action; + } + else + { + Logger.Warning?.Print(LogClass.GdbStub, + $"Received vCont command with unsupported default action: {rawAction}"); + } + } + } + + bool hasError = false; + + foreach (var (threadUid, action) in threadActionMap) + { + if (action.Action == VContAction.Step) + { + var thread = Debugger.DebugProcess.GetThread(threadUid); + if (!Debugger.DebugProcess.DebugStep(thread)) + { + hasError = true; + } + } + } + + // 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", `DebugProcess.DebugStep()` will continue and suspend other threads if needed, so we don't do anything here. + if (threadActionMap.Values.All(a => a.Action == VContAction.Continue)) + { + Debugger.DebugProcess.DebugContinue(); + } + else if (defaultAction == VContAction.None) + { + foreach (var (threadUid, action) in threadActionMap) + { + if (action.Action == VContAction.Continue) + { + Debugger.DebugProcess.DebugContinue(Debugger.DebugProcess.GetThread(threadUid)); + } + } + } + + if (hasError) + { + ReplyError(); + } + else + { + ReplyOK(); + } + + foreach (var (threadUid, action) in threadActionMap) + { + if (action.Action == VContAction.Step) + { + Debugger.GThread = Debugger.CThread = threadUid; + Reply($"T05thread:{threadUid:x};"); + } + } + } + + internal void HandleQRcmdCommand(string hexCommand) + { + try + { + string command = Helpers.FromHex(hexCommand); + Logger.Debug?.Print(LogClass.GdbStub, $"Received Rcmd: {command}"); + + string response = command.Trim().ToLowerInvariant() switch + { + "help" => "backtrace\nbt\nregisters\nreg\nget info\nminidump\n", + "get info" => Debugger.GetProcessInfo(), + "backtrace" or "bt" => Debugger.GetStackTrace(), + "registers" or "reg" => Debugger.GetRegisters(), + "minidump" => Debugger.GetMinidump(), + _ => $"Unknown command: {command}\n" + }; + + Reply(Helpers.ToHex(response)); + } + catch (Exception e) + { + Logger.Error?.Print(LogClass.GdbStub, $"Error processing Rcmd: {e.Message}"); + ReplyError(); + } + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/Gdb/Registers.cs b/src/Ryujinx.HLE/Debugger/Gdb/Registers.cs new file mode 100644 index 000000000..de2f6c25d --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Gdb/Registers.cs @@ -0,0 +1,160 @@ +using ARMeilleure.State; +using Ryujinx.Cpu; +using System; + +namespace Ryujinx.HLE.Debugger.Gdb +{ + static class GdbRegisters + { + /* + FPCR = FPSR & ~FpcrMask + All of FPCR's bits are reserved in FPCR and vice versa, + see ARM's documentation. + */ + private const uint FpcrMask = 0xfc1fffff; + + public static string Read64(IExecutionContext state, int gdbRegId) + { + switch (gdbRegId) + { + case >= 0 and <= 31: + return Helpers.ToHex(BitConverter.GetBytes(state.GetX(gdbRegId))); + case 32: + return Helpers.ToHex(BitConverter.GetBytes(state.DebugPc)); + case 33: + return Helpers.ToHex(BitConverter.GetBytes(state.Pstate)); + case >= 34 and <= 65: + return Helpers.ToHex(state.GetV(gdbRegId - 34).ToArray()); + case 66: + return Helpers.ToHex(BitConverter.GetBytes((uint)state.Fpsr)); + case 67: + return Helpers.ToHex(BitConverter.GetBytes((uint)state.Fpcr)); + default: + return null; + } + } + + public static bool Write64(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; + } + } + + public static string Read32(IExecutionContext state, int gdbRegId) + { + switch (gdbRegId) + { + case >= 0 and <= 14: + return Helpers.ToHex(BitConverter.GetBytes((uint)state.GetX(gdbRegId))); + case 15: + return Helpers.ToHex(BitConverter.GetBytes((uint)state.DebugPc)); + case 16: + return Helpers.ToHex(BitConverter.GetBytes((uint)state.Pstate)); + case >= 17 and <= 32: + return Helpers.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 Helpers.ToHex(BitConverter.GetBytes(value)); + case 65: + uint fpscr = (uint)state.Fpsr | (uint)state.Fpcr; + return Helpers.ToHex(BitConverter.GetBytes(fpscr)); + default: + return null; + } + } + + public static bool Write32(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; + } + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml b/src/Ryujinx.HLE/Debugger/Gdb/Xml/aarch64-core.xml similarity index 100% rename from src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml rename to src/Ryujinx.HLE/Debugger/Gdb/Xml/aarch64-core.xml diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml b/src/Ryujinx.HLE/Debugger/Gdb/Xml/aarch64-fpu.xml similarity index 100% rename from src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml rename to src/Ryujinx.HLE/Debugger/Gdb/Xml/aarch64-fpu.xml diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml b/src/Ryujinx.HLE/Debugger/Gdb/Xml/arm-core.xml similarity index 100% rename from src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml rename to src/Ryujinx.HLE/Debugger/Gdb/Xml/arm-core.xml diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml b/src/Ryujinx.HLE/Debugger/Gdb/Xml/arm-neon.xml similarity index 100% rename from src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml rename to src/Ryujinx.HLE/Debugger/Gdb/Xml/arm-neon.xml diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/target32.xml b/src/Ryujinx.HLE/Debugger/Gdb/Xml/target32.xml similarity index 100% rename from src/Ryujinx.HLE/Debugger/GdbXml/target32.xml rename to src/Ryujinx.HLE/Debugger/Gdb/Xml/target32.xml diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/target64.xml b/src/Ryujinx.HLE/Debugger/Gdb/Xml/target64.xml similarity index 100% rename from src/Ryujinx.HLE/Debugger/GdbXml/target64.xml rename to src/Ryujinx.HLE/Debugger/Gdb/Xml/target64.xml diff --git a/src/Ryujinx.HLE/Debugger/Helpers.cs b/src/Ryujinx.HLE/Debugger/Helpers.cs new file mode 100644 index 000000000..a2b802525 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Helpers.cs @@ -0,0 +1,50 @@ +using Gommon; +using System; +using System.Linq; +using System.Text; + +namespace Ryujinx.HLE.Debugger +{ + public static class Helpers + { + public static byte CalculateChecksum(string cmd) + { + byte checksum = 0; + foreach (char x in cmd) + { + unchecked + { + checksum += (byte)x; + } + } + + return checksum; + } + + public static string FromHex(string hexString) + { + if (string.IsNullOrEmpty(hexString)) + return string.Empty; + + byte[] bytes = Convert.FromHexString(hexString); + return Encoding.ASCII.GetString(bytes); + } + + public static string ToHex(byte[] bytes) => string.Join("", bytes.Select(x => $"{x:x2}")); + + public static string ToHex(string str) => ToHex(Encoding.ASCII.GetBytes(str)); + + public static string ToBinaryFormat(string str) => ToBinaryFormat(Encoding.ASCII.GetBytes(str)); + public static string ToBinaryFormat(byte[] bytes) => + bytes.Select(x => + x switch + { + (byte)'#' => "}\x03", + (byte)'$' => "}\x04", + (byte)'*' => "}\x0a", + (byte)'}' => "}\x5d", + _ => Convert.ToChar(x).ToString(), + } + ).JoinToString(string.Empty); + } +} diff --git a/src/Ryujinx.HLE/Debugger/StringStream.cs b/src/Ryujinx.HLE/Debugger/StringStream.cs index d8148a9c2..bc422f51f 100644 --- a/src/Ryujinx.HLE/Debugger/StringStream.cs +++ b/src/Ryujinx.HLE/Debugger/StringStream.cs @@ -3,7 +3,7 @@ using System.Globalization; namespace Ryujinx.HLE.Debugger { - class StringStream + internal class StringStream { private readonly string Data; private int Position; diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index 1938796e8..7e4c8a9e1 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -33,12 +33,12 @@ - - - - - - + + + + + + @@ -48,12 +48,12 @@ - - - - - - + + + + + + From 2b159dbca8f8017304f816b5f27d0f109074df07 Mon Sep 17 00:00:00 2001 From: Bluey Enjoyer Date: Fri, 17 Oct 2025 01:43:56 +0100 Subject: [PATCH 6/9] AHHHHHHHHHHHHHH (ryubing/ryujinx!170) See merge request ryubing/ryujinx!170 --- docs/compatibility.csv | 1 + src/Ryujinx.Common/TitleIDs.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/compatibility.csv b/docs/compatibility.csv index a75a50f42..2efd1d0d5 100644 --- a/docs/compatibility.csv +++ b/docs/compatibility.csv @@ -2280,6 +2280,7 @@ 01008F6008C5E000,"Pokémon™ Violet",gpu;nvdec;ldn-works;amd-vendor-bug;mac-bug,ingame,2024-07-30 02:51:48 0100187003A36000,"Pokémon™: Let’s Go, Eevee!",crash;nvdec;online-broken;ldn-broken,ingame,2024-06-01 15:03:04 010003F003A34000,"Pokémon™: Let’s Go, Pikachu!",crash;nvdec;online-broken;ldn-broken,ingame,2024-03-15 07:55:41 +0100F43008C44000,"Pokémon Legends: Z-A",gpu;crash;ldn-broken,ingame,2025-10-16 19:13:00 0100B3F000BE2000,"Pokkén Tournament™ DX",nvdec;ldn-works;opengl-backend-bug;LAN;amd-vendor-bug;intel-vendor-bug,playable,2024-07-18 23:11:08 010030D005AE6000,"Pokkén Tournament™ DX Demo",demo;opengl-backend-bug,playable,2022-08-10 12:03:19 0100A3500B4EC000,"Polandball: Can Into Space",,playable,2020-06-25 15:13:26 diff --git a/src/Ryujinx.Common/TitleIDs.cs b/src/Ryujinx.Common/TitleIDs.cs index 793848d6d..f77a2858f 100644 --- a/src/Ryujinx.Common/TitleIDs.cs +++ b/src/Ryujinx.Common/TitleIDs.cs @@ -106,6 +106,7 @@ namespace Ryujinx.Common "0100b3f000be2000", // Pokkén Tournament DX "0100187003a36000", // Pokémon: Let's Go Eevee! "010003f003a34000", // Pokémon: Let's Go Pikachu! + "0100f43008c44000", // Pokémon Legends: Z-A //Splatoon Franchise "0100f8f0000a2000", // Splatoon 2 (EU) From 9aacf9b37ba3c5774b47fd57080237d4df1c59e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hack=E8=8C=B6=E3=82=93?= Date: Thu, 16 Oct 2025 19:45:14 -0500 Subject: [PATCH 7/9] Update Korean translation (ryubing/ryujinx!168) See merge request ryubing/ryujinx!168 --- assets/locales.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/locales.json b/assets/locales.json index 3a5b45f5f..31a3242b8 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -3629,7 +3629,7 @@ "he_IL": "ממשק קלאסי (הפעלה מחדש דרושה)", "it_IT": "Interfaccia classica (Riavvio necessario)", "ja_JP": "クラシックインターフェース(再起動必要)", - "ko_KR": "클래식 인터페이스 (재시작 필요)", + "ko_KR": "클래식 인터페이스(다시 시작 필요)", "no_NO": "Klassisk grensesnitt (Krever omstart)", "pl_PL": "Klasyczny interfejs (Wymaga restartu)", "pt_BR": "Interface Clássica (Reinício necessário)", From 8e941e4a8f5b73878f03fd1a9eca06eaa4defde5 Mon Sep 17 00:00:00 2001 From: Coxxs <58-coxxs@users.noreply.git.ryujinx.app> Date: Thu, 16 Oct 2025 19:53:51 -0500 Subject: [PATCH 8/9] gdb: Cleanup (ryubing/ryujinx!171) See merge request ryubing/ryujinx!171 --- src/Ryujinx.HLE/Debugger/BreakpointManager.cs | 33 ++----------------- src/Ryujinx.HLE/Debugger/Debugger.cs | 5 +++ .../Debugger/Gdb/CommandProcessor.cs | 4 +-- .../Debugger/RegisterInformation.cs | 2 +- 4 files changed, 11 insertions(+), 33 deletions(-) diff --git a/src/Ryujinx.HLE/Debugger/BreakpointManager.cs b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs index bf462a781..c660b298d 100644 --- a/src/Ryujinx.HLE/Debugger/BreakpointManager.cs +++ b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs @@ -11,12 +11,9 @@ namespace Ryujinx.HLE.Debugger { public byte[] OriginalData { get; } - public bool IsStep { get; } - - public Breakpoint(byte[] originalData, bool isStep) + public Breakpoint(byte[] originalData) { OriginalData = originalData; - IsStep = isStep; } } @@ -44,7 +41,7 @@ namespace Ryujinx.HLE.Debugger /// 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) + public bool SetBreakPoint(ulong address, ulong length) { if (_breakpoints.ContainsKey(address)) { @@ -71,7 +68,7 @@ namespace Ryujinx.HLE.Debugger return false; } - var breakpoint = new Breakpoint(originalInstruction, isStep); + var breakpoint = new Breakpoint(originalInstruction); if (_breakpoints.TryAdd(address, breakpoint)) { Logger.Debug?.Print(LogClass.GdbStub, $"Breakpoint set at 0x{address:X16}"); @@ -124,30 +121,6 @@ namespace Ryujinx.HLE.Debugger 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) diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs index e03f05b7f..cc64a38eb 100644 --- a/src/Ryujinx.HLE/Debugger/Debugger.cs +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -103,6 +103,10 @@ namespace Ryujinx.HLE.Debugger { Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e); } + catch (ObjectDisposedException e) + { + Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e); + } } } @@ -304,6 +308,7 @@ namespace Ryujinx.HLE.Debugger WriteStream = null; ClientSocket.Close(); ClientSocket = null; + CommandProcessor = null; BreakpointManager.ClearAll(); } diff --git a/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs b/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs index 19b3b7a2b..e9986647d 100644 --- a/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs +++ b/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs @@ -310,7 +310,7 @@ namespace Ryujinx.HLE.Debugger.Gdb switch (type) { case "0": // Software breakpoint - if (!Commands.Debugger.BreakpointManager.SetBreakPoint(addr, len, false)) + if (!Commands.Debugger.BreakpointManager.SetBreakPoint(addr, len)) { Commands.ReplyError(); return; @@ -325,7 +325,7 @@ namespace Ryujinx.HLE.Debugger.Gdb Commands.ReplyError(); return; default: - Commands. ReplyError(); + Commands.ReplyError(); return; } } diff --git a/src/Ryujinx.HLE/Debugger/RegisterInformation.cs b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs index b5fd88ea5..b43899271 100644 --- a/src/Ryujinx.HLE/Debugger/RegisterInformation.cs +++ b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs @@ -17,7 +17,7 @@ namespace Ryujinx.HLE.Debugger private static string GetEmbeddedResourceContent(string resourceName) { - Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.HLE.Debugger.GdbXml." + resourceName); + Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.HLE.Debugger.Gdb.Xml." + resourceName); StreamReader reader = new StreamReader(stream); string result = reader.ReadToEnd(); reader.Dispose(); From 2a2ab523cbb8661a3cac81021c4893ead2e45f08 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Fri, 17 Oct 2025 00:09:51 -0500 Subject: [PATCH 9/9] gdb: Code cleanup pass #2 Moved the reply functionality into the command processor, move the main debugger thread into a dedicated class part, and more --- src/Ryujinx.HLE/Debugger/BreakpointManager.cs | 8 +- .../Debugger/Debugger.MainThread.cs | 115 ++++++++ src/Ryujinx.HLE/Debugger/Debugger.cs | 172 +++--------- .../Debugger/Gdb/CommandProcessor.cs | 216 ++++++++------- src/Ryujinx.HLE/Debugger/Gdb/Commands.cs | 246 ++++++++---------- src/Ryujinx.HLE/Debugger/Gdb/Registers.cs | 4 +- src/Ryujinx.HLE/Debugger/Helpers.cs | 2 +- .../Debugger/RegisterInformation.cs | 2 +- .../HOS/Kernel/Process/KProcess.cs | 24 +- 9 files changed, 391 insertions(+), 398 deletions(-) create mode 100644 src/Ryujinx.HLE/Debugger/Debugger.MainThread.cs diff --git a/src/Ryujinx.HLE/Debugger/BreakpointManager.cs b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs index c660b298d..7f45fbf8b 100644 --- a/src/Ryujinx.HLE/Debugger/BreakpointManager.cs +++ b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs @@ -25,9 +25,9 @@ namespace Ryujinx.HLE.Debugger 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 }; + 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) { @@ -123,7 +123,7 @@ namespace Ryujinx.HLE.Debugger private byte[] GetBreakInstruction(ulong length) { - if (_debugger.IsProcessAarch32) + if (_debugger.IsProcess32Bit) { if (length == 2) { diff --git a/src/Ryujinx.HLE/Debugger/Debugger.MainThread.cs b/src/Ryujinx.HLE/Debugger/Debugger.MainThread.cs new file mode 100644 index 000000000..2e1f40120 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Debugger.MainThread.cs @@ -0,0 +1,115 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.Debugger.Gdb; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Ryujinx.HLE.Debugger +{ + public partial class Debugger + { + private void DebuggerThreadMain() + { + IPEndPoint endpoint = new(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 || GetThreads().Length == 0) && retries-- > 0) + { + Thread.Sleep(200); + } + + if (DebugProcess == null || GetThreads().Length == 0) + { + 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); + _commands = new GdbCommands(_listenerSocket, _clientSocket, _readStream, _writeStream, this); + _commandProcessor = _commands.CreateProcessor(); + + Logger.Notice.Print(LogClass.GdbStub, "GDB client connected"); + + while (true) + { + try + { + switch (_readStream.ReadByte()) + { + case -1: + goto EndOfLoop; + 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 EndOfLoop; + if (x == '#') + break; + cmd += (char)x; + } + + string checksum = $"{(char)_readStream.ReadByte()}{(char)_readStream.ReadByte()}"; + if (checksum == $"{Helpers.CalculateChecksum(cmd):x2}") + { + _messages.Add(new CommandMessage(cmd)); + } + else + { + _messages.Add(new SendNackMessage()); + } + + break; + } + } + catch (IOException) + { + goto EndOfLoop; + } + } + + EndOfLoop: + Logger.Notice.Print(LogClass.GdbStub, "GDB client lost connection"); + _readStream.Close(); + _readStream = null; + _writeStream.Close(); + _writeStream = null; + _clientSocket.Close(); + _clientSocket = null; + _commandProcessor = null; + _commands = null; + + BreakpointManager.ClearAll(); + } + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs index cc64a38eb..1c28870b7 100644 --- a/src/Ryujinx.HLE/Debugger/Debugger.cs +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -6,37 +6,39 @@ 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; -using static Ryujinx.HLE.Debugger.Helpers; + namespace Ryujinx.HLE.Debugger { - public class Debugger : IDisposable + public partial 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(1); - private Thread DebuggerThread; - private Thread MessageHandlerThread; - private bool _shuttingDown = false; - private ManualResetEventSlim _breakHandlerEvent = new(false); + private readonly BlockingCollection _messages = new(1); + private readonly Thread _debuggerThread; + private readonly Thread _messageHandlerThread; - private GdbCommandProcessor CommandProcessor = null; + private TcpListener _listenerSocket; + private Socket _clientSocket; + private NetworkStream _readStream; + private NetworkStream _writeStream; + + private GdbCommandProcessor _commandProcessor; + private GdbCommands _commands; + + private bool _shuttingDown; + private readonly ManualResetEventSlim _breakHandlerEvent = new(false); internal ulong? CThread; internal ulong? GThread; - internal BreakpointManager BreakpointManager; + public readonly BreakpointManager BreakpointManager; public Debugger(Switch device, ushort port) { @@ -45,10 +47,10 @@ namespace Ryujinx.HLE.Debugger ARMeilleure.Optimizations.EnableDebugging = true; - DebuggerThread = new Thread(DebuggerThreadMain); - DebuggerThread.Start(); - MessageHandlerThread = new Thread(MessageHandlerMain); - MessageHandlerThread.Start(); + _debuggerThread = new Thread(DebuggerThreadMain); + _debuggerThread.Start(); + _messageHandlerThread = new Thread(MessageHandlerMain); + _messageHandlerThread.Start(); BreakpointManager = new BreakpointManager(this); } @@ -58,37 +60,37 @@ namespace Ryujinx.HLE.Debugger internal KThread[] GetThreads() => DebugProcess.GetThreadUids().Select(x => DebugProcess.GetThread(x)).ToArray(); - internal bool IsProcessAarch32 => DebugProcess.GetThread(GThread.Value).Context.IsAarch32; + internal bool IsProcess32Bit => DebugProcess.GetThread(GThread.Value).Context.IsAarch32; private void MessageHandlerMain() { while (!_shuttingDown) { - IMessage msg = Messages.Take(); + IMessage msg = _messages.Take(); try { switch (msg) { case BreakInMessage: Logger.Notice.Print(LogClass.GdbStub, "Break-in requested"); - CommandProcessor.Commands.CommandInterrupt(); + _commandProcessor.Commands.Interrupt(); break; case SendNackMessage: - WriteStream.WriteByte((byte)'-'); + _writeStream.WriteByte((byte)'-'); break; case CommandMessage { Command: var cmd }: Logger.Debug?.Print(LogClass.GdbStub, $"Received Command: {cmd}"); - WriteStream.WriteByte((byte)'+'); - CommandProcessor.Process(cmd); + _writeStream.WriteByte((byte)'+'); + _commandProcessor.Process(cmd); break; case ThreadBreakMessage { Context: var ctx }: DebugProcess.DebugStop(); GThread = CThread = ctx.ThreadUid; _breakHandlerEvent.Set(); - CommandProcessor.Commands.Reply($"T05thread:{ctx.ThreadUid:x};"); + _commandProcessor.Reply($"T05thread:{ctx.ThreadUid:x};"); break; case KillMessage: @@ -214,106 +216,6 @@ namespace Ryujinx.HLE.Debugger } } - 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 || GetThreads().Length == 0) && retries-- > 0) - { - Thread.Sleep(200); - } - - if (DebugProcess == null || GetThreads().Length == 0) - { - 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); - CommandProcessor = new GdbCommandProcessor(ListenerSocket, ClientSocket, ReadStream, WriteStream, this); - Logger.Notice.Print(LogClass.GdbStub, "GDB client connected"); - - while (true) - { - try - { - switch (ReadStream.ReadByte()) - { - case -1: - goto EndOfLoop; - 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 EndOfLoop; - 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 EndOfLoop; - } - } - - EndOfLoop: - Logger.Notice.Print(LogClass.GdbStub, "GDB client lost connection"); - ReadStream.Close(); - ReadStream = null; - WriteStream.Close(); - WriteStream = null; - ClientSocket.Close(); - ClientSocket = null; - CommandProcessor = null; - - BreakpointManager.ClearAll(); - } - } - public void Dispose() { Dispose(true); @@ -325,15 +227,15 @@ namespace Ryujinx.HLE.Debugger { _shuttingDown = true; - ListenerSocket.Stop(); - ClientSocket?.Shutdown(SocketShutdown.Both); - ClientSocket?.Close(); - ReadStream?.Close(); - WriteStream?.Close(); - DebuggerThread.Join(); - Messages.Add(new KillMessage()); - MessageHandlerThread.Join(); - Messages.Dispose(); + _listenerSocket.Stop(); + _clientSocket?.Shutdown(SocketShutdown.Both); + _clientSocket?.Close(); + _readStream?.Close(); + _writeStream?.Close(); + _debuggerThread.Join(); + _messages.Add(new KillMessage()); + _messageHandlerThread.Join(); + _messages.Dispose(); _breakHandlerEvent.Dispose(); } } @@ -343,7 +245,7 @@ namespace Ryujinx.HLE.Debugger DebugProcess.DebugInterruptHandler(ctx); _breakHandlerEvent.Reset(); - Messages.Add(new ThreadBreakMessage(ctx, address, imm)); + _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 BreakHandler being called multiple times from the same breakpoint diff --git a/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs b/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs index e9986647d..997b635e4 100644 --- a/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs +++ b/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs @@ -1,7 +1,8 @@ +using Gommon; using Ryujinx.Common; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Kernel.Threading; using System.Linq; -using System.Net.Sockets; using System.Text; namespace Ryujinx.HLE.Debugger.Gdb @@ -10,13 +11,41 @@ namespace Ryujinx.HLE.Debugger.Gdb { public readonly GdbCommands Commands; - public GdbCommandProcessor(TcpListener listenerSocket, Socket clientSocket, NetworkStream readStream, NetworkStream writeStream, Debugger debugger) + private Debugger Debugger => Commands.Debugger; + private BreakpointManager BreakpointManager => Commands.Debugger.BreakpointManager; + private IDebuggableProcess DebugProcess => Commands.Debugger.DebugProcess; + + public GdbCommandProcessor(GdbCommands commands) { - Commands = new GdbCommands(listenerSocket, clientSocket, readStream, writeStream, debugger); + Commands = commands; } - - private string previousThreadListXml = ""; - + + public void Reply(string cmd) + { + Logger.Debug?.Print(LogClass.GdbStub, $"Reply: {cmd}"); + Commands.WriteStream.Write(Encoding.ASCII.GetBytes($"${cmd}#{Helpers.CalculateChecksum(cmd):x2}")); + } + + public void ReplyOK() => Reply("OK"); + + public void ReplyError() => Reply("E01"); + + public void Reply(bool success) + { + if (success) + ReplyOK(); + else ReplyError(); + } + + public void Reply(bool success, string cmd) + { + if (success) + Reply(cmd); + else ReplyError(); + } + + private string _previousThreadListXml = string.Empty; + public void Process(string cmd) { StringStream ss = new(cmd); @@ -30,7 +59,7 @@ namespace Ryujinx.HLE.Debugger.Gdb } // Enable extended mode - Commands.ReplyOK(); + ReplyOK(); break; case '?': if (!ss.IsEmpty()) @@ -38,10 +67,10 @@ namespace Ryujinx.HLE.Debugger.Gdb goto unknownCommand; } - Commands.CommandQuery(); + Commands.Query(); break; case 'c': - Commands.CommandContinue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + Commands.Continue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); break; case 'D': if (!ss.IsEmpty()) @@ -49,7 +78,7 @@ namespace Ryujinx.HLE.Debugger.Gdb goto unknownCommand; } - Commands.CommandDetach(); + Commands.Detach(); break; case 'g': if (!ss.IsEmpty()) @@ -57,110 +86,99 @@ namespace Ryujinx.HLE.Debugger.Gdb goto unknownCommand; } - Commands.CommandReadRegisters(); + Commands.ReadRegisters(); break; case 'G': - Commands.CommandWriteRegisters(ss); + Commands.WriteRegisters(ss); break; case 'H': { char op = ss.ReadChar(); ulong? threadId = ss.ReadRemainingAsThreadUid(); - Commands.CommandSetThread(op, threadId); + Commands.SetThread(op, threadId); break; } case 'k': Logger.Notice.Print(LogClass.GdbStub, "Kill request received, detach instead"); - Commands.Reply(""); - Commands.CommandDetach(); + Reply(string.Empty); + Commands.Detach(); break; case 'm': { ulong addr = ss.ReadUntilAsHex(','); ulong len = ss.ReadRemainingAsHex(); - Commands.CommandReadMemory(addr, len); + Commands.ReadMemory(addr, len); break; } case 'M': { ulong addr = ss.ReadUntilAsHex(','); ulong len = ss.ReadUntilAsHex(':'); - Commands.CommandWriteMemory(addr, len, ss); + Commands.WriteMemory(addr, len, ss); break; } case 'p': { ulong gdbRegId = ss.ReadRemainingAsHex(); - Commands.CommandReadRegister((int)gdbRegId); + Commands.ReadRegister((int)gdbRegId); break; } case 'P': { ulong gdbRegId = ss.ReadUntilAsHex('='); - Commands.CommandWriteRegister((int)gdbRegId, ss); + Commands.WriteRegister((int)gdbRegId, ss); break; } case 'q': if (ss.ConsumeRemaining("GDBServerVersion")) { - Commands.Reply($"name:Ryujinx;version:{ReleaseInformation.Version};"); + Reply($"name:Ryujinx;version:{ReleaseInformation.Version};"); break; } if (ss.ConsumeRemaining("HostInfo")) { - if (Commands.Debugger.IsProcessAarch32) - { - Commands.Reply( - $"triple:{Helpers.ToHex("arm-unknown-linux-android")};endian:little;ptrsize:4;hostname:{Helpers.ToHex("Ryujinx")};"); - } - else - { - Commands.Reply( - $"triple:{Helpers.ToHex("aarch64-unknown-linux-android")};endian:little;ptrsize:8;hostname:{Helpers.ToHex("Ryujinx")};"); - } + Reply( + Debugger.IsProcess32Bit + ? $"triple:{Helpers.ToHex("arm-unknown-linux-android")};endian:little;ptrsize:4;hostname:{Helpers.ToHex("Ryujinx")};" + : $"triple:{Helpers.ToHex("aarch64-unknown-linux-android")};endian:little;ptrsize:8;hostname:{Helpers.ToHex("Ryujinx")};"); break; } if (ss.ConsumeRemaining("ProcessInfo")) { - if (Commands.Debugger.IsProcessAarch32) - { - Commands.Reply( - $"pid:1;cputype:12;cpusubtype:0;triple:{Helpers.ToHex("arm-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:4;"); - } - else - { - Commands.Reply( - $"pid:1;cputype:100000c;cpusubtype:0;triple:{Helpers.ToHex("aarch64-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:8;"); - } + Reply( + Debugger.IsProcess32Bit + ? $"pid:1;cputype:12;cpusubtype:0;triple:{Helpers.ToHex("arm-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:4;" + : $"pid:1;cputype:100000c;cpusubtype:0;triple:{Helpers.ToHex("aarch64-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:8;"); break; } if (ss.ConsumePrefix("Supported:") || ss.ConsumeRemaining("Supported")) { - Commands.Reply("PacketSize=10000;qXfer:features:read+;qXfer:threads:read+;vContSupported+"); + Reply("PacketSize=10000;qXfer:features:read+;qXfer:threads:read+;vContSupported+"); break; } if (ss.ConsumePrefix("Rcmd,")) { string hexCommand = ss.ReadRemaining(); - Commands.HandleQRcmdCommand(hexCommand); + Commands.Q_Rcmd(hexCommand); break; } if (ss.ConsumeRemaining("fThreadInfo")) { - Commands. Reply($"m{string.Join(",", Commands.Debugger.DebugProcess.GetThreadUids().Select(x => $"{x:x}"))}"); + Reply( + $"m{Debugger.DebugProcess.GetThreadUids().Select(x => $"{x:x}").JoinToString(",")}"); break; } if (ss.ConsumeRemaining("sThreadInfo")) { - Commands.Reply("l"); + Reply("l"); break; } @@ -169,15 +187,14 @@ namespace Ryujinx.HLE.Debugger.Gdb ulong? threadId = ss.ReadRemainingAsThreadUid(); if (threadId == null) { - Commands.ReplyError(); + ReplyError(); break; } - Commands.Reply(Helpers.ToHex( - Commands.Debugger.DebugProcess.IsThreadPaused( - Commands.Debugger.DebugProcess.GetThread(threadId.Value)) - ? "Paused" - : "Running" + Reply(Helpers.ToHex( + DebugProcess.IsThreadPaused(DebugProcess.GetThread(threadId.Value)) + ? "Paused" + : "Running" ) ); @@ -190,32 +207,32 @@ namespace Ryujinx.HLE.Debugger.Gdb ulong offset = ss.ReadUntilAsHex(','); ulong len = ss.ReadRemainingAsHex(); - var data = ""; + string data; if (offset > 0) { - data = previousThreadListXml; + data = _previousThreadListXml; } else { - previousThreadListXml = data = GetThreadListXml(); + _previousThreadListXml = data = GetThreadListXml(); } if (offset >= (ulong)data.Length) { - Commands.Reply("l"); + Reply("l"); break; } if (len >= (ulong)data.Length - offset) { - Commands.Reply("l" + Helpers.ToBinaryFormat(data.Substring((int)offset))); - break; + Reply("l" + Helpers.ToBinaryFormat(data.Substring((int)offset))); } else { - Commands.Reply("m" + Helpers.ToBinaryFormat(data.Substring((int)offset, (int)len))); - break; + Reply("m" + Helpers.ToBinaryFormat(data.Substring((int)offset, (int)len))); } + + break; } if (ss.ConsumePrefix("Xfer:features:read:")) @@ -226,46 +243,43 @@ namespace Ryujinx.HLE.Debugger.Gdb if (feature == "target.xml") { - feature = Commands.Debugger.IsProcessAarch32 ? "target32.xml" : "target64.xml"; + feature = Debugger.IsProcess32Bit ? "target32.xml" : "target64.xml"; } - string data; - if (RegisterInformation.Features.TryGetValue(feature, out data)) + if (!RegisterInformation.Features.TryGetValue(feature, out string data)) { - if (offset >= (ulong)data.Length) - { - Commands.Reply("l"); - break; - } + Reply("E00"); // Invalid annex + break; + } - if (len >= (ulong)data.Length - offset) - { - Commands.Reply("l" + Helpers.ToBinaryFormat(data.Substring((int)offset))); - break; - } - else - { - Commands.Reply("m" + Helpers.ToBinaryFormat(data.Substring((int)offset, (int)len))); - break; - } + if (offset >= (ulong)data.Length) + { + Reply("l"); + break; + } + + if (len >= (ulong)data.Length - offset) + { + Reply("l" + Helpers.ToBinaryFormat(data[(int)offset..])); } else { - Commands.Reply("E00"); // Invalid annex - break; + Reply("m" + Helpers.ToBinaryFormat(data.Substring((int)offset, (int)len))); } + + break; } goto unknownCommand; case 'Q': goto unknownCommand; case 's': - Commands.CommandStep(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + Commands.Step(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); break; case 'T': { ulong? threadId = ss.ReadRemainingAsThreadUid(); - Commands.CommandIsAlive(threadId); + Commands.IsAlive(threadId); break; } case 'v': @@ -273,13 +287,13 @@ namespace Ryujinx.HLE.Debugger.Gdb { if (ss.ConsumeRemaining("?")) { - Commands.Reply("vCont;c;C;s;S"); + Reply("vCont;c;C;s;S"); break; } if (ss.ConsumePrefix(";")) { - Commands.HandleVContCommand(ss); + Commands.VCont(ss); break; } @@ -288,7 +302,7 @@ namespace Ryujinx.HLE.Debugger.Gdb if (ss.ConsumeRemaining("MustReplyEmpty")) { - Commands.Reply(""); + Reply(string.Empty); break; } @@ -303,29 +317,29 @@ namespace Ryujinx.HLE.Debugger.Gdb if (extra.Length > 0) { Logger.Notice.Print(LogClass.GdbStub, $"Unsupported Z command extra data: {extra}"); - Commands.ReplyError(); + ReplyError(); return; } switch (type) { case "0": // Software breakpoint - if (!Commands.Debugger.BreakpointManager.SetBreakPoint(addr, len)) + if (!BreakpointManager.SetBreakPoint(addr, len)) { - Commands.ReplyError(); + ReplyError(); return; } - Commands.ReplyOK(); + ReplyOK(); return; + // ReSharper disable RedundantCaseLabel case "1": // Hardware breakpoint case "2": // Write watchpoint case "3": // Read watchpoint case "4": // Access watchpoint - Commands.ReplyError(); - return; + // ReSharper restore RedundantCaseLabel default: - Commands.ReplyError(); + ReplyError(); return; } } @@ -340,50 +354,50 @@ namespace Ryujinx.HLE.Debugger.Gdb if (extra.Length > 0) { Logger.Notice.Print(LogClass.GdbStub, $"Unsupported z command extra data: {extra}"); - Commands.ReplyError(); + ReplyError(); return; } switch (type) { case "0": // Software breakpoint - if (!Commands.Debugger.BreakpointManager.ClearBreakPoint(addr, len)) + if (!BreakpointManager.ClearBreakPoint(addr, len)) { - Commands.ReplyError(); + ReplyError(); return; } - Commands.ReplyOK(); + ReplyOK(); return; + // ReSharper disable RedundantCaseLabel case "1": // Hardware breakpoint case "2": // Write watchpoint case "3": // Read watchpoint case "4": // Access watchpoint - Commands.ReplyError(); - return; + // ReSharper restore RedundantCaseLabel default: - Commands.ReplyError(); + ReplyError(); return; } } default: unknownCommand: Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}"); - Commands.Reply(""); + Reply(string.Empty); break; } } private string GetThreadListXml() { - var sb = new StringBuilder(); + StringBuilder sb = new(); sb.Append("\n"); - foreach (var thread in Commands.Debugger.GetThreads()) + foreach (KThread thread in Debugger.GetThreads()) { string threadName = System.Security.SecurityElement.Escape(thread.GetThreadName()); sb.Append( - $"{(Commands.Debugger.DebugProcess.IsThreadPaused(thread) ? "Paused" : "Running")}\n"); + $"{(DebugProcess.IsThreadPaused(thread) ? "Paused" : "Running")}\n"); } sb.Append(""); diff --git a/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs b/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs index 6c0a258a0..33c7f3675 100644 --- a/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs +++ b/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs @@ -1,10 +1,11 @@ using Ryujinx.Common.Logging; +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.Memory; using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; -using System.Text; namespace Ryujinx.HLE.Debugger.Gdb { @@ -15,41 +16,48 @@ namespace Ryujinx.HLE.Debugger.Gdb public readonly Debugger Debugger; - private readonly TcpListener _listenerSocket; - private readonly Socket _clientSocket; - private readonly NetworkStream _readStream; - private readonly NetworkStream _writeStream; - + public GdbCommandProcessor Processor { get; private set; } + + internal readonly TcpListener ListenerSocket; + internal readonly Socket ClientSocket; + internal readonly NetworkStream ReadStream; + internal readonly NetworkStream WriteStream; + public GdbCommands(TcpListener listenerSocket, Socket clientSocket, NetworkStream readStream, NetworkStream writeStream, Debugger debugger) { - _listenerSocket = listenerSocket; - _clientSocket = clientSocket; - _readStream = readStream; - _writeStream = writeStream; + ListenerSocket = listenerSocket; + ClientSocket = clientSocket; + ReadStream = readStream; + WriteStream = writeStream; Debugger = debugger; } - public void Reply(string cmd) + public void SetProcessor(GdbCommandProcessor commandProcessor) { - Logger.Debug?.Print(LogClass.GdbStub, $"Reply: {cmd}"); - _writeStream.Write(Encoding.ASCII.GetBytes($"${cmd}#{Helpers.CalculateChecksum(cmd):x2}")); + if (Processor != null) return; + + Processor = commandProcessor; + } + + public GdbCommandProcessor CreateProcessor() + { + if (Processor != null) + return Processor; + + return Processor = new GdbCommandProcessor(this); } - public void ReplyOK() => Reply("OK"); - - public void ReplyError() => Reply("E01"); - - internal void CommandQuery() + internal void Query() { // GDB is performing initial contact. Stop everything. Debugger.DebugProcess.DebugStop(); Debugger.GThread = Debugger.CThread = Debugger.DebugProcess.GetThreadUids().First(); - Reply($"T05thread:{Debugger.CThread:x};"); + Processor.Reply($"T05thread:{Debugger.CThread:x};"); } - internal void CommandInterrupt() + internal void Interrupt() { // GDB is requesting an interrupt. Stop everything. Debugger.DebugProcess.DebugStop(); @@ -58,16 +66,16 @@ namespace Ryujinx.HLE.Debugger.Gdb Debugger.GThread = Debugger.CThread = Debugger.DebugProcess.GetThreadUids().First(); } - Reply($"T02thread:{Debugger.GThread:x};"); + Processor.Reply($"T02thread:{Debugger.GThread:x};"); } - internal void CommandContinue(ulong? newPc) + internal void Continue(ulong? newPc) { if (newPc.HasValue) { if (Debugger.CThread == null) { - ReplyError(); + Processor.ReplyError(); return; } @@ -77,23 +85,23 @@ namespace Ryujinx.HLE.Debugger.Gdb Debugger.DebugProcess.DebugContinue(); } - internal void CommandDetach() + internal void Detach() { Debugger.BreakpointManager.ClearAll(); - CommandContinue(null); + Continue(null); } - internal void CommandReadRegisters() + internal void ReadRegisters() { if (Debugger.GThread == null) { - ReplyError(); + Processor.ReplyError(); return; } - var ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; - string registers = ""; - if (Debugger.IsProcessAarch32) + IExecutionContext ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; + string registers = string.Empty; + if (Debugger.IsProcess32Bit) { for (int i = 0; i < GdbRegisterCount32; i++) { @@ -108,25 +116,25 @@ namespace Ryujinx.HLE.Debugger.Gdb } } - Reply(registers); + Processor.Reply(registers); } - internal void CommandWriteRegisters(StringStream ss) + internal void WriteRegisters(StringStream ss) { if (Debugger.GThread == null) { - ReplyError(); + Processor.ReplyError(); return; } - var ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; - if (Debugger.IsProcessAarch32) + IExecutionContext ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; + if (Debugger.IsProcess32Bit) { for (int i = 0; i < GdbRegisterCount32; i++) { if (!GdbRegisters.Write32(ctx, i, ss)) { - ReplyError(); + Processor.ReplyError(); return; } } @@ -137,30 +145,23 @@ namespace Ryujinx.HLE.Debugger.Gdb { if (!GdbRegisters.Write64(ctx, i, ss)) { - ReplyError(); + Processor.ReplyError(); return; } } } - if (ss.IsEmpty()) - { - ReplyOK(); - } - else - { - ReplyError(); - } + Processor.Reply(ss.IsEmpty()); } - internal void CommandSetThread(char op, ulong? threadId) + internal void SetThread(char op, ulong? threadId) { if (threadId is 0 or null) { - var threads = Debugger.GetThreads(); + KThread[] threads = Debugger.GetThreads(); if (threads.Length == 0) { - ReplyError(); + Processor.ReplyError(); return; } @@ -169,7 +170,7 @@ namespace Ryujinx.HLE.Debugger.Gdb if (Debugger.DebugProcess.GetThread(threadId.Value) == null) { - ReplyError(); + Processor.ReplyError(); return; } @@ -177,36 +178,36 @@ namespace Ryujinx.HLE.Debugger.Gdb { case 'c': Debugger.CThread = threadId; - ReplyOK(); + Processor.ReplyOK(); return; case 'g': Debugger.GThread = threadId; - ReplyOK(); + Processor.ReplyOK(); return; default: - ReplyError(); + Processor.ReplyError(); return; } } - internal void CommandReadMemory(ulong addr, ulong len) + internal void ReadMemory(ulong addr, ulong len) { try { var data = new byte[len]; Debugger.DebugProcess.CpuMemory.Read(addr, data); - Reply(Helpers.ToHex(data)); + Processor.Reply(Helpers.ToHex(data)); } 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(); + Processor.ReplyError(); } } - internal void CommandWriteMemory(ulong addr, ulong len, StringStream ss) + internal void WriteMemory(ulong addr, ulong len, StringStream ss) { try { @@ -218,92 +219,58 @@ namespace Ryujinx.HLE.Debugger.Gdb Debugger.DebugProcess.CpuMemory.Write(addr, data); Debugger.DebugProcess.InvalidateCacheRegion(addr, len); - ReplyOK(); + Processor.ReplyOK(); } catch (InvalidMemoryRegionException) { - ReplyError(); + Processor.ReplyError(); } } - internal void CommandReadRegister(int gdbRegId) + internal void ReadRegister(int gdbRegId) { if (Debugger.GThread == null) { - ReplyError(); + Processor.ReplyError(); return; } - var ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; - string result; - if (Debugger.IsProcessAarch32) - { - result = GdbRegisters.Read32(ctx, gdbRegId); - if (result != null) - { - Reply(result); - } - else - { - ReplyError(); - } - } - else - { - result = GdbRegisters.Read64(ctx, gdbRegId); - if (result != null) - { - Reply(result); - } - else - { - ReplyError(); - } - } + IExecutionContext ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; + string result = Debugger.IsProcess32Bit + ? GdbRegisters.Read32(ctx, gdbRegId) + : GdbRegisters.Read64(ctx, gdbRegId); + + Processor.Reply(result != null, result); } - internal void CommandWriteRegister(int gdbRegId, StringStream ss) + internal void WriteRegister(int gdbRegId, StringStream ss) { if (Debugger.GThread == null) { - ReplyError(); + Processor.ReplyError(); return; } - var ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; - if (Debugger.IsProcessAarch32) + IExecutionContext ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; + if (Debugger.IsProcess32Bit) { - if (GdbRegisters.Write32(ctx, gdbRegId, ss) && ss.IsEmpty()) - { - ReplyOK(); - } - else - { - ReplyError(); - } + Processor.Reply(GdbRegisters.Write32(ctx, gdbRegId, ss) && ss.IsEmpty()); } else { - if (GdbRegisters.Write64(ctx, gdbRegId, ss) && ss.IsEmpty()) - { - ReplyOK(); - } - else - { - ReplyError(); - } + Processor.Reply(GdbRegisters.Write64(ctx, gdbRegId, ss) && ss.IsEmpty()); } } - internal void CommandStep(ulong? newPc) + internal void Step(ulong? newPc) { if (Debugger.CThread == null) { - ReplyError(); + Processor.ReplyError(); return; } - var thread = Debugger.DebugProcess.GetThread(Debugger.CThread.Value); + KThread thread = Debugger.DebugProcess.GetThread(Debugger.CThread.Value); if (newPc.HasValue) { @@ -312,24 +279,24 @@ namespace Ryujinx.HLE.Debugger.Gdb if (!Debugger.DebugProcess.DebugStep(thread)) { - ReplyError(); + Processor.ReplyError(); } else { Debugger.GThread = Debugger.CThread = thread.ThreadUid; - Reply($"T05thread:{thread.ThreadUid:x};"); + Processor.Reply($"T05thread:{thread.ThreadUid:x};"); } } - internal void CommandIsAlive(ulong? threadId) + internal void IsAlive(ulong? threadId) { if (Debugger.GetThreads().Any(x => x.ThreadUid == threadId)) { - ReplyOK(); + Processor.ReplyOK(); } else { - Reply("E00"); + Processor.Reply("E00"); } } @@ -341,14 +308,14 @@ namespace Ryujinx.HLE.Debugger.Gdb Step } - record VContPendingAction(VContAction Action, ushort? Signal = null); + record VContPendingAction(VContAction Action/*, ushort? Signal = null*/); - internal void HandleVContCommand(StringStream ss) + internal void VCont(StringStream ss) { string[] rawActions = ss.ReadRemaining().Split(';', StringSplitOptions.RemoveEmptyEntries); - var threadActionMap = new Dictionary(); - foreach (var thread in Debugger.GetThreads()) + Dictionary threadActionMap = new(); + foreach (KThread thread in Debugger.GetThreads()) { threadActionMap[thread.ThreadUid] = new VContPendingAction(VContAction.None); } @@ -358,8 +325,8 @@ namespace Ryujinx.HLE.Debugger.Gdb // 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); + string rawAction = rawActions[i]; + StringStream stream = new(rawAction); char cmd = stream.ReadChar(); VContAction action = cmd switch @@ -371,10 +338,12 @@ namespace Ryujinx.HLE.Debugger.Gdb }; // Note: We don't support signals yet. - ushort? signal = null; + //ushort? signal = null; if (cmd is 'C' or 'S') { - signal = (ushort)stream.ReadLengthAsHex(2); + /*signal = (ushort)*/stream.ReadLengthAsHex(2); + // we still call the read length method even if we have signals commented + // since that method advances the underlying string position } ulong? threadId = null; @@ -387,14 +356,14 @@ namespace Ryujinx.HLE.Debugger.Gdb { if (threadActionMap.ContainsKey(threadId.Value)) { - threadActionMap[threadId.Value] = new VContPendingAction(action, signal); + threadActionMap[threadId.Value] = new VContPendingAction(action/*, signal*/); } } else { - foreach (var row in threadActionMap.ToList()) + foreach (ulong thread in threadActionMap.Keys) { - threadActionMap[row.Key] = new VContPendingAction(action, signal); + threadActionMap[thread] = new VContPendingAction(action/*, signal*/); } if (action == VContAction.Continue) @@ -411,11 +380,11 @@ namespace Ryujinx.HLE.Debugger.Gdb bool hasError = false; - foreach (var (threadUid, action) in threadActionMap) + foreach ((ulong threadUid, VContPendingAction action) in threadActionMap) { if (action.Action == VContAction.Step) { - var thread = Debugger.DebugProcess.GetThread(threadUid); + KThread thread = Debugger.DebugProcess.GetThread(threadUid); if (!Debugger.DebugProcess.DebugStep(thread)) { hasError = true; @@ -432,7 +401,7 @@ namespace Ryujinx.HLE.Debugger.Gdb } else if (defaultAction == VContAction.None) { - foreach (var (threadUid, action) in threadActionMap) + foreach ((ulong threadUid, VContPendingAction action) in threadActionMap) { if (action.Action == VContAction.Continue) { @@ -441,26 +410,19 @@ namespace Ryujinx.HLE.Debugger.Gdb } } - if (hasError) - { - ReplyError(); - } - else - { - ReplyOK(); - } + Processor.Reply(!hasError); - foreach (var (threadUid, action) in threadActionMap) + foreach ((ulong threadUid, VContPendingAction action) in threadActionMap) { if (action.Action == VContAction.Step) { Debugger.GThread = Debugger.CThread = threadUid; - Reply($"T05thread:{threadUid:x};"); + Processor.Reply($"T05thread:{threadUid:x};"); } } } - - internal void HandleQRcmdCommand(string hexCommand) + + internal void Q_Rcmd(string hexCommand) { try { @@ -477,12 +439,12 @@ namespace Ryujinx.HLE.Debugger.Gdb _ => $"Unknown command: {command}\n" }; - Reply(Helpers.ToHex(response)); + Processor.Reply(Helpers.ToHex(response)); } catch (Exception e) { Logger.Error?.Print(LogClass.GdbStub, $"Error processing Rcmd: {e.Message}"); - ReplyError(); + Processor.ReplyError(); } } } diff --git a/src/Ryujinx.HLE/Debugger/Gdb/Registers.cs b/src/Ryujinx.HLE/Debugger/Gdb/Registers.cs index de2f6c25d..f9ce7b153 100644 --- a/src/Ryujinx.HLE/Debugger/Gdb/Registers.cs +++ b/src/Ryujinx.HLE/Debugger/Gdb/Registers.cs @@ -9,10 +9,10 @@ namespace Ryujinx.HLE.Debugger.Gdb /* FPCR = FPSR & ~FpcrMask All of FPCR's bits are reserved in FPCR and vice versa, - see ARM's documentation. + see ARM's documentation. */ private const uint FpcrMask = 0xfc1fffff; - + public static string Read64(IExecutionContext state, int gdbRegId) { switch (gdbRegId) diff --git a/src/Ryujinx.HLE/Debugger/Helpers.cs b/src/Ryujinx.HLE/Debugger/Helpers.cs index a2b802525..eb243dc5b 100644 --- a/src/Ryujinx.HLE/Debugger/Helpers.cs +++ b/src/Ryujinx.HLE/Debugger/Helpers.cs @@ -43,7 +43,7 @@ namespace Ryujinx.HLE.Debugger (byte)'$' => "}\x04", (byte)'*' => "}\x0a", (byte)'}' => "}\x5d", - _ => Convert.ToChar(x).ToString(), + _ => Convert.ToChar(x).ToString() } ).JoinToString(string.Empty); } diff --git a/src/Ryujinx.HLE/Debugger/RegisterInformation.cs b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs index b43899271..c1c576558 100644 --- a/src/Ryujinx.HLE/Debugger/RegisterInformation.cs +++ b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs @@ -18,7 +18,7 @@ namespace Ryujinx.HLE.Debugger private static string GetEmbeddedResourceContent(string resourceName) { Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.HLE.Debugger.Gdb.Xml." + resourceName); - StreamReader reader = new StreamReader(stream); + StreamReader reader = new(stream); string result = reader.ReadToEnd(); reader.Dispose(); stream.Dispose(); diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs index b823d14f7..eb75fb9a1 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs @@ -1092,7 +1092,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process MemoryManager = new KPageTable(KernelContext, CpuMemory, Context.AddressSpaceSize); } - private bool InvalidAccessHandler(ulong va) + private static bool InvalidAccessHandler(ulong va) { KernelStatic.GetCurrentThread()?.PrintGuestStackTrace(); KernelStatic.GetCurrentThread()?.PrintGuestRegisterPrintout(); @@ -1104,7 +1104,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process return false; } - private void UndefinedInstructionHandler(IExecutionContext context, ulong address, int opCode) + private static void UndefinedInstructionHandler(IExecutionContext context, ulong address, int opCode) { KernelStatic.GetCurrentThread().PrintGuestStackTrace(); KernelStatic.GetCurrentThread()?.PrintGuestRegisterPrintout(); @@ -1208,16 +1208,16 @@ namespace Ryujinx.HLE.HOS.Kernel.Process private class DebuggerInterface : IDebuggableProcess { - private Barrier StepBarrier; + private readonly Barrier _stepBarrier; private readonly KProcess _parent; private readonly KernelContext _kernelContext; - private KThread steppingThread; + private KThread _steppingThread; public DebuggerInterface(KProcess p) { _parent = p; _kernelContext = p.KernelContext; - StepBarrier = new(2); + _stepBarrier = new(2); } public void DebugStop() @@ -1285,7 +1285,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process } _kernelContext.CriticalSection.Enter(); - steppingThread = target; + _steppingThread = target; bool waiting = target.MutexOwner != null || target.WaitingSync || target.WaitingInArbitration; target.Context.RequestDebugStep(); if (waiting) @@ -1305,14 +1305,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Process _kernelContext.CriticalSection.Leave(); bool stepTimedOut = false; - if (!StepBarrier.SignalAndWait(TimeSpan.FromMilliseconds(2000))) + 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; + _steppingThread = null; if (waiting) { lock (_parent._threadingLock) @@ -1334,7 +1334,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process return false; } - StepBarrier.SignalAndWait(); + _stepBarrier.SignalAndWait(); return true; } @@ -1369,12 +1369,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Process public void DebugInterruptHandler(IExecutionContext ctx) { _kernelContext.CriticalSection.Enter(); - bool stepping = steppingThread != null; + bool stepping = _steppingThread != null; _kernelContext.CriticalSection.Leave(); if (stepping) { - StepBarrier.SignalAndWait(); - StepBarrier.SignalAndWait(); + _stepBarrier.SignalAndWait(); + _stepBarrier.SignalAndWait(); } _parent.InterruptHandler(ctx); }