/* Copyright 2021 flyinghead This file is part of Flycast. Flycast is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Flycast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ #include "types.h" #ifdef GDB_SERVER #include "gdb_server.h" #include "debug_agent.h" #include "cfg/option.h" #include "oslib/oslib.h" #include "util/shared_this.h" #include #include #include #include #include #include namespace debugger { constexpr u32 MAX_PACKET_LEN = 4096; static u8 unpack(char c) { c = std::tolower(c); if (c <= '9') return c - '0'; else return c - 'a' + 10; } u32 unpack(const char *s, int l) { u32 r = 0; for (int i = 0; i < l && *s != '\0'; i += 2, s += 2) { r |= (unpack(s[0]) << 4 | unpack(s[1])) << (i * 4); } return r; } class GdbServer; class Connection : public SharedThis { public: asio::ip::tcp::socket& getSocket() { return socket; } void start() { asio::async_read_until(socket, asio::dynamic_string_buffer(message, MAX_PACKET_LEN), packetMatcher, std::bind(&Connection::handlePacket, shared_from_this(), asio::placeholders::error, asio::placeholders::bytes_transferred)); } private: Connection(GdbServer& server, asio::io_context& io_context) : server(server), io_context(io_context), socket(io_context) { } using iterator = asio::buffers_iterator; std::pair static packetMatcher(iterator begin, iterator end) { if (begin == end) return std::make_pair(begin, false); iterator i = begin; if (*i == '\03') // break return std::make_pair(i + 1, true); if (*i != '$') { // unexpected, or ack/nack ('+', '-') return std::make_pair(i + 1, true); } ++i; while (i != end && *i != '#') ++i; if (i + 3 <= end) // 2 chars for CRC return std::make_pair(i + 3, true); return std::make_pair(begin, false); } void handlePacket(const std::error_code& ec, size_t len); void send(const std::string& msg) { if (msg.empty()) start(); else asio::async_write(socket, asio::buffer(msg), std::bind(&Connection::writeDone, shared_from_this(), asio::placeholders::error, asio::placeholders::bytes_transferred)); } void writeDone(const std::error_code& ec, size_t len) { if (ec) WARN_LOG(COMMON, "Write error: %s", ec.message().c_str()); else start(); } GdbServer& server; asio::io_context& io_context; asio::ip::tcp::socket socket; std::string message; friend super; }; class TcpAcceptor { public: TcpAcceptor(GdbServer& server, asio::io_context& io_context, u16 port) : server(server), io_context(io_context), acceptor(asio::ip::tcp::acceptor(io_context, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port))) { asio::socket_base::reuse_address option(true); acceptor.set_option(option); start(); } private: void start() { Connection::Ptr newConnection = Connection::create(server, io_context); acceptor.async_accept(newConnection->getSocket(), std::bind(&TcpAcceptor::handleAccept, this, newConnection, asio::placeholders::error)); } void handleAccept(Connection::Ptr newConnection, const std::error_code& error); GdbServer& server; asio::io_context& io_context; asio::ip::tcp::acceptor acceptor; }; class GdbServer { public: struct Error : public std::runtime_error { Error(const char *reason) : std::runtime_error(reason) {} }; void init(int port) { this->port = port; EventManager::listen(Event::Resume, emuEventCallback, this); EventManager::listen(Event::Terminate, emuEventCallback, this); } void term() { EventManager::unlisten(Event::Resume, emuEventCallback, this); EventManager::unlisten(Event::Terminate, emuEventCallback, this); stop(); } void run() { if (thread.joinable()) return; DEBUG_LOG(COMMON, "GdbServer starting"); io_context = std::make_unique(); thread = std::thread(&GdbServer::serverThread, this); if (config::GDBWaitForConnection) { DEBUG_LOG(COMMON, "Waiting for GDB connection..."); agentInterrupt(); } } void stop() { if (thread.joinable()) { DEBUG_LOG(COMMON, "GdbServer stopping"); agent.resetAgent(); io_context->stop(); thread.join(); io_context.reset(); } } bool isRunning() const { return thread.joinable(); } // called on the emu thread void debugTrap(u32 event) { if (!attached) return; agent.debugTrap(event); reportException(); postDebugTrapNeeded = true; throw Stop(); } private: const u32 EXCEPT_NONE = 1; void serverThread() { ThreadName _("GdbServer"); try { TcpAcceptor server(*this, *io_context, port); io_context->run(); } catch (const std::exception& e) { ERROR_LOG(COMMON, "Gdb server exception: %s", e.what()); } attached = false; } std::string handleCommand(const std::string& packet) { try { if (postDebugTrapNeeded) { postDebugTrapNeeded = false; try { agent.postDebugTrap(); } catch (const FlycastException& e) { throw Error(e.what()); } } if (packet.empty()) return ""; DEBUG_LOG(NETWORK, "gdb: recv %s", packet.c_str()); std::vector replies; switch (packet[0]) { case '!': // Enable extended mode replies.push_back("OK"); break; case '?': // Sent when connection is first established to query the reason the target halted replies.push_back(reportException()); break; case 'A': // Initialized argv[] array passed into program. not supported replies.push_back("E01"); break; case 'b': // Change the serial line speed to baud. deprecated break; case 'B': // Set or clear a breakpoint at addr. deprecated break; case 'c': // Continue at addr, which is the address to resume. // If addr is omitted, resume at current address doContinue(packet); break; case 'C': // Continue with signal sig doContinue(packet); break; case 'd': // Toggle debug flag. deprecated break; case 'D': // Detach GDB from the remote system replies.push_back("OK"); agent.detach(); break; case 'F': // File-I/O protocol extension not currently supported break; case 'g': // Read general registers replies.push_back(readAllRegs()); break; case 'G': // Write general registers replies.push_back(writeAllRegs(packet)); break; case 'H': // Set thread for subsequent operations replies.push_back("OK"); break; case 'i': // Step the remote target by a single clock cycle case 'I': // Signal, then cycle step // not supported replies.push_back(""); break; case 'k': // Kill request. Stop process/system agent.kill(); break; case 'm': // Read length addressable memory units replies.push_back(readMem(packet)); break; case 'M': // Write length addressable memory units replies.push_back(writeMem(packet)); break; case 'p': // Read the value of register replies.push_back(readReg(packet)); break; case 'P': // Write register replies.push_back(writeReg(packet)); break; case 'q': // General query packets { auto v = query(packet); replies.insert(replies.end(), v.begin(), v.end()); } break; case 'Q': // General set packets { auto v = set(packet); replies.insert(replies.end(), v.begin(), v.end()); } break; case 'r': // Reset the entire system. Deprecated (use 'R' instead) break; case 'R': // Restart the program being debugged. restart(); break; case 's': // Single step replies.push_back(step(EXCEPT_NONE)); break; case 'S': // Step with signal replies.push_back(step()); break; case 't': // Search backwards. unsupported break; case 'T': // Find out if the thread is alive replies.push_back("OK"); break; case 'v': // 'v' packets to control execution { auto v = vpacket(packet); replies.insert(replies.end(), v.begin(), v.end()); } break; case 'X': // Write binary data to memory replies.push_back(writeMemBin(packet)); break; case 'z': // Remove a breakpoint/watchpoint. replies.push_back(removeMatchpoint(packet)); break; case 'Z': // Insert a breakpoint/watchpoint. replies.push_back(insertMatchpoint(packet)); break; case 3: replies.push_back(interrupt()); break; default: // Unknown commands are ignored WARN_LOG(COMMON, "Unknown gdb command: %s", packet.c_str()); break; } std::string data; for (const std::string& pkt : replies) { data.push_back('$'); u8 checksum = 0; for (char c : pkt) { if (c == '$' || c == '#' || c == '*' || c == '}') { c ^= 0x20; checksum += (u8)'}'; data.push_back('}'); } checksum += (u8)c; data.push_back(c); } data.push_back('#'); char s[9]; snprintf(s, sizeof(s), "%02x", checksum); data += s; } DEBUG_LOG(NETWORK, "gdb: sent %s", data.c_str()); return data; } catch (const Error& e) { ERROR_LOG(COMMON, "%s", e.what()); attached = false; throw e; } } std::string reportException() { char s[4]; snprintf(s, sizeof(s), "S%02X", agent.currentException()); return s; } void doContinue(const std::string& pkt) { if (pkt[0] != 'c') { WARN_LOG(COMMON, "Continue with signal not supported"); return; } if (pkt == "c") agent.doContinue(); else { // Get the pc at which to resume u32 addr; if (sscanf(pkt.c_str(), "c%x", &addr) != 1) { WARN_LOG(COMMON, "Continue address invalid %s", pkt.c_str()); return; } agent.doContinue(addr); } } std::string readAllRegs() { u32 *regs; int c = agent.readAllRegs(®s); std::string outpkt; for (int i = 0; i < c; i++) outpkt += pack(regs[i]); return outpkt; } std::string writeAllRegs(const std::string& pkt) { std::vector regs; for (auto it = pkt.begin() + 1; it <= pkt.end() - 8; it += 8) regs.push_back(unpack(&*it, 8)); agent.writeAllRegs(regs); return "OK"; } std::string readMem(const std::string& pkt) { u32 addr; u32 len; if (sscanf(pkt.c_str(), "m%x,%x:", &addr, &len) != 2) { WARN_LOG(COMMON, "readMem: invalid packet %s", pkt.c_str()); return "E01"; } const u8 *mem = agent.readMem(addr, len); std::string outpkt; for (u32 i = 0; i < len; i++) { char s[3]; snprintf(s, sizeof(s), "%02x", mem[i]); outpkt += s; } return outpkt; } std::string writeMem(const std::string& pkt) { u32 addr; u32 len; if (sscanf(pkt.c_str(), "M%x,%x:", &addr, &len) != 2) { WARN_LOG(COMMON, "writeMem: invalid packet %s", pkt.c_str()); return "E01"; } std::vector data(len); const char *p = &pkt[pkt.find(':')] + 1; for (u32 i = 0; i < len; i++, p += 2) { u32 b; sscanf(p,"%2x", &b); data[i] = (u8)b; } agent.writeMem(addr, data); return "OK"; } std::string writeMemBin(const std::string& pkt) { u32 addr; u32 len; if (sscanf(pkt.c_str(), "X%x,%x:", &addr, &len) != 2) { WARN_LOG(COMMON, "writeMemBin invalid command: %s", pkt.c_str()); return "E01"; } const char *p = &pkt[pkt.find(':')] + 1; std::vector data; data.reserve(pkt.length() - (p - &pkt[0])); for (; p <= &pkt.back(); p++) { u8 b = *p; if (b == '}') { b = *++p ^ 0x20; } data.push_back(b); } agent.writeMem(addr, data); return "OK"; } std::string readReg(const std::string& pkt) { u32 regNum; if (sscanf(pkt.c_str(), "p%x", ®Num) != 1) { WARN_LOG(COMMON, "readReg: invalid packet %s", pkt.c_str()); return "E01"; } u32 v = agent.readReg(regNum); return pack(v); } std::string writeReg(const std::string& pkt) { u32 regNum; char vstr[9]; if (sscanf(pkt.c_str(), "P%x=%8s", ®Num, vstr) != 2) { WARN_LOG(COMMON, "writeReg: invalid packet %s", pkt.c_str()); return "E01"; } agent.writeReg(regNum, unpack(vstr, 8)); return "OK"; } std::vector query(const std::string& pkt) { if (pkt == "qC") // Return the current thread ID. 0 is "any thread" return { "QC0.01" }; else if (pkt.rfind("qCRC", 0) == 0) { WARN_LOG(COMMON, "CRC compute not supported %s", pkt.c_str()); return { "E01" }; } else if (pkt == "qfThreadInfo") // Obtain a list of all active thread IDs (first call) return { "m0" }; else if (pkt == "qsThreadInfo") // Obtain a list of all active thread IDs (subsequent calls -> 'l' == end of list) return { "l" }; else if (pkt.rfind("qGetTLSAddr:", 0) == 0) // Fetch the address associated with thread local storage return { "" }; else if (pkt.rfind("qL", 0) == 0) // Obtain thread information. deprecated return { "qM001" }; else if (pkt == "qOffsets") // Get section offsets. Not supported return { "" }; else if (pkt.rfind("qP", 0) == 0) // Returns information on thread. deprecated return { "" }; else if (pkt.rfind("qRcmd,", 0) == 0) { std::string customCmd; for (const char *p = pkt.c_str() + 6; *p != '\0'; p += 2) customCmd += (char)unpack(p, 2); DEBUG_LOG(COMMON, "query: custom command %s", customCmd.c_str()); if (customCmd == "reset") restart(); else if (customCmd == "stack") { u32 len; const u32 *data = agent.getStack(len); len /= 4; #if _MSC_VER // Non-const array size is a GCC extension assert((len * 9 * 2 + 1) < MAX_PACKET_LEN); char reply[MAX_PACKET_LEN]; #else char reply[len * 9 * 2 + 1]; #endif char *r = reply; for (u32 i = 0; i < len; i++) { char n[10]; snprintf(n, sizeof(n), "%08x ", *data++); for (char *p = n; *p != 0; p++) { *r++ = packnb((*p >> 4) & 0xf); *r++ = packnb(*p & 0xf); } } *r = 0; return { reply }; } else return { "" }; } else if (pkt.rfind("qSupported", 0) == 0) { // Tell the remote stub about features supported by GDB, // and query the stub for features it supports char qsupported[128]; snprintf(qsupported, 128, "PacketSize=%i;vContSupported+", MAX_PACKET_LEN); return { qsupported }; } else if (pkt.rfind("qSymbol:", 0) == 0) // Notify the target that GDB is prepared to serve symbol lookup requests return { "OK" }; else if (pkt.rfind("qThreadExtraInfo,", 0) == 0) { // Obtain from the target OS a printable string description of thread attributes char s[19]; snprintf(s, sizeof(s), "%02x%02x%02x%02x%02x%02x%02x%02x%02x", 'R', 'u', 'n', 'n', 'a', 'b', 'l', 'e', 0); return { std::string(s, 18) }; } else if (pkt.rfind("qXfer:", 0) == 0) // Read uninterpreted bytes from the target’s special data area identified by the keyword object return { "" }; else if (pkt.rfind("qAttached", 0) == 0) // Return an indication of whether the remote server attached to an existing process // or created a new process return { "1" }; // existing process else if (pkt.rfind("qTfV", 0) == 0) // request data about trace state variables return { "" }; else if (pkt.rfind("qTfP", 0) == 0) // request data about tracepoints return { "" }; else if (pkt.rfind("qTStatus", 0) == 0) // Ask the stub if there is a trace experiment running right now return { "" }; WARN_LOG(COMMON, "query not supported %s", pkt.c_str()); return {}; } std::vector set(const std::string& pkt) { if (pkt.rfind("QPassSignals:", 0) == 0) // Passing signals not supported return { "" }; else if (pkt.rfind("QTDP", 0) == 0 || pkt.rfind("QFrame", 0) == 0 || pkt.rfind("QTStart", 0) == 0 || pkt.rfind("QTStop", 0) == 0 || pkt.rfind("QTinit", 0) == 0 || pkt.rfind("QTro", 0) == 0) // No tracepoint feature supported return { "" }; WARN_LOG(COMMON, "set not supported %s", pkt.c_str()); return {}; } std::vector vpacket(const std::string& pkt) { if (pkt.rfind("vAttach;", 0) == 0) return { "S05" }; else if (pkt.rfind("vCont?", 0) == 0) // supported vCont actions - (c)ontinue, (C)ontinue with signal, (s)tep, (S)tep with signal, (r)ange-step return { "vCont;c;C;s;S;t;r" }; else if (pkt.rfind("vCont", 0) == 0) { std::string vContCmd = pkt.substr(strlen("vCont;")); std::vector replies; switch (vContCmd[0]) { case 'c': case 'C': doContinue(vContCmd); return {}; case 's': return { step(EXCEPT_NONE) }; case 'S': replies.push_back(step()); [[fallthrough]]; case 'r': { u32 from, to; if (sscanf(vContCmd.c_str(), "r%x,%x", &from, &to) == 2) { auto v = stepRange(from, to); replies.insert(replies.end(), v.begin(), v.end()); } else { WARN_LOG(COMMON, "Unsupported vCont:r format %s", pkt.c_str()); doContinue("c"); } return replies; } default: WARN_LOG(COMMON, "vCont action not supported %s", pkt.c_str()); return {}; } } else if (pkt.rfind("vFile:", 0) == 0) // not supported return { "" }; else if (pkt.rfind("vFlashErase:", 0) == 0) // not supported return { "E01" }; else if (pkt.rfind("vFlashWrite:", 0) == 0) // not supported return { "E01" }; else if (pkt.rfind("vFlashDone:", 0) == 0) // not supported return { "E01" }; else if (pkt.rfind("vRun;", 0) == 0) { if (pkt != "vRun;") WARN_LOG(COMMON, "unexpected vRun args ignored: %s", pkt.c_str()); agent.restart(); return { "S05" }; } else if (pkt.rfind("vKill", 0) == 0) { agent.kill(); return { "OK" }; } else { WARN_LOG(COMMON, "unknown v packet: %s", pkt.c_str()); return { "" }; } } void restart() { agent.restart(); } std::string step(u32 what = 0) { try { agent.step(); return "S05"; } catch (const FlycastException& e) { throw Error(e.what()); } } std::vector stepRange(u32 from, u32 to) { try { agent.stepRange(from, to); return { "OK", "S05" }; } catch (const FlycastException& e) { throw Error(e.what()); } } std::string insertMatchpoint(const std::string& pkt) { u32 type; u32 addr; u32 len; if (sscanf(pkt.c_str(), "Z%1d,%x,%1d", &type, &addr, &len) != 3) { WARN_LOG(COMMON, "insertMatchpoint: unknown packet: %s", pkt.c_str()); return "E01"; } switch (type) { case DebugAgent::Breakpoint::BP_TYPE_SOFTWARE_BREAK: // soft bp if (agent.insertMatchpoint(DebugAgent::Breakpoint::BP_TYPE_SOFTWARE_BREAK, addr, len)) return "OK"; else return "E01"; break; case DebugAgent::Breakpoint::BP_TYPE_HARDWARE_BREAK: // hardware bp return ""; break; case DebugAgent::Breakpoint::BP_TYPE_WRITE_WATCHPOINT: // write watchpoint return ""; break; case DebugAgent::Breakpoint::BP_TYPE_READ_WATCHPOINT: // read watchpoint return ""; break; case DebugAgent::Breakpoint::BP_TYPE_ACCESS_WATCHPOINT: // access watchpoint return ""; break; default: return ""; break; } } std::string removeMatchpoint(const std::string& pkt) { u32 type; u32 addr; u32 len; if (sscanf(pkt.c_str(), "z%1d,%x,%1d", &type, &addr, &len) != 3) { WARN_LOG(COMMON, "removeMatchpoint: unknown packet: %s", pkt.c_str()); return "E01"; } switch (type) { case 0: // soft bp if (agent.removeMatchpoint(DebugAgent::Breakpoint::BP_TYPE_SOFTWARE_BREAK, addr, len)) return "OK"; else return "E01"; break; case 1: // hardware bp return ""; break; case 2: // write watchpoint return ""; break; case 3: // read watchpoint return ""; break; case 4: // access watchpoint return ""; break; default: return ""; break; } } std::string interrupt() { u32 signal = agentInterrupt(); char s[10]; snprintf(s, sizeof(s), "S%02x", signal); return s; } char packnb(u8 b) { if (b < 10) return '0' + b; else return 'a' + b - 10; } std::string packb(u8 v) { std::string s(1, packnb((v >> 4) & 0xf)); s += packnb(v & 0xf); return s; } std::string pack(u32 v) { return packb(v & 0xff) + packb((v >> 8) & 0xff) + packb((v >> 16) & 0xff) + packb((v >> 24) & 0xff); } u32 agentInterrupt() { try { return agent.interrupt(); } catch (const FlycastException& e) { throw Error(e.what()); } } void clientConnected() { attached = true; agentInterrupt(); } static void emuEventCallback(Event event, void *arg) { GdbServer *gdbServer = static_cast(arg); switch (event) { case Event::Resume: try { if (!gdbServer->isRunning()) gdbServer->run(); } catch (const GdbServer::Error& e) { ERROR_LOG(COMMON, "%s", e.what()); } break; case Event::Terminate: gdbServer->stop(); break; default: break; } } bool attached = false; bool postDebugTrapNeeded = false; std::thread thread; std::unique_ptr io_context; int port = DEFAULT_PORT; friend class TcpAcceptor; friend class Connection; public: DebugAgent agent; }; static GdbServer gdbServer; void TcpAcceptor::handleAccept(Connection::Ptr newConnection, const std::error_code& error) { if (!error) { server.clientConnected(); newConnection->start(); } start(); } void Connection::handlePacket(const std::error_code& ec, size_t len) { std::string msg = message.substr(0, len); message = message.substr(len); if (ec || len == 0) { // terminate the connection if (ec != asio::error::eof && ec != asio::error::operation_aborted) WARN_LOG(NETWORK, "Read error %s", ec.message().c_str()); return; } try { if (msg[0] == '\03') { // break send(server.handleCommand(msg)); return; } if (msg[0] != '$') { // Ignore unexpected chars send(""); return; } u8 cksum = 0; for (unsigned i = 1; i < len - 3; i++) cksum += (u8)msg[i]; if (cksum != (unpack(msg[len - 2]) << 4 | unpack(msg[len - 1]))) { // Invalid checksum WARN_LOG(COMMON, "Connection::handlePacket: invalid checksum: [%s]", msg.c_str()); send("-"); } else { // Positive ack std::string reply = "+"; reply += server.handleCommand(msg.substr(1, msg.length() - 4)); send(reply); } } catch (...) { // terminate the connection } } void init(int port) { gdbServer.init(port); } void term() { gdbServer.term(); } void debugTrap(u32 event) { gdbServer.debugTrap(event); } void subroutineCall() { gdbServer.agent.subroutineCall(); } void subroutineReturn() { gdbServer.agent.subroutineReturn(); } } #endif