diff --git a/src/core/gdb_server.cpp b/src/core/gdb_server.cpp
index c175cc206..ca4588952 100644
--- a/src/core/gdb_server.cpp
+++ b/src/core/gdb_server.cpp
@@ -9,6 +9,7 @@
#include "cpu_code_cache.h"
#include "cpu_core.h"
#include "cpu_core_private.h"
+#include "gte.h"
#include "system.h"
#include "common/assert.h"
@@ -120,6 +121,74 @@ static constexpr std::string_view TARGET_XML = R"(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)";
@@ -201,6 +270,9 @@ public:
void SendReplyWithAck(std::string_view reply = std::string_view());
void ResendLastReply();
+ void SetUseTargetDescription() { m_use_target_description = true; }
+ bool IsUsingTargetDescription() const { return m_use_target_description; }
+
void AddBreakpoint(GDBBreakpointType type, VirtualMemoryAddress address, u32 kind);
void RemoveBreakpoint(GDBBreakpointType type, VirtualMemoryAddress address, u32 kind);
void ClearBreakpoints();
@@ -214,6 +286,7 @@ private:
void SendPacket(std::string_view sv);
bool m_seen_resume = false;
+ bool m_use_target_description = false;
SmallString m_last_stop_reply;
SmallString m_last_reply;
std::vector m_breakpoints;
@@ -226,6 +299,8 @@ static u8 ComputeChecksum(std::string_view str);
static bool Cmd$_questionMark(ClientSocket* client, std::string_view data);
static bool Cmd$g(ClientSocket* client, std::string_view data);
static bool Cmd$G(ClientSocket* client, std::string_view data);
+static bool Cmd$p(ClientSocket* client, std::string_view data);
+static bool Cmd$P(ClientSocket* client, std::string_view data);
static bool Cmd$H(ClientSocket* client, std::string_view data);
static bool Cmd$m(ClientSocket* client, std::string_view data);
static bool Cmd$M(ClientSocket* client, std::string_view data);
@@ -245,6 +320,11 @@ static bool ParseSignalAndOptionalAddress(std::string_view data, u8* signal,
std::optional* address);
static bool SendXferRead(ClientSocket* client, std::string_view data, std::string_view expected_annex,
std::string_view contents);
+static void AppendRegisterValue(SmallStringBase* reply, u32 value);
+static bool DecodeRegisterValue(std::string_view data, std::optional* value);
+static std::optional ReadRegisterValue(u32 index);
+static bool IsGTERegisterWritable(u32 index);
+static bool WriteRegisterValue(u32 index, u32 value);
static void InvalidateMemoryWrite(VirtualMemoryAddress address, u32 length);
static CPU::BreakpointCallbackAction OnBreakpointHit(CPU::BreakpointType type, VirtualMemoryAddress pc,
VirtualMemoryAddress address);
@@ -255,13 +335,12 @@ static bool IsPacketInterrupt(std::string_view data);
static bool IsPacketComplete(std::string_view data);
static bool ProcessPacket(ClientSocket* socket, std::string_view data);
-/// yikes, lots of stack space
-using LargeReplyPacket = SmallStackString<768>;
-
-/// Number of registers in GDB remote protocol for MIPS III.
-static constexpr int NUM_GDB_REGISTERS = 73;
+/// Number of registers in the legacy GDB remote protocol packet for MIPS I.
+static constexpr u32 NUM_GDB_REGISTERS = 73;
+static constexpr u32 FIRST_GTE_GDB_REGISTER = NUM_GDB_REGISTERS;
+static constexpr u32 NUM_EXTENDED_GDB_REGISTERS = NUM_GDB_REGISTERS + GTE::NUM_REGS;
-/// List of GDB remote protocol registers for MIPS III (excluding FP).
+/// List of GDB remote protocol registers for MIPS I (excluding FP).
static constexpr std::array REGISTERS{
&CPU::g_state.regs.r[0],
&CPU::g_state.regs.r[1],
@@ -309,6 +388,8 @@ static constexpr std::pairappend_format("{:02x}{:02x}{:02x}{:02x}", value & 0xFFu, (value >> 8) & 0xFFu, (value >> 16) & 0xFFu,
+ value >> 24);
+}
+
+bool GDBServer::DecodeRegisterValue(std::string_view data, std::optional* value)
+{
+ value->reset();
+ if (data.size() != (sizeof(u32) * 2))
+ return false;
+
+ if (data.find_first_not_of("xX") == std::string_view::npos)
+ return true;
+
+ std::array bytes;
+ if (StringUtil::DecodeHex(bytes, data) != bytes.size())
+ return false;
+
+ *value = ZeroExtend32(bytes[0]) | (ZeroExtend32(bytes[1]) << 8) | (ZeroExtend32(bytes[2]) << 16) |
+ (ZeroExtend32(bytes[3]) << 24);
+ return true;
+}
+
+std::optional GDBServer::ReadRegisterValue(u32 index)
+{
+ if (index < REGISTERS.size())
+ return *REGISTERS[index];
+
+ if (index >= FIRST_GTE_GDB_REGISTER && index < NUM_EXTENDED_GDB_REGISTERS)
+ return GTE::ReadRegister(index - FIRST_GTE_GDB_REGISTER);
+
+ return std::nullopt;
+}
+
+bool GDBServer::IsGTERegisterWritable(u32 index)
+{
+ switch (index)
+ {
+ case 15: // SXYP: pushes the SXY FIFO.
+ case 28: // IRGB: updates IR1, IR2, and IR3.
+ case 29: // ORGB: read-only.
+ case 30: // LZCS: updates LZCR.
+ case 31: // LZCR: read-only.
+ return false;
+
+ default:
+ return true;
+ }
+}
+
+bool GDBServer::WriteRegisterValue(u32 index, u32 value)
+{
+ if (index < 32)
+ {
+ if (index != 0)
+ CPU::g_state.regs.r[index] = value;
+ return true;
+ }
+
+ switch (index)
+ {
+ case 32:
+ CPU::g_state.cop0_regs.sr.bits = (CPU::g_state.cop0_regs.sr.bits & ~CPU::Cop0Registers::SR::WRITE_MASK) |
+ (value & CPU::Cop0Registers::SR::WRITE_MASK);
+ CPU::UpdateMemoryPointers();
+ return true;
+
+ case 33:
+ CPU::g_state.regs.lo = value;
+ return true;
+
+ case 34:
+ CPU::g_state.regs.hi = value;
+ return true;
+
+ case 35:
+ // BadVaddr is read-only.
+ return true;
+
+ case 36:
+ CPU::g_state.cop0_regs.cause.bits = (CPU::g_state.cop0_regs.cause.bits & ~CPU::Cop0Registers::CAUSE::WRITE_MASK) |
+ (value & CPU::Cop0Registers::CAUSE::WRITE_MASK);
+ CPU::CheckForPendingInterrupt();
+ return true;
+
+ case 37:
+ if ((value & 3u) != 0)
+ return false;
+ if (value != CPU::g_state.pc)
+ CPU::SetPC(value);
+ return true;
+
+ default:
+ break;
+ }
+
+ if (index >= FIRST_GTE_GDB_REGISTER && index < NUM_EXTENDED_GDB_REGISTERS)
+ {
+ const u32 gte_index = index - FIRST_GTE_GDB_REGISTER;
+ if (!IsGTERegisterWritable(gte_index))
+ return false;
+
+ GTE::WriteRegister(gte_index, value);
+ return true;
+ }
+
+ return false;
+}
+
/// Get general registers.
bool GDBServer::Cmd$g(ClientSocket* client, std::string_view data)
{
@@ -371,19 +563,21 @@ bool GDBServer::Cmd$g(ClientSocket* client, std::string_view data)
return true;
}
- LargeReplyPacket reply;
+ SmallStackString<(NUM_EXTENDED_GDB_REGISTERS * sizeof(u32) * 2) + 1> reply;
for (const u32* reg : REGISTERS)
- {
- // Data is in host order (little endian).
- reply.append_format("{:02x}{:02x}{:02x}{:02x}", *reg & 0xFFu, (*reg >> 8) & 0xFFu, (*reg >> 16) & 0xFFu,
- (*reg >> 24));
- }
+ AppendRegisterValue(&reply, *reg);
// Pad with dummy data (FP registers stuff).
- for (int i = 0; i < NUM_GDB_REGISTERS - static_cast(REGISTERS.size()); i++)
+ for (u32 i = static_cast(REGISTERS.size()); i < NUM_GDB_REGISTERS; i++)
reply.append("xxxxxxxx");
+ if (client->IsUsingTargetDescription())
+ {
+ for (u32 i = 0; i < GTE::NUM_REGS; i++)
+ AppendRegisterValue(&reply, GTE::ReadRegister(i));
+ }
+
client->SendReplyWithAck(reply);
return true;
}
@@ -391,23 +585,36 @@ bool GDBServer::Cmd$g(ClientSocket* client, std::string_view data)
/// Set general registers.
bool GDBServer::Cmd$G(ClientSocket* client, std::string_view data)
{
- std::array bytes;
- if (data.size() != bytes.size() * 2 || StringUtil::DecodeHex(bytes, data) != bytes.size())
+ const u32 num_registers = client->IsUsingTargetDescription() ? NUM_EXTENDED_GDB_REGISTERS : NUM_GDB_REGISTERS;
+ if (data.size() != (num_registers * sizeof(u32) * 2))
{
- ERROR_LOG("Invalid payload for 'G' command, expected {} hex digits", bytes.size() * 2);
+ ERROR_LOG("Invalid payload for 'G' command, expected {} hex digits", num_registers * sizeof(u32) * 2);
client->SendReplyWithAck("E01");
return true;
}
- std::array values;
- for (size_t i = 0; i < values.size(); i++)
+ std::array, NUM_EXTENDED_GDB_REGISTERS> values;
+ for (u32 i = 0; i < num_registers; i++)
+ {
+ if (!DecodeRegisterValue(data.substr(i * sizeof(u32) * 2, sizeof(u32) * 2), &values[i]))
+ {
+ ERROR_LOG("Invalid register {} in 'G' command", i);
+ client->SendReplyWithAck("E01");
+ return true;
+ }
+ }
+
+ for (u32 i = 0; i < REGISTERS.size(); i++)
{
- const size_t offset = i * sizeof(u32);
- values[i] = ZeroExtend32(bytes[offset]) | (ZeroExtend32(bytes[offset + 1]) << 8) |
- (ZeroExtend32(bytes[offset + 2]) << 16) | (ZeroExtend32(bytes[offset + 3]) << 24);
+ if (!values[i].has_value())
+ {
+ ERROR_LOG("Required register {} is unavailable in 'G' command", i);
+ client->SendReplyWithAck("E01");
+ return true;
+ }
}
- const u32 new_pc = values[37];
+ const u32 new_pc = values[37].value();
if ((new_pc & 3u) != 0)
{
ERROR_LOG("Invalid PC 0x{:08X} in 'G' command", new_pc);
@@ -417,22 +624,84 @@ bool GDBServer::Cmd$G(ClientSocket* client, std::string_view data)
CPU::g_state.regs.r[0] = 0;
for (size_t i = 1; i < 32; i++)
- CPU::g_state.regs.r[i] = values[i];
+ CPU::g_state.regs.r[i] = values[i].value();
CPU::g_state.cop0_regs.sr.bits = (CPU::g_state.cop0_regs.sr.bits & ~CPU::Cop0Registers::SR::WRITE_MASK) |
- (values[32] & CPU::Cop0Registers::SR::WRITE_MASK);
+ (values[32].value() & CPU::Cop0Registers::SR::WRITE_MASK);
CPU::UpdateMemoryPointers();
- CPU::g_state.regs.lo = values[33];
- CPU::g_state.regs.hi = values[34];
+ CPU::g_state.regs.lo = values[33].value();
+ CPU::g_state.regs.hi = values[34].value();
CPU::g_state.cop0_regs.cause.bits = (CPU::g_state.cop0_regs.cause.bits & ~CPU::Cop0Registers::CAUSE::WRITE_MASK) |
- (values[36] & CPU::Cop0Registers::CAUSE::WRITE_MASK);
+ (values[36].value() & CPU::Cop0Registers::CAUSE::WRITE_MASK);
CPU::CheckForPendingInterrupt();
if (new_pc != CPU::g_state.pc)
CPU::SetPC(new_pc);
+ if (client->IsUsingTargetDescription())
+ {
+ for (u32 i = 0; i < GTE::NUM_REGS; i++)
+ {
+ const std::optional& value = values[FIRST_GTE_GDB_REGISTER + i];
+ if (value.has_value() && IsGTERegisterWritable(i) && value.value() != GTE::ReadRegister(i))
+ GTE::WriteRegister(i, value.value());
+ }
+ }
+
+ client->SendReplyWithAck("OK");
+ return true;
+}
+
+/// Get one register.
+bool GDBServer::Cmd$p(ClientSocket* client, std::string_view data)
+{
+ std::string_view end;
+ const std::optional index = StringUtil::FromChars(data, 16, &end);
+ const u32 num_registers = client->IsUsingTargetDescription() ? NUM_EXTENDED_GDB_REGISTERS : NUM_GDB_REGISTERS;
+ if (!index.has_value() || !end.empty() || index.value() >= num_registers)
+ {
+ ERROR_LOG("Invalid register in 'p' command: {}", data);
+ client->SendReplyWithAck("E01");
+ return true;
+ }
+
+ const std::optional value = ReadRegisterValue(index.value());
+ if (!value.has_value())
+ {
+ client->SendReplyWithAck("xxxxxxxx");
+ return true;
+ }
+
+ TinyString reply;
+ AppendRegisterValue(&reply, value.value());
+ client->SendReplyWithAck(reply);
+ return true;
+}
+
+/// Set one register.
+bool GDBServer::Cmd$P(ClientSocket* client, std::string_view data)
+{
+ std::string_view caret;
+ const std::optional index = StringUtil::FromChars(data, 16, &caret);
+ const u32 num_registers = client->IsUsingTargetDescription() ? NUM_EXTENDED_GDB_REGISTERS : NUM_GDB_REGISTERS;
+ if (!index.has_value() || caret.empty() || caret[0] != '=' || index.value() >= num_registers)
+ {
+ ERROR_LOG("Invalid register in 'P' command: {}", data);
+ client->SendReplyWithAck("E01");
+ return true;
+ }
+
+ std::optional value;
+ if (!DecodeRegisterValue(caret.substr(1), &value) || !value.has_value() ||
+ !WriteRegisterValue(index.value(), value.value()))
+ {
+ ERROR_LOG("Invalid register value in 'P' command: {}", data);
+ client->SendReplyWithAck("E01");
+ return true;
+ }
+
client->SendReplyWithAck("OK");
return true;
}
@@ -680,6 +949,9 @@ bool GDBServer::Cmd$qSupported(ClientSocket* client, std::string_view data)
bool GDBServer::Cmd$qXferFeaturesRead(ClientSocket* client, std::string_view data)
{
+ if (data.starts_with("target.xml:"))
+ client->SetUseTargetDescription();
+
return SendXferRead(client, data, "target.xml", TARGET_XML);
}