From 2fb1f342bc6ffa4c26bc10bc4cde64a4a87118f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 06:53:42 +0000 Subject: [PATCH 1/7] Add GDB-server debug endpoint (port from DuetOS) Port the DuetOS GDB-server system to MaNGOS Zero so a debugger or an AI agent can attach to the running mangosd and drive it: read process memory, inspect live game state, and run server commands over one endpoint. Two layers, matching the DuetOS design: - Native: a transport-agnostic GDB Remote Serial Protocol (RSP) stub over TCP (gdb / lldb / IDA). Framing, checksum, qSupported, ?, g/G (synthetic registers), guarded m/M memory, H, c/s/D/k, vCont, qRcmd. - Semantic: 'monitor mangos ' commands (status, players, tick, session, config, and a 'cmd' bridge to the full ChatCommand surface), also exposed on a plain-text TCP bridge for AI agents and non-RSP debuggers (WinDbg/CDB/x64dbg attach natively + use the bridge). The kernel NMI-freeze stop is replaced by a cooperative world-tick stop: the world thread runs the RSP pump loop (pausing the 50ms tick) while the ACE network thread only shuttles bytes. The stop loop honours World::IsStopped() so a paused server can still shut down. Memory access is guarded (Linux /proc/self/maps, Windows VirtualQuery) so a bad debugger address returns an RSP error instead of crashing. New subsystem: src/game/Debug/GdbServer/ (RSP core, monitor, verbs, guarded memory, facade) + src/mangosd/GdbServerThread (ACE listener modeled on RAThread). Config-gated, disabled by default, localhost bind. See doc/GdbServer.md. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016L1LfL8h1fGgLYjsfvyhEo --- doc/GdbServer.md | 121 ++++ src/game/CMakeLists.txt | 6 + src/game/Debug/GdbServer/GdbDbgMemory.cpp | 227 ++++++++ src/game/Debug/GdbServer/GdbDbgMemory.h | 67 +++ src/game/Debug/GdbServer/GdbMonitor.cpp | 427 ++++++++++++++ src/game/Debug/GdbServer/GdbMonitor.h | 106 ++++ src/game/Debug/GdbServer/GdbMonitorVerbs.cpp | 166 ++++++ src/game/Debug/GdbServer/GdbRsp.cpp | 572 +++++++++++++++++++ src/game/Debug/GdbServer/GdbRsp.h | 95 +++ src/game/Debug/GdbServer/GdbServer.cpp | 276 +++++++++ src/game/Debug/GdbServer/GdbServer.h | 142 +++++ src/game/WorldHandlers/World.cpp | 5 + src/mangosd/CMakeLists.txt | 2 + src/mangosd/GdbServerThread.cpp | 385 +++++++++++++ src/mangosd/GdbServerThread.h | 75 +++ src/mangosd/mangosd.conf.dist.in | 36 ++ src/mangosd/mangosd.cpp | 27 + 17 files changed, 2735 insertions(+) create mode 100644 doc/GdbServer.md create mode 100644 src/game/Debug/GdbServer/GdbDbgMemory.cpp create mode 100644 src/game/Debug/GdbServer/GdbDbgMemory.h create mode 100644 src/game/Debug/GdbServer/GdbMonitor.cpp create mode 100644 src/game/Debug/GdbServer/GdbMonitor.h create mode 100644 src/game/Debug/GdbServer/GdbMonitorVerbs.cpp create mode 100644 src/game/Debug/GdbServer/GdbRsp.cpp create mode 100644 src/game/Debug/GdbServer/GdbRsp.h create mode 100644 src/game/Debug/GdbServer/GdbServer.cpp create mode 100644 src/game/Debug/GdbServer/GdbServer.h create mode 100644 src/mangosd/GdbServerThread.cpp create mode 100644 src/mangosd/GdbServerThread.h diff --git a/doc/GdbServer.md b/doc/GdbServer.md new file mode 100644 index 000000000..e5d20b953 --- /dev/null +++ b/doc/GdbServer.md @@ -0,0 +1,121 @@ +# GDB-Server Debug Endpoint + +The GDB-server lets a debugger — or an AI agent — attach to a **running** +`mangosd` process and drive it directly: read process memory, inspect live game +state, and run server/GM commands, all over a single network endpoint. It is a +port of the GDB-server system from the DuetOS project, adapted from a kernel +stub to this userland, ACE-threaded server. + +It has two layers: + +1. **Native layer** — a real GDB **Remote Serial Protocol (RSP)** stub. Any + debugger that speaks RSP can attach: GDB, LLDB, IDA Pro (its built-in + "Remote GDB debugger" backend), radare2/rizin, Binary Ninja. +2. **Semantic layer** — mangos-specific introspection/control delivered over + GDB's standard `qRcmd` ("monitor") packet as `mangos ` commands. The + same verbs are also exposed on a plain-text TCP bridge for AI agents and for + debuggers that do not speak RSP (WinDbg, CDB, x64dbg). + +## Enabling + +In `mangosd.conf` (see the CONSOLE / REMOTE ACCESS section): + +``` +GdbServer.Enable = 1 +GdbServer.IP = 127.0.0.1 # never expose publicly +GdbServer.Port = 2345 # RSP port +GdbServer.MonitorPort = 2346 # plain-text bridge (0 to disable) +GdbServer.AllowMemWrite = 0 # allow RSP 'M' writes (dangerous) +``` + +> **Security:** anyone who can reach these ports gets full process-memory read +> and full server-command access. Keep the bind address on `127.0.0.1`. The +> subsystem is disabled by default. Memory writes (`M`) and are off by default. + +At boot the server runs a self-test and logs `[gdb-monitor-selftest] PASS`, +then (when enabled) `GdbServer Thread started (RSP on 127.0.0.1:2345 ...)`. + +## Attaching + +### GDB / LLDB / IDA (RSP) + +``` +# GDB +gdb -ex "target remote 127.0.0.1:2345" + +# LLDB +lldb -o "gdb-remote 127.0.0.1:2345" + +# IDA Pro: Debugger -> Attach -> Remote GDB debugger, +# host 127.0.0.1, port 2345 +``` + +Once attached, the semantic layer is reached with `monitor`: + +``` +(gdb) monitor mangos help +(gdb) monitor mangos status +(gdb) monitor mangos players +(gdb) monitor mangos config WorldServerPort +(gdb) monitor mangos cmd .server info +(gdb) x/16xb 0x # read live process memory +``` + +Pressing **Ctrl-C** in gdb interrupts the target: the world tick pauses and the +process enters a cooperative stop loop until you `continue`. The server still +honours shutdown signals while paused, so it can always be stopped. + +### AI agents / WinDbg / CDB / x64dbg (plain-text bridge) + +Non-RSP tools attach to the `mangosd` process natively (by PID) for low-level +work, and use the plain-text monitor bridge for the semantic layer: + +``` +# any raw TCP client / AI agent +$ nc 127.0.0.1 2346 +mangos help +mangos tick +mangos players +mangos cmd .save all +``` + +Each line is a `mangos ` command; the text reply comes straight back. + +## Monitor verbs + +| Verb | Description | +|------|-------------| +| `mangos help` | List verbs | +| `mangos status` | World summary: motd, uptime, sessions, world tick | +| `mangos players` (`ps`) | List online sessions/players | +| `mangos tick` | World loop counter + uptime | +| `mangos session ` | Session detail for an account id | +| `mangos config ` | Read a `mangosd.conf` value | +| `mangos cmd ` | Run any server/GM command (e.g. `.server info`, `.account onlinelist`) | + +The `cmd` verb bridges to the existing `ChatCommand` system at `SEC_CONSOLE` +level, so the entire console/GM command surface is drivable over the debug +channel. + +## What works today (Phase 1) and what is next (Phase 2) + +**Phase 1 (implemented):** RSP transport over TCP; `qSupported`, `?`, `g`/`G` +(synthetic registers), `m`/`M` (guarded process-memory read/write), `H`, +`c`/`s`/`D`/`k`, `vCont`, `qRcmd` monitor; the `mangos` semantic verbs incl. the +`cmd` bridge; cooperative world-tick stop on Ctrl-C; the plain-text bridge. + +**Phase 1 limitations:** +- Registers (`g`/`G`) are a synthetic zeroed x86_64 set — enough for the attach + handshake; the value is in `monitor` + memory, not register-level debugging. +- Breakpoints (`Z`/`z`) and real single-step are not implemented (gdb falls + back to its own software breakpoints, which will not function without the + native breakpoint support below). +- One debugger connection at a time. +- State seen during a stop is *world-tick-consistent*, not globally race-free: + the network, database and map-update threads keep running. + +**Phase 2 (planned):** real native registers and hardware/software breakpoints +via OS facilities (Linux ptrace/DR registers, Windows `DebugActiveProcess`/ +`SetThreadContext`); game-level breakpoints ("break on opcode X", "break when a +player enters map Y"); and crash-dump integration (tying into the Windows +Wheaty report and a Linux backtrace). diff --git a/src/game/CMakeLists.txt b/src/game/CMakeLists.txt index f4784b20d..b8055d9d7 100644 --- a/src/game/CMakeLists.txt +++ b/src/game/CMakeLists.txt @@ -81,6 +81,10 @@ source_group("Warden\\Modules" FILES ${SRC_GRP_WARDEN_MODULES}) file(GLOB SRC_GRP_WORLD_HANDLERS WorldHandlers/*.cpp WorldHandlers/*.h) source_group("World\\Handlers" FILES ${SRC_GRP_WORLD_HANDLERS}) +#GDB debug server group +file(GLOB SRC_GRP_DEBUG_GDB Debug/GdbServer/*.cpp Debug/GdbServer/*.h) +source_group("Debug\\GdbServer" FILES ${SRC_GRP_DEBUG_GDB}) + # Build the Eluna library if enabled if(SCRIPT_LIB_ELUNA) file(GLOB SRC_GRP_ELUNA @@ -238,6 +242,7 @@ add_library(game STATIC ${SRC_GRP_WARDEN} ${SRC_GRP_WARDEN_MODULES} ${SRC_GRP_WORLD_HANDLERS} + ${SRC_GRP_DEBUG_GDB} $<$:${SRC_GRP_ELUNA}> $<$:${SRC_GRP_BOTS}> ) @@ -260,6 +265,7 @@ target_include_directories(game Warden Warden/Modules WorldHandlers + Debug/GdbServer $<$: ${CMAKE_SOURCE_DIR}/src/modules/Eluna ${CMAKE_SOURCE_DIR}/src/modules/Eluna/hooks diff --git a/src/game/Debug/GdbServer/GdbDbgMemory.cpp b/src/game/Debug/GdbServer/GdbDbgMemory.cpp new file mode 100644 index 000000000..efcb41c4f --- /dev/null +++ b/src/game/Debug/GdbServer/GdbDbgMemory.cpp @@ -0,0 +1,227 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#include "GdbDbgMemory.h" + +#include +#include +#include + +#ifdef _WIN32 +# include +#else +# include +#endif + +namespace GdbDbg +{ + namespace + { + // Guards both the validation path and the memcpy so a concurrent + // map change on another thread cannot be observed mid-copy. + std::mutex g_lock; + +#ifndef _WIN32 + struct Region + { + uintptr_t start; + uintptr_t end; + bool readable; + bool writable; + }; + + std::vector g_regions; + + // (Re)parse /proc/self/maps into g_regions. Lines look like: + // 7f...000-7f...000 rw-p 00000000 00:00 0 [heap] + void ParseMaps() + { + g_regions.clear(); + + FILE* f = fopen("/proc/self/maps", "r"); + if (f == nullptr) + { + return; + } + + char line[512]; + while (fgets(line, sizeof(line), f) != nullptr) + { + unsigned long long start = 0; + unsigned long long end = 0; + char perms[8] = {0}; + if (sscanf(line, "%llx-%llx %7s", &start, &end, perms) != 3) + { + continue; + } + + Region r; + r.start = static_cast(start); + r.end = static_cast(end); + r.readable = (perms[0] == 'r'); + r.writable = (perms[1] == 'w'); + g_regions.push_back(r); + } + fclose(f); + } + + // True when [addr, addr+len) lies entirely inside one region that + // satisfies the requested protection. Refreshes the cache once on a + // miss (the map may have grown since the last parse). + bool RangeOk(uintptr_t addr, size_t len, bool needWrite) + { + if (len == 0) + { + return false; + } + // Reject wraparound. + if (addr + len < addr) + { + return false; + } + + for (int attempt = 0; attempt < 2; ++attempt) + { + for (const Region& r : g_regions) + { + if (addr >= r.start && (addr + len) <= r.end) + { + if (!r.readable) + { + return false; + } + if (needWrite && !r.writable) + { + return false; + } + return true; + } + } + if (attempt == 0) + { + ParseMaps(); + } + } + return false; + } +#else // _WIN32 + bool RangeOk(uintptr_t addr, size_t len, bool needWrite) + { + if (len == 0) + { + return false; + } + if (addr + len < addr) + { + return false; + } + + uintptr_t cur = addr; + const uintptr_t last = addr + len; + while (cur < last) + { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(reinterpret_cast(cur), &mbi, sizeof(mbi)) == 0) + { + return false; + } + if (mbi.State != MEM_COMMIT) + { + return false; + } + + const DWORD prot = mbi.Protect; + if ((prot & PAGE_GUARD) || (prot & PAGE_NOACCESS)) + { + return false; + } + + const DWORD readMask = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | + PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; + if ((prot & readMask) == 0) + { + return false; + } + if (needWrite) + { + const DWORD writeMask = PAGE_READWRITE | PAGE_WRITECOPY | + PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; + if ((prot & writeMask) == 0) + { + return false; + } + } + + const uintptr_t regionEnd = + reinterpret_cast(mbi.BaseAddress) + static_cast(mbi.RegionSize); + if (regionEnd <= cur) + { + return false; + } + cur = regionEnd; + } + return true; + } +#endif + } // namespace + + bool IsReadable(uintptr_t addr, size_t len) + { + std::lock_guard guard(g_lock); + return RangeOk(addr, len, false); + } + + bool IsWritable(uintptr_t addr, size_t len) + { + std::lock_guard guard(g_lock); + return RangeOk(addr, len, true); + } + + size_t MemRead(uintptr_t addr, void* out, size_t len) + { + std::lock_guard guard(g_lock); + if (!RangeOk(addr, len, false)) + { + return 0; + } + memcpy(out, reinterpret_cast(addr), len); + return len; + } + + size_t MemWrite(uintptr_t addr, const void* in, size_t len) + { + std::lock_guard guard(g_lock); + if (!RangeOk(addr, len, true)) + { + return 0; + } + memcpy(reinterpret_cast(addr), in, len); + return len; + } +} // namespace GdbDbg +/// @} diff --git a/src/game/Debug/GdbServer/GdbDbgMemory.h b/src/game/Debug/GdbServer/GdbDbgMemory.h new file mode 100644 index 000000000..7eb877c58 --- /dev/null +++ b/src/game/Debug/GdbServer/GdbDbgMemory.h @@ -0,0 +1,67 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#ifndef MANGOS_H_GDB_DBGMEMORY +#define MANGOS_H_GDB_DBGMEMORY + +#include "Common.h" + +#include +#include + +/** + * Guarded access to the mangosd process' own address space, used by the + * GDB-server `m` / `M` packets. The debugger is in-process, so a read is + * simply a memcpy of our own memory — but a typo'd address from the + * debugger must NOT crash the server. Each access is therefore validated + * against the live memory map first: + * - Linux: a cached parse of /proc/self/maps (refresh-on-miss). + * - Windows: VirtualQuery walked across the requested range. + * A SIGSEGV-trampoline is deliberately avoided: other server threads keep + * running during a debug stop and a process-global fault handler would + * fight with them. + */ +namespace GdbDbg +{ + /// Validate that [addr, addr+len) is fully mapped and readable. + bool IsReadable(uintptr_t addr, size_t len); + + /// Validate that [addr, addr+len) is fully mapped and writable. + bool IsWritable(uintptr_t addr, size_t len); + + /// Read up to @p len bytes from @p addr into @p out. Returns the number + /// of bytes copied (0 when the range is not fully readable). + size_t MemRead(uintptr_t addr, void* out, size_t len); + + /// Write @p len bytes from @p in to @p addr. Returns the number of bytes + /// written (0 when the range is not fully writable). + size_t MemWrite(uintptr_t addr, const void* in, size_t len); +} + +#endif +/// @} diff --git a/src/game/Debug/GdbServer/GdbMonitor.cpp b/src/game/Debug/GdbServer/GdbMonitor.cpp new file mode 100644 index 000000000..d55549577 --- /dev/null +++ b/src/game/Debug/GdbServer/GdbMonitor.cpp @@ -0,0 +1,427 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#include "GdbMonitor.h" + +#include "Log.h" + +namespace GdbMon +{ + // ----------------------------------------------------------------------- + // MonitorWriter + // ----------------------------------------------------------------------- + + MonitorWriter::MonitorWriter(char* buf, uint32 cap) : m_buf(buf), m_cap(cap) + { + if (m_buf != nullptr && m_cap != 0) + { + m_buf[0] = '\0'; + } + } + + void MonitorWriter::Char(char c) + { + if (m_buf == nullptr || m_pos + 1 >= m_cap) + { + m_truncated = true; + return; + } + m_buf[m_pos++] = c; + m_buf[m_pos] = '\0'; + } + + void MonitorWriter::Str(const char* s) + { + if (s == nullptr) + { + return; + } + for (uint32 i = 0; s[i] != '\0'; ++i) + { + if (m_pos + 1 >= m_cap) + { + m_truncated = true; + return; + } + m_buf[m_pos++] = s[i]; + } + if (m_buf != nullptr && m_pos < m_cap) + { + m_buf[m_pos] = '\0'; + } + } + + void MonitorWriter::U64(uint64 v) + { + char tmp[24]; + uint32 n = 0; + if (v == 0) + { + tmp[n++] = '0'; + } + while (v != 0 && n < sizeof(tmp)) + { + tmp[n++] = static_cast('0' + (v % 10)); + v /= 10; + } + while (n != 0) + { + Char(tmp[--n]); + } + } + + void MonitorWriter::Hex(uint64 v, uint32 min_digits) + { + char tmp[16]; + uint32 n = 0; + if (v == 0) + { + tmp[n++] = '0'; + } + while (v != 0 && n < sizeof(tmp)) + { + const uint32 nib = static_cast(v & 0xF); + tmp[n++] = static_cast(nib < 10 ? ('0' + nib) : ('a' + nib - 10)); + v >>= 4; + } + while (n < min_digits && n < sizeof(tmp)) + { + tmp[n++] = '0'; + } + while (n != 0) + { + Char(tmp[--n]); + } + } + + void MonitorWriter::Line() + { + Char('\n'); + } + + // ----------------------------------------------------------------------- + // Tokenizer + small parse helpers + // ----------------------------------------------------------------------- + + namespace + { + constexpr uint32 kMaxArgs = 8; + + // Split into whitespace-delimited tokens (runs collapsed), pointing + // into the caller-owned mutable `line` copy. Returns argc. + uint32 Tokenize(char* line, uint32 len, const char** argv) + { + uint32 argc = 0; + uint32 i = 0; + while (i < len && argc < kMaxArgs) + { + while (i < len && (line[i] == ' ' || line[i] == '\t')) + { + ++i; + } + if (i >= len || line[i] == '\0') + { + break; + } + argv[argc++] = &line[i]; + while (i < len && line[i] != ' ' && line[i] != '\t' && line[i] != '\0') + { + ++i; + } + if (i < len) + { + line[i++] = '\0'; + } + } + return argc; + } + + // Pointer to the substring of `cmd` that follows the first `skip` + // whitespace-delimited tokens (used for verbs whose tail can itself + // contain spaces, e.g. `config` keys or `cmd` command lines). + const char* RestAfter(const char* cmd, uint32 skip) + { + uint32 i = 0; + for (uint32 t = 0; t < skip; ++t) + { + while (cmd[i] == ' ' || cmd[i] == '\t') + { + ++i; + } + if (cmd[i] == '\0') + { + return &cmd[i]; + } + while (cmd[i] != '\0' && cmd[i] != ' ' && cmd[i] != '\t') + { + ++i; + } + } + while (cmd[i] == ' ' || cmd[i] == '\t') + { + ++i; + } + return &cmd[i]; + } + + bool Eq(const char* a, const char* b) + { + if (a == nullptr || b == nullptr) + { + return false; + } + uint32 i = 0; + while (a[i] != '\0' && b[i] != '\0') + { + if (a[i] != b[i]) + { + return false; + } + ++i; + } + return a[i] == b[i]; + } + + bool Contains(const char* hay, const char* needle) + { + if (hay == nullptr || needle == nullptr || needle[0] == '\0') + { + return false; + } + for (uint32 i = 0; hay[i] != '\0'; ++i) + { + uint32 j = 0; + while (needle[j] != '\0' && hay[i + j] == needle[j]) + { + ++j; + } + if (needle[j] == '\0') + { + return true; + } + } + return false; + } + + // Parse a decimal or 0x-hex unsigned. Returns false on empty / + // malformed input so callers can reject it explicitly. + bool ParseU64(const char* s, uint64* out) + { + if (s == nullptr || s[0] == '\0') + { + return false; + } + uint64 v = 0; + uint32 i = 0; + if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) + { + i = 2; + if (s[i] == '\0') + { + return false; + } + for (; s[i] != '\0'; ++i) + { + const char c = s[i]; + uint32 d; + if (c >= '0' && c <= '9') + d = static_cast(c - '0'); + else if (c >= 'a' && c <= 'f') + d = static_cast(10 + c - 'a'); + else if (c >= 'A' && c <= 'F') + d = static_cast(10 + c - 'A'); + else + return false; + v = (v << 4) | d; + } + } + else + { + for (; s[i] != '\0'; ++i) + { + if (s[i] < '0' || s[i] > '9') + { + return false; + } + v = v * 10 + static_cast(s[i] - '0'); + } + } + *out = v; + return true; + } + + void Usage(MonitorWriter& out) + { + out.Str("MaNGOS GDB monitor - verb namespace 'mangos'\n" + " mangos help this text\n" + " mangos status world summary (uptime, sessions, tick)\n" + " mangos players list online players\n" + " mangos tick world loop counter + uptime\n" + " mangos session session detail for an account id\n" + " mangos config read a mangosd.conf value\n" + " mangos cmd run a server/GM command (e.g. .server info)\n"); + } + } // namespace + + // ----------------------------------------------------------------------- + // Dispatch + // ----------------------------------------------------------------------- + + bool GdbMonitorDispatch(const char* cmd, uint32 cmd_len, MonitorWriter& out) + { + if (cmd == nullptr) + { + return false; + } + + static char line[1024]; + uint32 ln = 0; + for (uint32 i = 0; i < cmd_len && cmd[i] != '\0' && ln + 1 < sizeof(line); ++i) + { + line[ln++] = cmd[i]; + } + line[ln] = '\0'; + + const char* argv[kMaxArgs] = {}; + const uint32 argc = Tokenize(line, ln, argv); + if (argc == 0 || !Eq(argv[0], "mangos")) + { + return false; + } + if (argc == 1 || Eq(argv[1], "help")) + { + Usage(out); + return true; + } + + const char* sub = argv[1]; + + if (Eq(sub, "status")) + { + verbs::CmdStatus(out); + } + else if (Eq(sub, "players") || Eq(sub, "ps")) + { + verbs::CmdPlayers(out); + } + else if (Eq(sub, "tick")) + { + verbs::CmdTick(out); + } + else if (Eq(sub, "session")) + { + uint64 accId = 0; + if (argc < 3 || !ParseU64(argv[2], &accId)) + { + out.Str("session: usage: mangos session \n"); + return true; + } + verbs::CmdSession(accId, out); + } + else if (Eq(sub, "config")) + { + if (argc < 3) + { + out.Str("config: usage: mangos config \n"); + return true; + } + verbs::CmdConfig(RestAfter(cmd, 2), out); + } + else if (Eq(sub, "cmd")) + { + const char* rest = RestAfter(cmd, 2); + if (rest[0] == '\0') + { + out.Str("cmd: usage: mangos cmd \n"); + return true; + } + verbs::CmdCmd(rest, out); + } + else + { + out.Str("mangos: unknown command '"); + out.Str(sub); + out.Str("' - try 'mangos help'\n"); + } + + if (out.Truncated()) + { + out.Str("\n[truncated]\n"); + } + return true; + } + + // ----------------------------------------------------------------------- + // Self-test + // ----------------------------------------------------------------------- + + bool GdbMonitorSelfTest() + { + char buf[2048]; + + // help must list the verb surface. + { + MonitorWriter w(buf, sizeof(buf)); + if (!GdbMonitorDispatch("mangos help", 11, w)) + { + sLog.outError("[gdb-monitor-selftest] FAIL: 'mangos help' not recognized"); + return false; + } + if (!Contains(w.Data(), "players") || !Contains(w.Data(), "config")) + { + sLog.outError("[gdb-monitor-selftest] FAIL: help text missing expected verbs"); + return false; + } + } + + // tick always produces output. + { + MonitorWriter w(buf, sizeof(buf)); + GdbMonitorDispatch("mangos tick", 11, w); + if (w.Len() == 0) + { + sLog.outError("[gdb-monitor-selftest] FAIL: 'mangos tick' produced no output"); + return false; + } + } + + // non-"mangos" lines must fall through to the unsupported reply. + { + MonitorWriter w(buf, sizeof(buf)); + if (GdbMonitorDispatch("info registers", 14, w)) + { + sLog.outError("[gdb-monitor-selftest] FAIL: non-mangos line wrongly accepted"); + return false; + } + } + + sLog.outString("[gdb-monitor-selftest] PASS"); + return true; + } +} // namespace GdbMon +/// @} diff --git a/src/game/Debug/GdbServer/GdbMonitor.h b/src/game/Debug/GdbServer/GdbMonitor.h new file mode 100644 index 000000000..f8fbb1868 --- /dev/null +++ b/src/game/Debug/GdbServer/GdbMonitor.h @@ -0,0 +1,106 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#ifndef MANGOS_H_GDB_MONITOR +#define MANGOS_H_GDB_MONITOR + +#include "Common.h" + +/* + * GDB `monitor` (qRcmd) command surface for mangosd, ported from the + * DuetOS `duet` monitor. The generic GDB remote-serial server (GdbRsp) + * speaks raw registers / memory; stock GDB cannot express mangos-specific + * state (online players, the world tick, config values, server commands). + * This TU implements a `mangos [args]` command surface delivered + * over the standard GDB `qRcmd,` ("monitor") packet, so a specialised + * client OR stock `gdb` (`monitor mangos ...`) — OR any AI over the plain + * monitor bridge — gets full introspection + control. + * + * EXECUTION CONTEXT + * GdbMonitorDispatch only ever runs on the world thread (from the + * GdbServer per-tick service or stop loop), so verbs may touch game + * state directly without locking. + */ +namespace GdbMon +{ + /** + * Bounded text builder for monitor replies. Stops writing on overflow + * and latches Truncated(); never writes out of bounds. NUL-terminates + * on every mutation so Data() is always a valid C string. + */ + class MonitorWriter + { + public: + MonitorWriter(char* buf, uint32 cap); + + void Str(const char* s); + void Char(char c); + void U64(uint64 v); // decimal + void Hex(uint64 v, uint32 min_digits = 0); // lowercase, no "0x" + void Line(); // emits '\n' + + bool Truncated() const { return m_truncated; } + uint32 Len() const { return m_pos; } + const char* Data() const { return m_buf; } + + private: + char* m_buf; + uint32 m_cap; + uint32 m_pos = 0; + bool m_truncated = false; + }; + + /** + * Execute one decoded monitor command line. Returns true when @p cmd was + * a recognised `mangos ...` line (the reply is in @p out, even for an + * unknown subcommand — a friendly usage hint). Returns false ONLY when + * @p cmd is not a `mangos` line at all, so the RSP caller can answer the + * GDB packet with the empty "unsupported" reply. + */ + bool GdbMonitorDispatch(const char* cmd, uint32 cmd_len, MonitorWriter& out); + + /// Boot-time self-test. Exercises the dispatcher directly (no socket + /// I/O) and logs a grep-able "[gdb-monitor-selftest] PASS" line. + /// Returns false on failure (caller logs an error; never aborts). + bool GdbMonitorSelfTest(); + + // Mangos-specific read verbs. Defined in GdbMonitorVerbs.cpp; the + // dispatch table in GdbMonitor.cpp routes here. + namespace verbs + { + void CmdStatus(MonitorWriter& out); + void CmdPlayers(MonitorWriter& out); + void CmdTick(MonitorWriter& out); + void CmdSession(uint64 accountId, MonitorWriter& out); + void CmdConfig(const char* key, MonitorWriter& out); + void CmdCmd(const char* serverCommand, MonitorWriter& out); + } +} + +#endif +/// @} diff --git a/src/game/Debug/GdbServer/GdbMonitorVerbs.cpp b/src/game/Debug/GdbServer/GdbMonitorVerbs.cpp new file mode 100644 index 000000000..364dc7130 --- /dev/null +++ b/src/game/Debug/GdbServer/GdbMonitorVerbs.cpp @@ -0,0 +1,166 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#include "GdbMonitor.h" + +#include "Chat.h" +#include "Config.h" +#include "ObjectGuid.h" +#include "Player.h" +#include "Util.h" +#include "World.h" +#include "WorldSession.h" + +namespace GdbMon +{ + namespace + { + // CliHandler print adapter: appends each emitted line to the bound + // MonitorWriter so a server command's console output flows back to + // the debugger / monitor client verbatim. + void MonitorPrint(void* arg, char const* txt) + { + if (txt != nullptr && arg != nullptr) + { + static_cast(arg)->Str(txt); + } + } + + void WriteSessionLine(WorldSession* sess, MonitorWriter& out) + { + out.Str(" acc="); + out.U64(sess->GetAccountId()); + out.Str(" sec="); + out.U64(static_cast(sess->GetSecurity())); + out.Str(" lat="); + out.U64(sess->GetLatency()); + + Player* p = sess->GetPlayer(); + if (p != nullptr) + { + out.Str(" name="); + out.Str(p->GetName()); + out.Str(" lvl="); + out.U64(p->getLevel()); + out.Str(" map="); + out.U64(p->GetMapId()); + out.Str(" zone="); + out.U64(p->GetZoneId()); + out.Str(" guid="); + out.Str(p->GetObjectGuid().GetString().c_str()); + } + else + { + const char* n = sess->GetPlayerName(); + out.Str(" name="); + out.Str((n != nullptr && n[0] != '\0') ? n : "(char-select)"); + } + out.Line(); + } + } // namespace + + namespace verbs + { + void CmdStatus(MonitorWriter& out) + { + out.Str("motd: "); + out.Str(sWorld.GetMotd()); + out.Line(); + out.Str("uptime: "); + out.Str(secsToTimeString(sWorld.GetUptime()).c_str()); + out.Line(); + out.Str("sessions: active="); + out.U64(sWorld.GetActiveSessionCount()); + out.Str(" total="); + out.U64(sWorld.GetAllSessions().size()); + out.Line(); + out.Str("world-tick: "); + out.U64(static_cast(World::m_worldLoopCounter.value())); + out.Line(); + } + + void CmdPlayers(MonitorWriter& out) + { + const SessionMap& sessions = sWorld.GetAllSessions(); + if (sessions.empty()) + { + out.Str(" (no sessions)\n"); + return; + } + for (SessionMap::const_iterator it = sessions.begin(); it != sessions.end(); ++it) + { + if (it->second != nullptr) + { + WriteSessionLine(it->second, out); + } + } + } + + void CmdTick(MonitorWriter& out) + { + out.Str("world-tick="); + out.U64(static_cast(World::m_worldLoopCounter.value())); + out.Str(" uptime-sec="); + out.U64(sWorld.GetUptime()); + out.Line(); + } + + void CmdSession(uint64 accountId, MonitorWriter& out) + { + WorldSession* sess = sWorld.FindSession(static_cast(accountId)); + if (sess == nullptr) + { + out.Str("session: no online session for account "); + out.U64(accountId); + out.Line(); + return; + } + WriteSessionLine(sess, out); + } + + void CmdConfig(const char* key, MonitorWriter& out) + { + std::string val = sConfig.GetStringDefault(key, ""); + out.Str(key); + out.Str(" = "); + out.Str(val.c_str()); + out.Line(); + } + + void CmdCmd(const char* serverCommand, MonitorWriter& out) + { + // Runs on the world thread (monitor dispatch context), so the + // command executes synchronously and inline — no cliCmdQueue + // round-trip is needed. Account id 0 + SEC_CONSOLE grants the + // full command surface, matching the local console. + CliHandler handler(0, SEC_CONSOLE, &out, &MonitorPrint); + handler.ParseCommands(serverCommand); + } + } // namespace verbs +} // namespace GdbMon +/// @} diff --git a/src/game/Debug/GdbServer/GdbRsp.cpp b/src/game/Debug/GdbServer/GdbRsp.cpp new file mode 100644 index 000000000..053f6be78 --- /dev/null +++ b/src/game/Debug/GdbServer/GdbRsp.cpp @@ -0,0 +1,572 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#include "GdbRsp.h" + +#include "GdbDbgMemory.h" +#include "GdbMonitor.h" + +namespace GdbRsp +{ + namespace + { + // GDB's maximum packet — 4 KiB matches the advertised PacketSize and + // leaves room for a full target.xml chunk and multi-KiB memory reads. + constexpr uint32 kPacketMax = 4096; + + enum class State : uint8 + { + Idle, // waiting for '$' + Body, // accumulating packet body until '#' + Csum1, // first checksum hex digit + Csum2, // second checksum hex digit + }; + + State g_state = State::Idle; + uint8 g_packet[kPacketMax]; + uint32 g_packet_len = 0; + uint8 g_csum_calc = 0; + uint8 g_csum_recv = 0; + + WriteByte g_sink = nullptr; + bool g_allow_mem_write = false; + + ResumeAction g_resume_pending = ResumeAction::None; + + uint64 g_packets_received = 0; + uint64 g_packets_bad_csum = 0; + uint64 g_packets_handled = 0; + + bool IsHexDigit(uint8 c) + { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } + + uint8 HexDigitValue(uint8 c) + { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return 10 + (c - 'a'); + if (c >= 'A' && c <= 'F') + return 10 + (c - 'A'); + return 0; + } + + uint8 HexDigitChar(uint8 v) + { + return (v < 10) ? uint8('0' + v) : uint8('a' + (v - 10)); + } + + void EmitByte(uint8 b) + { + if (g_sink != nullptr) + { + g_sink(b); + } + } + + bool MatchPrefix(const uint8* body, uint32 len, const char* prefix) + { + for (uint32 i = 0; prefix[i] != '\0'; ++i) + { + if (i >= len || body[i] != static_cast(prefix[i])) + { + return false; + } + } + return true; + } + + // Send a `$#` reply: frame + checksum the raw bytes. + void SendReply(const char* payload, uint32 len) + { + EmitByte('$'); + uint8 csum = 0; + for (uint32 i = 0; i < len; ++i) + { + const uint8 b = static_cast(payload[i]); + EmitByte(b); + csum = static_cast(csum + b); + } + EmitByte('#'); + EmitByte(HexDigitChar((csum >> 4) & 0xF)); + EmitByte(HexDigitChar(csum & 0xF)); + } + + void SendCStr(const char* payload) + { + uint32 len = 0; + while (payload[len] != '\0') + { + ++len; + } + SendReply(payload, len); + } + + // 24-register x86_64 target description matching the `g` reply + // exactly, so gdb's amd64-tdep validator accepts our register count + // instead of falling back to its 154-register default. Ported + // verbatim from the DuetOS stub. + const char* TargetXml() + { + static const char kTargetXml[] = + "l" + "" + "" + "i386:x86-64" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + return kTargetXml; + } + + // Reply to a `g` (read registers) packet. Phase 1: a zeroed 24-reg + // x86_64 block (16 GPR + rip + eflags + 6 segments) in the canonical + // little-endian hex order our target.xml declares. + void HandleReadRegisters() + { + // 16 GPR (16 hex) + rip (16) + eflags (8) + 6 segs (8) = 328. + constexpr uint32 kReplyChars = 16 * 16 + 16 + 8 + 6 * 8; + char buf[kReplyChars + 1]; + for (uint32 i = 0; i < kReplyChars; ++i) + { + buf[i] = '0'; + } + buf[kReplyChars] = '\0'; + SendReply(buf, kReplyChars); + } + + // Parse the `m,` arguments out of the packet starting at + // index 1. Returns false on a malformed header. + bool ParseAddrLen(uint32 start, uint64& addr, uint64& len, uint32& consumed) + { + addr = 0; + len = 0; + uint32 i = start; + while (i < g_packet_len && g_packet[i] != ',') + { + if (!IsHexDigit(g_packet[i])) + { + return false; + } + addr = (addr << 4) | HexDigitValue(g_packet[i]); + ++i; + } + if (i >= g_packet_len || g_packet[i] != ',') + { + return false; + } + ++i; + while (i < g_packet_len && g_packet[i] != ':') + { + if (!IsHexDigit(g_packet[i])) + { + break; + } + len = (len << 4) | HexDigitValue(g_packet[i]); + ++i; + } + consumed = i; + return true; + } + + void HandleReadMemory() + { + uint64 addr = 0; + uint64 len = 0; + uint32 consumed = 0; + if (!ParseAddrLen(1, addr, len, consumed)) + { + SendCStr("E01"); + return; + } + + constexpr uint64 kMaxLen = (kPacketMax / 2) - 8; + if (len > kMaxLen) + { + len = kMaxLen; + } + if (len == 0) + { + SendCStr("00"); + return; + } + + static uint8 raw[kPacketMax / 2]; + const size_t got = GdbDbg::MemRead(static_cast(addr), raw, static_cast(len)); + if (got == 0) + { + SendCStr("E14"); // EFAULT-style + return; + } + + char buf[kPacketMax]; + uint32 off = 0; + for (size_t k = 0; k < got && off + 2 <= sizeof(buf); ++k) + { + buf[off++] = HexDigitChar((raw[k] >> 4) & 0xF); + buf[off++] = HexDigitChar(raw[k] & 0xF); + } + SendReply(buf, off); + } + + void HandleWriteMemory() + { + if (!g_allow_mem_write) + { + SendCStr("E01"); // write disabled by config + return; + } + + uint64 addr = 0; + uint64 len = 0; + uint32 consumed = 0; + if (!ParseAddrLen(1, addr, len, consumed) || consumed >= g_packet_len || g_packet[consumed] != ':') + { + SendCStr("E01"); + return; + } + uint32 i = consumed + 1; // skip ':' + + static uint8 raw[kPacketMax / 2]; + uint64 n = 0; + for (; n < len && n < sizeof(raw); ++n) + { + if (i + 1 >= g_packet_len) + { + break; + } + const uint8 hi = HexDigitValue(g_packet[i]); + const uint8 lo = HexDigitValue(g_packet[i + 1]); + raw[n] = static_cast((hi << 4) | lo); + i += 2; + } + + const size_t wrote = GdbDbg::MemWrite(static_cast(addr), raw, static_cast(n)); + SendCStr(wrote == n && n > 0 ? "OK" : "E14"); + } + + // Decode a qRcmd hex payload, dispatch through the mangos monitor, + // and hex-encode the text reply back to gdb. + void HandleMonitor() + { + static char mon_cmd[1024]; + static char mon_txt[2048]; + static char mon_hex[2 * sizeof(mon_txt)]; + + const uint32 hoff = 6; // strlen("qRcmd,") + uint32 dn = 0; + for (uint32 i = hoff; i + 1 < g_packet_len && dn + 1 < sizeof(mon_cmd); i += 2) + { + mon_cmd[dn++] = static_cast((HexDigitValue(g_packet[i]) << 4) | HexDigitValue(g_packet[i + 1])); + } + mon_cmd[dn] = '\0'; + + GdbMon::MonitorWriter w(mon_txt, sizeof(mon_txt)); + if (!GdbMon::GdbMonitorDispatch(mon_cmd, dn, w)) + { + SendCStr(""); // not a "mangos" line — unsupported + return; + } + + uint32 ho = 0; + for (uint32 i = 0; i < w.Len() && ho + 2 < sizeof(mon_hex); ++i) + { + const uint8 b = static_cast(w.Data()[i]); + mon_hex[ho++] = HexDigitChar((b >> 4) & 0xF); + mon_hex[ho++] = HexDigitChar(b & 0xF); + } + SendReply(mon_hex, ho); + } + + // Pick the first action ('c' or 's') out of a vCont; packet. + void HandleVCont() + { + uint32 i = 6; // skip "vCont;" + while (i < g_packet_len) + { + const uint8 act = g_packet[i]; + if (act == 'c' || act == 'C') + { + g_resume_pending = ResumeAction::Continue; + break; + } + if (act == 's' || act == 'S') + { + g_resume_pending = ResumeAction::Step; + break; + } + ++i; + } + // No explicit reply: gdb expects a stop reply once the target + // halts again, which the cooperative stop loop emits. + } + + void HandlePacket() + { + ++g_packets_handled; + + if (g_packet_len == 0) + { + SendCStr(""); + return; + } + if (MatchPrefix(g_packet, g_packet_len, "qSupported")) + { + SendCStr("PacketSize=1000;swbreak+;qXfer:features:read+;vContSupported+"); + return; + } + if (MatchPrefix(g_packet, g_packet_len, "qXfer:features:read:target.xml:")) + { + SendCStr(TargetXml()); + return; + } + if (MatchPrefix(g_packet, g_packet_len, "qRcmd,")) + { + HandleMonitor(); + return; + } + if (MatchPrefix(g_packet, g_packet_len, "qfThreadInfo")) + { + SendCStr("m1"); // one logical thread: the world thread + return; + } + if (MatchPrefix(g_packet, g_packet_len, "qsThreadInfo")) + { + SendCStr("l"); + return; + } + if (g_packet_len == 2 && g_packet[0] == 'q' && g_packet[1] == 'C') + { + SendCStr("QC1"); + return; + } + if (g_packet[0] == 'T' && g_packet_len > 1) + { + SendCStr("OK"); // single logical thread is always alive + return; + } + if (g_packet[0] == '?') + { + SendCStr("S05"); // SIGTRAP + return; + } + if (g_packet[0] == 'g') + { + HandleReadRegisters(); + return; + } + if (g_packet[0] == 'G') + { + SendCStr("OK"); // Phase 1: synthetic registers, accept + ignore + return; + } + if (g_packet[0] == 'm') + { + HandleReadMemory(); + return; + } + if (g_packet[0] == 'M') + { + HandleWriteMemory(); + return; + } + if (g_packet[0] == 'H') + { + SendCStr("OK"); // single logical thread — accept any selection + return; + } + if (MatchPrefix(g_packet, g_packet_len, "vCont?")) + { + SendCStr("vCont;c;C;s;S"); + return; + } + if (MatchPrefix(g_packet, g_packet_len, "vCont;")) + { + HandleVCont(); + return; + } + if (g_packet[0] == 'c') + { + g_resume_pending = ResumeAction::Continue; + return; + } + if (g_packet[0] == 's') + { + g_resume_pending = ResumeAction::Step; + return; + } + if (g_packet[0] == 'D') + { + g_resume_pending = ResumeAction::Detached; + SendCStr("OK"); + return; + } + if (g_packet[0] == 'k') + { + g_resume_pending = ResumeAction::Killed; + return; // no reply expected + } + // Z / z (breakpoints) are Phase 2 — report unsupported so gdb + // falls back to its own software breakpoints. + SendCStr(""); + } + + void ResetParser() + { + g_state = State::Idle; + g_packet_len = 0; + g_csum_calc = 0; + g_csum_recv = 0; + } + } // namespace + + void SetSink(WriteByte sink) + { + g_sink = sink; + } + + void SetAllowMemWrite(bool allow) + { + g_allow_mem_write = allow; + } + + void ResetSession() + { + ResetParser(); + g_resume_pending = ResumeAction::None; + } + + void ReceiveByte(uint8 byte) + { + switch (g_state) + { + case State::Idle: + if (byte == '$') + { + g_packet_len = 0; + g_csum_calc = 0; + g_state = State::Body; + } + break; + case State::Body: + if (byte == '#') + { + g_state = State::Csum1; + } + else if (g_packet_len < kPacketMax) + { + g_packet[g_packet_len++] = byte; + g_csum_calc = static_cast(g_csum_calc + byte); + } + break; + case State::Csum1: + if (IsHexDigit(byte)) + { + g_csum_recv = static_cast(HexDigitValue(byte) << 4); + g_state = State::Csum2; + } + else + { + ResetParser(); + } + break; + case State::Csum2: + if (IsHexDigit(byte)) + { + g_csum_recv |= HexDigitValue(byte); + ++g_packets_received; + if (g_csum_recv == g_csum_calc) + { + EmitByte('+'); + HandlePacket(); + } + else + { + ++g_packets_bad_csum; + EmitByte('-'); + } + } + ResetParser(); + break; + } + } + + ResumeAction TakeResume() + { + const ResumeAction r = g_resume_pending; + g_resume_pending = ResumeAction::None; + return r; + } + + void SendStopReply() + { + SendCStr("S05"); + } + + uint64 PacketsReceived() + { + return g_packets_received; + } + + uint64 PacketsBadChecksum() + { + return g_packets_bad_csum; + } + + uint64 PacketsHandled() + { + return g_packets_handled; + } +} // namespace GdbRsp +/// @} diff --git a/src/game/Debug/GdbServer/GdbRsp.h b/src/game/Debug/GdbServer/GdbRsp.h new file mode 100644 index 000000000..de5d988af --- /dev/null +++ b/src/game/Debug/GdbServer/GdbRsp.h @@ -0,0 +1,95 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#ifndef MANGOS_H_GDB_RSP +#define MANGOS_H_GDB_RSP + +#include "Common.h" + +/* + * GDB Remote Serial Protocol (RSP) engine for mangosd, ported from the + * DuetOS kernel stub (kernel/diag/gdb_server.*). Transport-agnostic: bytes + * arrive via ReceiveByte() and replies leave through a byte-sink callback, + * so the same engine drives a TCP socket here that drove COM2 in DuetOS. + * + * The native-debug realities of a userland process differ from a kernel: + * - Registers (g/G): Phase 1 reports a synthetic (zeroed) x86_64 set — + * enough for stock gdb's attach handshake; the value for AI/users is + * the `monitor` surface + memory reads, not register-level debugging. + * - Memory (m/M): the debugger is in-process, so reads/writes are guarded + * accesses to our own address space (see GdbDbg). Writes are gated. + * - Breakpoints (Z/z) and real single-step are Phase 2. + * + * SINGLE SESSION: one attached debugger at a time. A new connection calls + * ResetSession() which re-initialises the parser + resume state. + */ +namespace GdbRsp +{ + /// Output sink — invoked once per byte the engine wants to send. + using WriteByte = void (*)(uint8 byte); + + /// Configure the output sink (the active socket writer). + void SetSink(WriteByte sink); + + /// Allow / forbid the `M` (write memory) packet. Default: forbidden. + void SetAllowMemWrite(bool allow); + + /// Re-initialise parser + resume state for a freshly attached debugger. + void ResetSession(); + + /// Feed one received byte to the parser. On a complete, checksum-valid + /// packet this ACKs and dispatches to the relevant handler, emitting the + /// reply through the sink. + void ReceiveByte(uint8 byte); + + /// Resume action requested by the debugger via c / s / D / k. + enum class ResumeAction : uint8 + { + None, + Continue, + Step, + Detached, + Killed, + }; + + /// Return and clear any resume action requested since the last call. + ResumeAction TakeResume(); + + /// Emit an unsolicited stop reply ("S05" = SIGTRAP). Used when entering + /// the cooperative world stop so the attached debugger learns the target + /// has halted. + void SendStopReply(); + + /// Diagnostic counters. + uint64 PacketsReceived(); + uint64 PacketsBadChecksum(); + uint64 PacketsHandled(); +} + +#endif +/// @} diff --git a/src/game/Debug/GdbServer/GdbServer.cpp b/src/game/Debug/GdbServer/GdbServer.cpp new file mode 100644 index 000000000..545dd98aa --- /dev/null +++ b/src/game/Debug/GdbServer/GdbServer.cpp @@ -0,0 +1,276 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#include "GdbServer.h" + +#include "Config.h" +#include "GdbMonitor.h" +#include "GdbRsp.h" +#include "Log.h" +#include "World.h" + +#include +#include + +GdbServer& GdbServer::Instance() +{ + static GdbServer s_instance; + return s_instance; +} + +void GdbServer::Init() +{ + m_enabled = sConfig.GetBoolDefault("GdbServer.Enable", false); + m_allowMemWrite = sConfig.GetBoolDefault("GdbServer.AllowMemWrite", false); + + GdbRsp::SetSink(&GdbServer::RspSinkThunk); + GdbRsp::SetAllowMemWrite(m_allowMemWrite); + + // Always validate the monitor surface, even when the listener is off, so + // a broken port is caught at boot rather than on first connect. + GdbMon::GdbMonitorSelfTest(); + + if (m_enabled) + { + sLog.outString("GdbServer: monitor surface ready (mem-write %s)", + m_allowMemWrite ? "ENABLED" : "disabled"); + } +} + +// ----------------------------------------------------------------------------- +// RSP output sink (world thread only) +// ----------------------------------------------------------------------------- + +void GdbServer::RspSinkThunk(uint8 b) +{ + Instance().m_outBuf.push_back(b); +} + +void GdbServer::FlushRspOut() +{ + if (m_outBuf.empty()) + { + return; + } + std::lock_guard guard(m_writerLock); + if (m_rspWriter != nullptr && !m_peerClosed.load()) + { + m_rspWriter(m_rspCtx, m_outBuf.data(), static_cast(m_outBuf.size())); + } + m_outBuf.clear(); +} + +// ----------------------------------------------------------------------------- +// Network-thread entry points +// ----------------------------------------------------------------------------- + +void GdbServer::AttachRsp(void* ctx, RspWriter writer) +{ + { + std::lock_guard guard(m_writerLock); + m_rspCtx = ctx; + m_rspWriter = writer; + } + m_peerClosed.store(false); + m_interrupt.store(false); + + { + std::lock_guard guard(m_inLock); + m_inbound.clear(); + } + + // Defer the engine reset to the world thread — the RSP parser state must + // only ever be mutated there. + m_resetPending.store(true); + sLog.outString("GdbServer: debugger attached"); +} + +void GdbServer::DetachRsp(void* ctx) +{ + std::lock_guard guard(m_writerLock); + if (m_rspCtx == ctx) + { + m_rspWriter = nullptr; + m_rspCtx = nullptr; + m_peerClosed.store(true); + sLog.outString("GdbServer: debugger detached"); + } +} + +void GdbServer::FeedRsp(const uint8* data, uint32 len) +{ + if (data == nullptr || len == 0) + { + return; + } + // A GDB interrupt is a bare 0x03 byte outside packet framing. + for (uint32 i = 0; i < len; ++i) + { + if (data[i] == 0x03) + { + m_interrupt.store(true); + } + } + std::lock_guard guard(m_inLock); + m_inbound.emplace_back(data, data + len); +} + +void GdbServer::SubmitMonitorLine(void* ctx, MonWriter writer, const char* line) +{ + if (line == nullptr || writer == nullptr) + { + return; + } + MonitorReq req; + req.ctx = ctx; + req.writer = writer; + req.line = line; + std::lock_guard guard(m_monLock); + m_monitor.push_back(std::move(req)); +} + +// ----------------------------------------------------------------------------- +// World-thread service +// ----------------------------------------------------------------------------- + +void GdbServer::DrainMonitorRequests() +{ + for (;;) + { + MonitorReq req; + { + std::lock_guard guard(m_monLock); + if (m_monitor.empty()) + { + break; + } + req = std::move(m_monitor.front()); + m_monitor.pop_front(); + } + + char buf[4096]; + GdbMon::MonitorWriter w(buf, sizeof(buf)); + if (!GdbMon::GdbMonitorDispatch(req.line.c_str(), static_cast(req.line.size()), w)) + { + req.writer(req.ctx, "error: not a 'mangos' command (try 'mangos help')\n"); + } + else + { + req.writer(req.ctx, w.Data()); + } + } +} + +void GdbServer::DrainAndServiceRsp() +{ + for (;;) + { + std::vector chunk; + { + std::lock_guard guard(m_inLock); + if (m_inbound.empty()) + { + break; + } + chunk = std::move(m_inbound.front()); + m_inbound.pop_front(); + } + for (uint8 b : chunk) + { + GdbRsp::ReceiveByte(b); + } + } + FlushRspOut(); +} + +void GdbServer::EnterCooperativeStop() +{ + // Pause the world tick: this function runs inside World::Update, so the + // 50 ms heartbeat is blocked for as long as we stay here. All game-state + // reads issued by the debugger therefore see a quiescent world. + sLog.outString("GdbServer: target stopped (debugger interrupt)"); + + // Discard any stale resume left by a 'c'/'s' issued while the target was + // running, so this stop actually waits for a fresh resume command. + (void)GdbRsp::TakeResume(); + + GdbRsp::SendStopReply(); + FlushRspOut(); + + bool resumed = false; + while (!resumed) + { + // Top priority: never hold the world hostage past a shutdown request. + if (World::IsStopped() || m_peerClosed.load()) + { + break; + } + + DrainAndServiceRsp(); + + switch (GdbRsp::TakeResume()) + { + case GdbRsp::ResumeAction::Continue: + case GdbRsp::ResumeAction::Step: + case GdbRsp::ResumeAction::Detached: + case GdbRsp::ResumeAction::Killed: + resumed = true; + break; + case GdbRsp::ResumeAction::None: + default: + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + break; + } + } + + sLog.outString("GdbServer: target resumed"); +} + +void GdbServer::OnWorldUpdate() +{ + if (!m_enabled) + { + return; + } + + if (m_resetPending.exchange(false)) + { + GdbRsp::ResetSession(); + m_outBuf.clear(); + } + + DrainMonitorRequests(); + DrainAndServiceRsp(); + + // PollAsyncStop: a pending GDB interrupt enters the cooperative stop. + if (m_interrupt.exchange(false)) + { + EnterCooperativeStop(); + } +} +/// @} diff --git a/src/game/Debug/GdbServer/GdbServer.h b/src/game/Debug/GdbServer/GdbServer.h new file mode 100644 index 000000000..dc62c1e2a --- /dev/null +++ b/src/game/Debug/GdbServer/GdbServer.h @@ -0,0 +1,142 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#ifndef MANGOS_H_GDB_SERVER +#define MANGOS_H_GDB_SERVER + +#include "Common.h" + +#include +#include +#include +#include +#include + +/** + * GdbServer — facade and cooperative stop controller that bridges the + * network listener thread (GdbServerThread, in src/mangosd) and the world + * thread, where all protocol/state work happens. + * + * Data flow: + * - The network thread pushes received RSP bytes (FeedRsp) and plain-text + * monitor lines (SubmitMonitorLine) into thread-safe queues, and + * registers a per-connection output writer (AttachRsp / AttachMonitor). + * - The world thread drains those queues once per tick (OnWorldUpdate), + * feeds bytes to the RSP engine, runs monitor verbs, and flushes replies + * back through the registered writer. + * - A GDB interrupt (0x03) raises an async-stop request; OnWorldUpdate + * then enters a cooperative stop loop that pauses the 50 ms world tick + * and pumps packets until the debugger resumes. The loop always honours + * World::IsStopped() so a paused-in-debugger server can still shut down. + * + * Consistency is "world-tick-consistent", NOT globally race-free: the + * network, database and map-update threads keep running during a stop. + */ +class GdbServer +{ + public: + typedef void (*RspWriter)(void* ctx, const uint8* data, uint32 len); + typedef void (*MonWriter)(void* ctx, const char* text); + + static GdbServer& Instance(); + + /// Read config, wire the RSP output sink, run the monitor self-test. + /// Called once at startup regardless of whether the listener starts. + void Init(); + + bool IsEnabled() const { return m_enabled; } + + // --- network thread side ------------------------------------------- + + /// Register the active RSP connection's output writer. Resets the + /// protocol session for the freshly attached debugger. + void AttachRsp(void* ctx, RspWriter writer); + /// Drop the active RSP connection (called from the socket's close). + void DetachRsp(void* ctx); + /// Queue inbound RSP bytes; scans for the 0x03 interrupt sentinel. + void FeedRsp(const uint8* data, uint32 len); + + /// Submit one plain-text monitor line ("mangos ...") for execution on + /// the world thread; the reply is delivered via @p writer. + void SubmitMonitorLine(void* ctx, MonWriter writer, const char* line); + + // --- world thread side --------------------------------------------- + + /// Drain queues, service protocol traffic, and handle async stops. + /// Invoked once per tick from World::Update. + void OnWorldUpdate(); + + private: + GdbServer() = default; + GdbServer(const GdbServer&) = delete; + GdbServer& operator=(const GdbServer&) = delete; + + static void RspSinkThunk(uint8 b); // appends to m_outBuf (world thread) + + void DrainMonitorRequests(); + void DrainAndServiceRsp(); + void FlushRspOut(); + void EnterCooperativeStop(); + + struct MonitorReq + { + void* ctx = nullptr; + MonWriter writer = nullptr; + std::string line; + }; + + bool m_enabled = false; + bool m_allowMemWrite = false; + + // Active RSP writer (guarded so a concurrent socket close cannot race + // an in-progress flush). + std::mutex m_writerLock; + void* m_rspCtx = nullptr; + RspWriter m_rspWriter = nullptr; + std::atomic m_peerClosed{false}; + std::atomic m_interrupt{false}; + // Set by AttachRsp (network thread); consumed by OnWorldUpdate so all + // RSP engine state mutation happens on the world thread. + std::atomic m_resetPending{false}; + + // Inbound RSP byte chunks (network -> world). + std::mutex m_inLock; + std::deque> m_inbound; + + // Plain-text monitor requests (network -> world). + std::mutex m_monLock; + std::deque m_monitor; + + // Outbound RSP scratch (world thread only). + std::vector m_outBuf; +}; + +#define sGdbServer GdbServer::Instance() + +#endif +/// @} diff --git a/src/game/WorldHandlers/World.cpp b/src/game/WorldHandlers/World.cpp index 153f13d9c..a7bb561f2 100644 --- a/src/game/WorldHandlers/World.cpp +++ b/src/game/WorldHandlers/World.cpp @@ -91,6 +91,7 @@ #include "CommandMgr.h" #include "GitRevision.h" #include "UpdateTime.h" +#include "Debug/GdbServer/GdbServer.h" #include "GameTime.h" #include "ScheduledExit.h" @@ -1936,6 +1937,10 @@ void World::Update(uint32 diff) // And last, but not least handle the issued cli commands ProcessCliCommands(); + // Service the GDB-server debug endpoint: drain protocol traffic and, on a + // debugger interrupt, enter the cooperative stop loop (pauses this tick). + sGdbServer.OnWorldUpdate(); + // cleanup unused GridMap objects as well as VMaps sTerrainMgr.Update(diff); } diff --git a/src/mangosd/CMakeLists.txt b/src/mangosd/CMakeLists.txt index ad8727239..9e4ca68d1 100644 --- a/src/mangosd/CMakeLists.txt +++ b/src/mangosd/CMakeLists.txt @@ -28,6 +28,8 @@ set(SRC_GRP_MAIN AFThread.h CliThread.cpp CliThread.h + GdbServerThread.cpp + GdbServerThread.h RAThread.cpp RAThread.h WorldThread.cpp diff --git a/src/mangosd/GdbServerThread.cpp b/src/mangosd/GdbServerThread.cpp new file mode 100644 index 000000000..10fdf1b40 --- /dev/null +++ b/src/mangosd/GdbServerThread.cpp @@ -0,0 +1,385 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#include +#include +#include +#include +#include + +#include "GdbServerThread.h" + +#include "Log.h" +#include "World.h" +#include "Debug/GdbServer/GdbServer.h" + +#include + +// ----------------------------------------------------------------------------- +// Shared socket plumbing — buffered, mutex-guarded output that can be written +// to from the world thread (the GdbServer flush path) into a socket living in +// this reactor's thread, exactly like RASocket::sendf. +// ----------------------------------------------------------------------------- + +class GdbSocketBase : public ACE_Svc_Handler < ACE_SOCK_STREAM, ACE_NULL_SYNCH > +{ + protected: + typedef ACE_Svc_Handler < ACE_SOCK_STREAM, ACE_NULL_SYNCH > Base; + + enum { GDB_BUFF_SIZE = 65536 }; + + GdbSocketBase() : Base(), outActive(false), outputBufferLen(0), outBufferLock() + { + reference_counting_policy().value(ACE_Event_Handler::Reference_Counting_Policy::ENABLED); + } + + virtual ~GdbSocketBase() + { + peer().close(); + } + + // Thread-safe append + reactor wakeup. Binary-safe (length carried). + int sendBytes(const uint8* data, uint32 len) + { + ACE_GUARD_RETURN(ACE_Thread_Mutex, Guard, outBufferLock, -1); + + if (closing_) + { + return -1; + } + if (outputBufferLen + len > GDB_BUFF_SIZE) + { + return -1; + } + + ACE_OS::memcpy(outputBuffer + outputBufferLen, data, len); + outputBufferLen += len; + + if (!outActive) + { + if (reactor()->schedule_wakeup(this, ACE_Event_Handler::WRITE_MASK) == -1) + { + sLog.outError("GdbSocket: schedule_wakeup failed"); + return -1; + } + outActive = true; + } + return 0; + } + + int sendCStr(const char* s) + { + return sendBytes(reinterpret_cast(s), static_cast(strlen(s))); + } + + int handle_output(ACE_HANDLE /*h*/ = ACE_INVALID_HANDLE) override + { + ACE_GUARD_RETURN(ACE_Thread_Mutex, Guard, outBufferLock, -1); + + if (closing_) + { + return -1; + } + + if (!outputBufferLen) + { + if (reactor()->cancel_wakeup(this, ACE_Event_Handler::WRITE_MASK) == -1) + { + return -1; + } + outActive = false; + return 0; + } +#ifdef MSG_NOSIGNAL + ssize_t n = peer().send(outputBuffer, outputBufferLen, MSG_NOSIGNAL); +#else + ssize_t n = peer().send(outputBuffer, outputBufferLen); +#endif + if (n <= 0) + { + return -1; + } + + ACE_OS::memmove(outputBuffer, outputBuffer + n, outputBufferLen - n); + outputBufferLen -= n; + return 0; + } + + // Derived classes detach from GdbServer here before the reference is + // dropped, so the world thread stops using this socket as a writer. + virtual void onClose() {} + + int close(u_long /*unused*/) override + { + if (closing_) + { + return -1; + } + shutdown(); + closing_ = true; + onClose(); + remove_reference(); + return 0; + } + + int handle_close(ACE_HANDLE /*h*/ = ACE_INVALID_HANDLE, + ACE_Reactor_Mask /*mask*/ = ACE_Event_Handler::ALL_EVENTS_MASK) override + { + { + ACE_GUARD_RETURN(ACE_Thread_Mutex, Guard, outBufferLock, -1); + if (closing_) + { + return -1; + } + closing_ = true; + } + onClose(); + remove_reference(); + return 0; + } + + bool outActive; + char outputBuffer[GDB_BUFF_SIZE]; + uint32 outputBufferLen; + ACE_Thread_Mutex outBufferLock; +}; + +// ----------------------------------------------------------------------------- +// GDB RSP socket — feeds raw bytes to the protocol engine and is registered as +// the active RSP output writer. +// ----------------------------------------------------------------------------- + +class GdbRspSocket : public GdbSocketBase +{ + public: + friend class ACE_Acceptor; + + protected: + int open(void* /*unused*/) override + { + if (reactor()->register_handler(this, + ACE_Event_Handler::READ_MASK | ACE_Event_Handler::WRITE_MASK) == -1) + { + sLog.outError("GdbRspSocket::open: register_handler failed: %s", ACE_OS::strerror(errno)); + return -1; + } + + ACE_INET_Addr remote_addr; + if (peer().get_remote_addr(remote_addr) != -1) + { + sLog.outString("GdbServer: RSP connection from %s", remote_addr.get_host_addr()); + } + + sGdbServer.AttachRsp(this, &GdbRspSocket::WriteThunk); + return 0; + } + + int handle_input(ACE_HANDLE = ACE_INVALID_HANDLE) override + { + if (closing_) + { + return -1; + } + uint8 buf[8192]; + ssize_t readBytes = peer().recv(buf, sizeof(buf)); + if (readBytes <= 0) + { + return -1; + } + sGdbServer.FeedRsp(buf, static_cast(readBytes)); + return 0; + } + + void onClose() override + { + sGdbServer.DetachRsp(this); + } + + private: + // GdbServer output writer: invoked from the world thread. + static void WriteThunk(void* ctx, const uint8* data, uint32 len) + { + static_cast(ctx)->sendBytes(data, len); + } +}; + +// ----------------------------------------------------------------------------- +// Plain-text monitor socket — line oriented; submits "mangos ..." lines and +// writes the text reply back. For AI agents and non-RSP debuggers. +// ----------------------------------------------------------------------------- + +class GdbMonSocket : public GdbSocketBase +{ + public: + friend class ACE_Acceptor; + + protected: + GdbMonSocket() : GdbSocketBase(), inputBufferLen(0) {} + + int open(void* /*unused*/) override + { + if (reactor()->register_handler(this, + ACE_Event_Handler::READ_MASK | ACE_Event_Handler::WRITE_MASK) == -1) + { + sLog.outError("GdbMonSocket::open: register_handler failed: %s", ACE_OS::strerror(errno)); + return -1; + } + sendCStr("MaNGOS GDB monitor bridge. Type 'mangos help'.\n"); + return 0; + } + + int handle_input(ACE_HANDLE = ACE_INVALID_HANDLE) override + { + if (closing_) + { + return -1; + } + ssize_t readBytes = peer().recv(inputBuffer + inputBufferLen, + MON_BUFF_SIZE - inputBufferLen - 1); + if (readBytes <= 0) + { + return -1; + } + inputBufferLen += static_cast(readBytes); + + // Process every complete line currently in the buffer. + uint32 lineStart = 0; + for (uint32 i = 0; i < inputBufferLen; ++i) + { + if (inputBuffer[i] == '\n' || inputBuffer[i] == '\r') + { + inputBuffer[i] = '\0'; + if (i > lineStart) + { + sGdbServer.SubmitMonitorLine(this, &GdbMonSocket::WriteTextThunk, + inputBuffer + lineStart); + } + lineStart = i + 1; + } + } + + // Shift any partial trailing line to the front. + if (lineStart > 0) + { + uint32 remain = inputBufferLen - lineStart; + if (remain > 0) + { + ACE_OS::memmove(inputBuffer, inputBuffer + lineStart, remain); + } + inputBufferLen = remain; + } + else if (inputBufferLen >= MON_BUFF_SIZE - 1) + { + // Overlong line with no newline — drop it to avoid a stall. + inputBufferLen = 0; + } + return 0; + } + + private: + enum { MON_BUFF_SIZE = 8192 }; + + static void WriteTextThunk(void* ctx, const char* text) + { + static_cast(ctx)->sendCStr(text); + } + + char inputBuffer[MON_BUFF_SIZE]; + uint32 inputBufferLen; +}; + +// ----------------------------------------------------------------------------- +// Listener thread +// ----------------------------------------------------------------------------- + +GdbServerThread::GdbServerThread(uint16 rspPort, uint16 monPort, const char* host) + : m_RspAcceptor(nullptr), m_MonAcceptor(nullptr), + m_rspAddr(rspPort, host), m_monAddr(monPort, host), m_monEnabled(monPort != 0) +{ + ACE_Reactor_Impl* imp = new ACE_TP_Reactor(); + imp->max_notify_iterations(128); + m_Reactor = new ACE_Reactor(imp, 1); + m_RspAcceptor = new GdbRspAcceptor; + if (m_monEnabled) + { + m_MonAcceptor = new GdbMonAcceptor; + } +} + +GdbServerThread::~GdbServerThread() +{ + delete m_Reactor; + delete m_RspAcceptor; + delete m_MonAcceptor; +} + +int GdbServerThread::open(void* /*unused*/) +{ + if (m_RspAcceptor->open(m_rspAddr, m_Reactor, ACE_NONBLOCK) == -1) + { + sLog.outError("GdbServer can not bind RSP port %d on %s", + m_rspAddr.get_port_number(), m_rspAddr.get_host_addr()); + return -1; + } + if (m_monEnabled && m_MonAcceptor->open(m_monAddr, m_Reactor, ACE_NONBLOCK) == -1) + { + sLog.outError("GdbServer can not bind monitor port %d on %s", + m_monAddr.get_port_number(), m_monAddr.get_host_addr()); + return -1; + } + activate(); + return 0; +} + +int GdbServerThread::svc() +{ + sLog.outString("GdbServer Thread started (RSP on %s:%d%s)", + m_rspAddr.get_host_addr(), m_rspAddr.get_port_number(), + m_monEnabled ? ", monitor bridge enabled" : ""); + + while (!m_Reactor->reactor_event_loop_done()) + { + ACE_Time_Value interval(0, 10000); + if (m_Reactor->run_reactor_event_loop(interval) == -1) + { + break; + } + if (World::IsStopped()) + { + m_RspAcceptor->close(); + if (m_monEnabled) + { + m_MonAcceptor->close(); + } + break; + } + } + sLog.outString("GdbServer Thread stopped"); + return 0; +} +/// @} diff --git a/src/mangosd/GdbServerThread.h b/src/mangosd/GdbServerThread.h new file mode 100644 index 000000000..bc1b65bbb --- /dev/null +++ b/src/mangosd/GdbServerThread.h @@ -0,0 +1,75 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#ifndef MANGOS_H_GDBSERVERTHREAD +#define MANGOS_H_GDBSERVERTHREAD + +#include +#include +#include +#include + +#include "Common.h" + +class GdbRspSocket; +class GdbMonSocket; +class ACE_Reactor; + +typedef ACE_Acceptor < GdbRspSocket, ACE_SOCK_ACCEPTOR > GdbRspAcceptor; +typedef ACE_Acceptor < GdbMonSocket, ACE_SOCK_ACCEPTOR > GdbMonAcceptor; + +/** + * Listener thread for the GDB-server debug endpoint. Accepts a GDB RSP + * connection (for gdb / lldb / IDA) on one port and an optional plain-text + * monitor bridge (for AI / non-RSP debuggers) on another, both serviced by a + * single reactor. Network I/O only — all protocol/state work is handed to + * GdbServer on the world thread. Modeled on RAThread. + */ +class GdbServerThread : public ACE_Task_Base +{ + private: + ACE_Reactor* m_Reactor; + GdbRspAcceptor* m_RspAcceptor; + GdbMonAcceptor* m_MonAcceptor; + ACE_INET_Addr m_rspAddr; + ACE_INET_Addr m_monAddr; + bool m_monEnabled; + + public: + /// @param rspPort GDB RSP listen port. + /// @param monPort Plain-text monitor port (0 to disable). + /// @param host Bind address (default 127.0.0.1 in config). + GdbServerThread(uint16 rspPort, uint16 monPort, const char* host); + virtual ~GdbServerThread(); + + int open(void* unused) override; + int svc() override; +}; + +#endif +/// @} diff --git a/src/mangosd/mangosd.conf.dist.in b/src/mangosd/mangosd.conf.dist.in index c20263309..ee2fa71f3 100644 --- a/src/mangosd/mangosd.conf.dist.in +++ b/src/mangosd/mangosd.conf.dist.in @@ -1745,6 +1745,36 @@ Network.KickOnBadPacket = 0 # SOAP port # Default: 7878 # +# GdbServer.Enable +# Enable the GDB-server debug endpoint. Lets a debugger (gdb, lldb, +# IDA's remote GDB backend) or an AI agent attach to the running +# mangosd and drive it: read process memory, inspect live game state, +# and run server commands via the 'monitor mangos ...' surface. +# WARNING: anyone who can reach the port gets full memory and command +# access. Keep it bound to localhost. Disabled by default. +# 0 - off +# Default: 0 - off +# +# GdbServer.IP +# Bind address for the GDB-server. Do NOT expose this publicly. +# Default: 127.0.0.1 +# +# GdbServer.Port +# GDB Remote Serial Protocol (RSP) port. Connect with +# 'gdb -ex "target remote :"'. +# Default: 2345 +# +# GdbServer.MonitorPort +# Plain-text monitor bridge port for AI agents / non-RSP debuggers. +# Send 'mangos help' as a line and read the text reply. 0 to disable. +# Default: 2346 +# +# GdbServer.AllowMemWrite +# Allow the RSP 'M' (write memory) packet to modify live process +# memory. Dangerous; off by default. +# 0 - off +# Default: 0 - off +# ################################################################################ Console.Enable = 1 @@ -1760,6 +1790,12 @@ SOAP.Enabled = 0 SOAP.IP = 127.0.0.1 SOAP.Port = 7878 +GdbServer.Enable = 0 +GdbServer.IP = 127.0.0.1 +GdbServer.Port = 2345 +GdbServer.MonitorPort = 2346 +GdbServer.AllowMemWrite = 0 + ################################################################################ # CharDelete.Method # Character deletion behavior diff --git a/src/mangosd/mangosd.cpp b/src/mangosd/mangosd.cpp index 63a5ebd33..8a31640a9 100644 --- a/src/mangosd/mangosd.cpp +++ b/src/mangosd/mangosd.cpp @@ -70,6 +70,8 @@ #include "CliThread.h" #include "AFThread.h" #include "RAThread.h" +#include "GdbServerThread.h" +#include "Debug/GdbServer/GdbServer.h" #ifdef ENABLE_SOAP #include "SOAP/SoapThread.h" @@ -515,6 +517,10 @@ int main(int argc, char** argv) ///- Catch termination signals hook_signals(); + ///- Prepare the GDB-server debug subsystem (reads config, wires the RSP + /// sink, runs the monitor self-test) before any thread is started. + sGdbServer.Init(); + //************************************************************************************************************************ // 1. Start the World thread //************************************************************************************************************************ @@ -538,6 +544,22 @@ int main(int argc, char** argv) raThread->open(0); } + //************************************************************************************************************************ + // 2b. Start the GDB-server debug listener thread, if enabled + //************************************************************************************************************************ + GdbServerThread* gdbThread = NULL; + if (sConfig.GetBoolDefault("GdbServer.Enable", false)) + { + // Default bind is localhost: this endpoint exposes process memory and + // the full server command surface, so never bind it publicly by default. + uint16 gdbPort = sConfig.GetIntDefault("GdbServer.Port", 2345); + uint16 gdbMonPort = sConfig.GetIntDefault("GdbServer.MonitorPort", 2346); + std::string gdbHost = sConfig.GetStringDefault("GdbServer.IP", "127.0.0.1"); + + gdbThread = new GdbServerThread(gdbPort, gdbMonPort, gdbHost.c_str()); + gdbThread->open(0); + } + //************************************************************************************************************************ // 3. Start the SOAP listener thread, if enabled //************************************************************************************************************************ @@ -606,6 +628,11 @@ int main(int argc, char** argv) delete raThread; } + if (gdbThread) + { + delete gdbThread; + } + delete worldThread; ///- Remove signal handling before leaving From 5c0401c4f4616e21b845f2f9a577556d40d7b53d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 06:58:34 +0000 Subject: [PATCH 2/7] GdbServer: scrub external project lineage from comments and docs Reword header comments and documentation to describe the debug endpoint as a native mangosd feature built on the GDB Remote Serial Protocol, removing references to the upstream project and kernel-debugging framing. No behavioural change. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016L1LfL8h1fGgLYjsfvyhEo --- doc/GdbServer.md | 6 +++--- src/game/Debug/GdbServer/GdbMonitor.h | 8 ++++---- src/game/Debug/GdbServer/GdbRsp.cpp | 5 ++--- src/game/Debug/GdbServer/GdbRsp.h | 18 +++++++++--------- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/doc/GdbServer.md b/doc/GdbServer.md index e5d20b953..7f294d150 100644 --- a/doc/GdbServer.md +++ b/doc/GdbServer.md @@ -2,9 +2,9 @@ The GDB-server lets a debugger — or an AI agent — attach to a **running** `mangosd` process and drive it directly: read process memory, inspect live game -state, and run server/GM commands, all over a single network endpoint. It is a -port of the GDB-server system from the DuetOS project, adapted from a kernel -stub to this userland, ACE-threaded server. +state, and run server/GM commands, all over a single network endpoint. It is +built on the GDB Remote Serial Protocol and integrated into the mangosd world +loop and ACE networking. It has two layers: diff --git a/src/game/Debug/GdbServer/GdbMonitor.h b/src/game/Debug/GdbServer/GdbMonitor.h index f8fbb1868..fe94e8818 100644 --- a/src/game/Debug/GdbServer/GdbMonitor.h +++ b/src/game/Debug/GdbServer/GdbMonitor.h @@ -32,10 +32,10 @@ #include "Common.h" /* - * GDB `monitor` (qRcmd) command surface for mangosd, ported from the - * DuetOS `duet` monitor. The generic GDB remote-serial server (GdbRsp) - * speaks raw registers / memory; stock GDB cannot express mangos-specific - * state (online players, the world tick, config values, server commands). + * GDB `monitor` (qRcmd) command surface for mangosd. The RSP engine + * (GdbRsp) speaks raw registers / memory; stock GDB cannot express + * mangos-specific state (online players, the world tick, config values, + * server commands). * This TU implements a `mangos [args]` command surface delivered * over the standard GDB `qRcmd,` ("monitor") packet, so a specialised * client OR stock `gdb` (`monitor mangos ...`) — OR any AI over the plain diff --git a/src/game/Debug/GdbServer/GdbRsp.cpp b/src/game/Debug/GdbServer/GdbRsp.cpp index 053f6be78..c233503ad 100644 --- a/src/game/Debug/GdbServer/GdbRsp.cpp +++ b/src/game/Debug/GdbServer/GdbRsp.cpp @@ -130,9 +130,8 @@ namespace GdbRsp } // 24-register x86_64 target description matching the `g` reply - // exactly, so gdb's amd64-tdep validator accepts our register count - // instead of falling back to its 154-register default. Ported - // verbatim from the DuetOS stub. + // exactly, so gdb accepts our register count instead of falling back + // to its larger built-in default and rejecting the short `g` reply. const char* TargetXml() { static const char kTargetXml[] = diff --git a/src/game/Debug/GdbServer/GdbRsp.h b/src/game/Debug/GdbServer/GdbRsp.h index de5d988af..64e6cd22f 100644 --- a/src/game/Debug/GdbServer/GdbRsp.h +++ b/src/game/Debug/GdbServer/GdbRsp.h @@ -32,17 +32,17 @@ #include "Common.h" /* - * GDB Remote Serial Protocol (RSP) engine for mangosd, ported from the - * DuetOS kernel stub (kernel/diag/gdb_server.*). Transport-agnostic: bytes - * arrive via ReceiveByte() and replies leave through a byte-sink callback, - * so the same engine drives a TCP socket here that drove COM2 in DuetOS. + * GDB Remote Serial Protocol (RSP) engine for mangosd. Transport-agnostic: + * bytes arrive via ReceiveByte() and replies leave through a byte-sink + * callback, so the engine is fully decoupled from the TCP transport that + * carries it. * - * The native-debug realities of a userland process differ from a kernel: + * mangosd is debugged in-process, which shapes a few packets: * - Registers (g/G): Phase 1 reports a synthetic (zeroed) x86_64 set — - * enough for stock gdb's attach handshake; the value for AI/users is - * the `monitor` surface + memory reads, not register-level debugging. - * - Memory (m/M): the debugger is in-process, so reads/writes are guarded - * accesses to our own address space (see GdbDbg). Writes are gated. + * enough for stock gdb's attach handshake; the value for users and AI + * is the `monitor` surface + memory reads, not register-level debugging. + * - Memory (m/M): reads/writes are guarded accesses to the server's own + * address space (see GdbDbg). Writes are gated behind config. * - Breakpoints (Z/z) and real single-step are Phase 2. * * SINGLE SESSION: one attached debugger at a time. A new connection calls From d07a3f6845d7c76003f532d844ec50f0c7c526a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 07:21:43 +0000 Subject: [PATCH 3/7] GdbServer Phase 2: live registers, game breakpoints, backtrace Build on the Phase 1 endpoint with the in-process debugging features that let a debugger or AI agent genuinely drive the server: - Live register capture: at any stop the world thread's real registers are captured (Linux getcontext, Windows RtlCaptureContext on x86_64) and returned by the 'g' packet, so gdb can backtrace the actual call stack through the 'm' memory packets. - Game-level breakpoints: arm pauses on a received opcode, on player map-entry, or on a named label. When one fires and a debugger is attached, the world thread stops inline at the call site, captures context, and waits for resume. Driven over the monitor surface (mangos break ...). Hot-path cost is a single relaxed atomic load when nothing is armed; an armed-but-unattended breakpoint never stalls the server. - monitor mangos dump: world-thread backtrace (Linux symbols; addresses elsewhere). Wired demonstration call sites: opcode dispatch (WorldSession::Update) and player map-entry (Player::AddToWorld), each behind a GDB_BREAK_* macro. Native instruction breakpoints / hardware single-step are intentionally not implemented: patching int3 or self-setting debug registers in a live multi-threaded server is unsafe; the cooperative game-level breakpoints are the supported equivalent. See doc/GdbServer.md. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016L1LfL8h1fGgLYjsfvyhEo --- doc/GdbServer.md | 59 +++++-- src/game/Debug/GdbServer/GdbBreakpoints.cpp | 157 +++++++++++++++++++ src/game/Debug/GdbServer/GdbBreakpoints.h | 100 ++++++++++++ src/game/Debug/GdbServer/GdbMonitor.cpp | 67 +++++++- src/game/Debug/GdbServer/GdbMonitor.h | 1 + src/game/Debug/GdbServer/GdbMonitorVerbs.cpp | 51 ++++++ src/game/Debug/GdbServer/GdbRsp.cpp | 51 +++++- src/game/Debug/GdbServer/GdbRsp.h | 19 +++ src/game/Debug/GdbServer/GdbServer.cpp | 75 ++++++++- src/game/Debug/GdbServer/GdbServer.h | 18 ++- src/game/Object/Player.cpp | 4 + src/game/Server/WorldSession.cpp | 5 + 12 files changed, 575 insertions(+), 32 deletions(-) create mode 100644 src/game/Debug/GdbServer/GdbBreakpoints.cpp create mode 100644 src/game/Debug/GdbServer/GdbBreakpoints.h diff --git a/doc/GdbServer.md b/doc/GdbServer.md index 7f294d150..1834548a0 100644 --- a/doc/GdbServer.md +++ b/doc/GdbServer.md @@ -92,30 +92,57 @@ Each line is a `mangos ` command; the text reply comes straight back. | `mangos session ` | Session detail for an account id | | `mangos config ` | Read a `mangosd.conf` value | | `mangos cmd ` | Run any server/GM command (e.g. `.server info`, `.account onlinelist`) | +| `mangos break ...` | Arm/list/clear game-level breakpoints (see below) | +| `mangos dump` | Backtrace of the world thread | The `cmd` verb bridges to the existing `ChatCommand` system at `SEC_CONSOLE` level, so the entire console/GM command surface is drivable over the debug channel. -## What works today (Phase 1) and what is next (Phase 2) +## Game-level breakpoints -**Phase 1 (implemented):** RSP transport over TCP; `qSupported`, `?`, `g`/`G` -(synthetic registers), `m`/`M` (guarded process-memory read/write), `H`, -`c`/`s`/`D`/`k`, `vCont`, `qRcmd` monitor; the `mangos` semantic verbs incl. the -`cmd` bridge; cooperative world-tick stop on Ctrl-C; the plain-text bridge. +Beyond the native protocol, the server can pause itself at semantically +meaningful points and hand control to the attached debugger. When a breakpoint +fires (and a debugger is attached), the world thread stops **inline at the call +site**, captures the live registers, and waits — so `bt` in gdb shows the real +call stack and `monitor mangos ...` reads quiescent game state. `continue` +resumes the tick. -**Phase 1 limitations:** -- Registers (`g`/`G`) are a synthetic zeroed x86_64 set — enough for the attach - handshake; the value is in `monitor` + memory, not register-level debugging. -- Breakpoints (`Z`/`z`) and real single-step are not implemented (gdb falls - back to its own software breakpoints, which will not function without the - native breakpoint support below). +``` +(gdb) monitor mangos break opcode 0x12E # pause when this opcode arrives +(gdb) monitor mangos break map 0 # pause when a player enters map 0 +(gdb) monitor mangos break list +(gdb) monitor mangos break del opcode:0x12E +(gdb) monitor mangos break clear +``` + +Breakpoints only fire while a debugger is attached, so an armed-but-unattended +breakpoint never stalls the server. Current call sites: received-opcode dispatch +and player map-entry; more sites are added with a single `GDB_BREAK_*` macro. + +## Capabilities and limits + +**Implemented:** +- RSP transport over TCP; `qSupported`, `?`, `g`/`G`, `m`/`M` (guarded + process-memory read/write), `H`, `c`/`s`/`D`/`k`, `vCont`, `qRcmd` monitor. +- The `mangos` semantic verbs including the `cmd` bridge; the plain-text bridge. +- Cooperative world-tick stop on Ctrl-C. +- **Live register capture** (`g`): at a stop the world thread's real registers + are captured (Linux `getcontext`, Windows `RtlCaptureContext` on x86_64), so + gdb can `bt` through the actual call stack via the `m` memory packets. +- **Game-level breakpoints** on opcodes, map-entry, and named labels. +- `mangos dump` backtrace. + +**Limits:** +- `g`/`G` cover the x86_64 integer register file; FPU/SSE are reported as zero, + and non-x86_64 builds report zeroed registers (memory + monitor still work). +- Native instruction breakpoints (`Z`/`z`) and hardware single-step are not + implemented — by design. Driving `int3` patching or self-set debug registers + in a live, multi-threaded server is unsafe; the game-level breakpoints above + are the supported, cooperative equivalent. - One debugger connection at a time. - State seen during a stop is *world-tick-consistent*, not globally race-free: the network, database and map-update threads keep running. -**Phase 2 (planned):** real native registers and hardware/software breakpoints -via OS facilities (Linux ptrace/DR registers, Windows `DebugActiveProcess`/ -`SetThreadContext`); game-level breakpoints ("break on opcode X", "break when a -player enters map Y"); and crash-dump integration (tying into the Windows -Wheaty report and a Linux backtrace). +**Possible future work:** symbol-resolved backtraces on Windows (via the +existing crash-report tooling) and additional `GDB_BREAK_*` call sites. diff --git a/src/game/Debug/GdbServer/GdbBreakpoints.cpp b/src/game/Debug/GdbServer/GdbBreakpoints.cpp new file mode 100644 index 000000000..4dc9c9e34 --- /dev/null +++ b/src/game/Debug/GdbServer/GdbBreakpoints.cpp @@ -0,0 +1,157 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#include "GdbBreakpoints.h" + +#include "GdbMonitor.h" +#include "GdbServer.h" + +#include +#include +#include +#include + +namespace GdbBp +{ + std::atomic g_armedCount{0}; + + namespace + { + std::set g_opcodes; + std::set g_maps; + std::set g_labels; + uint64 g_hits = 0; + + void Recount() + { + g_armedCount.store(static_cast(g_opcodes.size() + g_maps.size() + g_labels.size()), + std::memory_order_relaxed); + } + } // namespace + + bool OpcodeArmed(uint32 opcode) { return g_opcodes.find(opcode) != g_opcodes.end(); } + bool MapEnterArmed(uint32 mapId) { return g_maps.find(mapId) != g_maps.end(); } + bool LabelArmed(const char* name) { return name != nullptr && g_labels.find(name) != g_labels.end(); } + + bool ArmOpcode(uint32 opcode) + { + const bool inserted = g_opcodes.insert(opcode).second; + Recount(); + return inserted; + } + + bool ArmMapEnter(uint32 mapId) + { + const bool inserted = g_maps.insert(mapId).second; + Recount(); + return inserted; + } + + bool ArmLabel(const char* name) + { + if (name == nullptr || name[0] == '\0') + { + return false; + } + const bool inserted = g_labels.insert(name).second; + Recount(); + return inserted; + } + + bool Disarm(const char* spec) + { + if (spec == nullptr) + { + return false; + } + bool removed = false; + const std::string s(spec); + if (s.rfind("opcode:", 0) == 0) + { + removed = g_opcodes.erase(static_cast(strtoul(s.c_str() + 7, nullptr, 0))) != 0; + } + else if (s.rfind("map:", 0) == 0) + { + removed = g_maps.erase(static_cast(strtoul(s.c_str() + 4, nullptr, 0))) != 0; + } + else + { + removed = g_labels.erase(s) != 0; + } + Recount(); + return removed; + } + + void DisarmAll() + { + g_opcodes.clear(); + g_maps.clear(); + g_labels.clear(); + Recount(); + } + + void List(GdbMon::MonitorWriter& out) + { + if (g_armedCount.load(std::memory_order_relaxed) == 0) + { + out.Str(" (no breakpoints armed)\n"); + } + for (uint32 op : g_opcodes) + { + out.Str(" opcode:"); + out.U64(op); + out.Line(); + } + for (uint32 m : g_maps) + { + out.Str(" map:"); + out.U64(m); + out.Line(); + } + for (const std::string& l : g_labels) + { + out.Str(" label:"); + out.Str(l.c_str()); + out.Line(); + } + out.Str(" hits="); + out.U64(g_hits); + out.Line(); + } + + void Hit(const char* kind, uint64 detail) + { + ++g_hits; + char reason[80]; + snprintf(reason, sizeof(reason), "breakpoint %s %llu", kind, static_cast(detail)); + // No-op unless a debugger is attached (EnterBreak guards that), so an + // armed-but-unattended breakpoint never hangs the server. + sGdbServer.EnterBreak(reason); + } +} // namespace GdbBp +/// @} diff --git a/src/game/Debug/GdbServer/GdbBreakpoints.h b/src/game/Debug/GdbServer/GdbBreakpoints.h new file mode 100644 index 000000000..04037eebd --- /dev/null +++ b/src/game/Debug/GdbServer/GdbBreakpoints.h @@ -0,0 +1,100 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#ifndef MANGOS_H_GDB_BREAKPOINTS +#define MANGOS_H_GDB_BREAKPOINTS + +#include "Common.h" + +#include + +namespace GdbMon { class MonitorWriter; } + +/* + * Game-level breakpoints for the GDB-server debug endpoint. + * + * Unlike native instruction breakpoints, these fire at semantically + * meaningful points in the server (a received opcode, a player entering a + * map, a named code label). When one fires AND a debugger is attached, the + * world thread enters the cooperative stop inline at the call site — so the + * attached debugger sees the real, live call stack and can inspect game + * state via the `monitor` surface, then resume. + * + * The hot-path guard AnyArmed() is a single relaxed atomic load, so call + * sites cost effectively nothing when no breakpoint is armed. All arming and + * checking happens on the world thread (monitor dispatch + game code), so the + * registry needs no locking. + */ +namespace GdbBp +{ + /// Number of armed breakpoints; the hot-path gate. Defined in the .cpp. + extern std::atomic g_armedCount; + + /// Cheap gate for call sites — true when any breakpoint is armed. + inline bool AnyArmed() { return g_armedCount.load(std::memory_order_relaxed) != 0; } + + // Match tests (world thread). + bool OpcodeArmed(uint32 opcode); + bool MapEnterArmed(uint32 mapId); + bool LabelArmed(const char* name); + + // Arming / management (world thread, from the monitor surface). + bool ArmOpcode(uint32 opcode); + bool ArmMapEnter(uint32 mapId); + bool ArmLabel(const char* name); + bool Disarm(const char* spec); ///< "opcode:" | "map:" | "