diff --git a/src/common/string_util.h b/src/common/string_util.h index 2e903f046..bff1cb25a 100644 --- a/src/common/string_util.h +++ b/src/common/string_util.h @@ -415,6 +415,19 @@ void ReplaceAll(std::string* subject, const char search, const char replacement) /// Parses an assignment string (Key = Value) into its two components. bool ParseAssignmentString(const std::string_view str, std::string_view* key, std::string_view* value); +/// Helper for tokenizing strings. +ALWAYS_INLINE std::optional GetNextToken(std::string_view& caret, char separator) +{ + std::optional ret; + const std::string_view::size_type pos = caret.find(separator); + if (pos != std::string_view::npos) + { + ret = caret.substr(0, pos); + caret = caret.substr(pos + 1); + } + return ret; +} + /// Unicode replacement character. static constexpr char32_t UNICODE_REPLACEMENT_CHARACTER = 0xFFFD; diff --git a/src/core/gdb_server.cpp b/src/core/gdb_server.cpp index f79268d2b..ba4742559 100644 --- a/src/core/gdb_server.cpp +++ b/src/core/gdb_server.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin and contributors. +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin and contributors. // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "gdb_server.h" @@ -11,39 +11,61 @@ #include "common/log.h" #include "common/small_string.h" #include "common/string_util.h" +#include "common/thirdparty/SmallVector.h" #include "util/sockets.h" -#include #include -#include -#include LOG_CHANNEL(GDBServer); namespace GDBServer { -static u8* GetMemoryPointer(PhysicalMemoryAddress address, u32 length); -static u8 ComputeChecksum(std::string_view str); -static std::optional DeserializePacket(std::string_view in); -static std::string SerializePacket(std::string_view in); +namespace { +class ClientSocket final : public BufferedStreamSocket +{ +public: + ClientSocket(SocketMultiplexer& multiplexer, SocketDescriptor descriptor); + ~ClientSocket() override; -static std::optional Cmd$_questionMark(std::string_view data); -static std::optional Cmd$g(std::string_view data); -static std::optional Cmd$G(std::string_view data); -static std::optional Cmd$m(std::string_view data); -static std::optional Cmd$M(std::string_view data); -static std::optional Cmd$z1(std::string_view data); -static std::optional Cmd$Z1(std::string_view data); -static std::optional Cmd$vMustReplyEmpty(std::string_view data); -static std::optional Cmd$qSupported(std::string_view data); + void OnSystemPaused(); + void OnSystemResumed(); + + void SendReplyWithAck(std::string_view reply = std::string_view()); + +protected: + void OnConnected() override; + void OnDisconnected(const Error& error) override; + void OnRead() override; + +private: + void SendPacket(std::string_view sv); + + bool m_seen_resume = false; +}; +} // namespace + +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$m(ClientSocket* client, std::string_view data); +static bool Cmd$M(ClientSocket* client, std::string_view data); +static bool Cmd$z1(ClientSocket* client, std::string_view data); +static bool Cmd$Z1(ClientSocket* client, std::string_view data); +static bool Cmd$vMustReplyEmpty(ClientSocket* client, std::string_view data); +static bool Cmd$qSupported(ClientSocket* client, std::string_view data); static bool IsPacketAck(std::string_view data); static bool IsPacketInterrupt(std::string_view data); static bool IsPacketContinue(std::string_view data); static bool IsPacketComplete(std::string_view data); -static std::string ProcessPacket(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. constexpr int NUM_GDB_REGISTERS = 73; @@ -92,7 +114,7 @@ static const std::array REGISTERS{ }; /// List of all GDB remote protocol packets supported by us. -static constexpr std::pair (*)(std::string_view)> COMMANDS[] = { +static constexpr std::pair COMMANDS[] = { {"?", Cmd$_questionMark}, {"g", Cmd$g}, {"G", Cmd$G}, @@ -106,127 +128,69 @@ static constexpr std::pair (*)(std: {"qSupported", Cmd$qSupported}, }; -namespace { -class ClientSocket final : public BufferedStreamSocket -{ -public: - ClientSocket(SocketMultiplexer& multiplexer, SocketDescriptor descriptor); - ~ClientSocket() override; - - void OnSystemPaused(); - void OnSystemResumed(); - -protected: - void OnConnected() override; - void OnDisconnected(const Error& error) override; - void OnRead() override; - -private: - void SendPacket(std::string_view sv); - - bool m_seen_resume = false; -}; -} // namespace - static std::shared_ptr s_gdb_listen_socket; static std::vector> s_gdb_clients; } // namespace GDBServer -u8* GDBServer::GetMemoryPointer(PhysicalMemoryAddress address, u32 length) -{ - auto region = Bus::GetMemoryRegionForAddress(address); - if (region) - { - u8* data = GetMemoryRegionPointer(*region); - if (data && (address + length <= GetMemoryRegionEnd(*region))) - { - return data + (address - GetMemoryRegionStart(*region)); - } - } - - return nullptr; -} - u8 GDBServer::ComputeChecksum(std::string_view str) { u8 checksum = 0; for (char c : str) - { checksum = (checksum + c) % 256; - } + return checksum; } -std::optional GDBServer::DeserializePacket(std::string_view in) -{ - if ((in.size() < 4) || (in[0] != '$') || (in[in.size() - 3] != '#')) - { - return std::nullopt; - } - std::string_view data = in.substr(1, in.size() - 4); - - u8 packetChecksum = StringUtil::FromChars(in.substr(in.size() - 2, 2), 16).value_or(0); - u8 computedChecksum = ComputeChecksum(data); - - if (packetChecksum == computedChecksum) - { - return {data}; - } - else - { - return std::nullopt; - } -} - -std::string GDBServer::SerializePacket(std::string_view in) -{ - std::stringstream ss; - ss << '$' << in << '#' << TinyString::from_format("{:02x}", ComputeChecksum(in)); - return ss.str(); -} - /// Get stop reason. -std::optional GDBServer::Cmd$_questionMark(std::string_view data) +bool GDBServer::Cmd$_questionMark(ClientSocket* client, std::string_view data) { - return {"S02"}; + client->SendReplyWithAck("S02"); + return true; } /// Get general registers. -std::optional GDBServer::Cmd$g(std::string_view data) +bool GDBServer::Cmd$g(ClientSocket* client, std::string_view data) { - std::stringstream ss; + LargeReplyPacket reply; - for (u32* reg : REGISTERS) + for (const u32* reg : REGISTERS) { // Data is in host order (little endian). - ss << StringUtil::EncodeHex(reinterpret_cast(reg), 4); + reply.append_format("{:02x}{:02x}{:02x}{:02x}", *reg & 0xFFu, (*reg >> 8) & 0xFFu, (*reg >> 16) & 0xFFu, + (*reg >> 24)); } // Pad with dummy data (FP registers stuff). for (int i = 0; i < NUM_GDB_REGISTERS - static_cast(REGISTERS.size()); i++) - { - ss << "00000000"; - } + reply.append("00000000"); - return {ss.str()}; + client->SendReplyWithAck(reply); + return true; } /// Set general registers. -std::optional GDBServer::Cmd$G(std::string_view data) +bool GDBServer::Cmd$G(ClientSocket* client, std::string_view data) { if (data.size() == NUM_GDB_REGISTERS * 8) { - int offset = 0; + size_t offset = 0; for (u32* reg : REGISTERS) { // Data is in host order (little endian). - auto value = StringUtil::DecodeHex({data.data() + offset, 8}); - if (value) + const std::string_view tex_value = data.substr(offset, 8); + std::array le_value; + if (StringUtil::DecodeHex(le_value, tex_value) == 4) { - *reg = *reinterpret_cast(&(*value)[0]); + *reg = ZeroExtend32(le_value[0]) | (ZeroExtend32(le_value[1]) << 8) | (ZeroExtend32(le_value[2]) << 16) | + (ZeroExtend32(le_value[3]) << 16); } + else + { + ERROR_LOG("Invalid register set value: {}", tex_value); + } + offset += 8; } } @@ -235,103 +199,128 @@ std::optional GDBServer::Cmd$G(std::string_view data) ERROR_LOG("Wrong payload size for 'G' command, expected {} got {}", NUM_GDB_REGISTERS * 8, data.size()); } - return {""}; + client->SendReplyWithAck(); + return true; } /// Get memory. -std::optional GDBServer::Cmd$m(std::string_view data) +bool GDBServer::Cmd$m(ClientSocket* client, std::string_view data) { - std::stringstream ss{std::string{data}}; - std::string dataAddress, dataLength; - - std::getline(ss, dataAddress, ','); - std::getline(ss, dataLength, '\0'); - - auto address = StringUtil::FromChars(dataAddress, 16); - auto length = StringUtil::FromChars(dataLength, 16); - - if (address && length) + // address,length + std::string_view caret = data; + std::optional address; + std::optional length; + if (!(address = StringUtil::FromChars(caret, 16, &caret)).has_value() || caret.empty() || + caret[0] != ',' || !(length = StringUtil::FromChars(caret.substr(1), 16)).has_value()) { - PhysicalMemoryAddress phys_addr = *address & CPU::PHYSICAL_MEMORY_ADDRESS_MASK; - u32 phys_length = *length; - - u8* ptr_data = GetMemoryPointer(phys_addr, phys_length); - if (ptr_data) - { - return {StringUtil::EncodeHex(ptr_data, phys_length)}; - } + ERROR_LOG("Invalid packet: {}", data); + return false; } - return {"E00"}; + + // large enough for most requests + llvm::SmallVector buffer; + buffer.resize_for_overwrite(length.value()); + if (!CPU::SafeReadMemoryBytes(address.value(), buffer.data(), length.value())) + { + ERROR_LOG("Failed to read {} bytes from address 0x{:08X}", buffer.size(), address.value()); + client->SendReplyWithAck("E00"); + return true; + } + + SmallString reply; + reply.append_hex(buffer.data(), buffer.size()); + client->SendReplyWithAck(reply); + return true; } /// Set memory. -std::optional GDBServer::Cmd$M(std::string_view data) +bool GDBServer::Cmd$M(ClientSocket* client, std::string_view data) { - std::stringstream ss{std::string{data}}; - std::string dataAddress, dataLength, dataPayload; - - std::getline(ss, dataAddress, ','); - std::getline(ss, dataLength, ':'); - std::getline(ss, dataPayload, '\0'); - - auto address = StringUtil::FromChars(dataAddress, 16); - auto length = StringUtil::FromChars(dataLength, 16); - auto payload = StringUtil::DecodeHex(dataPayload); - - if (address && length && payload && (payload->size() == *length)) + // address,length:data + std::string_view caret = data; + std::optional address; + std::optional length; + if (!(address = StringUtil::FromChars(caret, 16, &caret)).has_value() || caret.empty() || + caret[0] != ',' || !(length = StringUtil::FromChars(caret.substr(1), 16, &caret)).has_value() || + caret.empty() || caret[0] != ':') { - u32 phys_addr = *address & CPU::PHYSICAL_MEMORY_ADDRESS_MASK; - u32 phys_length = *length; - - u8* ptr_data = GetMemoryPointer(phys_addr, phys_length); - if (ptr_data) - { - memcpy(ptr_data, payload->data(), phys_length); - return {"OK"}; - } + ERROR_LOG("Invalid packet: {}", data); + return false; } - return {"E00"}; + // remove ':' + caret = caret.substr(1); + if (length.value() != (caret.size() / 2)) + { + ERROR_LOG("Invalid length in packet {}", data); + return false; + } + + // large enough for most requests + llvm::SmallVector buffer; + buffer.resize_for_overwrite(length.value()); + if (!StringUtil::DecodeHex(buffer, caret)) + { + ERROR_LOG("Invalid hex in packet {}", data); + return false; + } + + if (!CPU::SafeWriteMemoryBytes(address.value(), buffer)) + { + ERROR_LOG("Failed to write {} bytes to {}", buffer.size(), address.value()); + client->SendReplyWithAck("E00"); + return true; + } + + client->SendReplyWithAck("OK"); + return true; } /// Remove hardware breakpoint. -std::optional GDBServer::Cmd$z1(std::string_view data) +bool GDBServer::Cmd$z1(ClientSocket* client, std::string_view data) { - auto address = StringUtil::FromChars(data, 16); - if (address) + const std::optional address = StringUtil::FromChars(data, 16); + if (address.has_value()) { CPU::RemoveBreakpoint(CPU::BreakpointType::Execute, *address); - return {"OK"}; + client->SendReplyWithAck("OK"); + return true; } else { - return std::nullopt; + ERROR_LOG("Invalid address to remove hw breakpoint: ", data); + client->SendReplyWithAck(); + return false; } } /// Insert hardware breakpoint. -std::optional GDBServer::Cmd$Z1(std::string_view data) +bool GDBServer::Cmd$Z1(ClientSocket* client, std::string_view data) { - auto address = StringUtil::FromChars(data, 16); + const std::optional address = StringUtil::FromChars(data, 16); if (address) { CPU::AddBreakpoint(CPU::BreakpointType::Execute, *address, false); - return {"OK"}; + client->SendReplyWithAck("OK"); + return true; } else { - return std::nullopt; + ERROR_LOG("Invalid address to insert hw breakpoint: ", data); + return false; } } -std::optional GDBServer::Cmd$vMustReplyEmpty(std::string_view data) +bool GDBServer::Cmd$vMustReplyEmpty(ClientSocket* client, std::string_view data) { - return {""}; + client->SendReplyWithAck(); + return true; } -std::optional GDBServer::Cmd$qSupported(std::string_view data) +bool GDBServer::Cmd$qSupported(ClientSocket* client, std::string_view data) { - return {""}; + client->SendReplyWithAck(); + return true; } bool GDBServer::IsPacketAck(std::string_view data) @@ -356,37 +345,41 @@ bool GDBServer::IsPacketComplete(std::string_view data) return ((data.size() == 1) && (data[0] == '\003')) || ((data.size() > 3) && (*(data.end() - 3) == '#')); } -std::string GDBServer::ProcessPacket(std::string_view data) +bool GDBServer::ProcessPacket(ClientSocket* client, std::string_view data) { // Validate packet. - auto packet = DeserializePacket(data); - if (!packet) + if ((data.size() < 4) || (data[0] != '$') || (data[data.size() - 3] != '#')) { - ERROR_LOG("Malformed packet '{}'", data); - return "-"; + ERROR_LOG("Invalid packet: {}", data); + return false; } - std::optional reply = {""}; + // Verify checksum. + const std::string_view request = data.substr(1, data.size() - 4); + const u8 packet_checksum = StringUtil::FromChars(data.substr(data.size() - 2, 2), 16).value_or(0); + const u8 computed_checksum = ComputeChecksum(request); + if (packet_checksum != computed_checksum) + { + ERROR_LOG("Incorrect checksum, expected 0x{:02x} got 0x{:02x} for '{}'", computed_checksum, packet_checksum, data); + return false; + } // Try to invoke packet command. - bool processed = false; for (const auto& command : COMMANDS) { - if (packet->starts_with(command.first)) + if (request.starts_with(command.first)) { DEV_LOG("Processing command '{}'", command.first); // Invoke command, remove command name from payload. - reply = command.second(packet->substr(command.first.size())); - processed = true; - break; + return command.second(client, request.substr(command.first.size())); } } - if (!processed) - WARNING_LOG("Failed to process packet '{}'", data); - - return reply ? "+" + SerializePacket(*reply) : "+"; + // Don't bail out on unknown command + WARNING_LOG("Failed to process packet '{}'", request); + client->SendReplyWithAck({}); + return true; } GDBServer::ClientSocket::ClientSocket(SocketMultiplexer& multiplexer, SocketDescriptor descriptor) @@ -464,7 +457,9 @@ void GDBServer::ClientSocket::OnRead() { // TODO: Make this not copy. DEV_LOG("{} > {}", GetRemoteAddress().ToString(), current_packet); - SendPacket(GDBServer::ProcessPacket(current_packet)); + if (!ProcessPacket(this, current_packet)) + SendPacket("-"); + packet_complete = true; break; } @@ -504,7 +499,7 @@ void GDBServer::ClientSocket::OnSystemPaused() m_seen_resume = false; // Generate a stop reply packet, insert '?' command to generate it. - SendPacket(GDBServer::ProcessPacket("$?#3f")); + GDBServer::ProcessPacket(this, "$?#3f"); } void GDBServer::ClientSocket::OnSystemResumed() @@ -515,6 +510,11 @@ void GDBServer::ClientSocket::OnSystemResumed() SendPacket("+"); } +void GDBServer::ClientSocket::SendReplyWithAck(std::string_view reply) +{ + SendPacket(SmallString::from_format("+${}#{:02x}", reply, ComputeChecksum(reply))); +} + bool GDBServer::Initialize(u16 port) { Error error;