From c33a97f01c631941512140f8dd583ce3029a790f Mon Sep 17 00:00:00 2001 From: GreemDev Date: Thu, 16 Oct 2025 17:32:04 -0500 Subject: [PATCH] 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 @@ - - - - - - + + + + + +