diff --git a/doc/GdbServer.md b/doc/GdbServer.md new file mode 100644 index 000000000..7aef426eb --- /dev/null +++ b/doc/GdbServer.md @@ -0,0 +1,180 @@ +# 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 +built on the GDB Remote Serial Protocol and integrated into the mangosd world +loop and ACE networking. + +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`) | +| `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. + +## Game-level breakpoints + +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. + +A breakpoint is an **event** plus an optional numeric **filter** (`0`/omitted = +"any"). Arm, list, and clear them over the monitor surface: + +``` +(gdb) monitor mangos break events # list all event names +(gdb) monitor mangos break spellcast 133 # pause on cast of spell 133 +(gdb) monitor mangos break opcode 0x12E # pause on a given opcode +(gdb) monitor mangos break death 0 # pause on any unit death +(gdb) monitor mangos break worldtick # stop every tick (single-step the world) +(gdb) monitor mangos break list +(gdb) monitor mangos break del spellcast 133 +(gdb) monitor mangos break clear +``` + +`mangos break events` prints the authoritative, up-to-date list. Events span +every major subsystem (all wired to real call sites): + +| Group | Events (filter) | +|-------|-----------------| +| Core gameplay | `opcode` (opcode), `login`/`logout` (account), `mapenter`/`mapleave` (map), `spellcast`/`spellprepare` (spell), `death` (entry), `damage` (victim entry), `levelup` (level), `loot` (type), `questaccept`/`questcomplete`/`questreward` (quest), `chat`, `itemuse`, `gossip` (id), `creaturecreate` (entry), `gobjectuse` (entry), `gmcmd`, `worldtick` | +| Netcode / auth | `netaccept`, `netclose`, `authsession`, `packetsend` (opcode), `packetrecv` (opcode) | +| Database (shared layer) | `dbquery`, `dbexecute`, `dbasyncquery` | +| Warden anti-cheat | `wardencheck`, `wardenviolation` | +| Scripting | `scriptai` (entry), `eluna`, `sd3` | +| Creature AI | `aicombat`/`aicombatend` (entry), `aiupdate` (entry), `aispawn` (entry) | +| Maps / instances | `mapcreate` (map), `gridload` (map), `instancecreate` (map), `instancereset` (map) | +| Economy / social | `mailsend`, `mailrecv`, `auctionadd`, `auctionbuy`, `trade`, `groupjoin` | +| BG / pet / item / pvp / move | `bgstart`/`bgend` (type), `petsummon` (entry), `itemequip`/`itemdestroy` (entry), `pvpkill`, `movementinform` | + +Note: `opcode` with a filter already covers *every* client packet, and +`dbquery`/`dbexecute` cover *every* SQL statement, so those single events span +their whole subsystem without one breakpoint per id. + +Breakpoints only fire while a debugger is attached, so an armed-but-unattended +breakpoint never stalls the server; stops never nest. The hot-path guard is a +single relaxed atomic load, so unarmed call sites are effectively free. The +database hooks live in the shared library and reach the engine through a thin +registered bridge (`shared/Debug/DebugBreakHook`). New game sites are added +with a one-line `GDB_BREAK(, )`; new shared-layer sites with +`GDB_BREAK_SHARED(, )`. + +## 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** across ~55 event families spanning every major + subsystem — core gameplay, netcode/auth, database, warden, scripting, + creature AI, maps/instances, economy/social, and battleground/pet/item/pvp — + each with an optional numeric filter. `mangos break events` lists them. +- `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. + +**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/BattleGround/BattleGround.cpp b/src/game/BattleGround/BattleGround.cpp index 4f7a27166..a70a01fab 100644 --- a/src/game/BattleGround/BattleGround.cpp +++ b/src/game/BattleGround/BattleGround.cpp @@ -53,6 +53,7 @@ #include "Formulas.h" #include "GridNotifiersImpl.h" #include "Chat.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #ifdef ENABLE_ELUNA #include "LuaEngine.h" #endif /* ENABLE_ELUNA */ @@ -489,6 +490,8 @@ void BattleGround::Update(uint32 diff) StartingEventOpenDoors(); SendMessageToAll(m_StartMessageIds[BG_STARTING_EVENT_FOURTH], CHAT_MSG_BG_SYSTEM_NEUTRAL); + // GDB-server game breakpoint + GDB_BREAK(BgStart, GetTypeID()); SetStatus(STATUS_IN_PROGRESS); SetStartDelayTime(m_StartDelayTimes[BG_STARTING_EVENT_FOURTH]); @@ -703,17 +706,6 @@ void BattleGround::CastSpellOnTeam(uint32 SpellID, Team teamId) } - - - - - - - - - - - /** * @brief Blocks the movement of the player. * @@ -1115,15 +1107,6 @@ void BattleGround::UpdatePlayerScore(Player* Source, uint32 type, uint32 value) } - - - - - - - - - /** * @brief Sends a message to all players in the battleground. * diff --git a/src/game/BattleGround/BattleGroundReward.cpp b/src/game/BattleGround/BattleGroundReward.cpp index d30a61d14..f3dce09b2 100644 --- a/src/game/BattleGround/BattleGroundReward.cpp +++ b/src/game/BattleGround/BattleGroundReward.cpp @@ -39,6 +39,7 @@ #include "BattleGround.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "Object.h" #include "Player.h" #include "BattleGroundMgr.h" @@ -174,6 +175,8 @@ void BattleGround::UpdateWorldStateForPlayer(uint32 Field, uint32 Value, Player* */ void BattleGround::EndBattleGround(Team winner) { + // GDB-server game breakpoint + GDB_BREAK(BgEnd, GetTypeID()); #ifdef ENABLE_ELUNA if (Eluna* e = GetBgMap()->GetEluna()) { diff --git a/src/game/CMakeLists.txt b/src/game/CMakeLists.txt index 88147d963..9de374aa6 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/GdbBreakpoints.cpp b/src/game/Debug/GdbServer/GdbBreakpoints.cpp new file mode 100644 index 000000000..3659f820e --- /dev/null +++ b/src/game/Debug/GdbServer/GdbBreakpoints.cpp @@ -0,0 +1,229 @@ +/** + * 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 "Debug/DebugBreakHook.h" +#include "GdbMonitor.h" +#include "GdbServer.h" + +#include +#include +#include +#include + +namespace GdbBp +{ + std::atomic g_armedMask{0}; + + namespace + { + // One armed breakpoint: an event and an optional filter (0 = any). + struct Entry + { + Event ev; + uint64 filter; + }; + + std::vector g_entries; + uint64 g_hits = 0; + + // Canonical lower-case name per Event, indexed by enum value. Keep in + // lockstep with the GdbEvent enum in shared/Debug/GdbEvents.h. + const char* const kEventNames[] = { + "opcode", "login", "logout", "mapenter", "mapleave", + "spellcast", "spellprepare", "death", "damage", "levelup", + "loot", "questaccept", "questcomplete", "questreward", + "chat", "itemuse", "gossip", "creaturecreate", + "gobjectuse", "gmcmd", "worldtick", + "netaccept", "netclose", "authsession", "packetrecv", "packetsend", + "dbquery", "dbexecute", "dbasyncquery", + "wardencheck", "wardenviolation", + "scriptai", "eluna", "sd3", + "aicombat", "aicombatend", "aiupdate", "aispawn", + "mapcreate", "gridload", "instancecreate", "instancereset", + "mailsend", "mailrecv", "auctionadd", "auctionbuy", "trade", + "bgstart", "bgend", "petsummon", "itemequip", "itemdestroy", + "groupjoin", "pvpkill", "movementinform", + }; + static_assert(sizeof(kEventNames) / sizeof(kEventNames[0]) == + static_cast(Event::Count), + "kEventNames must match the Event enum"); + + void Recount() + { + uint64 mask = 0; + for (const Entry& e : g_entries) + { + mask |= (1ULL << static_cast(e.ev)); + } + g_armedMask.store(mask, std::memory_order_relaxed); + } + + bool StrEq(const char* a, const char* b) + { + return a != nullptr && b != nullptr && std::strcmp(a, b) == 0; + } + } // namespace + + bool Matches(Event e, uint64 detail) + { + for (const Entry& entry : g_entries) + { + if (entry.ev == e && (entry.filter == 0 || entry.filter == detail)) + { + return true; + } + } + return false; + } + + bool Arm(Event e, uint64 filter) + { + for (const Entry& entry : g_entries) + { + if (entry.ev == e && entry.filter == filter) + { + return false; // already armed + } + } + g_entries.push_back(Entry{e, filter}); + Recount(); + return true; + } + + bool Disarm(Event e, uint64 filter) + { + for (size_t i = 0; i < g_entries.size(); ++i) + { + if (g_entries[i].ev == e && g_entries[i].filter == filter) + { + g_entries.erase(g_entries.begin() + i); + Recount(); + return true; + } + } + return false; + } + + void DisarmAll() + { + g_entries.clear(); + Recount(); + } + + void List(GdbMon::MonitorWriter& out) + { + if (g_entries.empty()) + { + out.Str(" (no breakpoints armed)\n"); + } + for (const Entry& e : g_entries) + { + out.Str(" "); + out.Str(EventName(e.ev)); + if (e.filter != 0) + { + out.Str(" filter="); + out.U64(e.filter); + } + else + { + out.Str(" (any)"); + } + out.Line(); + } + out.Str(" hits="); + out.U64(g_hits); + out.Line(); + } + + bool ParseEvent(const char* name, Event& out) + { + for (uint32 i = 0; i < static_cast(Event::Count); ++i) + { + if (StrEq(name, kEventNames[i])) + { + out = static_cast(i); + return true; + } + } + return false; + } + + const char* EventName(Event e) + { + const uint32 i = static_cast(e); + return (i < static_cast(Event::Count)) ? kEventNames[i] : "?"; + } + + void ListEventNames(GdbMon::MonitorWriter& out) + { + for (uint32 i = 0; i < static_cast(Event::Count); ++i) + { + out.Str(i == 0 ? " " : " "); + out.Str(kEventNames[i]); + } + out.Line(); + } + + void Hit(Event e, uint64 detail) + { + ++g_hits; + char reason[96]; + snprintf(reason, sizeof(reason), "breakpoint %s %llu", EventName(e), + 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 + { + // Bridge entry points handed to the shared-layer shim (DbgBreak). + std::uint64_t CurrentMask() + { + return static_cast(g_armedMask.load(std::memory_order_relaxed)); + } + + void SharedHit(std::uint32_t ev, std::uint64_t detail) + { + const Event e = static_cast(ev); + if (Matches(e, detail)) + { + Hit(e, detail); + } + } + } // namespace + + void Init() + { + DbgBreak::Register(&CurrentMask, &SharedHit); + } +} // namespace GdbBp +/// @} diff --git a/src/game/Debug/GdbServer/GdbBreakpoints.h b/src/game/Debug/GdbServer/GdbBreakpoints.h new file mode 100644 index 000000000..3f737db91 --- /dev/null +++ b/src/game/Debug/GdbServer/GdbBreakpoints.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_BREAKPOINTS +#define MANGOS_H_GDB_BREAKPOINTS + +#include "Common.h" +#include "Debug/GdbEvents.h" + +#include +#include + +namespace GdbMon { class MonitorWriter; } + +/* + * Game-level breakpoints for the GDB-server debug endpoint. + * + * Unlike native instruction breakpoints, these fire at semantically + * meaningful points across the server (a received opcode, a spell cast, a + * death, a quest step, a player entering a map, ...). Each breakpoint is an + * (event, filter) pair: the filter is an optional numeric (spell id, map id, + * quest id, ...); filter 0 means "any". When an armed breakpoint matches AND + * a debugger is attached, the world thread enters the cooperative stop inline + * at the call site, so the attached debugger sees the real call stack and can + * inspect game state via the `monitor` surface, then resume. + * + * The hot-path guard is a single relaxed atomic load of a per-event bitmask, + * so call sites cost effectively nothing when their event is not armed. All + * arming and matching happens on the world thread (monitor dispatch + game + * code), so the registry needs no locking. + */ +namespace GdbBp +{ + /// The event catalogue lives in the shared layer (Debug/GdbEvents.h) so + /// shared subsystems and game code share one set of ids. + using Event = ::GdbEvent; + + /// Per-event armed bitmask; the hot-path gate. Defined in the .cpp. + extern std::atomic g_armedMask; + + /// Register the shared-layer break bridge (DbgBreak) with this engine so + /// shared subsystems (e.g. the database) can raise breakpoints. Called + /// once at startup. + void Init(); + + /// Cheap gate for call sites — true when @p e has any breakpoint armed. + inline bool Armed(Event e) + { + return (g_armedMask.load(std::memory_order_relaxed) >> static_cast(e)) & 1ULL; + } + + /// True when an armed breakpoint for @p e matches @p detail (filter 0 or + /// filter == detail). Only called after Armed(e) passes. + bool Matches(Event e, uint64 detail); + + // Management (world thread, from the monitor surface). + bool Arm(Event e, uint64 filter); ///< filter 0 = any + bool Disarm(Event e, uint64 filter); + void DisarmAll(); + void List(GdbMon::MonitorWriter& out); + + // Name <-> event mapping for the monitor command surface. + bool ParseEvent(const char* name, Event& out); + const char* EventName(Event e); + void ListEventNames(GdbMon::MonitorWriter& out); + + /// A breakpoint matched: record the hit and, if a debugger is attached, + /// enter the cooperative stop. + void Hit(Event e, uint64 detail); +} + +/// Call-site macro — negligible cost when the event is not armed. +#define GDB_BREAK(ev, detail) \ + do { \ + if (GdbBp::Armed(GdbBp::Event::ev) && \ + GdbBp::Matches(GdbBp::Event::ev, static_cast(detail))) \ + GdbBp::Hit(GdbBp::Event::ev, static_cast(detail)); \ + } while (0) + +#endif +/// @} 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..1d0d95900 --- /dev/null +++ b/src/game/Debug/GdbServer/GdbMonitor.cpp @@ -0,0 +1,496 @@ +/** + * 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 "GdbBreakpoints.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" + " mangos break list|events|clear| [filter]|del [filter]\n" + " game breakpoints; 'events' lists event names\n" + " mangos dump write a backtrace of the world thread\n"); + } + + // mangos break list | events | clear + // | [filter] (arm) + // | del [filter] (disarm) + void CmdBreak(uint32 argc, const char** argv, MonitorWriter& out) + { + if (argc < 3 || Eq(argv[2], "list")) + { + GdbBp::List(out); + return; + } + if (Eq(argv[2], "events")) + { + out.Str("events:\n"); + GdbBp::ListEventNames(out); + return; + } + if (Eq(argv[2], "clear")) + { + GdbBp::DisarmAll(); + out.Str("all breakpoints disarmed\n"); + return; + } + + const bool del = Eq(argv[2], "del"); + const uint32 evIdx = del ? 3 : 2; + if (argc <= evIdx) + { + out.Str("usage: mangos break [filter] | del [filter]\n"); + return; + } + + GdbBp::Event ev; + if (!GdbBp::ParseEvent(argv[evIdx], ev)) + { + out.Str("break: unknown event '"); + out.Str(argv[evIdx]); + out.Str("' - try 'mangos break events'\n"); + return; + } + + uint64 filter = 0; + if (argc > evIdx + 1 && !ParseU64(argv[evIdx + 1], &filter)) + { + out.Str("break: bad filter value\n"); + return; + } + + if (del) + { + out.Str(GdbBp::Disarm(ev, filter) ? "breakpoint removed\n" : "breakpoint not found\n"); + } + else + { + out.Str(GdbBp::Arm(ev, filter) ? "breakpoint armed\n" : "breakpoint already armed\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 if (Eq(sub, "break")) + { + CmdBreak(argc, argv, out); + } + else if (Eq(sub, "dump")) + { + verbs::CmdDump(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..57f3ca5ef --- /dev/null +++ b/src/game/Debug/GdbServer/GdbMonitor.h @@ -0,0 +1,107 @@ +/** + * 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. 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 + * 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); + void CmdDump(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..bdc2f363e --- /dev/null +++ b/src/game/Debug/GdbServer/GdbMonitorVerbs.cpp @@ -0,0 +1,217 @@ +/** + * 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" + +#include +#if defined(_WIN32) +# include +#elif defined(__linux__) +# include +#endif + +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); + } + + void CmdDump(MonitorWriter& out) + { + out.Str("world-thread backtrace:\n"); +#if defined(__linux__) + void* frames[64]; + const int n = backtrace(frames, 64); + char** syms = backtrace_symbols(frames, n); + for (int i = 0; i < n; ++i) + { + out.Str(" #"); + out.U64(static_cast(i)); + out.Str(" "); + if (syms != nullptr && syms[i] != nullptr) + { + out.Str(syms[i]); + } + else + { + out.Str("0x"); + out.Hex(reinterpret_cast(frames[i])); + } + out.Line(); + } + if (syms != nullptr) + { + free(syms); + } +#elif defined(_WIN32) + void* frames[64]; + const USHORT n = CaptureStackBackTrace(0, 64, frames, nullptr); + for (USHORT i = 0; i < n; ++i) + { + out.Str(" #"); + out.U64(i); + out.Str(" 0x"); + out.Hex(reinterpret_cast(frames[i])); + out.Line(); + } + out.Str(" (symbol resolution: see the crash report written on fault)\n"); +#else + out.Str(" (backtrace not supported on this platform)\n"); +#endif + } + } // 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..b7e8687a3 --- /dev/null +++ b/src/game/Debug/GdbServer/GdbRsp.cpp @@ -0,0 +1,606 @@ +/** + * 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; + const RegSnapshot* g_regs = nullptr; + + 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 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[] = + "l" + "" + "" + "i386:x86-64" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + return kTargetXml; + } + + // Reply to a `g` (read registers) packet: a 24-reg x86_64 block + // (16 GPR + rip + eflags + 6 segments) in the canonical little-endian + // hex order our target.xml declares. Uses the published snapshot when + // available (so gdb can unwind the live stack), zeros otherwise. + 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]; + uint32 off = 0; + + auto put_u64 = [&](uint64 v) + { + for (uint32 i = 0; i < 8; ++i) + { + const uint8 b = static_cast(v >> (i * 8)); + buf[off++] = HexDigitChar((b >> 4) & 0xF); + buf[off++] = HexDigitChar(b & 0xF); + } + }; + auto put_u32 = [&](uint32 v) + { + for (uint32 i = 0; i < 4; ++i) + { + const uint8 b = static_cast(v >> (i * 8)); + buf[off++] = HexDigitChar((b >> 4) & 0xF); + buf[off++] = HexDigitChar(b & 0xF); + } + }; + + const RegSnapshot z{}; + const RegSnapshot& r = (g_regs != nullptr) ? *g_regs : z; + put_u64(r.rax); put_u64(r.rbx); put_u64(r.rcx); put_u64(r.rdx); + put_u64(r.rsi); put_u64(r.rdi); put_u64(r.rbp); put_u64(r.rsp); + put_u64(r.r8); put_u64(r.r9); put_u64(r.r10); put_u64(r.r11); + put_u64(r.r12); put_u64(r.r13); put_u64(r.r14); put_u64(r.r15); + put_u64(r.rip); + put_u32(static_cast(r.rflags & 0xFFFFFFFFu)); + put_u32(r.cs); put_u32(r.ss); put_u32(r.ds); + put_u32(r.es); put_u32(r.fs); put_u32(r.gs); + + buf[off] = '\0'; + SendReply(buf, off); + } + + // 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 PublishRegisters(const RegSnapshot* snap) + { + g_regs = snap; + } + + 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..9a464ef46 --- /dev/null +++ b/src/game/Debug/GdbServer/GdbRsp.h @@ -0,0 +1,114 @@ +/** + * 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. 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. + * + * 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 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 + * ResetSession() which re-initialises the parser + resume state. + */ +namespace GdbRsp +{ + /// Snapshot of the x86_64 integer register file in GDB's canonical + /// order, as returned by the `g` packet. When a real snapshot is + /// published (e.g. captured at a breakpoint or stop point), gdb can + /// unwind the live stack — it reads stack memory through the `m` packets. + struct RegSnapshot + { + uint64 rax, rbx, rcx, rdx; + uint64 rsi, rdi, rbp, rsp; + uint64 r8, r9, r10, r11; + uint64 r12, r13, r14, r15; + uint64 rip, rflags; + uint32 cs, ss, ds, es, fs, gs; + }; + + /// Publish the register snapshot the next `g` packet should report. + /// Pass nullptr to revert to a zeroed reply. The caller owns the storage + /// and must keep it alive until a different snapshot is published. + void PublishRegisters(const RegSnapshot* snap); + + /// 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..db216a224 --- /dev/null +++ b/src/game/Debug/GdbServer/GdbServer.cpp @@ -0,0 +1,346 @@ +/** + * 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 "GdbBreakpoints.h" +#include "GdbMonitor.h" +#include "GdbRsp.h" +#include "Log.h" +#include "World.h" + +#include +#include + +#if defined(_WIN32) +# include +#elif defined(__x86_64__) +# include +#endif + +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); + + // Wire the shared-layer break bridge so shared subsystems (database) can + // raise game-level breakpoints through this engine. + GdbBp::Init(); + + // 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::CaptureContext(GdbRsp::RegSnapshot& out) +{ + out = GdbRsp::RegSnapshot{}; +#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__)) + CONTEXT ctx; + RtlCaptureContext(&ctx); + out.rax = ctx.Rax; out.rbx = ctx.Rbx; out.rcx = ctx.Rcx; out.rdx = ctx.Rdx; + out.rsi = ctx.Rsi; out.rdi = ctx.Rdi; out.rbp = ctx.Rbp; out.rsp = ctx.Rsp; + out.r8 = ctx.R8; out.r9 = ctx.R9; out.r10 = ctx.R10; out.r11 = ctx.R11; + out.r12 = ctx.R12; out.r13 = ctx.R13; out.r14 = ctx.R14; out.r15 = ctx.R15; + out.rip = ctx.Rip; out.rflags = ctx.EFlags; + out.cs = ctx.SegCs; out.ss = ctx.SegSs; out.ds = ctx.SegDs; + out.es = ctx.SegEs; out.fs = ctx.SegFs; out.gs = ctx.SegGs; +#elif defined(__x86_64__) + ucontext_t uc; + if (getcontext(&uc) != 0) + { + return; + } + const greg_t* g = uc.uc_mcontext.gregs; + out.rax = g[REG_RAX]; out.rbx = g[REG_RBX]; out.rcx = g[REG_RCX]; out.rdx = g[REG_RDX]; + out.rsi = g[REG_RSI]; out.rdi = g[REG_RDI]; out.rbp = g[REG_RBP]; out.rsp = g[REG_RSP]; + out.r8 = g[REG_R8]; out.r9 = g[REG_R9]; out.r10 = g[REG_R10]; out.r11 = g[REG_R11]; + out.r12 = g[REG_R12]; out.r13 = g[REG_R13]; out.r14 = g[REG_R14]; out.r15 = g[REG_R15]; + out.rip = g[REG_RIP]; out.rflags = static_cast(g[REG_EFL]); + // CSGSFS packs cs (bits 0-15), gs (16-31), fs (32-47). + const uint64 csgsfs = static_cast(g[REG_CSGSFS]); + out.cs = static_cast(csgsfs & 0xFFFF); + out.gs = static_cast((csgsfs >> 16) & 0xFFFF); + out.fs = static_cast((csgsfs >> 32) & 0xFFFF); +#else + // non-x86_64: registers stay zeroed (memory + monitor still work) +#endif +} + +void GdbServer::EnterStop(const char* reason) +{ + // Pause the world tick: this function runs inside the world thread (either + // from OnWorldUpdate or inline at a breakpoint), 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 (%s)", reason); + + m_inStop = true; + + // Capture the live register context so the debugger can backtrace the real + // call stack (it reads stack memory through the 'm' packets). On + // architectures without a capture path the snapshot is left zeroed; we + // publish it either way so the `g` reply is always well-formed. + CaptureContext(m_capturedRegs); + GdbRsp::PublishRegisters(&m_capturedRegs); + + // 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; + } + } + + GdbRsp::PublishRegisters(nullptr); + m_inStop = false; + sLog.outString("GdbServer: target resumed"); +} + +void GdbServer::EnterBreak(const char* reason) +{ + // Only break when a debugger is attached — otherwise no one could resume + // the server and the world would hang. Never nest stops (a breakpoint may + // fire from a command executed while already stopped). + if (!m_enabled || m_inStop || !DebuggerAttached()) + { + return; + } + EnterStop(reason); +} + +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)) + { + EnterStop("debugger interrupt"); + } +} +/// @} diff --git a/src/game/Debug/GdbServer/GdbServer.h b/src/game/Debug/GdbServer/GdbServer.h new file mode 100644 index 000000000..35d235046 --- /dev/null +++ b/src/game/Debug/GdbServer/GdbServer.h @@ -0,0 +1,161 @@ +/** + * 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 "GdbRsp.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(); + + /// Enter the cooperative stop from a game-level breakpoint site (on + /// the world thread). No-op unless enabled AND a debugger is attached + /// — otherwise there would be no one to resume the server. Captures + /// the live register context so the debugger can backtrace the real + /// call stack, then pumps packets until the debugger resumes. + void EnterBreak(const char* reason); + + /// True when an RSP debugger is currently attached (used by the + /// breakpoint hot path to skip work when nobody can act on a stop). + bool DebuggerAttached() const { return m_rspWriter != nullptr && !m_peerClosed.load(); } + + 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 EnterStop(const char* reason); + void CaptureContext(GdbRsp::RegSnapshot& out); + + 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}; + // True while inside the stop loop; blocks a breakpoint that fires from + // a command run during the stop from nesting another stop. + bool m_inStop = 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; + + // Register context captured at the current stop (world thread only). + GdbRsp::RegSnapshot m_capturedRegs{}; +}; + +#define sGdbServer GdbServer::Instance() + +#endif +/// @} diff --git a/src/game/MotionGenerators/PointMovementGenerator.cpp b/src/game/MotionGenerators/PointMovementGenerator.cpp index 249457cbc..0e420e2d7 100644 --- a/src/game/MotionGenerators/PointMovementGenerator.cpp +++ b/src/game/MotionGenerators/PointMovementGenerator.cpp @@ -29,6 +29,7 @@ #include "World.h" #include "movement/MoveSplineInit.h" #include "movement/MoveSpline.h" +#include "Debug/GdbServer/GdbBreakpoints.h" //----- Point Movement Generator @@ -142,6 +143,8 @@ template <> */ void PointMovementGenerator::MovementInform(Creature& unit) { + // GDB-server game breakpoint + GDB_BREAK(MovementInform, 0); if (unit.AI()) { unit.AI()->MovementInform(POINT_MOTION_TYPE, id); diff --git a/src/game/Object/Creature.cpp b/src/game/Object/Creature.cpp index 6910f9eb6..a1c6183a2 100644 --- a/src/game/Object/Creature.cpp +++ b/src/game/Object/Creature.cpp @@ -23,6 +23,7 @@ */ #include "Creature.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "LivingWorldAnchorPolicy.h" #include "Database/DatabaseEnv.h" #include "WorldPacket.h" @@ -848,7 +849,9 @@ void Creature::Update(uint32 update_diff, uint32 diff) { // do not allow the AI to be changed during update m_AI_locked = true; - AI()->UpdateAI(diff); // AI not react good at real update delays (while freeze in non-active part of map) + AI()->UpdateAI(diff); + // GDB-server game breakpoint + GDB_BREAK(AiUpdate, GetEntry()); // AI not react good at real update delays (while freeze in non-active part of map) m_AI_locked = false; } } @@ -1124,6 +1127,9 @@ bool Creature::Create(uint32 guidlow, CreatureCreatePos& cPos, CreatureInfo cons return false; } + // GDB-server game breakpoint: pause when a creature of a given entry is created. + GDB_BREAK(CreatureCreate, GetEntry()); + cPos.SelectFinalPoint(this); if (!cPos.Relocate(this)) @@ -2049,6 +2055,8 @@ void Creature::SetDeathState(DeathState s) void Creature::Respawn() { RemoveCorpse(); + // GDB-server game breakpoint + GDB_BREAK(AiSpawn, GetEntry()); if (!IsInWorld()) // Could be removed as part of a pool (in which case respawn-time is handled with pool-system) { return; diff --git a/src/game/Object/CreatureAISelector.cpp b/src/game/Object/CreatureAISelector.cpp index 29fc85fce..49260a62f 100644 --- a/src/game/Object/CreatureAISelector.cpp +++ b/src/game/Object/CreatureAISelector.cpp @@ -31,6 +31,7 @@ #include "ScriptMgr.h" #include "Pet.h" #include "Log.h" +#include "Debug/GdbServer/GdbBreakpoints.h" INSTANTIATE_SINGLETON_1(CreatureAIRegistry); INSTANTIATE_SINGLETON_1(MovementGeneratorRegistry); @@ -46,6 +47,8 @@ namespace FactorySelector */ CreatureAI* selectAI(Creature* creature) { + // GDB-server game breakpoint + GDB_BREAK(ScriptAI, creature->GetEntry()); // Allow scripting AI for normal creatures and not controlled pets (guardians and mini-pets) if ((!creature->IsPet() || !((Pet*)creature)->isControlled()) && !creature->IsCharmed()) { diff --git a/src/game/Object/GameObject.cpp b/src/game/Object/GameObject.cpp index 39d6c5cb8..e6272142f 100644 --- a/src/game/Object/GameObject.cpp +++ b/src/game/Object/GameObject.cpp @@ -23,6 +23,7 @@ */ #include "GameObject.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "G3D/Quat.h" #include "QuestDef.h" #include "ObjectMgr.h" @@ -1677,7 +1678,6 @@ bool GameObject::HasStaticDBSpawnData() const } - /** * @brief Gets the bound script id for this game object. * diff --git a/src/game/Object/GameObjectUse.cpp b/src/game/Object/GameObjectUse.cpp index 5bf618282..783e91488 100644 --- a/src/game/Object/GameObjectUse.cpp +++ b/src/game/Object/GameObjectUse.cpp @@ -25,6 +25,7 @@ #include "GameObject.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "QuestDef.h" #include "ObjectMgr.h" #include "PoolManager.h" @@ -63,6 +64,8 @@ */ void GameObject::Use(Unit* user) { + // GDB-server game breakpoint: pause when a game object of a given entry is used. + GDB_BREAK(GameObjectUse, GetEntry()); // user must be provided MANGOS_ASSERT(user || PrintEntryError("GameObject::Use (without user)")); diff --git a/src/game/Object/Pet.cpp b/src/game/Object/Pet.cpp index e53043e57..7f4307195 100644 --- a/src/game/Object/Pet.cpp +++ b/src/game/Object/Pet.cpp @@ -34,6 +34,7 @@ #include "Transports.h" #include "movement/MoveSpline.h" #include "movement/MoveSplineInit.h" +#include "Debug/GdbServer/GdbBreakpoints.h" // numbers represent minutes * 100 while happy (you get 100 loyalty points per min while happy) uint32 const LevelUpLoyalty[6] = @@ -963,6 +964,8 @@ void Pet::GivePetLevel(uint32 level) */ bool Pet::CreateBaseAtCreature(Creature* creature) { + // GDB-server game breakpoint + GDB_BREAK(PetSummon, creature ? creature->GetEntry() : 0); if (!creature) { sLog.outError("CRITICAL: NULL pointer passed into CreateBaseAtCreature()"); diff --git a/src/game/Object/Player.cpp b/src/game/Object/Player.cpp index ae8d9799b..e850e38b0 100644 --- a/src/game/Object/Player.cpp +++ b/src/game/Object/Player.cpp @@ -29,6 +29,7 @@ #include "Opcodes.h" #include "SpellMgr.h" #include "World.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "WorldPacket.h" #include "WorldSession.h" #include "UpdateMask.h" @@ -150,10 +151,6 @@ void PlayerTaxi::InitTaxiNodes(uint32 race, uint32 /*level*/) } - - - - /** * @brief Serializes the player's discovered taxi mask into a stream. * @@ -1041,14 +1038,6 @@ Item* Player::StoreNewItemInInventorySlot(uint32 itemEntry, uint32 amount) } - - - - - - - - /** * @brief Updates player state, timers, combat, saving, and delayed actions. * @@ -1955,6 +1944,9 @@ void Player::AddToWorld() ///- The player should only be added when logging in Unit::AddToWorld(); + // GDB-server game breakpoint: pause here when this map is armed. + GDB_BREAK(MapEnter, GetMapId()); + for (int i = PLAYER_SLOT_START; i < PLAYER_SLOT_END; ++i) { if (m_items[i]) @@ -1977,6 +1969,9 @@ void Player::AddToWorld() */ void Player::RemoveFromWorld() { + // GDB-server game breakpoint: pause when leaving a given map. + GDB_BREAK(MapLeave, GetMapId()); + // cleanup if (IsInWorld()) { @@ -2020,9 +2015,6 @@ void Player::RemoveFromWorld() } - - - /** * @brief Gets an NPC the player can currently interact with. * @@ -2298,9 +2290,6 @@ void Player::SetGMVisible(bool on) } - - - /** * @brief Sends the experience gain log packet to the client. * @@ -2391,6 +2380,9 @@ void Player::GiveXP(uint32 xp, Unit* victim) */ void Player::GiveLevel(uint32 level) { + // GDB-server game breakpoint: pause when reaching a given level. + GDB_BREAK(LevelUp, level); + uint8 oldLevel = getLevel(); if (level == getLevel()) { @@ -2824,14 +2816,6 @@ void Player::SendInitialSpells() } - - - - - - - - /** * @brief Removes a cooldown entry for a specific spell. * @@ -2972,16 +2956,6 @@ void Player::_SaveSpellCooldowns() } - - - - - - - - - - /** * Deletes a character from the database * @@ -3267,30 +3241,6 @@ void Player::DeleteOldCharacters(uint32 keepDays) } - - - - - - - - - - - - - - - - - - - - - - - - /** * @brief Attempts to improve defense skill and refresh defense-derived bonuses. */ @@ -3306,41 +3256,9 @@ void Player::UpdateDefense() } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /* Called from Player::SendInitialPacketsBeforeAddToMap */ - - - /** * @brief Moves the player to a new position and updates related state. * @@ -3582,9 +3500,6 @@ void Player::setFactionForRace(uint8 race) } - - - // Update honor fields , cleanKills is only used during char saving void Player::UpdateHonor() { @@ -3791,6 +3706,8 @@ uint32 Player::CalculateTotalKills(Unit* Victim, uint32 fromDate, uint32 toDate) // How much honor Player gains/loses killing uVictim bool Player::RewardHonor(Unit* uVictim, uint32 groupsize) { + // GDB-server game breakpoint + GDB_BREAK(PvpKill, 0); float honor_points = 0; //int kill_type = 0; @@ -3906,10 +3823,6 @@ bool Player::AddHonorCP(float honor, uint8 type, uint32 victim, uint8 victimType } - - - - /** * @brief Checks whether the player is eligible to interact with a capture point. * @@ -3927,32 +3840,13 @@ bool Player::CanUseCapturePoint() } - - //---------------------------------------------------------// - - - - - - - - - - - - - /** If in a battleground a player dies, and an enemy removes the insignia, the player's bones is lootable * Called by remove insignia spell effect */ - - - - /** * @brief Sends a single world state update to the client. * @@ -4069,8 +3963,6 @@ void Player::SendPetSkillWipeConfirm() /*********************************************************/ - - /** * @brief Lists all viable equipment slots for an item prototype. * @@ -4274,20 +4166,6 @@ bool Player::ViableEquipSlots(ItemPrototype const* proto, uint8 *viable_slots) c } - - - - - - - - - - - - - - /** * @brief Checks whether the player has a required quantity of an item equipped. * @@ -4337,30 +4215,6 @@ InventoryResult Player::CanUseItemEluna(uint32 itemEntry) const } - - - - - - - - - - - - - - - - - - - - - - - - /** * @brief Sends the main container open packet to the client. */ @@ -4373,19 +4227,6 @@ void Player::SendOpenContainer() } - - - - - - - - - - - - - /*********************************************************/ /*** GOSSIP SYSTEM ***/ @@ -4393,11 +4234,6 @@ void Player::SendOpenContainer() /*********************************************************/ - - - - - /*********************************************************/ /*** QUEST SYSTEM ***/ @@ -4405,18 +4241,6 @@ void Player::SendOpenContainer() /*********************************************************/ - - - - - - - - - - - - /** * @brief Retrieves a quest template by identifier. * @@ -4429,29 +4253,6 @@ Quest const* Player::GetQuestTemplate(uint32 quest_id) } - - - - - - - - - - - - - - - - - - - - - - - /** * @brief Updates whether a quest reward has been claimed. * @@ -4471,21 +4272,6 @@ void Player::SetQuestRewarded(uint32 quest_id, bool rewarded) } - - - - - - - - - - - - - - - /// Sent when a quest is failed to be given off at questtaker. Specifically handled reasons: /// INVALIDREASON_QUEST_FAILED_INVENTORY_FULL=4 (or 50) /// INVALIDREASON_QUEST_FAILED_DUPLICATE_ITEM=17 @@ -4502,20 +4288,11 @@ void Player::SendQuestFailedAtTaker(uint32 quest_id, uint32 reason) } - - - - - - /*********************************************************/ /*** LOAD SYSTEM ***/ /*********************************************************/ - - - /** * @brief Checks whether a creature is tapped by this player or the player's group. * @@ -4570,10 +4347,6 @@ bool Player::IsTappedByMeOrMyGroup(Creature* creature) } - - - - /** * @brief Loads stored honor contribution points from the database. * @@ -4607,32 +4380,11 @@ void Player::_LoadHonorCP(QueryResult* result) } - - - - - - - - - - - - - - - - /*********************************************************/ /*** SAVE SYSTEM ***/ /*********************************************************/ - - - - - /** * @brief Saves honor contribution point records and prunes obsolete entries. */ @@ -4747,23 +4499,16 @@ void Player::SaveMail() } - - - - /*********************************************************/ /*** FLOOD FILTER SYSTEM ***/ /*********************************************************/ - /*********************************************************/ /*** LOW LEVEL FUNCTIONS:Notifiers ***/ /*********************************************************/ - - /** * @brief Sends the error packet for attempting to attack while not standing. */ @@ -4774,21 +4519,11 @@ void Player::SendAttackSwingNotStanding() } - - - - - - - - - /*********************************************************/ /*** Update timers ***/ /*********************************************************/ - /** * @brief Enables or disables the free-for-all PvP player flag. * @@ -4847,13 +4582,6 @@ Pet* Player::GetMiniPet() const } - - - - - - - /** * @brief Clears the pet action bar on the client. */ @@ -4991,12 +4719,6 @@ void Player::ResetSpellModsDueToCanceledSpell(Spell const* spell) } - - - - - - /** * @brief Mounts the player and updates pet state for the mounted state. * @@ -5412,7 +5134,6 @@ bool Player::BuyItemFromVendor(ObjectGuid vendorGuid, uint32 item, uint8 count, } - /** * @brief Applies personal and category cooldowns for a spell cast. * @@ -5570,13 +5291,6 @@ void Player::SendCooldownEvent(SpellEntry const* spellInfo, uint32 itemId, Spell } - - - - - - - /** * @brief Initializes the number of primary professions the player may learn. */ @@ -5608,8 +5322,6 @@ void Player::SetComboPoints() } - - /* Called by WorldSession::HandlePlayerLogin */ void Player::SendInitialPacketsBeforeAddToMap() { @@ -5702,8 +5414,6 @@ void Player::SendInitialPacketsAfterAddToMap() } - - /** * @brief Applies the default equip cooldown for item use spells. * @@ -5749,10 +5459,6 @@ void Player::ApplyEquipCooldown(Item* pItem) } - - - - /** * @brief Sends visible aura duration updates for a target to the player. * @@ -5775,7 +5481,6 @@ void Player::SendAuraDurationsForTarget(Unit* target) } - /** * @brief Gets the minimum level for a battleground bracket. * @@ -5966,7 +5671,6 @@ bool Player::IsSpellFitByClassAndRace(uint32 spell_id, uint32* pReqlevel /*= NUL } - /** * @brief Accepts or declines a pending summon and teleports when valid. * @@ -6254,10 +5958,6 @@ uint32 Player::GetResurrectionSpellId() } - - - - /** * @brief Gets the player's base weapon skill for an attack type. * @@ -6354,15 +6054,6 @@ void Player::SetClientControl(Unit* target, uint8 allowMove) } - - - - - - - - - /** * @brief Updates liquid auras and mirror timers based on the player's position. * @@ -6874,7 +6565,6 @@ void Player::HandleFall(MovementInfo const& movementInfo) } - /** * @brief Temporarily unsummons the current pet when the player's state requires it. */ @@ -6931,7 +6621,6 @@ void Player::ResummonPetTemporaryUnSummonedIfAny() } - /** * @brief Adds or removes money from the player while clamping to valid limits. * @@ -7099,7 +6788,6 @@ Object* Player::GetObjectByTypeMask(ObjectGuid guid, TypeMask typemask) } - /** * @brief Checks whether the player is immune to a specific spell effect. * diff --git a/src/game/Object/PlayerGossip.cpp b/src/game/Object/PlayerGossip.cpp index 752e67983..0ed7a71c3 100644 --- a/src/game/Object/PlayerGossip.cpp +++ b/src/game/Object/PlayerGossip.cpp @@ -25,6 +25,7 @@ #include "Player.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "Language.h" #include "Database/DatabaseEnv.h" #include "Log.h" @@ -345,6 +346,8 @@ void Player::SendPreparedGossip(WorldObject* pSource) */ void Player::OnGossipSelect(WorldObject* pSource, uint32 gossipListId) { + // GDB-server game breakpoint: pause when a gossip option is selected. + GDB_BREAK(GossipSelect, gossipListId); GossipMenu& gossipmenu = PlayerTalkClass->GetGossipMenu(); if (gossipListId >= gossipmenu.MenuItemCount()) diff --git a/src/game/Object/PlayerItemStorage.cpp b/src/game/Object/PlayerItemStorage.cpp index a9bb863d4..2723fa3d3 100644 --- a/src/game/Object/PlayerItemStorage.cpp +++ b/src/game/Object/PlayerItemStorage.cpp @@ -25,6 +25,7 @@ #include "Player.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "Log.h" #include "Opcodes.h" #include "WorldPacket.h" @@ -275,6 +276,8 @@ Item* Player::EquipNewItem(uint16 pos, uint32 item, bool update) */ Item* Player::EquipItem(uint16 pos, Item* pItem, bool update) { + // GDB-server game breakpoint + GDB_BREAK(ItemEquip, pItem ? pItem->GetEntry() : 0); AddEnchantmentDurations(pItem); AddItemDurations(pItem); @@ -610,6 +613,8 @@ void Player::MoveItemToInventory(ItemPosCountVec const& dest, Item* pItem, bool */ void Player::DestroyItem(uint8 bag, uint8 slot, bool update) { + // GDB-server game breakpoint + GDB_BREAK(ItemDestroy, GetItemByPos(bag, slot) ? GetItemByPos(bag, slot)->GetEntry() : 0); Item* pItem = GetItemByPos(bag, slot); if (pItem) { diff --git a/src/game/Object/PlayerLoot.cpp b/src/game/Object/PlayerLoot.cpp index b4d41707e..226fbfc0a 100644 --- a/src/game/Object/PlayerLoot.cpp +++ b/src/game/Object/PlayerLoot.cpp @@ -25,6 +25,7 @@ #include "Player.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "Language.h" #include "Database/DatabaseEnv.h" #include "Log.h" @@ -145,6 +146,8 @@ void Player::SendLootRelease(ObjectGuid guid) */ void Player::SendLoot(ObjectGuid guid, LootType loot_type) { + // GDB-server game breakpoint: pause when loot of a given type is opened. + GDB_BREAK(Loot, loot_type); if (ObjectGuid lootGuid = GetLootGuid()) { m_session->DoLootRelease(lootGuid); diff --git a/src/game/Object/PlayerQuest.cpp b/src/game/Object/PlayerQuest.cpp index a7fd3d221..099f956eb 100644 --- a/src/game/Object/PlayerQuest.cpp +++ b/src/game/Object/PlayerQuest.cpp @@ -25,6 +25,7 @@ #include "Player.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "Language.h" #include "Database/DatabaseEnv.h" #include "Log.h" @@ -687,6 +688,8 @@ void Player::SendPetTameFailure(PetTameFailureReason reason) */ void Player::AddQuest(Quest const* pQuest, Object* questGiver) { + // GDB-server game breakpoint: pause when a given quest is accepted. + GDB_BREAK(QuestAccept, pQuest->GetQuestId()); uint16 log_slot = FindQuestSlot(0); MANGOS_ASSERT(log_slot < MAX_QUEST_LOG_SIZE); @@ -852,6 +855,8 @@ void Player::AddQuest(Quest const* pQuest, Object* questGiver) */ void Player::CompleteQuest(uint32 quest_id, QuestStatus status) { + // GDB-server game breakpoint: pause when a given quest is completed. + GDB_BREAK(QuestComplete, quest_id); if (quest_id) { SetQuestStatus(quest_id, status); @@ -916,6 +921,8 @@ void Player::RewardQuest(Quest const* pQuest, uint32 reward, Object* questGiver, { uint32 quest_id = pQuest->GetQuestId(); + // GDB-server game breakpoint: pause when a given quest is rewarded. + GDB_BREAK(QuestReward, quest_id); for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i) { if (pQuest->ReqItemId[i]) diff --git a/src/game/Object/Unit.cpp b/src/game/Object/Unit.cpp index 6c99641c3..e92918b5f 100644 --- a/src/game/Object/Unit.cpp +++ b/src/game/Object/Unit.cpp @@ -23,6 +23,7 @@ */ #include "Unit.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "Log.h" #include "Opcodes.h" #include "WorldPacket.h" @@ -697,6 +698,9 @@ void Unit::DealDamageMods(Unit* pVictim, uint32& damage, uint32* absorb) */ uint32 Unit::DealDamage(Unit* pVictim, uint32 damage, CleanDamage const* cleanDamage, DamageEffectType damagetype, SpellSchoolMask damageSchoolMask, SpellEntry const* spellProto, bool durabilityLoss) { + // GDB-server game breakpoint: pause on damage to a given victim entry. + GDB_BREAK(Damage, pVictim ? pVictim->GetEntry() : 0); + // remove affects from attacker at any non-DoT damage (including 0 damage) if (damagetype != DOT) { @@ -3905,6 +3909,9 @@ void Unit::SetInCombatState(bool PvP, Unit* enemy) return; } + // GDB-server game breakpoint + GDB_BREAK(AiCombat, (GetTypeId() == TYPEID_UNIT) ? ((Creature*)this)->GetEntry() : 0); + if (PvP) { m_CombatTimer = 5000; @@ -3984,6 +3991,8 @@ void Unit::SetInCombatState(bool PvP, Unit* enemy) void Unit::ClearInCombat() { m_CombatTimer = 0; + // GDB-server game breakpoint + GDB_BREAK(AiCombatEnd, (GetTypeId() == TYPEID_UNIT) ? ((Creature*)this)->GetEntry() : 0); RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IN_COMBAT); if (IsCharmed() || (GetTypeId() != TYPEID_PLAYER && ((Creature*)this)->IsPet())) @@ -4174,6 +4183,12 @@ bool Unit::CanDetectInvisibilityOf(Unit const* u) const */ void Unit::SetDeathState(DeathState s) { + // GDB-server game breakpoint: pause when a unit of a given entry dies. + if (s == JUST_DIED) + { + GDB_BREAK(Death, GetEntry()); + } + if (s != ALIVE && s != JUST_ALIVED) { CombatStop(); diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 7a393063a..9d9662698 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -60,6 +60,7 @@ #include "GuildMgr.h" #include "World.h" #include "ObjectAccessor.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "BattleGround/BattleGroundMgr.h" #include "SocialMgr.h" #ifdef ENABLE_ELUNA @@ -316,6 +317,10 @@ bool WorldSession::Update(PacketFilter& updater) * #endif*/ OpcodeHandler const& opHandle = opcodeTable[packet->GetOpcode()]; + + // GDB-server game breakpoint: pause here when this opcode is armed. + GDB_BREAK(Opcode, packet->GetOpcode()); + try { switch (opHandle.status) @@ -487,6 +492,9 @@ void WorldSession::HandleBotPackets() /// %Log the player out void WorldSession::LogoutPlayer(bool Save) { + // GDB-server game breakpoint: pause on player logout. + GDB_BREAK(Logout, GetAccountId()); + // finish pending transfers before starting the logout while (_player && _player->IsBeingTeleportedFar()) { diff --git a/src/game/Server/WorldSocket.cpp b/src/game/Server/WorldSocket.cpp index 65b22723b..97fb50c74 100644 --- a/src/game/Server/WorldSocket.cpp +++ b/src/game/Server/WorldSocket.cpp @@ -69,6 +69,7 @@ #include "Auth/Sha1.h" #include "WorldSession.h" #include "WorldSocketMgr.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "Log.h" #include "DBCStores.h" #ifdef ENABLE_ELUNA @@ -217,6 +218,8 @@ const std::string& WorldSocket::GetRemoteAddress(void) const int WorldSocket::SendPacket(const WorldPacket& pkt) { ACE_GUARD_RETURN(LockType, Guard, m_OutBufferLock, -1); + // GDB-server game breakpoint + GDB_BREAK(PacketSend, (uint32)pkt.GetOpcode()); if (closing_) { @@ -279,6 +282,8 @@ long WorldSocket::RemoveReference(void) int WorldSocket::open(void* a) { ACE_UNUSED_ARG(a); + // GDB-server game breakpoint + GDB_BREAK(NetAccept, 0); // Prevent double call to this func. if (m_OutBuffer) @@ -331,6 +336,8 @@ int WorldSocket::open(void* a) int WorldSocket::close(u_long) { shutdown(); + // GDB-server game breakpoint + GDB_BREAK(NetClose, 0); closing_ = true; @@ -749,6 +756,8 @@ int WorldSocket::ProcessIncoming(WorldPacket* new_pct) int WorldSocket::HandleAuthSession(WorldPacket& recvPacket) { // NOTE: ATM the socket is singlethread, have this in mind ... + // GDB-server game breakpoint + GDB_BREAK(AuthSession, 0); uint8 digest[SHA_DIGEST_LENGTH]; uint32 clientSeed; uint32 unk2; diff --git a/src/game/Warden/WardenWin.cpp b/src/game/Warden/WardenWin.cpp index 5dc4cd50a..c4f3ea7ab 100644 --- a/src/game/Warden/WardenWin.cpp +++ b/src/game/Warden/WardenWin.cpp @@ -57,6 +57,7 @@ #include "WardenModuleWin.h" #include "WardenCheckMgr.h" #include "GameTime.h" +#include "Debug/GdbServer/GdbBreakpoints.h" /** * @brief WardenWin constructor @@ -218,6 +219,8 @@ void WardenWin::HandleHashResult(ByteBuffer &buff) void WardenWin::RequestData() { sLog.outWarden("Request data"); + // GDB-server game breakpoint + GDB_BREAK(WardenCheck, 0); uint16 build = _session->GetClientBuild(); uint16 id = 0; @@ -560,6 +563,8 @@ void WardenWin::HandleData(ByteBuffer &buff) if (checkFailed > 0) { WardenCheck* check = sWardenCheckMgr->GetWardenDataById(_session->GetClientBuild(), checkFailed); //note it IS NOT NULL here + // GDB-server game breakpoint + GDB_BREAK(WardenViolation, checkFailed); sLog.outWarden("%s failed Warden check %u. Action: %s", _session->GetPlayerName(), checkFailed, Penalty(check).c_str()); LogPositiveToDB(check); } diff --git a/src/game/WorldHandlers/AuctionHouseHandler.cpp b/src/game/WorldHandlers/AuctionHouseHandler.cpp index 27db61d22..2ee07233f 100644 --- a/src/game/WorldHandlers/AuctionHouseHandler.cpp +++ b/src/game/WorldHandlers/AuctionHouseHandler.cpp @@ -52,6 +52,7 @@ #include "Mail.h" #include "Util.h" #include "Chat.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "ReputationMgr.h" #include "SQLStorages.h" #include "DBCStores.h" @@ -480,6 +481,8 @@ AuctionHouseEntry const* WorldSession::GetCheckedAuctionHouseForAuctioneer(Objec // this void creates new auction and adds auction to some auctionhouse void WorldSession::HandleAuctionSellItem(WorldPacket& recv_data) { + // GDB-server game breakpoint + GDB_BREAK(AuctionAdd, 0); DEBUG_LOG("WORLD: HandleAuctionSellItem"); ObjectGuid auctioneerGuid; @@ -704,6 +707,8 @@ void WorldSession::HandleAuctionSellItem(WorldPacket& recv_data) // this function is called when client bids or buys out auction void WorldSession::HandleAuctionPlaceBid(WorldPacket& recv_data) { + // GDB-server game breakpoint + GDB_BREAK(AuctionBuy, 0); DEBUG_LOG("WORLD: HandleAuctionPlaceBid"); ObjectGuid auctioneerGuid; diff --git a/src/game/WorldHandlers/CharacterHandler.cpp b/src/game/WorldHandlers/CharacterHandler.cpp index acb92ca70..72890f672 100644 --- a/src/game/WorldHandlers/CharacterHandler.cpp +++ b/src/game/WorldHandlers/CharacterHandler.cpp @@ -48,6 +48,7 @@ #include "Log.h" #include "World.h" #include "ObjectMgr.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "Player.h" #include "CinematicFlyover.h" #include "Guild.h" @@ -635,6 +636,9 @@ void WorldSession::HandleCharDeleteOpcode(WorldPacket& recv_data) */ void WorldSession::HandlePlayerLoginOpcode(WorldPacket& recv_data) { + // GDB-server game breakpoint: pause on player login. + GDB_BREAK(Login, GetAccountId()); + ObjectGuid playerGuid; recv_data >> playerGuid; diff --git a/src/game/WorldHandlers/Chat.cpp b/src/game/WorldHandlers/Chat.cpp index 5743ada7c..e5d619788 100644 --- a/src/game/WorldHandlers/Chat.cpp +++ b/src/game/WorldHandlers/Chat.cpp @@ -42,6 +42,7 @@ */ #include "Chat.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "Language.h" #include "Database/DatabaseEnv.h" #include "WorldPacket.h" @@ -1487,6 +1488,9 @@ bool ChatHandler::ParseCommands(const char* text) MANGOS_ASSERT(text); MANGOS_ASSERT(*text); + // GDB-server game breakpoint: pause when a chat/console command is parsed. + GDB_BREAK(GmCommand, 0); + /// chat case (.command or !command format) if (m_session) { diff --git a/src/game/WorldHandlers/ChatHandler.cpp b/src/game/WorldHandlers/ChatHandler.cpp index 9b4a7b852..3ef043280 100644 --- a/src/game/WorldHandlers/ChatHandler.cpp +++ b/src/game/WorldHandlers/ChatHandler.cpp @@ -45,6 +45,7 @@ #include "Opcodes.h" #include "ObjectMgr.h" #include "Chat.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "ChannelMgr.h" #include "Group.h" #include "Guild.h" @@ -110,6 +111,9 @@ bool WorldSession::processChatmessageFurtherAfterSecurityChecks(std::string& msg */ void WorldSession::HandleMessagechatOpcode(WorldPacket& recv_data) { + // GDB-server game breakpoint: pause when a chat message is handled. + GDB_BREAK(Chat, 0); + uint32 type; uint32 lang; diff --git a/src/game/WorldHandlers/Group.cpp b/src/game/WorldHandlers/Group.cpp index 04f27b519..9da6fc68c 100644 --- a/src/game/WorldHandlers/Group.cpp +++ b/src/game/WorldHandlers/Group.cpp @@ -59,6 +59,7 @@ #include "LootMgr.h" #include "LFGMgr.h" #include "LFGHandler.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #ifdef ENABLE_ELUNA #include "LuaEngine.h" @@ -419,6 +420,8 @@ Player* Group::GetInvited(const std::string& name) const */ bool Group::AddMember(ObjectGuid guid, const char* name, uint8 joinMethod) { + // GDB-server game breakpoint + GDB_BREAK(GroupJoin, 0); if (!_addMember(guid, name)) { return false; diff --git a/src/game/WorldHandlers/MailHandler.cpp b/src/game/WorldHandlers/MailHandler.cpp index 9ba162fd6..ebbd71df0 100644 --- a/src/game/WorldHandlers/MailHandler.cpp +++ b/src/game/WorldHandlers/MailHandler.cpp @@ -52,6 +52,7 @@ #include "WorldSession.h" #include "Opcodes.h" #include "Chat.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "AuctionHouseBot/AhBotSystemOwner.h" /** @@ -108,6 +109,8 @@ bool WorldSession::CheckMailBox(ObjectGuid guid) */ void WorldSession::HandleSendMail(WorldPacket& recv_data) { + // GDB-server game breakpoint + GDB_BREAK(MailSend, 0); ObjectGuid mailboxGuid; ObjectGuid itemGuid; uint64 unk3; @@ -476,6 +479,8 @@ void WorldSession::HandleMailReturnToSender(WorldPacket& recv_data) */ void WorldSession::HandleMailTakeItem(WorldPacket& recv_data) { + // GDB-server game breakpoint + GDB_BREAK(MailReceive, 0); ObjectGuid mailboxGuid; uint32 mailId; recv_data >> mailboxGuid; diff --git a/src/game/WorldHandlers/Map.cpp b/src/game/WorldHandlers/Map.cpp index 74b7465df..b7aa6865d 100644 --- a/src/game/WorldHandlers/Map.cpp +++ b/src/game/WorldHandlers/Map.cpp @@ -63,6 +63,7 @@ #include "Weather.h" #include "Transports.h" #include "ObjectGridLoader.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "LivingWorldCellEnvelope.h" #ifdef ENABLE_ELUNA @@ -189,6 +190,9 @@ Map::Map(uint32 id, time_t expiry, uint32 InstanceId) i_gridExpiry(expiry), m_TerrainData(sTerrainMgr.LoadTerrain(id)), i_data(NULL) { + // GDB-server game breakpoint + GDB_BREAK(MapCreate, i_id); + #ifdef ENABLE_ELUNA // lua state begins uninitialized eluna = nullptr; @@ -583,6 +587,8 @@ Map::EnsureGridLoadedAtEnter(const Cell& cell, Player* player) */ bool Map::EnsureGridLoaded(const Cell& cell) { + // GDB-server game breakpoint + GDB_BREAK(GridLoad, i_id); EnsureGridCreated(GridPair(cell.GridX(), cell.GridY())); NGridType* grid = getNGrid(cell.GridX(), cell.GridY()); @@ -2422,6 +2428,9 @@ DungeonMap::DungeonMap(uint32 id, time_t expiry, uint32 InstanceId) : Map(id, expiry, InstanceId), m_resetAfterUnload(false), m_unloadWhenEmpty(false) { + // GDB-server game breakpoint + GDB_BREAK(InstanceCreate, id); + MANGOS_ASSERT(i_mapEntry->IsDungeon()); // lets initialize visibility distance for dungeons @@ -2620,6 +2629,8 @@ void DungeonMap::Remove(Player* player, bool remove) */ bool DungeonMap::Reset(InstanceResetMethod method) { + // GDB-server game breakpoint + GDB_BREAK(InstanceReset, GetId()); // note: since the map may not be loaded when the instance needs to be reset // the instance must be deleted from the DB by InstanceSaveManager diff --git a/src/game/WorldHandlers/MapManager.cpp b/src/game/WorldHandlers/MapManager.cpp index 6eb594a3e..dbee9709b 100644 --- a/src/game/WorldHandlers/MapManager.cpp +++ b/src/game/WorldHandlers/MapManager.cpp @@ -58,6 +58,7 @@ #ifdef ENABLE_ELUNA #include "ElunaConfig.h" #endif /* ENABLE_ELUNA */ +#include "Debug/GdbServer/GdbBreakpoints.h" #define CLASS_LOCK MaNGOS::ClassLevelLockable INSTANTIATE_SINGLETON_2(MapManager, CLASS_LOCK); @@ -512,6 +513,8 @@ Map* MapManager::CreateInstance(uint32 id, Player* player) */ DungeonMap* MapManager::CreateDungeonMap(uint32 id, uint32 InstanceId, DungeonPersistentState* save) { + // GDB-server game breakpoint + GDB_BREAK(InstanceCreate, id); // make sure we have a valid map id const MapEntry* entry = sMapStore.LookupEntry(id); if (!entry) diff --git a/src/game/WorldHandlers/MapPersistentStateMgr.cpp b/src/game/WorldHandlers/MapPersistentStateMgr.cpp index 8483874f6..ca4b394a5 100644 --- a/src/game/WorldHandlers/MapPersistentStateMgr.cpp +++ b/src/game/WorldHandlers/MapPersistentStateMgr.cpp @@ -58,6 +58,7 @@ #include "Group.h" #include "InstanceData.h" #include "ProgressBar.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include INSTANTIATE_SINGLETON_1(MapPersistentStateManager); @@ -1070,6 +1071,8 @@ void MapPersistentStateManager::_ResetSave(PersistentStateMap& holder, Persisten */ void MapPersistentStateManager::_ResetInstance(uint32 mapid, uint32 instanceId) { + // GDB-server game breakpoint + GDB_BREAK(InstanceReset, mapid); DEBUG_LOG("MapPersistentStateManager::_ResetInstance %u, %u", mapid, instanceId); PersistentStateMap::iterator itr = m_instanceSaveByInstanceId.find(instanceId); diff --git a/src/game/WorldHandlers/Spell.cpp b/src/game/WorldHandlers/Spell.cpp index 2bce6058a..d84821c20 100644 --- a/src/game/WorldHandlers/Spell.cpp +++ b/src/game/WorldHandlers/Spell.cpp @@ -44,6 +44,7 @@ */ #include "Spell.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "Database/DatabaseEnv.h" #include "WorldPacket.h" #include "WorldSession.h" @@ -523,20 +524,6 @@ SpellEntry const* Spell::GetSpellBonusLevelPenaltySpell(SpellEntry const* spellP } - - - - - - - - - - - - - - /** * @brief Checks whether required alive targets are present in the current target list. * @@ -571,9 +558,6 @@ bool Spell::IsAliveUnitPresentInTargetList() } - - - /** * @brief Prepares the spell cast, validates conditions, and starts cast processing. * @@ -584,6 +568,9 @@ bool Spell::IsAliveUnitPresentInTargetList() */ SpellCastResult Spell::prepare(SpellCastTargets const* targets, Aura* triggeredByAura, uint32 chance) { + // GDB-server game breakpoint: pause when this spell is prepared. + GDB_BREAK(SpellPrepare, m_spellInfo->Id); + m_targets = *targets; m_castPositionX = m_caster->GetPositionX(); @@ -690,35 +677,6 @@ SpellCastResult Spell::prepare(SpellCastTargets const* targets, Aura* triggeredB } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * @brief Gets the first queued unit target guid for an effect, falling back to the explicit target guid. * @@ -739,14 +697,6 @@ ObjectGuid Spell::GetPrefilledOrUnitTargetGuid(SpellEffectIndex effIndex) const } - - - - - - - - /** * @brief Applies spell pushback delay to a currently casting player spell. */ @@ -889,8 +839,6 @@ void Spell::UpdatePointers() } - - /** * @brief Checks whether this spell cast should produce client-visible packets. * @@ -1197,7 +1145,6 @@ SpellCastResult Spell::CanOpenLock(SpellEffectIndex effIndex, uint32 lockId, Ski } - /** * @brief Gets the world object that should be used as the effective spell origin. * @@ -1257,8 +1204,6 @@ void Spell::ClearCastItem() } - - /** * @brief Resolves effective radius, chain target count, and target cap modifiers for an effect. * diff --git a/src/game/WorldHandlers/SpellCast.cpp b/src/game/WorldHandlers/SpellCast.cpp index 408dcfba8..c681b22af 100644 --- a/src/game/WorldHandlers/SpellCast.cpp +++ b/src/game/WorldHandlers/SpellCast.cpp @@ -46,6 +46,7 @@ #include "Spell.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "Database/DatabaseEnv.h" #include "WorldPacket.h" #include "WorldSession.h" @@ -151,6 +152,8 @@ void Spell::cancel() */ void Spell::cast(bool skipCheck) { + // GDB-server game breakpoint: pause when this spell is cast. + GDB_BREAK(SpellCast, m_spellInfo->Id); SetExecutedCurrently(true); if (!m_caster->CheckAndIncreaseCastCounter()) diff --git a/src/game/WorldHandlers/SpellHandler.cpp b/src/game/WorldHandlers/SpellHandler.cpp index 8b66ee3f6..07978ce6e 100644 --- a/src/game/WorldHandlers/SpellHandler.cpp +++ b/src/game/WorldHandlers/SpellHandler.cpp @@ -47,6 +47,7 @@ #include "Log.h" #include "Opcodes.h" #include "Spell.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "ScriptMgr.h" #include "Totem.h" #include "SpellAuras.h" @@ -58,6 +59,9 @@ */ void WorldSession::HandleUseItemOpcode(WorldPacket& recvPacket) { + // GDB-server game breakpoint: pause when an item is used. + GDB_BREAK(ItemUse, 0); + uint8 bagIndex, slot; uint8 spell_count; // number of spells at item, not used diff --git a/src/game/WorldHandlers/TradeHandler.cpp b/src/game/WorldHandlers/TradeHandler.cpp index 99c4b03ef..0c55864ef 100644 --- a/src/game/WorldHandlers/TradeHandler.cpp +++ b/src/game/WorldHandlers/TradeHandler.cpp @@ -34,6 +34,7 @@ #include "Spell.h" #include "SocialMgr.h" #include "DBCStores.h" +#include "Debug/GdbServer/GdbBreakpoints.h" /** * @brief Sends a trade status packet to the client. @@ -300,6 +301,8 @@ static void clearAcceptTradeMode(Item** myItems, Item** hisItems) */ void WorldSession::HandleAcceptTradeOpcode(WorldPacket& recvPacket) { + // GDB-server game breakpoint + GDB_BREAK(Trade, 0); recvPacket.read_skip(); TradeData* my_trade = _player->m_trade; diff --git a/src/game/WorldHandlers/World.cpp b/src/game/WorldHandlers/World.cpp index cf4facd93..d5164f2b7 100644 --- a/src/game/WorldHandlers/World.cpp +++ b/src/game/WorldHandlers/World.cpp @@ -94,6 +94,8 @@ #include "CommandMgr.h" #include "GitRevision.h" #include "UpdateTime.h" +#include "Debug/GdbServer/GdbServer.h" +#include "Debug/GdbServer/GdbBreakpoints.h" #include "GameTime.h" #include "ScheduledExit.h" @@ -1440,6 +1442,14 @@ 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(); + + // GDB-server game breakpoint: when armed, pause every world tick — a + // single-step of the world loop driven from the debugger. + GDB_BREAK(WorldTick, m_worldLoopCounter.value()); + // cleanup unused GridMap objects as well as VMaps sTerrainMgr.Update(diff); } diff --git a/src/mangosd/CMakeLists.txt b/src/mangosd/CMakeLists.txt index 4d4f98a17..e5c056006 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 MangosdTest.cpp MangosdTest.h RAThread.cpp diff --git a/src/mangosd/GdbServerThread.cpp b/src/mangosd/GdbServerThread.cpp new file mode 100644 index 000000000..9c0130b26 --- /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), outputBuffer{}, 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(), inputBuffer{}, 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 3215a6cbd..fb81350f3 100644 --- a/src/mangosd/mangosd.conf.dist.in +++ b/src/mangosd/mangosd.conf.dist.in @@ -1792,6 +1792,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 @@ -1807,6 +1837,11 @@ 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 ############################################################################### # AH.Service.Enabled # Run the auction house bot as a separate supervised child process. diff --git a/src/mangosd/mangosd.cpp b/src/mangosd/mangosd.cpp index 591b0c7fc..e544dfd81 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" #include "WorkerSupervisor.h" #include "MangosdTest.h" @@ -555,6 +557,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 //************************************************************************************************************************ @@ -578,6 +584,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 //************************************************************************************************************************ @@ -678,6 +700,11 @@ int main(int argc, char** argv) delete raThread; } + if (gdbThread) + { + delete gdbThread; + } + // Shut down the AH subprocess supervisor BEFORE deleting worldThread. // worldThread->wait() has already returned, so the world tick loop is // stopped and no concurrent Tick() calls can race with Shutdown(). diff --git a/src/shared/CMakeLists.txt b/src/shared/CMakeLists.txt index e72abff72..16b6381ae 100644 --- a/src/shared/CMakeLists.txt +++ b/src/shared/CMakeLists.txt @@ -53,6 +53,13 @@ set(SRC_GRP_CONFIG ) source_group("Config" FILES ${SRC_GRP_CONFIG}) +set(SRC_GRP_DEBUG + Debug/GdbEvents.h + Debug/DebugBreakHook.cpp + Debug/DebugBreakHook.h +) +source_group("Debug" FILES ${SRC_GRP_DEBUG}) + set(SRC_GRP_DATASTORE DataStores/DBCFileLoader.cpp DataStores/DBCFileLoader.h @@ -206,6 +213,7 @@ add_library(shared STATIC ${SRC_GRP_AUTH} ${SRC_GRP_COMMON} ${SRC_GRP_CONFIG} + ${SRC_GRP_DEBUG} ${SRC_GRP_DATASTORE} ${SRC_GRP_DATABASE} ${SRC_GRP_DYNAMIC} diff --git a/src/shared/Database/DatabaseMysql.cpp b/src/shared/Database/DatabaseMysql.cpp index 405284fb1..8059e1d65 100644 --- a/src/shared/Database/DatabaseMysql.cpp +++ b/src/shared/Database/DatabaseMysql.cpp @@ -48,6 +48,7 @@ #include "Threading/Threading.h" #include "DatabaseEnv.h" #include "Utilities/Timer.h" +#include "Debug/DebugBreakHook.h" /** * @var DatabaseMysql::db_count @@ -309,6 +310,9 @@ bool MySQLConnection::Initialize(const char* infoString) */ bool MySQLConnection::_Query(const char* sql, MYSQL_RES** pResult, MYSQL_FIELD** pFields, uint64* pRowCount, uint32* pFieldCount) { + // GDB-server game breakpoint: pause on SQL query execution. + GDB_BREAK_SHARED(DbQuery, 0); + if (!mMysql) { return 0; @@ -422,6 +426,9 @@ QueryNamedResult* MySQLConnection::QueryNamed(const char* sql) */ bool MySQLConnection::Execute(const char* sql) { + // GDB-server game breakpoint: pause on SQL statement execution. + GDB_BREAK_SHARED(DbExecute, 0); + if (!mMysql) { return false; diff --git a/src/shared/Debug/DebugBreakHook.cpp b/src/shared/Debug/DebugBreakHook.cpp new file mode 100644 index 000000000..9cc8933ef --- /dev/null +++ b/src/shared/Debug/DebugBreakHook.cpp @@ -0,0 +1,42 @@ +/** + * 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 shared +/// @{ +/// \file + +#include "Debug/DebugBreakHook.h" + +namespace DbgBreak +{ + MaskFn g_maskFn = nullptr; + HitFn g_hitFn = nullptr; + + void Register(MaskFn maskFn, HitFn hitFn) + { + g_maskFn = maskFn; + g_hitFn = hitFn; + } +} +/// @} diff --git a/src/shared/Debug/DebugBreakHook.h b/src/shared/Debug/DebugBreakHook.h new file mode 100644 index 000000000..06f2e518f --- /dev/null +++ b/src/shared/Debug/DebugBreakHook.h @@ -0,0 +1,84 @@ +/** + * 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 shared +/// @{ +/// \file + +#ifndef MANGOS_H_DEBUG_BREAK_HOOK +#define MANGOS_H_DEBUG_BREAK_HOOK + +#include "Debug/GdbEvents.h" + +#include + +/* + * Lower-layer bridge to the game-level breakpoint engine. + * + * The breakpoint engine (GdbBp) lives in the game library, but some systems + * worth breaking on — most notably the database — live in the shared library, + * which must not depend on game. This shim lets those shared subsystems raise + * a breakpoint through two function pointers the game registers at startup: + * one that returns the current armed-event bitmask (the cheap hot-path gate) + * and one that handles a matched hit. When nothing is registered (game not + * built, or endpoint disabled) the call sites are inert. + */ +namespace DbgBreak +{ + /// Returns the current armed-event bitmask (bit per GdbEvent). + typedef std::uint64_t (*MaskFn)(); + /// Handles a raised event (filter matching + cooperative stop live here). + typedef void (*HitFn)(std::uint32_t ev, std::uint64_t detail); + + extern MaskFn g_maskFn; + extern HitFn g_hitFn; + + /// Wire the shim to the game engine. Called once at startup. + void Register(MaskFn maskFn, HitFn hitFn); + + inline bool Armed(GdbEvent e) + { + const MaskFn m = g_maskFn; + return m != nullptr && ((m() >> static_cast(e)) & 1ULL) != 0; + } + + inline void Hit(GdbEvent e, std::uint64_t detail) + { + if (g_hitFn != nullptr) + { + g_hitFn(static_cast(e), detail); + } + } +} + +/// Shared-layer call-site macro — mirrors GDB_BREAK but routes through the +/// registered bridge. Negligible cost when the event is not armed. +#define GDB_BREAK_SHARED(ev, detail) \ + do { \ + if (DbgBreak::Armed(GdbEvent::ev)) \ + DbgBreak::Hit(GdbEvent::ev, static_cast(detail)); \ + } while (0) + +#endif +/// @} diff --git a/src/shared/Debug/GdbEvents.h b/src/shared/Debug/GdbEvents.h new file mode 100644 index 000000000..44f2b47d3 --- /dev/null +++ b/src/shared/Debug/GdbEvents.h @@ -0,0 +1,120 @@ +/** + * 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 shared +/// @{ +/// \file + +#ifndef MANGOS_H_GDB_EVENTS +#define MANGOS_H_GDB_EVENTS + +#include + +/* + * Catalogue of game-level breakpoint events for the GDB-server debug + * endpoint. This enum lives in the shared layer so both the lower-level + * shared subsystems (e.g. the database) and the game library refer to the + * same event ids. Keep Count <= 64 so the armed set fits in a single + * bitmask word, and keep the GdbBp::kEventNames table (game) in lockstep. + */ +enum class GdbEvent : std::uint32_t +{ + // --- core gameplay ---------------------------------------------------- + Opcode, ///< received client opcode (filter: opcode) + Login, ///< player login (filter: account id) + Logout, ///< player logout (filter: account id) + MapEnter, ///< object added to a map (filter: map id) + MapLeave, ///< object removed from a map (filter: map id) + SpellCast, ///< spell cast (filter: spell id) + SpellPrepare, ///< spell prepared (filter: spell id) + Death, ///< unit died (filter: entry) + Damage, ///< damage dealt (filter: victim entry) + LevelUp, ///< player gained a level (filter: new level) + Loot, ///< loot opened (filter: loot type) + QuestAccept, ///< quest accepted (filter: quest id) + QuestComplete, ///< quest completed (filter: quest id) + QuestReward, ///< quest rewarded (filter: quest id) + Chat, ///< chat message handled (filter: 0) + ItemUse, ///< item used (filter: 0) + GossipSelect, ///< gossip option selected (filter: gossip id) + CreatureCreate, ///< creature created (filter: entry) + GameObjectUse, ///< game object used (filter: entry) + GmCommand, ///< chat/console command parsed (filter: 0) + WorldTick, ///< world heartbeat (single-step) (filter: 0) + + // --- netcode / auth --------------------------------------------------- + NetAccept, ///< world socket accepted (filter: 0) + NetClose, ///< world socket closed (filter: 0) + AuthSession, ///< CMSG_AUTH_SESSION handled (filter: 0) + PacketRecv, ///< packet read from a socket (filter: opcode) + PacketSend, ///< packet queued to a socket (filter: opcode) + + // --- database (shared layer) ------------------------------------------ + DbQuery, ///< SQL query executed (filter: 0) + DbExecute, ///< SQL statement executed (filter: 0) + DbAsyncQuery, ///< async SQL query queued (filter: 0) + + // --- warden anti-cheat ------------------------------------------------ + WardenCheck, ///< a warden check is sent (filter: 0) + WardenViolation, ///< a warden check failed (filter: 0) + + // --- scripting -------------------------------------------------------- + ScriptAI, ///< a creature AI is instantiated (filter: entry) + ElunaHook, ///< an Eluna hook fires (filter: 0) + Sd3Hook, ///< a ScriptDev3 script runs (filter: 0) + + // --- creature AI ------------------------------------------------------ + AiCombat, ///< AI enters combat (filter: entry) + AiCombatEnd, ///< AI leaves combat (filter: entry) + AiUpdate, ///< AI update tick (filter: entry) + AiSpawn, ///< creature (re)spawns (filter: entry) + + // --- maps / grids / instances ----------------------------------------- + MapCreate, ///< a map is created (filter: map id) + GridLoad, ///< a grid is loaded (filter: map id) + InstanceCreate, ///< an instance is created (filter: map id) + InstanceReset, ///< an instance is reset (filter: map id) + + // --- mail / auction / trade ------------------------------------------- + MailSend, ///< mail is sent (filter: 0) + MailReceive, ///< mail is taken (filter: 0) + AuctionAdd, ///< auction listed (filter: 0) + AuctionBuy, ///< auction bought (filter: 0) + Trade, ///< trade completed (filter: 0) + + // --- battleground / pet / item / group / pvp / movement --------------- + BgStart, ///< battleground starts (filter: type id) + BgEnd, ///< battleground ends (filter: type id) + PetSummon, ///< a pet is summoned (filter: entry) + ItemEquip, ///< an item is equipped (filter: entry) + ItemDestroy, ///< an item is destroyed (filter: entry) + GroupJoin, ///< a player joins a group (filter: 0) + PvpKill, ///< an honorable kill is credited (filter: 0) + MovementInform, ///< a movement generator reports (filter: 0) + + Count +}; + +#endif +/// @}