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 @@ - + diff --git a/assets/locales.json b/assets/locales.json index bbc30563a..cac65dd80 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)", 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/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(); 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) diff --git a/src/Ryujinx.HLE/Debugger/BreakpointManager.cs b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs index bf462a781..7f45fbf8b 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; } } @@ -28,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) { @@ -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,33 +121,9 @@ 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) + 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 2c935ca7f..1c28870b7 100644 --- a/src/Ryujinx.HLE/Debugger/Debugger.cs +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -1,45 +1,44 @@ -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; using System.Net.Sockets; using System.Text; using System.Threading; using IExecutionContext = Ryujinx.Cpu.IExecutionContext; + 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 BlockingCollection(1); - private Thread DebuggerThread; - private Thread MessageHandlerThread; - private bool _shuttingDown = false; - private ManualResetEventSlim _breakHandlerEvent = new ManualResetEventSlim(false); + private readonly BlockingCollection _messages = new(1); + private readonly Thread _debuggerThread; + private readonly Thread _messageHandlerThread; - private ulong? cThread; - private ulong? gThread; + private TcpListener _listenerSocket; + private Socket _clientSocket; + private NetworkStream _readStream; + private NetworkStream _writeStream; - private BreakpointManager BreakpointManager; + private GdbCommandProcessor _commandProcessor; + private GdbCommands _commands; - private string previousThreadListXml = ""; + private bool _shuttingDown; + private readonly ManualResetEventSlim _breakHandlerEvent = new(false); + + internal ulong? CThread; + internal ulong? GThread; + + public readonly BreakpointManager BreakpointManager; public Debugger(Switch device, ushort port) { @@ -48,198 +47,50 @@ 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); } 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 IsProcess32Bit => DebugProcess.GetThread(GThread.Value).Context.IsAarch32; private void MessageHandlerMain() { while (!_shuttingDown) { - IMessage msg = Messages.Take(); - try { + IMessage msg = _messages.Take(); + try + { switch (msg) { case BreakInMessage: Logger.Notice.Print(LogClass.GdbStub, "Break-in requested"); - 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)'+'); - ProcessCommand(cmd); + _writeStream.WriteByte((byte)'+'); + _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.Reply($"T05thread:{ctx.ThreadUid:x};"); break; case KillMessage: @@ -254,837 +105,36 @@ namespace Ryujinx.HLE.Debugger { Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e); } - } - } - - private void ProcessCommand(string cmd) - { - StringStream ss = new StringStream(cmd); - - switch (ss.ReadChar()) - { - case '!': - if (!ss.IsEmpty()) - { - goto unknownCommand; - } - - // Enable extended mode - ReplyOK(); - break; - case '?': - if (!ss.IsEmpty()) - { - goto unknownCommand; - } - - CommandQuery(); - break; - case 'c': - CommandContinue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); - break; - case 'D': - if (!ss.IsEmpty()) - { - goto unknownCommand; - } - - CommandDetach(); - break; - case 'g': - if (!ss.IsEmpty()) - { - goto unknownCommand; - } - - CommandReadRegisters(); - break; - case 'G': - CommandWriteRegisters(ss); - break; - case 'H': - { - char op = ss.ReadChar(); - ulong? threadId = ss.ReadRemainingAsThreadUid(); - CommandSetThread(op, threadId); - break; - } - case 'k': - Logger.Notice.Print(LogClass.GdbStub, "Kill request received, detach instead"); - Reply(""); - CommandDetach(); - break; - case 'm': - { - ulong addr = ss.ReadUntilAsHex(','); - ulong len = ss.ReadRemainingAsHex(); - CommandReadMemory(addr, len); - break; - } - case 'M': - { - ulong addr = ss.ReadUntilAsHex(','); - ulong len = ss.ReadUntilAsHex(':'); - CommandWriteMemory(addr, len, ss); - break; - } - case 'p': - { - ulong gdbRegId = ss.ReadRemainingAsHex(); - CommandReadRegister((int)gdbRegId); - break; - } - case 'P': - { - ulong gdbRegId = ss.ReadUntilAsHex('='); - CommandWriteRegister((int)gdbRegId, ss); - break; - } - case 'q': - if (ss.ConsumeRemaining("GDBServerVersion")) - { - Reply($"name:Ryujinx;version:{ReleaseInformation.Version};"); - break; - } - - if (ss.ConsumeRemaining("HostInfo")) - { - if (IsProcessAarch32) - { - Reply( - $"triple:{ToHex("arm-unknown-linux-android")};endian:little;ptrsize:4;hostname:{ToHex("Ryujinx")};"); - } - else - { - Reply( - $"triple:{ToHex("aarch64-unknown-linux-android")};endian:little;ptrsize:8;hostname:{ToHex("Ryujinx")};"); - } - break; - } - - if (ss.ConsumeRemaining("ProcessInfo")) - { - if (IsProcessAarch32) - { - Reply( - $"pid:1;cputype:12;cpusubtype:0;triple:{ToHex("arm-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:4;"); - } - else - { - Reply( - $"pid:1;cputype:100000c;cpusubtype:0;triple:{ToHex("aarch64-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:8;"); - } - break; - } - - if (ss.ConsumePrefix("Supported:") || ss.ConsumeRemaining("Supported")) - { - Reply("PacketSize=10000;qXfer:features:read+;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 + catch (ObjectDisposedException e) { - '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};"); + Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e); } } } - private string GetThreadListXml() + public string GetStackTrace() { - 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 +170,7 @@ namespace Ryujinx.HLE.Debugger return response.ToString(); } - private string GetProcessInfo() + public string GetProcessInfo() { try { @@ -1130,15 +180,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 +206,7 @@ namespace Ryujinx.HLE.Debugger sb.AppendLine($" 0x{image.BaseAddress:x10} - 0x{endAddress:x10} {name}"); } } - + return sb.ToString(); } catch (Exception e) @@ -1162,170 +216,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); - 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); - Logger.Notice.Print(LogClass.GdbStub, "GDB client connected"); - - while (true) - { - try - { - switch (ReadStream.ReadByte()) - { - case -1: - goto eof; - case '+': - continue; - case '-': - Logger.Notice.Print(LogClass.GdbStub, "NACK received!"); - continue; - case '\x03': - Messages.Add(new BreakInMessage()); - break; - case '$': - string cmd = ""; - while (true) - { - int x = ReadStream.ReadByte(); - if (x == -1) - goto eof; - if (x == '#') - break; - cmd += (char)x; - } - - string checksum = $"{(char)ReadStream.ReadByte()}{(char)ReadStream.ReadByte()}"; - if (checksum == $"{CalculateChecksum(cmd):x2}") - { - Messages.Add(new CommandMessage(cmd)); - } - else - { - Messages.Add(new SendNackMessage()); - } - - break; - } - } - catch (IOException) - { - goto eof; - } - } - - eof: - Logger.Notice.Print(LogClass.GdbStub, "GDB client lost connection"); - ReadStream.Close(); - ReadStream = null; - WriteStream.Close(); - WriteStream = null; - ClientSocket.Close(); - ClientSocket = null; - - BreakpointManager.ClearAll(); - } - } - - 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); @@ -1337,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(); } } @@ -1355,10 +245,10 @@ 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 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..997b635e4 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Gdb/CommandProcessor.cs @@ -0,0 +1,407 @@ +using Gommon; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Kernel.Threading; +using System.Linq; +using System.Text; + +namespace Ryujinx.HLE.Debugger.Gdb +{ + class GdbCommandProcessor + { + public readonly GdbCommands Commands; + + private Debugger Debugger => Commands.Debugger; + private BreakpointManager BreakpointManager => Commands.Debugger.BreakpointManager; + private IDebuggableProcess DebugProcess => Commands.Debugger.DebugProcess; + + public GdbCommandProcessor(GdbCommands commands) + { + Commands = commands; + } + + 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); + + switch (ss.ReadChar()) + { + case '!': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + // Enable extended mode + ReplyOK(); + break; + case '?': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + Commands.Query(); + break; + case 'c': + Commands.Continue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + break; + case 'D': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + Commands.Detach(); + break; + case 'g': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + Commands.ReadRegisters(); + break; + case 'G': + Commands.WriteRegisters(ss); + break; + case 'H': + { + char op = ss.ReadChar(); + ulong? threadId = ss.ReadRemainingAsThreadUid(); + Commands.SetThread(op, threadId); + break; + } + case 'k': + Logger.Notice.Print(LogClass.GdbStub, "Kill request received, detach instead"); + Reply(string.Empty); + Commands.Detach(); + break; + case 'm': + { + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadRemainingAsHex(); + Commands.ReadMemory(addr, len); + break; + } + case 'M': + { + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadUntilAsHex(':'); + Commands.WriteMemory(addr, len, ss); + break; + } + case 'p': + { + ulong gdbRegId = ss.ReadRemainingAsHex(); + Commands.ReadRegister((int)gdbRegId); + break; + } + case 'P': + { + ulong gdbRegId = ss.ReadUntilAsHex('='); + Commands.WriteRegister((int)gdbRegId, ss); + break; + } + case 'q': + if (ss.ConsumeRemaining("GDBServerVersion")) + { + Reply($"name:Ryujinx;version:{ReleaseInformation.Version};"); + break; + } + + if (ss.ConsumeRemaining("HostInfo")) + { + 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")) + { + 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")) + { + Reply("PacketSize=10000;qXfer:features:read+;qXfer:threads:read+;vContSupported+"); + break; + } + + if (ss.ConsumePrefix("Rcmd,")) + { + string hexCommand = ss.ReadRemaining(); + Commands.Q_Rcmd(hexCommand); + break; + } + + if (ss.ConsumeRemaining("fThreadInfo")) + { + Reply( + $"m{Debugger.DebugProcess.GetThreadUids().Select(x => $"{x:x}").JoinToString(",")}"); + break; + } + + if (ss.ConsumeRemaining("sThreadInfo")) + { + Reply("l"); + break; + } + + if (ss.ConsumePrefix("ThreadExtraInfo,")) + { + ulong? threadId = ss.ReadRemainingAsThreadUid(); + if (threadId == null) + { + ReplyError(); + break; + } + + Reply(Helpers.ToHex( + DebugProcess.IsThreadPaused(DebugProcess.GetThread(threadId.Value)) + ? "Paused" + : "Running" + ) + ); + + break; + } + + if (ss.ConsumePrefix("Xfer:threads:read:")) + { + ss.ReadUntil(':'); + ulong offset = ss.ReadUntilAsHex(','); + ulong len = ss.ReadRemainingAsHex(); + + string 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" + Helpers.ToBinaryFormat(data.Substring((int)offset))); + } + else + { + 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 = Debugger.IsProcess32Bit ? "target32.xml" : "target64.xml"; + } + + if (!RegisterInformation.Features.TryGetValue(feature, out string data)) + { + Reply("E00"); // Invalid annex + break; + } + + if (offset >= (ulong)data.Length) + { + Reply("l"); + break; + } + + if (len >= (ulong)data.Length - offset) + { + Reply("l" + Helpers.ToBinaryFormat(data[(int)offset..])); + } + else + { + Reply("m" + Helpers.ToBinaryFormat(data.Substring((int)offset, (int)len))); + } + + break; + } + + goto unknownCommand; + case 'Q': + goto unknownCommand; + case 's': + Commands.Step(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + break; + case 'T': + { + ulong? threadId = ss.ReadRemainingAsThreadUid(); + Commands.IsAlive(threadId); + break; + } + case 'v': + if (ss.ConsumePrefix("Cont")) + { + if (ss.ConsumeRemaining("?")) + { + Reply("vCont;c;C;s;S"); + break; + } + + if (ss.ConsumePrefix(";")) + { + Commands.VCont(ss); + break; + } + + goto unknownCommand; + } + + if (ss.ConsumeRemaining("MustReplyEmpty")) + { + Reply(string.Empty); + 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)) + { + ReplyError(); + return; + } + + ReplyOK(); + return; + // ReSharper disable RedundantCaseLabel + case "1": // Hardware breakpoint + case "2": // Write watchpoint + case "3": // Read watchpoint + case "4": // Access watchpoint + // ReSharper restore RedundantCaseLabel + 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; + // ReSharper disable RedundantCaseLabel + case "1": // Hardware breakpoint + case "2": // Write watchpoint + case "3": // Read watchpoint + case "4": // Access watchpoint + // ReSharper restore RedundantCaseLabel + default: + ReplyError(); + return; + } + } + default: + unknownCommand: + Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}"); + Reply(string.Empty); + break; + } + } + + private string GetThreadListXml() + { + StringBuilder sb = new(); + sb.Append("\n"); + + foreach (KThread thread in Debugger.GetThreads()) + { + string threadName = System.Security.SecurityElement.Escape(thread.GetThreadName()); + sb.Append( + $"{(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..33c7f3675 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Gdb/Commands.cs @@ -0,0 +1,451 @@ +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; + +namespace Ryujinx.HLE.Debugger.Gdb +{ + class GdbCommands + { + const int GdbRegisterCount64 = 68; + const int GdbRegisterCount32 = 66; + + public readonly Debugger Debugger; + + 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; + Debugger = debugger; + } + + public void SetProcessor(GdbCommandProcessor commandProcessor) + { + if (Processor != null) return; + + Processor = commandProcessor; + } + + public GdbCommandProcessor CreateProcessor() + { + if (Processor != null) + return Processor; + + return Processor = new GdbCommandProcessor(this); + } + + internal void Query() + { + // GDB is performing initial contact. Stop everything. + Debugger.DebugProcess.DebugStop(); + Debugger.GThread = Debugger.CThread = Debugger.DebugProcess.GetThreadUids().First(); + Processor.Reply($"T05thread:{Debugger.CThread:x};"); + } + + internal void Interrupt() + { + // 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(); + } + + Processor.Reply($"T02thread:{Debugger.GThread:x};"); + } + + internal void Continue(ulong? newPc) + { + if (newPc.HasValue) + { + if (Debugger.CThread == null) + { + Processor.ReplyError(); + return; + } + + Debugger.DebugProcess.GetThread(Debugger.CThread.Value).Context.DebugPc = newPc.Value; + } + + Debugger.DebugProcess.DebugContinue(); + } + + internal void Detach() + { + Debugger.BreakpointManager.ClearAll(); + Continue(null); + } + + internal void ReadRegisters() + { + if (Debugger.GThread == null) + { + Processor.ReplyError(); + return; + } + + IExecutionContext ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; + string registers = string.Empty; + if (Debugger.IsProcess32Bit) + { + 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); + } + } + + Processor.Reply(registers); + } + + internal void WriteRegisters(StringStream ss) + { + if (Debugger.GThread == null) + { + Processor.ReplyError(); + return; + } + + 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)) + { + Processor.ReplyError(); + return; + } + } + } + else + { + for (int i = 0; i < GdbRegisterCount64; i++) + { + if (!GdbRegisters.Write64(ctx, i, ss)) + { + Processor.ReplyError(); + return; + } + } + } + + Processor.Reply(ss.IsEmpty()); + } + + internal void SetThread(char op, ulong? threadId) + { + if (threadId is 0 or null) + { + KThread[] threads = Debugger.GetThreads(); + if (threads.Length == 0) + { + Processor.ReplyError(); + return; + } + + threadId = threads.First().ThreadUid; + } + + if (Debugger.DebugProcess.GetThread(threadId.Value) == null) + { + Processor.ReplyError(); + return; + } + + switch (op) + { + case 'c': + Debugger.CThread = threadId; + Processor.ReplyOK(); + return; + case 'g': + Debugger.GThread = threadId; + Processor.ReplyOK(); + return; + default: + Processor.ReplyError(); + return; + } + } + + internal void ReadMemory(ulong addr, ulong len) + { + try + { + var data = new byte[len]; + Debugger.DebugProcess.CpuMemory.Read(addr, 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}"); + Processor.ReplyError(); + } + } + + internal void WriteMemory(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); + Processor.ReplyOK(); + } + catch (InvalidMemoryRegionException) + { + Processor.ReplyError(); + } + } + + internal void ReadRegister(int gdbRegId) + { + if (Debugger.GThread == null) + { + Processor.ReplyError(); + return; + } + + 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 WriteRegister(int gdbRegId, StringStream ss) + { + if (Debugger.GThread == null) + { + Processor.ReplyError(); + return; + } + + IExecutionContext ctx = Debugger.DebugProcess.GetThread(Debugger.GThread.Value).Context; + if (Debugger.IsProcess32Bit) + { + Processor.Reply(GdbRegisters.Write32(ctx, gdbRegId, ss) && ss.IsEmpty()); + } + else + { + Processor.Reply(GdbRegisters.Write64(ctx, gdbRegId, ss) && ss.IsEmpty()); + } + } + + internal void Step(ulong? newPc) + { + if (Debugger.CThread == null) + { + Processor.ReplyError(); + return; + } + + KThread thread = Debugger.DebugProcess.GetThread(Debugger.CThread.Value); + + if (newPc.HasValue) + { + thread.Context.DebugPc = newPc.Value; + } + + if (!Debugger.DebugProcess.DebugStep(thread)) + { + Processor.ReplyError(); + } + else + { + Debugger.GThread = Debugger.CThread = thread.ThreadUid; + Processor.Reply($"T05thread:{thread.ThreadUid:x};"); + } + } + + internal void IsAlive(ulong? threadId) + { + if (Debugger.GetThreads().Any(x => x.ThreadUid == threadId)) + { + Processor.ReplyOK(); + } + else + { + Processor.Reply("E00"); + } + } + + enum VContAction + { + None, + Continue, + Stop, + Step + } + + record VContPendingAction(VContAction Action/*, ushort? Signal = null*/); + + internal void VCont(StringStream ss) + { + string[] rawActions = ss.ReadRemaining().Split(';', StringSplitOptions.RemoveEmptyEntries); + + Dictionary threadActionMap = new(); + foreach (KThread 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--) + { + string rawAction = rawActions[i]; + StringStream stream = new(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); + // we still call the read length method even if we have signals commented + // since that method advances the underlying string position + } + + 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 (ulong thread in threadActionMap.Keys) + { + threadActionMap[thread] = 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 ((ulong threadUid, VContPendingAction action) in threadActionMap) + { + if (action.Action == VContAction.Step) + { + KThread 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 ((ulong threadUid, VContPendingAction action) in threadActionMap) + { + if (action.Action == VContAction.Continue) + { + Debugger.DebugProcess.DebugContinue(Debugger.DebugProcess.GetThread(threadUid)); + } + } + } + + Processor.Reply(!hasError); + + foreach ((ulong threadUid, VContPendingAction action) in threadActionMap) + { + if (action.Action == VContAction.Step) + { + Debugger.GThread = Debugger.CThread = threadUid; + Processor.Reply($"T05thread:{threadUid:x};"); + } + } + } + + internal void Q_Rcmd(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" + }; + + Processor.Reply(Helpers.ToHex(response)); + } + catch (Exception e) + { + Logger.Error?.Print(LogClass.GdbStub, $"Error processing Rcmd: {e.Message}"); + Processor.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..f9ce7b153 --- /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..eb243dc5b --- /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/RegisterInformation.cs b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs index b5fd88ea5..c1c576558 100644 --- a/src/Ryujinx.HLE/Debugger/RegisterInformation.cs +++ b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs @@ -17,8 +17,8 @@ namespace Ryujinx.HLE.Debugger private static string GetEmbeddedResourceContent(string resourceName) { - Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.HLE.Debugger.GdbXml." + resourceName); - StreamReader reader = new StreamReader(stream); + Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.HLE.Debugger.Gdb.Xml." + resourceName); + StreamReader reader = new(stream); string result = reader.ReadToEnd(); reader.Dispose(); stream.Dispose(); 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/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..eb75fb9a1 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); @@ -1088,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(); @@ -1100,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(); @@ -1204,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() @@ -1281,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) @@ -1301,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) @@ -1330,7 +1334,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process return false; } - StepBarrier.SignalAndWait(); + _stepBarrier.SignalAndWait(); return true; } @@ -1365,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); } 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) 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 @@ - - - - - - + + + + + + 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)