diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs index 8ce358898..63b4b2edc 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs @@ -136,6 +136,13 @@ namespace Ryujinx.Cpu.AppleHv /// public void DebugContinue() => _impl.DebugContinue(); + /// + public ulong DebugPc + { + get => _impl.DebugPc; + set => _impl.DebugPc = value; + } + /// public void StopRunning() { diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs index b4e96b561..b9cc08fb2 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs @@ -21,6 +21,8 @@ namespace Ryujinx.Cpu.AppleHv private readonly ulong[] _x; private readonly V128[] _v; + public ulong DebugPc { get; set; } + public HvExecutionContextShadow() { _x = new ulong[32]; diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs index a55a2aba2..c581698b9 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs @@ -19,7 +19,7 @@ namespace Ryujinx.Cpu.AppleHv internal Barrier _stepBarrier = new Barrier(2); // This is only valid while debugging is enabled. - public ulong DebugPc; + public ulong DebugPc { get; set; } static HvExecutionContextVcpu() { diff --git a/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs b/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs index b385cd78e..f12ba9dfc 100644 --- a/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs +++ b/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs @@ -47,5 +47,7 @@ namespace Ryujinx.Cpu.AppleHv void DebugStop(); bool DebugStep(); void DebugContinue(); + + ulong DebugPc { get; set; } } } diff --git a/src/Ryujinx.Cpu/IExecutionContext.cs b/src/Ryujinx.Cpu/IExecutionContext.cs index ad07a2766..927ba120d 100644 --- a/src/Ryujinx.Cpu/IExecutionContext.cs +++ b/src/Ryujinx.Cpu/IExecutionContext.cs @@ -113,5 +113,7 @@ namespace Ryujinx.Cpu void DebugStop(); bool DebugStep(); void DebugContinue(); + + ulong DebugPc { get; set; } } } diff --git a/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs b/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs index a3224c366..fda9e54cb 100644 --- a/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs +++ b/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs @@ -118,6 +118,13 @@ namespace Ryujinx.Cpu.Jit /// public void DebugContinue() => _impl.DebugContinue(); + /// + public ulong DebugPc + { + get => _impl.DebugPc; + set => _impl.DebugPc = value; + } + /// public void StopRunning() { diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs new file mode 100644 index 000000000..534938e66 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -0,0 +1,410 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace Ryujinx.HLE.Debugger +{ + public class Debugger : IDisposable + { + internal Switch Device { get; private set; } + + public ushort GdbStubPort { get; private set; } + + private TcpListener ListenerSocket; + private Socket ClientSocket = null; + private NetworkStream ReadStream = null; + private NetworkStream WriteStream = null; + private BlockingCollection Messages = new BlockingCollection(1); + private Thread SocketThread; + private Thread HandlerThread; + + private ulong cThread; + private ulong gThread; + + public Debugger(Switch device, ushort port) + { + Device = device; + GdbStubPort = port; + + ARMeilleure.Optimizations.EnableDebugging = true; + + SocketThread = new Thread(SocketReaderThreadMain); + HandlerThread = new Thread(HandlerThreadMain); + SocketThread.Start(); + HandlerThread.Start(); + } + + private void HaltApplication() => Device.System.DebugGetApplicationProcess().DebugStopAllThreads(); + private ulong[] GetThreadIds() => Device.System.DebugGetApplicationProcess().DebugGetThreadUids(); + private Ryujinx.Cpu.IExecutionContext GetThread(ulong threadUid) => Device.System.DebugGetApplicationProcess().DebugGetThreadContext(threadUid); + private Ryujinx.Cpu.IExecutionContext[] GetThreads() => GetThreadIds().Select(x => GetThread(x)).ToArray(); + private IVirtualMemoryManager GetMemory() => Device.System.DebugGetApplicationProcess().CpuMemory; + + const int GdbRegisterCount = 34; + + private int GdbRegisterHexSize(int gdbRegId) + { + switch (gdbRegId) + { + case >= 0 and <= 31: + return 16; + case 32: + return 16; + case 33: + return 8; + default: + throw new ArgumentException(); + } + } + + private string GdbReadRegister(Ryujinx.Cpu.IExecutionContext state, int gdbRegId) + { + switch (gdbRegId) + { + case >= 0 and <= 31: + return $"{state.GetX(gdbRegId):x16}"; + case 32: + return $"{state.DebugPc:x16}"; + case 33: + return $"{state.Pstate:x8}"; + default: + throw new ArgumentException(); + } + } + + private void GdbWriteRegister(Ryujinx.Cpu.IExecutionContext state, int gdbRegId, ulong value) + { + switch (gdbRegId) + { + case >= 0 and <= 31: + state.SetX(gdbRegId, value); + return; + case 32: + state.DebugPc = value; + return; + case 33: + state.Pstate = (uint)value; + return; + default: + throw new ArgumentException(); + } + } + + private void HandlerThreadMain() + { + while (true) + { + switch (Messages.Take()) + { + case AbortMessage _: + return; + + case BreakInMessage _: + Logger.Notice.Print(LogClass.GdbStub, "Break-in requested"); + // TODO + break; + + case SendNackMessage _: + WriteStream.WriteByte((byte)'-'); + break; + + case CommandMessage { Command: var cmd }: + Logger.Debug?.Print(LogClass.GdbStub, $"Received Command: {cmd}"); + WriteStream.WriteByte((byte)'+'); + ProcessCommand(cmd); + break; + } + } + } + + private void ProcessCommand(string cmd) + { + StringStream ss = new StringStream(cmd); + + switch (ss.ReadChar()) + { + case '!': + if (!ss.IsEmpty()) + { + goto default; + } + // Enable extended mode + Reply("OK"); + break; + case '?': + if (!ss.IsEmpty()) + { + goto default; + } + CommandQuery(); + break; + case 'c': + CommandContinue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + break; + case 'D': + if (!ss.IsEmpty()) + { + goto default; + } + CommandDetach(); + break; + case 'g': + if (!ss.IsEmpty()) + { + goto default; + } + CommandReadGeneralRegisters(); + break; + case 'G': + CommandWriteGeneralRegisters(ss); + break; + case 'H': + { + char op = ss.ReadChar(); + ulong threadId = ss.ReadRemainingAsHex(); + CommandSetThread(op, threadId); + break; + } + case 'k': + Logger.Notice.Print(LogClass.GdbStub, "Kill request received"); + Reply(""); + 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(); + CommandReadGeneralRegister((int)gdbRegId); + break; + } + case 'P': + { + ulong gdbRegId = ss.ReadUntilAsHex('='); + ulong value = ss.ReadRemainingAsHex(); + CommandWriteGeneralRegister((int)gdbRegId, value); + break; + } + default: + Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}"); + Reply(""); + break; + } + } + + void CommandQuery() + { + // GDB is performing initial contact. Stop everything. + HaltApplication(); + gThread = cThread = GetThreadIds().First(); + Reply($"T05thread:{cThread:x}"); + } + + void CommandContinue(ulong? newPc) + { + if (newPc.HasValue) + { + GetThread(cThread).DebugPc = newPc.Value; + } + + foreach (var thread in GetThreads()) + { + thread.DebugContinue(); + } + } + + void CommandDetach() + { + // TODO: Remove all breakpoints + CommandContinue(null); + } + + void CommandReadGeneralRegisters() + { + var ctx = GetThread(gThread); + string registers = ""; + for (int i = 0; i < GdbRegisterCount; i++) + { + registers += GdbReadRegister(ctx, i); + } + Reply(registers); + } + + void CommandWriteGeneralRegisters(StringStream ss) + { + var ctx = GetThread(gThread); + for (int i = 0; i < GdbRegisterCount; i++) + { + GdbWriteRegister(ctx, i, ss.ReadLengthAsHex(GdbRegisterHexSize(i))); + } + Reply(ss.IsEmpty() ? "OK" : "E99"); + } + + void CommandSetThread(char op, ulong threadId) + { + switch (op) + { + case 'c': + cThread = threadId; + Reply("OK"); + return; + case 'g': + gThread = threadId; + Reply("OK"); + return; + default: + Reply("E99"); + return; + } + } + + void CommandReadMemory(ulong addr, ulong len) + { + var data = new byte[len]; + GetMemory().Read(addr, data); + Reply(string.Join("", data.Select(x => $"{x:x2}"))); + } + + void CommandWriteMemory(ulong addr, ulong len, StringStream ss) + { + var data = new byte[len]; + for (ulong i = 0; i < len; i++) + { + data[i] = (byte)ss.ReadLengthAsHex(2); + } + GetMemory().Write(addr, data); + } + + void CommandReadGeneralRegister(int gdbRegId) + { + var ctx = GetThread(gThread); + Reply(GdbReadRegister(ctx, gdbRegId)); + } + + void CommandWriteGeneralRegister(int gdbRegId, ulong value) + { + var ctx = GetThread(gThread); + GdbWriteRegister(ctx, gdbRegId, value); + Reply("OK"); + } + + private void Reply(string cmd) + { + WriteStream.Write(Encoding.ASCII.GetBytes($"${cmd}#{CalculateChecksum(cmd):x2}")); + } + + private void SocketReaderThreadMain() + { + restartListen: + try + { + 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"); + + ClientSocket = ListenerSocket.AcceptSocket(); + 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) + { + 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()}"; + // Debug.Assert(checksum == $"{CalculateChecksum(cmd):x2}"); + + Messages.Add(new CommandMessage(cmd)); + break; + } + } + + eof: + Logger.Notice.Print(LogClass.GdbStub, "GDB client lost connection"); + goto restartListen; + } + catch (Exception) + { + Logger.Notice.Print(LogClass.GdbStub, "GDB stub socket closed"); + return; + } + } + + private byte CalculateChecksum(string cmd) + { + byte checksum = 0; + foreach (char x in cmd) + { + unchecked + { + checksum += (byte)x; + } + } + return checksum; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (HandlerThread.IsAlive) + { + Messages.Add(new AbortMessage()); + } + ListenerSocket.Stop(); + ClientSocket?.Shutdown(SocketShutdown.Both); + ClientSocket?.Close(); + ReadStream?.Close(); + WriteStream?.Close(); + SocketThread.Join(); + HandlerThread.Join(); + } + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/GdbSignal.cs b/src/Ryujinx.HLE/Debugger/GdbSignal.cs new file mode 100644 index 000000000..687194ef3 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbSignal.cs @@ -0,0 +1,15 @@ +namespace Ryujinx.HLE.Debugger +{ + enum GdbSignal + { + Zero = 0, + Int = 2, + Quit = 3, + Trap = 5, + Abort = 6, + Alarm = 14, + IO = 23, + XCPU = 24, + Unknown = 143 + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/AbortMessage.cs b/src/Ryujinx.HLE/Debugger/Message/AbortMessage.cs new file mode 100644 index 000000000..2c0d6bb79 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/AbortMessage.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.HLE.Debugger +{ + struct AbortMessage : IMessage + { + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs b/src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs new file mode 100644 index 000000000..24cfb0b4a --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.HLE.Debugger +{ + struct BreakInMessage : IMessage + { + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs b/src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs new file mode 100644 index 000000000..72320532f --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.Debugger +{ + struct CommandMessage : IMessage + { + public string Command; + + public CommandMessage(string cmd) + { + Command = cmd; + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/IMessage.cs b/src/Ryujinx.HLE/Debugger/Message/IMessage.cs new file mode 100644 index 000000000..d530d00d3 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/IMessage.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.HLE.Debugger +{ + interface IMessage + { + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs b/src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs new file mode 100644 index 000000000..f599ee91c --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.HLE.Debugger +{ + struct SendNackMessage : IMessage + { + } +} diff --git a/src/Ryujinx.HLE/Debugger/StringStream.cs b/src/Ryujinx.HLE/Debugger/StringStream.cs new file mode 100644 index 000000000..b654f6f54 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/StringStream.cs @@ -0,0 +1,68 @@ +using System.Globalization; + +namespace Ryujinx.HLE.Debugger +{ + class StringStream + { + private readonly string Data; + private int Position; + + public StringStream(string s) + { + Data = s; + } + + public char ReadChar() + { + return Data[Position++]; + } + + public string ReadUntil(char needle) + { + int needlePos = Data.IndexOf(needle, Position); + + if (needlePos == -1) + { + needlePos = Data.Length; + } + + string result = Data.Substring(Position, needlePos - Position); + Position = needlePos + 1; + return result; + } + + public string ReadLength(int len) + { + string result = Data.Substring(Position, len); + Position += len; + return result; + } + + public string ReadRemaining() + { + string result = Data.Substring(Position); + Position = Data.Length; + return result; + } + + public ulong ReadRemainingAsHex() + { + return ulong.Parse(ReadRemaining(), NumberStyles.HexNumber); + } + + public ulong ReadUntilAsHex(char needle) + { + return ulong.Parse(ReadUntil(needle), NumberStyles.HexNumber); + } + + public ulong ReadLengthAsHex(int len) + { + return ulong.Parse(ReadLength(len), NumberStyles.HexNumber); + } + + public bool IsEmpty() + { + return Position >= Data.Length; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs index 6050f88f2..f9718280e 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs @@ -21,6 +21,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Process private readonly ulong[] _x = new ulong[32]; + public ulong DebugPc { get; set; } + public ulong GetX(int index) => _x[index]; public void SetX(int index, ulong value) => _x[index] = value; diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index 9dfc69892..89bf7b975 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -26,6 +26,7 @@ namespace Ryujinx.HLE public Hid Hid { get; } public TamperMachine TamperMachine { get; } public IHostUIHandler UIHandler { get; } + public Debugger.Debugger Debugger { get; } public bool EnableDeviceVsync { get; set; } = true; @@ -53,6 +54,7 @@ namespace Ryujinx.HLE Statistics = new PerformanceStatistics(); Hid = new Hid(this, System.HidStorage); Processes = new ProcessLoader(this); + Debugger = Configuration.EnableGdbStub ? new Debugger.Debugger(this, configuration.GdbStubPort) : null; TamperMachine = new TamperMachine(); System.InitializeServices(); @@ -154,6 +156,7 @@ namespace Ryujinx.HLE AudioDeviceDriver.Dispose(); FileSystem.Dispose(); Memory.Dispose(); + Debugger.Dispose(); } } } diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs index ea2063758..5891319c7 100644 --- a/src/Ryujinx.Headless.SDL2/Options.cs +++ b/src/Ryujinx.Headless.SDL2/Options.cs @@ -229,5 +229,13 @@ namespace Ryujinx.Headless.SDL2 [Value(0, MetaName = "input", HelpText = "Input to load.", Required = true)] public string InputPath { get; set; } + + // Debugging + + [Option("enable-gdb-stub", Required = false, Default = false, HelpText = "Enable the GDB stub.")] + public bool EnableGdbStub { get; set; } + + [Option("gdb-stub-port", Required = false, Default = 55555, HelpText = "GDB stub port.")] + public ushort GdbStubPort { get; set; } } } diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index 85aff6712..d6cf7d6d6 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -571,7 +571,9 @@ namespace Ryujinx.Headless.SDL2 options.AudioVolume, options.UseHypervisor ?? true, options.MultiplayerLanInterfaceId, - Common.Configuration.Multiplayer.MultiplayerMode.Disabled); + Common.Configuration.Multiplayer.MultiplayerMode.Disabled, + options.EnableGdbStub, + options.GdbStubPort); return new Switch(configuration); } diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index 0db8ef414..65ef63926 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -872,7 +872,9 @@ namespace Ryujinx.Ava ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, - ConfigurationState.Instance.Multiplayer.Mode); + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Debug.EnableGdbStub.Value, + ConfigurationState.Instance.Debug.GdbStubPort.Value); Device = new Switch(configuration); }