// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin and contributors. // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "gdb_server.h" #include "bus.h" #include "cpu_core.h" #include "cpu_core_private.h" #include "system.h" #include "common/log.h" #include "common/small_string.h" #include "common/string_util.h" #include "util/sockets.h" #include #include #include #include #include #include Log_SetChannel(GDBProtocol); namespace GDBProtocol { 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); } // namespace GDBProtocol namespace GDBProtocol { static u8* 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; } static u8 ComputeChecksum(std::string_view str) { u8 checksum = 0; for (char c : str) { checksum = (checksum + c) % 256; } return checksum; } static std::optional 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; } } static std::string SerializePacket(std::string_view in) { std::stringstream ss; ss << '$' << in << '#' << TinyString::from_format("{:02x}", ComputeChecksum(in)); return ss.str(); } /// List of GDB remote protocol registers for MIPS III (excluding FP). static const std::array REGISTERS{ &CPU::g_state.regs.r[0], &CPU::g_state.regs.r[1], &CPU::g_state.regs.r[2], &CPU::g_state.regs.r[3], &CPU::g_state.regs.r[4], &CPU::g_state.regs.r[5], &CPU::g_state.regs.r[6], &CPU::g_state.regs.r[7], &CPU::g_state.regs.r[8], &CPU::g_state.regs.r[9], &CPU::g_state.regs.r[10], &CPU::g_state.regs.r[11], &CPU::g_state.regs.r[12], &CPU::g_state.regs.r[13], &CPU::g_state.regs.r[14], &CPU::g_state.regs.r[15], &CPU::g_state.regs.r[16], &CPU::g_state.regs.r[17], &CPU::g_state.regs.r[18], &CPU::g_state.regs.r[19], &CPU::g_state.regs.r[20], &CPU::g_state.regs.r[21], &CPU::g_state.regs.r[22], &CPU::g_state.regs.r[23], &CPU::g_state.regs.r[24], &CPU::g_state.regs.r[25], &CPU::g_state.regs.r[26], &CPU::g_state.regs.r[27], &CPU::g_state.regs.r[28], &CPU::g_state.regs.r[29], &CPU::g_state.regs.r[30], &CPU::g_state.regs.r[31], &CPU::g_state.cop0_regs.sr.bits, &CPU::g_state.regs.lo, &CPU::g_state.regs.hi, &CPU::g_state.cop0_regs.BadVaddr, &CPU::g_state.cop0_regs.cause.bits, &CPU::g_state.pc, }; /// Number of registers in GDB remote protocol for MIPS III. constexpr int NUM_GDB_REGISTERS = 73; /// Get stop reason. static std::optional Cmd$_questionMark(std::string_view data) { return {"S02"}; } /// Get general registers. static std::optional Cmd$g(std::string_view data) { std::stringstream ss; for (u32* reg : REGISTERS) { // Data is in host order (little endian). ss << StringUtil::EncodeHex(reinterpret_cast(reg), 4); } // Pad with dummy data (FP registers stuff). for (int i = 0; i < NUM_GDB_REGISTERS - static_cast(REGISTERS.size()); i++) { ss << "00000000"; } return {ss.str()}; } /// Set general registers. static std::optional Cmd$G(std::string_view data) { if (data.size() == NUM_GDB_REGISTERS * 8) { int offset = 0; for (u32* reg : REGISTERS) { // Data is in host order (little endian). auto value = StringUtil::DecodeHex({data.data() + offset, 8}); if (value) { *reg = *reinterpret_cast(&(*value)[0]); } offset += 8; } } else { ERROR_LOG("Wrong payload size for 'G' command, expected {} got {}", NUM_GDB_REGISTERS * 8, data.size()); } return {""}; } /// Get memory. static std::optional Cmd$m(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) { 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)}; } } return {"E00"}; } /// Set memory. static std::optional Cmd$M(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)) { 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"}; } } return {"E00"}; } /// Remove hardware breakpoint. static std::optional Cmd$z1(std::string_view data) { auto address = StringUtil::FromChars(data, 16); if (address) { CPU::RemoveBreakpoint(CPU::BreakpointType::Execute, *address); return {"OK"}; } else { return std::nullopt; } } /// Insert hardware breakpoint. static std::optional Cmd$Z1(std::string_view data) { auto address = StringUtil::FromChars(data, 16); if (address) { CPU::AddBreakpoint(CPU::BreakpointType::Execute, *address, false); return {"OK"}; } else { return std::nullopt; } } static std::optional Cmd$vMustReplyEmpty(std::string_view data) { return {""}; } static std::optional Cmd$qSupported(std::string_view data) { return {""}; } /// List of all GDB remote protocol packets supported by us. static const std::map(std::string_view)>> COMMANDS{ {"?", Cmd$_questionMark}, {"g", Cmd$g}, {"G", Cmd$G}, {"m", Cmd$m}, {"M", Cmd$M}, {"z0,", Cmd$z1}, {"Z0,", Cmd$Z1}, {"z1,", Cmd$z1}, {"Z1,", Cmd$Z1}, {"vMustReplyEmpty", Cmd$vMustReplyEmpty}, {"qSupported", Cmd$qSupported}, }; bool IsPacketInterrupt(std::string_view data) { return (data.size() >= 1) && (data[data.size() - 1] == '\003'); } bool IsPacketContinue(std::string_view data) { return (data.size() >= 5) && (data.substr(data.size() - 5) == "$c#63"); } bool IsPacketComplete(std::string_view data) { return ((data.size() == 1) && (data[0] == '\003')) || ((data.size() > 3) && (*(data.end() - 3) == '#')); } std::string ProcessPacket(std::string_view data) { std::string_view trimmedData = data; // Eat ACKs. while (!trimmedData.empty() && (trimmedData[0] == '+' || trimmedData[0] == '-')) { if (trimmedData[0] == '-') { ERROR_LOG("Received negative ack"); } trimmedData = trimmedData.substr(1); } // Validate packet. auto packet = DeserializePacket(trimmedData); if (!packet) { ERROR_LOG("Malformed packet '{}'", trimmedData); return "-"; } std::optional reply = {""}; // Try to invoke packet command. bool processed = false; for (const auto& command : COMMANDS) { if (packet->starts_with(command.first)) { DEBUG_LOG("Processing command '{}'", command.first); // Invoke command, remove command name from payload. reply = command.second(packet->substr(strlen(command.first))); processed = true; break; } } if (!processed) WARNING_LOG("Failed to process packet '{}'", trimmedData); return reply ? "+" + SerializePacket(*reply) : "+"; } } // namespace GDBProtocol namespace GDBServer { 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 GDBServer::ClientSocket::ClientSocket(SocketMultiplexer& multiplexer, SocketDescriptor descriptor) : BufferedStreamSocket(multiplexer, descriptor, 65536, 65536) { } GDBServer::ClientSocket::~ClientSocket() = default; void GDBServer::ClientSocket::OnConnected() { INFO_LOG("Client {} connected.", GetRemoteAddress().ToString()); m_seen_resume = System::IsPaused(); System::PauseSystem(true); s_gdb_clients.push_back(std::static_pointer_cast(shared_from_this())); } void GDBServer::ClientSocket::OnDisconnected(const Error& error) { INFO_LOG("Client {} disconnected: {}", GetRemoteAddress().ToString(), error.GetDescription()); const auto iter = std::find_if(s_gdb_clients.begin(), s_gdb_clients.end(), [this](const std::shared_ptr& rhs) { return (rhs.get() == this); }); if (iter == s_gdb_clients.end()) { ERROR_LOG("Unknown GDB client disconnected? This should never happen."); return; } s_gdb_clients.erase(iter); } void GDBServer::ClientSocket::OnRead() { const std::span buffer = AcquireReadBuffer(); if (buffer.empty()) return; size_t buffer_offset = 0; while (buffer_offset < buffer.size()) { size_t current_packet_size = 1; bool packet_complete = false; for (; (buffer_offset + current_packet_size) <= buffer.size(); current_packet_size++) { const std::string_view current_packet(reinterpret_cast(buffer.data() + buffer_offset), current_packet_size); if (GDBProtocol::IsPacketInterrupt(current_packet)) { DEV_LOG("{} > Interrupt request", GetRemoteAddress().ToString()); System::PauseSystem(true); packet_complete = true; break; } else if (GDBProtocol::IsPacketContinue(current_packet)) { DEV_LOG("{} > Continue request", GetRemoteAddress().ToString()); System::PauseSystem(false); packet_complete = true; break; } else if (GDBProtocol::IsPacketComplete(current_packet)) { // TODO: Make this not copy. DEV_LOG("{} > {}", GetRemoteAddress().ToString(), current_packet); SendPacket(GDBProtocol::ProcessPacket(current_packet)); packet_complete = true; break; } } if (!packet_complete) { WARNING_LOG("Incomplete packet, got {} bytes.", buffer.size() - buffer_offset); break; } else { buffer_offset += current_packet_size; } } ReleaseReadBuffer(buffer_offset); } void GDBServer::ClientSocket::SendPacket(std::string_view sv) { if (sv.empty()) return; WARNING_LOG("Write: {}", sv); if (size_t written = Write(sv.data(), sv.length()); written != sv.length()) ERROR_LOG("Only wrote {} of {} bytes.", written, sv.length()); } void GDBServer::ClientSocket::OnSystemPaused() { if (!m_seen_resume) return; m_seen_resume = false; // Generate a stop reply packet, insert '?' command to generate it. SendPacket(GDBProtocol::ProcessPacket("$?#3f")); } void GDBServer::ClientSocket::OnSystemResumed() { m_seen_resume = true; // Send ack, in case GDB sent a continue request. SendPacket("+"); } bool GDBServer::Initialize(u16 port) { Error error; Assert(!s_gdb_listen_socket); const std::optional address = SocketAddress::Parse(SocketAddress::Type::IPv4, "127.0.0.1", port, &error); if (!address.has_value()) { ERROR_LOG("Failed to parse address: {}", error.GetDescription()); return false; } SocketMultiplexer* multiplexer = System::GetSocketMultiplexer(); if (!multiplexer) return false; s_gdb_listen_socket = multiplexer->CreateListenSocket(address.value(), &error); if (!s_gdb_listen_socket) { ERROR_LOG("Failed to create listen socket: {}", error.GetDescription()); System::ReleaseSocketMultiplexer(); return false; } INFO_LOG("GDB server is now listening on {}.", address->ToString()); return true; } bool GDBServer::HasAnyClients() { return !s_gdb_clients.empty(); } void GDBServer::Shutdown() { if (!s_gdb_listen_socket) return; INFO_LOG("Disconnecting {} GDB clients...", s_gdb_clients.size()); while (!s_gdb_clients.empty()) { // maintain a reference so we don't delete while in scope std::shared_ptr client = s_gdb_clients.back(); client->Close(); } INFO_LOG("Stopping GDB server."); s_gdb_listen_socket->Close(); s_gdb_listen_socket.reset(); System::ReleaseSocketMultiplexer(); } void GDBServer::OnSystemPaused() { for (auto& it : s_gdb_clients) it->OnSystemPaused(); } void GDBServer::OnSystemResumed() { for (auto& it : s_gdb_clients) it->OnSystemResumed(); }