diff --git a/.gitignore b/.gitignore index 8dd4c381..51700872 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ .idea/ build/ +cmake-build-*-*/ out/ .cache/ bin/Cemu_* diff --git a/README.md b/README.md index ee7beb49..723e64d3 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,13 @@ It's written in C/C++ and is being actively developed with new features and fixe Cemu is currently only available for 64-bit Windows, Linux & macOS devices. ### Links: - - [Original 2.0 announcement post](https://www.reddit.com/r/cemu/comments/wwa22c/cemu_20_announcement_linux_builds_opensource_and/) + - [Open Source Announcement](https://www.reddit.com/r/cemu/comments/wwa22c/cemu_20_announcement_linux_builds_opensource_and/) - [Official Website](https://cemu.info) - [Compatibility List/Wiki](https://wiki.cemu.info/wiki/Main_Page) - [Official Subreddit](https://reddit.com/r/Cemu) - [Official Discord](https://discord.gg/5psYsup) - [Official Matrix Server](https://matrix.to/#/#cemu:cemu.info) - - [Unofficial Setup Guide](https://cemu.cfw.guide) + - [Setup Guide](https://cemu.cfw.guide) #### Other relevant repositories: - [Cemu-Language](https://github.com/cemu-project/Cemu-Language) diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index 3c49b60d..0daffbb8 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -38,6 +38,9 @@ add_library(CemuCafe HW/Espresso/Debugger/Debugger.h HW/Espresso/Debugger/DebugSymbolStorage.cpp HW/Espresso/Debugger/DebugSymbolStorage.h + HW/Espresso/Debugger/GDBStub.h + HW/Espresso/Debugger/GDBStub.cpp + HW/Espresso/Debugger/GDBBreakpoints.h HW/Espresso/EspressoISA.h HW/Espresso/Interpreter/PPCInterpreterALU.hpp HW/Espresso/Interpreter/PPCInterpreterFPU.cpp diff --git a/src/Cafe/CafeSystem.cpp b/src/Cafe/CafeSystem.cpp index ef3e2806..6e9c5ce6 100644 --- a/src/Cafe/CafeSystem.cpp +++ b/src/Cafe/CafeSystem.cpp @@ -30,6 +30,7 @@ #include "GamePatch.h" #include +#include "HW/Espresso/Debugger/GDBStub.h" #include "Cafe/IOSU/legacy/iosu_ioctl.h" #include "Cafe/IOSU/legacy/iosu_act.h" @@ -398,6 +399,11 @@ void cemu_initForGame() InfoLog_PrintActiveSettings(); Latte_Start(); // check for debugger entrypoint bp + if (g_gdbstub) + { + g_gdbstub->HandleEntryStop(_entryPoint); + g_gdbstub->Initialize(); + } debugger_handleEntryBreakpoint(_entryPoint); // load graphic packs forceLog_printf("------- Activate graphic packs -------"); diff --git a/src/Cafe/HW/Espresso/Debugger/Debugger.cpp b/src/Cafe/HW/Espresso/Debugger/Debugger.cpp index fc015285..b6417080 100644 --- a/src/Cafe/HW/Espresso/Debugger/Debugger.cpp +++ b/src/Cafe/HW/Espresso/Debugger/Debugger.cpp @@ -103,7 +103,7 @@ void debugger_updateExecutionBreakpoint(uint32 address, bool forceRestore) if (bpItr->enabled && forceRestore == false) { // write TW instruction to memory - debugger_updateMemoryU32(address, (31 << 26) | (4 << 1)); + debugger_updateMemoryU32(address, DEBUGGER_BP_T_DEBUGGER_TW); return; } else @@ -171,14 +171,25 @@ void debugger_updateMemoryBreakpoint(DebuggerBreakpoint* bp) { ctx.Dr0 = (DWORD64)memory_getPointerFromVirtualOffset(bp->address); ctx.Dr1 = (DWORD64)memory_getPointerFromVirtualOffset(bp->address); - ctx.Dr7 = 1 | (1 << 16) | (3 << 18); // enable dr0, track write, 4 byte length - ctx.Dr7 |= (4 | (3 << 20) | (3 << 22)); // enable dr1, track read+write, 4 byte length + // breakpoint 0 + SetBits(ctx.Dr7, 0, 1, 1); // breakpoint #0 enabled: true + SetBits(ctx.Dr7, 16, 2, 1); // breakpoint #0 condition: 1 (write) + SetBits(ctx.Dr7, 18, 2, 3); // breakpoint #0 length: 3 (4 bytes) + // breakpoint 1 + SetBits(ctx.Dr7, 2, 1, 1); // breakpoint #1 enabled: true + SetBits(ctx.Dr7, 20, 2, 3); // breakpoint #1 condition: 3 (read & write) + SetBits(ctx.Dr7, 22, 2, 3); // breakpoint #1 length: 3 (4 bytes) } else { - ctx.Dr0 = (DWORD64)0; - ctx.Dr1 = (DWORD64)0; - ctx.Dr7 = 0; // disable dr0 + // breakpoint 0 + SetBits(ctx.Dr7, 0, 1, 0); // breakpoint #0 enabled: false + SetBits(ctx.Dr7, 16, 2, 0); // breakpoint #0 condition: 1 (write) + SetBits(ctx.Dr7, 18, 2, 0); // breakpoint #0 length: 3 (4 bytes) + // breakpoint 1 + SetBits(ctx.Dr7, 2, 1, 0); // breakpoint #1 enabled: false + SetBits(ctx.Dr7, 20, 2, 0); // breakpoint #1 condition: 3 (read & write) + SetBits(ctx.Dr7, 22, 2, 0); // breakpoint #1 length: 3 (4 bytes) } SetThreadContext(hThread, &ctx); ResumeThread(hThread); @@ -188,10 +199,10 @@ void debugger_updateMemoryBreakpoint(DebuggerBreakpoint* bp) #endif } -void debugger_handleSingleStepException(uint32 drMask) +void debugger_handleSingleStepException(uint64 dr6) { - bool triggeredDR0 = (drMask & (1 << 0)) != 0; // write - bool triggeredDR1 = (drMask & (1 << 1)) != 0; // read + bool triggeredDR0 = GetBits(dr6, 0, 1); // write + bool triggeredDR1 = GetBits(dr6, 1, 1); // read and write bool catchBP = false; if (triggeredDR0 && triggeredDR1) { diff --git a/src/Cafe/HW/Espresso/Debugger/Debugger.h b/src/Cafe/HW/Espresso/Debugger/Debugger.h index 3bb90bea..08cbd90a 100644 --- a/src/Cafe/HW/Espresso/Debugger/Debugger.h +++ b/src/Cafe/HW/Espresso/Debugger/Debugger.h @@ -3,11 +3,6 @@ #include #include "Cafe/HW/Espresso/PPCState.h" -//#define DEBUGGER_BP_TYPE_NORMAL (1<<0) // normal breakpoint -//#define DEBUGGER_BP_TYPE_ONE_SHOT (1<<1) // normal breakpoint -//#define DEBUGGER_BP_TYPE_MEMORY_READ (1<<2) // memory breakpoint -//#define DEBUGGER_BP_TYPE_MEMORY_WRITE (1<<3) // memory breakpoint - #define DEBUGGER_BP_T_NORMAL 0 // normal breakpoint #define DEBUGGER_BP_T_ONE_SHOT 1 // normal breakpoint, deletes itself after trigger (used for stepping) #define DEBUGGER_BP_T_MEMORY_READ 2 // memory breakpoint @@ -16,6 +11,9 @@ #define DEBUGGER_BP_T_GDBSTUB 1 // breakpoint created by GDBStub #define DEBUGGER_BP_T_DEBUGGER 2 // breakpoint created by Cemu's debugger +#define DEBUGGER_BP_T_GDBSTUB_TW 0x7C010008 +#define DEBUGGER_BP_T_DEBUGGER_TW 0x7C020008 + struct DebuggerBreakpoint { diff --git a/src/Cafe/HW/Espresso/Debugger/GDBBreakpoints.h b/src/Cafe/HW/Espresso/Debugger/GDBBreakpoints.h new file mode 100644 index 00000000..dc538a4c --- /dev/null +++ b/src/Cafe/HW/Espresso/Debugger/GDBBreakpoints.h @@ -0,0 +1,286 @@ +#include + +#if BOOST_OS_LINUX +#include +#include +#include + +// helpers for accessing debug register +typedef unsigned long DRType; + +DRType _GetDR(pid_t tid, int drIndex) +{ + unsigned long v; + v = ptrace (PTRACE_PEEKUSER, tid, offsetof (struct user, u_debugreg[drIndex]), 0); + return (DRType)v; +} + +void _SetDR(pid_t tid, int drIndex, DRType newValue) +{ + unsigned long v = newValue; + ptrace (PTRACE_POKEUSER, tid, offsetof (struct user, u_debugreg[drIndex]), v); +} + +#endif + +namespace coreinit +{ + std::vector& OSGetSchedulerThreads(); +} + +enum class BreakpointType +{ + BP_SINGLE, + BP_PERSISTENT, + BP_RESTORE_POINT, + BP_STEP_POINT +}; + +class GDBServer::ExecutionBreakpoint { +public: + ExecutionBreakpoint(MPTR address, BreakpointType type, bool visible, std::string reason) + : m_address(address), m_removedAfterInterrupt(false), m_reason(std::move(reason)) + { + if (type == BreakpointType::BP_SINGLE) + { + this->m_pauseThreads = true; + this->m_restoreAfterInterrupt = false; + this->m_deleteAfterAnyInterrupt = false; + this->m_pauseOnNextInterrupt = false; + this->m_visible = visible; + } + else if (type == BreakpointType::BP_PERSISTENT) + { + this->m_pauseThreads = true; + this->m_restoreAfterInterrupt = true; + this->m_deleteAfterAnyInterrupt = false; + this->m_pauseOnNextInterrupt = false; + this->m_visible = visible; + } + else if (type == BreakpointType::BP_RESTORE_POINT) + { + this->m_pauseThreads = false; + this->m_restoreAfterInterrupt = false; + this->m_deleteAfterAnyInterrupt = false; + this->m_pauseOnNextInterrupt = false; + this->m_visible = false; + } + else if (type == BreakpointType::BP_STEP_POINT) + { + this->m_pauseThreads = false; + this->m_restoreAfterInterrupt = false; + this->m_deleteAfterAnyInterrupt = true; + this->m_pauseOnNextInterrupt = true; + this->m_visible = false; + } + + this->m_origOpCode = memory_readU32(address); + memory_writeU32(address, DEBUGGER_BP_T_GDBSTUB_TW); + PPCRecompiler_invalidateRange(address, address + 4); + }; + ~ExecutionBreakpoint() + { + memory_writeU32(this->m_address, this->m_origOpCode); + PPCRecompiler_invalidateRange(this->m_address, this->m_address + 4); + }; + + [[nodiscard]] uint32 GetVisibleOpCode() const + { + if (this->m_visible) + return memory_readU32(this->m_address); + else + return this->m_origOpCode; + }; + [[nodiscard]] bool ShouldBreakThreads() const + { + return this->m_pauseThreads; + }; + [[nodiscard]] bool ShouldBreakThreadsOnNextInterrupt() + { + bool shouldPause = this->m_pauseOnNextInterrupt; + this->m_pauseOnNextInterrupt = false; + return shouldPause; + }; + [[nodiscard]] bool IsPersistent() const + { + return this->m_restoreAfterInterrupt; + }; + [[nodiscard]] bool IsSkipBreakpoint() const + { + return this->m_deleteAfterAnyInterrupt; + }; + [[nodiscard]] bool IsRemoved() const + { + return this->m_removedAfterInterrupt; + }; + [[nodiscard]] std::string GetReason() const + { + return m_reason; + }; + + void RemoveTemporarily() + { + memory_writeU32(this->m_address, this->m_origOpCode); + PPCRecompiler_invalidateRange(this->m_address, this->m_address + 4); + this->m_restoreAfterInterrupt = true; + }; + void Restore() + { + memory_writeU32(this->m_address, DEBUGGER_BP_T_GDBSTUB_TW); + PPCRecompiler_invalidateRange(this->m_address, this->m_address + 4); + this->m_restoreAfterInterrupt = false; + }; + void PauseOnNextInterrupt() + { + this->m_pauseOnNextInterrupt = true; + }; + + void WriteNewOpCode(uint32 newOpCode) + { + this->m_origOpCode = newOpCode; + }; + +private: + const MPTR m_address; + std::string m_reason; + uint32 m_origOpCode; + bool m_visible; + bool m_pauseThreads; + // type + bool m_pauseOnNextInterrupt; + bool m_restoreAfterInterrupt; + bool m_deleteAfterAnyInterrupt; + bool m_removedAfterInterrupt; +}; + +enum class AccessPointType +{ + BP_WRITE = 2, + BP_READ = 3, + BP_BOTH = 4 +}; + +class GDBServer::AccessBreakpoint { +public: + AccessBreakpoint(MPTR address, AccessPointType type) + : m_address(address), m_type(type) + { +#if defined(ARCH_X86_64) && BOOST_OS_WINDOWS + for (auto& hThreadNH : coreinit::OSGetSchedulerThreads()) + { + HANDLE hThread = (HANDLE)hThreadNH; + CONTEXT ctx{}; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + SuspendThread(hThread); + GetThreadContext(hThread, &ctx); + + // use BP 2/3 for gdb stub since cemu's internal debugger uses BP 0/1 already + ctx.Dr2 = (DWORD64)memory_getPointerFromVirtualOffset(address); + ctx.Dr3 = (DWORD64)memory_getPointerFromVirtualOffset(address); + // breakpoint 2 + SetBits(ctx.Dr7, 4, 1, 1); // breakpoint #3 enabled: true + SetBits(ctx.Dr7, 24, 2, 1); // breakpoint #3 condition: 1 (write) + SetBits(ctx.Dr7, 26, 2, 3); // breakpoint #3 length: 3 (4 bytes) + // breakpoint 3 + SetBits(ctx.Dr7, 6, 1, 1); // breakpoint #4 enabled: true + SetBits(ctx.Dr7, 28, 2, 3); // breakpoint #4 condition: 3 (read & write) + SetBits(ctx.Dr7, 30, 2, 3); // breakpoint #4 length: 3 (4 bytes) + + SetThreadContext(hThread, &ctx); + ResumeThread(hThread); + } +#elif defined(ARCH_X86_64) && BOOST_OS_LINUX + for (auto& hThreadNH : coreinit::OSGetSchedulerThreads()) + { + pid_t pid = (pid_t)(uintptr_t)hThreadNH; + ptrace(PTRACE_ATTACH, pid, nullptr, nullptr); + waitpid(pid, nullptr, 0); + + DRType dr7 = _GetDR(pid, 7); + // use BP 2/3 for gdb stub since cemu's internal debugger uses BP 0/1 already + DRType dr2 = (uint64)memory_getPointerFromVirtualOffset(address); + DRType dr3 = (uint64)memory_getPointerFromVirtualOffset(address); + // breakpoint 2 + SetBits(dr7, 4, 1, 1); // breakpoint #3 enabled: true + SetBits(dr7, 24, 2, 1); // breakpoint #3 condition: 1 (write) + SetBits(dr7, 26, 2, 3); // breakpoint #3 length: 3 (4 bytes) + // breakpoint 3 + SetBits(dr7, 6, 1, 1); // breakpoint #4 enabled: true + SetBits(dr7, 28, 2, 3); // breakpoint #4 condition: 3 (read & write) + SetBits(dr7, 30, 2, 3); // breakpoint #4 length: 3 (4 bytes) + + _SetDR(pid, 2, dr2); + _SetDR(pid, 3, dr3); + _SetDR(pid, 7, dr7); + ptrace(PTRACE_DETACH, pid, nullptr, nullptr); + } +#else + cemuLog_log(LogType::Force, "Debugger read/write breakpoints are not supported on non-x86 CPUs yet."); +#endif + }; + ~AccessBreakpoint() + { +#if defined(ARCH_X86_64) && BOOST_OS_WINDOWS + for (auto& hThreadNH : coreinit::OSGetSchedulerThreads()) + { + HANDLE hThread = (HANDLE)hThreadNH; + CONTEXT ctx{}; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + SuspendThread(hThread); + GetThreadContext(hThread, &ctx); + + // reset BP 2/3 to zero + ctx.Dr2 = (DWORD64)0; + ctx.Dr3 = (DWORD64)0; + // breakpoint 2 + SetBits(ctx.Dr7, 4, 1, 0); + SetBits(ctx.Dr7, 24, 2, 0); + SetBits(ctx.Dr7, 26, 2, 0); + // breakpoint 3 + SetBits(ctx.Dr7, 6, 1, 0); + SetBits(ctx.Dr7, 28, 2, 0); + SetBits(ctx.Dr7, 30, 2, 0); + SetThreadContext(hThread, &ctx); + ResumeThread(hThread); + } +#elif defined(ARCH_X86_64) && BOOST_OS_LINUX + for (auto& hThreadNH : coreinit::OSGetSchedulerThreads()) + { + pid_t pid = (pid_t)(uintptr_t)hThreadNH; + ptrace(PTRACE_ATTACH, pid, nullptr, nullptr); + waitpid(pid, nullptr, 0); + + DRType dr7 = _GetDR(pid, 7); + // reset BP 2/3 to zero + DRType dr2 = 0; + DRType dr3 = 0; + // breakpoint 2 + SetBits(dr7, 4, 1, 0); + SetBits(dr7, 24, 2, 0); + SetBits(dr7, 26, 2, 0); + // breakpoint 3 + SetBits(dr7, 6, 1, 0); + SetBits(dr7, 28, 2, 0); + SetBits(dr7, 30, 2, 0); + + _SetDR(pid, 2, dr2); + _SetDR(pid, 3, dr3); + _SetDR(pid, 7, dr7); + ptrace(PTRACE_DETACH, pid, nullptr, nullptr); + } +#endif + }; + + MPTR GetAddress() const + { + return m_address; + }; + AccessPointType GetType() const + { + return m_type; + }; + +private: + const MPTR m_address; + const AccessPointType m_type; +}; \ No newline at end of file diff --git a/src/Cafe/HW/Espresso/Debugger/GDBStub.cpp b/src/Cafe/HW/Espresso/Debugger/GDBStub.cpp new file mode 100644 index 00000000..d83ea46b --- /dev/null +++ b/src/Cafe/HW/Espresso/Debugger/GDBStub.cpp @@ -0,0 +1,979 @@ +#include "GDBStub.h" +#include "Debugger.h" +#include "Cafe/HW/Espresso/Recompiler/PPCRecompiler.h" +#include "GDBBreakpoints.h" +#include "util/helpers/helpers.h" +#include "util/ThreadPool/ThreadPool.h" +#include "Cafe/OS/RPL/rpl.h" +#include "Cafe/OS/RPL/rpl_structs.h" +#include "Cafe/OS/libs/coreinit/coreinit_Scheduler.h" +#include "Cafe/OS/libs/coreinit/coreinit_Thread.h" +#include "Cafe/HW/Espresso/Interpreter/PPCInterpreterInternal.h" +#include "Cafe/HW/Espresso/EspressoISA.h" +#include "Common/socket.h" + +#define GET_THREAD_ID(threadPtr) memory_getVirtualOffsetFromPointer(threadPtr) +#define GET_THREAD_BY_ID(threadId) (OSThread_t*)memory_getPointerFromPhysicalOffset(threadId) + +static std::vector findNextInstruction(MPTR currAddress, uint32 lr, uint32 ctr) +{ + using namespace Espresso; + + uint32 nextInstr = memory_readU32(currAddress); + if (GetPrimaryOpcode(nextInstr) == PrimaryOpcode::B) + { + uint32 LI; + bool AA, LK; + decodeOp_B(nextInstr, LI, AA, LK); + if (!AA) + LI += currAddress; + return {LI}; + } + if (GetPrimaryOpcode(nextInstr) == PrimaryOpcode::BC) + { + uint32 BD, BI; + BOField BO{}; + bool AA, LK; + decodeOp_BC(nextInstr, BD, BO, BI, AA, LK); + if (!LK) + BD += currAddress; + return {currAddress + 4, BD}; + } + if (GetPrimaryOpcode(nextInstr) == PrimaryOpcode::GROUP_19 && GetGroup19Opcode(nextInstr) == Opcode19::BCLR) + { + return {currAddress + 4, lr}; + } + if (GetPrimaryOpcode(nextInstr) == PrimaryOpcode::GROUP_19 && GetGroup19Opcode(nextInstr) == Opcode19::BCCTR) + { + return {currAddress + 4, ctr}; + } + return {currAddress + 4}; +} + +template +static void selectThread(sint64 selectorId, F&& action_for_thread) +{ + __OSLockScheduler(); + cemu_assert_debug(activeThreadCount != 0); + + if (selectorId == -1) + { + for (sint32 i = 0; i < activeThreadCount; i++) + { + action_for_thread(GET_THREAD_BY_ID(activeThread[i])); + } + } + else if (selectorId == 0) + { + // Use first thread if attempted to be stopped + // todo: would this work better if it used main? + action_for_thread(coreinit::OSGetDefaultThread(1)); + } + else if (selectorId > 0) + { + for (sint32 i = 0; i < activeThreadCount; i++) + { + auto* thread = GET_THREAD_BY_ID(activeThread[i]); + if (GET_THREAD_ID(thread) == selectorId) + { + action_for_thread(thread); + break; + } + } + } + __OSUnlockScheduler(); +} + +template +static void selectAndBreakThread(sint64 selectorId, F&& action_for_thread) +{ + __OSLockScheduler(); + cemu_assert_debug(activeThreadCount != 0); + + std::vector pausedThreads; + if (selectorId == -1) + { + for (sint32 i = 0; i < activeThreadCount; i++) + { + coreinit::__OSSuspendThreadNolock(GET_THREAD_BY_ID(activeThread[i])); + pausedThreads.emplace_back(GET_THREAD_BY_ID(activeThread[i])); + } + } + else if (selectorId == 0) + { + // Use first thread if attempted to be stopped + OSThread_t* thread = GET_THREAD_BY_ID(activeThread[0]); + for (sint32 i = 0; i < activeThreadCount; i++) + { + if (GET_THREAD_ID(GET_THREAD_BY_ID(activeThread[i])) < GET_THREAD_ID(thread)) + { + thread = GET_THREAD_BY_ID(activeThread[i]); + } + } + coreinit::__OSSuspendThreadNolock(thread); + pausedThreads.emplace_back(thread); + } + else if (selectorId > 0) + { + for (sint32 i = 0; i < activeThreadCount; i++) + { + auto* thread = GET_THREAD_BY_ID(activeThread[i]); + if (GET_THREAD_ID(thread) == selectorId) + { + coreinit::__OSSuspendThreadNolock(thread); + pausedThreads.emplace_back(thread); + break; + } + } + } + __OSUnlockScheduler(); + + for (OSThread_t* thread : pausedThreads) + { + while (coreinit::OSIsThreadRunning(thread)) + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + action_for_thread(thread); + } +} + +static void selectAndResumeThread(sint64 selectorId) +{ + __OSLockScheduler(); + cemu_assert_debug(activeThreadCount != 0); + + if (selectorId == -1) + { + for (sint32 i = 0; i < activeThreadCount; i++) + { + coreinit::__OSResumeThreadInternal(GET_THREAD_BY_ID(activeThread[i]), 4); + } + } + else if (selectorId == 0) + { + // Use first thread if attempted to be stopped + coreinit::__OSResumeThreadInternal(coreinit::OSGetDefaultThread(1), 1); + } + else if (selectorId > 0) + { + for (sint32 i = 0; i < activeThreadCount; i++) + { + auto* thread = GET_THREAD_BY_ID(activeThread[i]); + if (GET_THREAD_ID(thread) == selectorId) + { + coreinit::__OSResumeThreadInternal(thread, 1); + break; + } + } + } + __OSUnlockScheduler(); +} + +static void waitForBrokenThreads(std::unique_ptr context, std::string_view reason) +{ + // This should pause all threads except trapped thread + // It should however wait for the trapped thread + // The trapped thread should be paused by the trap word instruction handler (aka the running thread) + std::vector threadsList; + __OSLockScheduler(); + for (sint32 i = 0; i < activeThreadCount; i++) + { + threadsList.emplace_back(GET_THREAD_BY_ID(activeThread[i])); + } + __OSUnlockScheduler(); + + for (OSThread_t* thread : threadsList) + { + while (coreinit::OSIsThreadRunning(thread)) + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + context->QueueResponse(reason); +} + +static void breakThreads(sint64 trappedThread) +{ + __OSLockScheduler(); + cemu_assert_debug(activeThreadCount != 0); + + // First, break other threads + OSThread_t* mainThread = nullptr; + for (sint32 i = 0; i < activeThreadCount; i++) + { + if (GET_THREAD_ID(GET_THREAD_BY_ID(activeThread[i])) == trappedThread) + { + mainThread = GET_THREAD_BY_ID(activeThread[i]); + } + else + { + coreinit::__OSSuspendThreadNolock(GET_THREAD_BY_ID(activeThread[i])); + } + } + + // Second, break trapped thread itself which should also pause execution of this handler + // This will temporarily lift the scheduler lock until it's resumed from its suspension + coreinit::__OSSuspendThreadNolock(mainThread); + + __OSUnlockScheduler(); +} + +std::unique_ptr g_gdbstub; + +GDBServer::GDBServer(uint16 port) + : m_port(port) +{ +#if BOOST_OS_WINDOWS + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); +#endif +} + +GDBServer::~GDBServer() +{ + if (m_client_socket != INVALID_SOCKET) + { + // close socket from other thread to forcefully stop accept() call + closesocket(m_client_socket); + m_client_socket = INVALID_SOCKET; + } + + if (m_server_socket != INVALID_SOCKET) + { + closesocket(m_server_socket); + } +#if BOOST_OS_WINDOWS + WSACleanup(); +#endif + + m_stopRequested = false; + m_thread.join(); +} + +bool GDBServer::Initialize() +{ + cemuLog_createLogFile(false); + + if (m_server_socket = socket(PF_INET, SOCK_STREAM, 0); m_server_socket == SOCKET_ERROR) + return false; + + int reuseEnabled = TRUE; + if (setsockopt(m_server_socket, SOL_SOCKET, SO_REUSEADDR, (char*)&reuseEnabled, sizeof(reuseEnabled)) == SOCKET_ERROR) + { + closesocket(m_server_socket); + m_server_socket = INVALID_SOCKET; + return false; + } + + memset(&m_server_addr, 0, sizeof(m_server_addr)); + m_server_addr.sin_family = AF_INET; + m_server_addr.sin_addr.s_addr = htonl(INADDR_ANY); + m_server_addr.sin_port = htons(m_port); + + if (bind(m_server_socket, (sockaddr*)&m_server_addr, sizeof(m_server_addr)) == SOCKET_ERROR) + { + closesocket(m_server_socket); + m_server_socket = INVALID_SOCKET; + return false; + } + + if (listen(m_server_socket, s_maxGDBClients) == SOCKET_ERROR) + { + closesocket(m_server_socket); + m_server_socket = INVALID_SOCKET; + return false; + } + + m_thread = std::thread(std::bind(&GDBServer::ThreadFunc, this)); + + return true; +} + +void GDBServer::ThreadFunc() +{ + SetThreadName("GDBServer::ThreadFunc"); + + while (!m_stopRequested) + { + if (!m_client_connected) + { + cemuLog_logDebug(LogType::Force, "[GDBStub] Waiting for client to connect on port {}...", m_port); + socklen_t client_addr_size = sizeof(m_client_addr); + m_client_socket = accept(m_server_socket, (struct sockaddr*)&m_client_addr, &client_addr_size); + m_client_connected = m_client_socket != SOCKET_ERROR; + } + else + { + auto receiveMessage = [&](char* buffer, const int32_t length) -> bool { + if (recv(m_client_socket, buffer, length, 0) != SOCKET_ERROR) + return false; + return true; + }; + + auto readChar = [&]() -> char { + char ret = 0; + recv(m_client_socket, &ret, 1, 0); + return ret; + }; + + char packetPrefix = readChar(); + + switch (packetPrefix) + { + case '+': + case '-': + break; + case '\x03': + { + cemuLog_logDebug(LogType::Force, "[GDBStub] Received interrupt (pressed CTRL+C?) from client!"); + selectAndBreakThread(-1, [](OSThread_t* thread) { + }); + auto thread_status = fmt::format("T05thread:{:08X};", GET_THREAD_ID(coreinit::OSGetDefaultThread(1))); + if (this->m_resumed_context) + { + this->m_resumed_context->QueueResponse(thread_status); + this->m_resumed_context.reset(); + } + else + { + auto response_full = fmt::format("+${}#{:02x}", thread_status, CommandContext::CalculateChecksum(thread_status)); + send(m_client_socket, response_full.c_str(), (int)response_full.size(), 0); + } + break; + } + case '$': + { + std::string message; + uint8 checkedSum = 0; + for (uint32_t i = 1;; i++) + { + char c = readChar(); + if (c == '#') + break; + checkedSum += static_cast(c); + message.push_back(c); + + if (i >= s_maxPacketSize) + cemuLog_logDebug(LogType::Force, "[GDBStub] Received too big of a buffer: {}", message); + } + char checkSumStr[2]; + receiveMessage(checkSumStr, 2); + uint32_t checkSum = std::stoi(checkSumStr, nullptr, 16); + assert((checkedSum & 0xFF) == checkSum); + + HandleCommand(message); + break; + } + default: + // cemuLog_logDebug(LogType::Force, "[GDBStub] Unknown packet start: {}", packetPrefix); + break; + } + } + } + + if (m_client_socket != INVALID_SOCKET) + closesocket(m_client_socket); +} + +void GDBServer::HandleCommand(const std::string& command_str) +{ + auto context = std::make_unique(this, command_str); + + if (context->IsValid()) + { + // cemuLog_logDebug(LogType::Force, "[GDBStub] Extracted Command {}", fmt::join(context->GetArgs(), ",")); + } + + switch (context->GetType()) + { + // Extended commands + case CMDType::QUERY_GET: + case CMDType::QUERY_SET: + return HandleQuery(context); + case CMDType::VCONT: + return HandleVCont(context); + // Regular commands + case CMDType::IS_THREAD_RUNNING: + return CMDIsThreadActive(context); + case CMDType::SET_ACTIVE_THREAD: + return CMDSetActiveThread(context); + case CMDType::ACTIVE_THREAD_STATUS: + return CMDGetThreadStatus(context); + case CMDType::CONTINUE: + return CMDContinue(context); + case CMDType::ACTIVE_THREAD_STEP: + break; + case CMDType::REGISTER_READ: + return CMDReadRegister(context); + case CMDType::REGISTER_SET: + return CMDWriteRegister(context); + case CMDType::REGISTERS_READ: + return CMDReadRegisters(context); + case CMDType::REGISTERS_WRITE: + return CMDWriteRegisters(context); + case CMDType::MEMORY_READ: + return CMDReadMemory(context); + case CMDType::MEMORY_WRITE: + return CMDWriteMemory(context); + case CMDType::BREAKPOINT_SET: + return CMDInsertBreakpoint(context); + case CMDType::BREAKPOINT_REMOVE: + return CMDDeleteBreakpoint(context); + case CMDType::INVALID: + default: + return CMDNotFound(context); + } + + CMDNotFound(context); +} + +void GDBServer::HandleQuery(std::unique_ptr& context) const +{ + if (!context->IsValid()) + return context->QueueResponse(RESPONSE_EMPTY); + + const auto& query_cmd = context->GetArgs()[0]; + const auto& query_args = context->GetArgs().begin() + 1; + + if (query_cmd == "qSupported") + { + context->QueueResponse(s_supportedFeatures); + } + else if (query_cmd == "qAttached") + { + context->QueueResponse("1"); + } + else if (query_cmd == "qRcmd") + { + } + else if (query_cmd == "qC") + { + context->QueueResponse("QC"); + context->QueueResponse(std::to_string(m_activeThreadContinueSelector)); + } + else if (query_cmd == "qOffsets") + { + const auto module_count = RPLLoader_GetModuleCount(); + const auto module_list = RPLLoader_GetModuleList(); + for (sint32 i = 0; i < module_count; i++) + { + const RPLModule* rpl = module_list[i]; + if (rpl->entrypoint == m_entry_point) + { + context->QueueResponse(fmt::format("TextSeg={:08X};DataSeg={:08X}", rpl->regionMappingBase_text.GetMPTR(), rpl->regionMappingBase_data)); + } + } + } + else if (query_cmd == "qfThreadInfo") + { + std::vector threadIds; + selectThread(-1, [&threadIds](OSThread_t* thread) { + threadIds.emplace_back(fmt::format("{:08X}", memory_getVirtualOffsetFromPointer(thread))); + }); + context->QueueResponse(fmt::format("m{}", fmt::join(threadIds, ","))); + } + else if (query_cmd == "qsThreadInfo") + { + context->QueueResponse("l"); + } + else if (query_cmd == "qXfer") + { + auto& type = query_args[0]; + + if (type == "features") + { + auto& annex = query_args[1]; + sint64 read_offset = std::stoul(query_args[2], nullptr, 16); + sint64 read_length = std::stoul(query_args[3], nullptr, 16); + if (annex == "target.xml") + { + if (read_offset >= GDBTargetXML.size()) + context->QueueResponse("l"); + else + { + auto paginated_str = GDBTargetXML.substr(read_offset, read_length); + context->QueueResponse((paginated_str.size() == read_length) ? "m" : "l"); + context->QueueResponse(paginated_str); + } + } + else + cemuLog_logDebug(LogType::Force, "[GDBStub] qXfer:features:read:{} isn't a known feature document", annex); + } + else if (type == "threads") + { + sint64 read_offset = std::stoul(query_args[1], nullptr, 16); + sint64 read_length = std::stoul(query_args[2], nullptr, 16); + + std::string threads_res; + threads_res += R"()"; + threads_res += ""; + // note: clion seems to default to the first thread + std::map threads_list; + selectThread(-1, [&threads_list](OSThread_t* thread) { + std::string entry; + entry += fmt::format(R"(context.upir.value()); + if (!thread->threadName.IsNull()) + entry += fmt::format(R"( name="{}")", CommandContext::EscapeXMLString(thread->threadName.GetPtr())); + // todo: could add a human-readable description of the thread here + entry += fmt::format(">"); + threads_list.emplace(GET_THREAD_ID(thread), entry); + }); + for (auto& entry : threads_list) + { + threads_res += entry.second; + } + threads_res += ""; + + if (read_offset >= threads_res.size()) + context->QueueResponse("l"); + else + { + auto paginated_str = threads_res.substr(read_offset, read_length); + context->QueueResponse((paginated_str.size() == read_length) ? "m" : "l"); + context->QueueResponse(paginated_str); + } + } + else if (type == "libraries") + { + sint64 read_offset = std::stoul(query_args[1], nullptr, 16); + sint64 read_length = std::stoul(query_args[2], nullptr, 16); + + std::string library_list; + library_list += R"()"; + library_list += ""; + + const auto module_count = RPLLoader_GetModuleCount(); + const auto module_list = RPLLoader_GetModuleList(); + for (sint32 i = 0; i < module_count; i++) + { + library_list += fmt::format(R"()", CommandContext::EscapeXMLString(module_list[i]->moduleName2), module_list[i]->regionMappingBase_text.GetMPTR()); + } + library_list += ""; + + if (read_offset >= library_list.size()) + context->QueueResponse("l"); + else + { + auto paginated_str = library_list.substr(read_offset, read_length); + context->QueueResponse((paginated_str.size() == read_length) ? "m" : "l"); + context->QueueResponse(paginated_str); + } + } + else + { + context->QueueResponse(RESPONSE_EMPTY); + } + } + else + { + context->QueueResponse(RESPONSE_EMPTY); + } +} + +void GDBServer::HandleVCont(std::unique_ptr& context) +{ + if (!context->IsValid()) + { + cemuLog_logDebug(LogType::Force, "[GDBStub] Received unsupported vCont command: {}", context->GetCommand()); + // cemu_assert_unimplemented(); + return context->QueueResponse(RESPONSE_EMPTY); + } + + const std::string& vcont_cmd = context->GetArgs()[0]; + if (vcont_cmd == "vCont?") + return context->QueueResponse("vCont;c;C;s;S"); + + else if (vcont_cmd != "vCont;") + return context->QueueResponse(RESPONSE_EMPTY); + + m_resumed_context = std::move(context); + + bool resumedNoThreads = true; + for (const auto operation : TokenizeView(m_resumed_context->GetArgs()[1], ';')) + { + // todo: this might have issues with the signal versions (C/S) + // todo: test whether this works with multiple vCont;c:123123;c:123123 + std::string_view operationType = operation.substr(0, operation.find(':')); + sint64 threadSelector = operationType.size() == operation.size() ? -1 : std::stoll(std::string(operation.substr(operationType.size() + 1)), nullptr, 16); + + if (operationType == "c" || operationType.starts_with("C")) + { + selectAndResumeThread(threadSelector); + resumedNoThreads = false; + } + else if (operationType == "s" || operationType.starts_with("S")) + { + selectThread(threadSelector, [this](OSThread_t* thread) { + auto nextInstructions = findNextInstruction(thread->context.srr0, thread->context.lr, thread->context.ctr); + for (MPTR nextInstr : nextInstructions) + { + auto bpIt = m_patchedInstructions.find(nextInstr); + if (bpIt == m_patchedInstructions.end()) + this->m_patchedInstructions.try_emplace(nextInstr, nextInstr, BreakpointType::BP_STEP_POINT, false, "swbreak:;"); + else + bpIt->second.PauseOnNextInterrupt(); + } + }); + } + } + + if (resumedNoThreads) + { + selectAndResumeThread(-1); + cemuLog_logDebug(LogType::Force, "[GDBStub] Resumed all threads after skip instructions"); + } +} + +void GDBServer::CMDContinue(std::unique_ptr& context) +{ + m_resumed_context = std::move(context); + selectAndResumeThread(m_activeThreadContinueSelector); +} + +void GDBServer::CMDNotFound(std::unique_ptr& context) +{ + return context->QueueResponse(RESPONSE_EMPTY); +} + +void GDBServer::CMDIsThreadActive(std::unique_ptr& context) +{ + sint64 threadSelector = std::stoll(context->GetArgs()[1], nullptr, 16); + bool foundThread = false; + selectThread(threadSelector, [&foundThread](OSThread_t* thread) { + foundThread = true; + }); + + if (foundThread) + return context->QueueResponse(RESPONSE_OK); + else + return context->QueueResponse(RESPONSE_ERROR); +} + +void GDBServer::CMDSetActiveThread(std::unique_ptr& context) +{ + sint64 threadSelector = std::stoll(context->GetArgs()[2], nullptr, 16); + if (threadSelector >= 0) + { + bool foundThread = false; + selectThread(threadSelector, [&foundThread](OSThread_t* thread) { + foundThread = true; + }); + if (!foundThread) + return context->QueueResponse(RESPONSE_ERROR); + } + if (context->GetArgs()[1] == "c") + m_activeThreadContinueSelector = threadSelector; + else + m_activeThreadSelector = threadSelector; + return context->QueueResponse(RESPONSE_OK); +} + +void GDBServer::CMDGetThreadStatus(std::unique_ptr& context) +{ + selectThread(0, [&context](OSThread_t* thread) { + context->QueueResponse(fmt::format("T05thread:{:08X};", memory_getVirtualOffsetFromPointer(thread))); + }); +} + +void GDBServer::CMDReadRegister(std::unique_ptr& context) const +{ + sint32 reg = std::stoi(context->GetArgs()[1], nullptr, 16); + selectThread(m_activeThreadSelector, [reg, &context](OSThread_t* thread) { + auto& cpu = thread->context; + if (reg >= RegisterID::R0_START && reg <= RegisterID::R31_END) + { + return context->QueueResponse(fmt::format("{:08X}", CPU_swapEndianU32(cpu.gpr[reg]))); + } + else if (reg >= RegisterID::F0_START && reg <= RegisterID::F31_END) + { + return context->QueueResponse(fmt::format("{:016X}", cpu.fp_ps0[reg - RegisterID::F0_START].value())); + } + else if (reg == RegisterID::FPSCR) + { + return context->QueueResponse(fmt::format("{:08X}", cpu.fpscr.fpscr.value())); + } + else + { + switch (reg) + { + case RegisterID::PC: return context->QueueResponse(fmt::format("{:08X}", cpu.srr0)); + case RegisterID::MSR: return context->QueueResponse("xxxxxxxx"); + case RegisterID::CR: return context->QueueResponse(fmt::format("{:08X}", cpu.cr)); + case RegisterID::LR: return context->QueueResponse(fmt::format("{:08X}", CPU_swapEndianU32(cpu.lr))); + case RegisterID::CTR: return context->QueueResponse(fmt::format("{:08X}", cpu.ctr)); + case RegisterID::XER: return context->QueueResponse(fmt::format("{:08X}", cpu.xer)); + default: break; + } + } + }); +} + +void GDBServer::CMDWriteRegister(std::unique_ptr& context) const +{ + sint32 reg = std::stoi(context->GetArgs()[1], nullptr, 16); + uint64 value = std::stoll(context->GetArgs()[2], nullptr, 16); + selectThread(m_activeThreadSelector, [reg, value, &context](OSThread_t* thread) { + auto& cpu = thread->context; + if (reg >= RegisterID::R0_START && reg <= RegisterID::R31_END) + { + cpu.gpr[reg] = CPU_swapEndianU32(value); + return context->QueueResponse(RESPONSE_OK); + } + else if (reg >= RegisterID::F0_START && reg <= RegisterID::F31_END) + { + // todo: figure out how to properly write to paired single registers + cpu.fp_ps0[reg - RegisterID::F0_START] = uint64be{value}; + return context->QueueResponse(RESPONSE_OK); + } + else if (reg == RegisterID::FPSCR) + { + cpu.fpscr.fpscr = uint32be{(uint32)value}; + return context->QueueResponse(RESPONSE_OK); + } + else + { + switch (reg) + { + case RegisterID::PC: + cpu.srr0 = value; + return context->QueueResponse(RESPONSE_OK); + case RegisterID::MSR: + return context->QueueResponse(RESPONSE_ERROR); + case RegisterID::CR: + cpu.cr = value; + return context->QueueResponse(RESPONSE_OK); + case RegisterID::LR: + cpu.lr = CPU_swapEndianU32(value); + return context->QueueResponse(RESPONSE_OK); + case RegisterID::CTR: + cpu.ctr = value; + return context->QueueResponse(RESPONSE_OK); + case RegisterID::XER: + cpu.xer = value; + return context->QueueResponse(RESPONSE_OK); + default: + return context->QueueResponse(RESPONSE_ERROR); + } + } + }); +} + +void GDBServer::CMDReadRegisters(std::unique_ptr& context) const +{ + selectThread(m_activeThreadSelector, [&context](OSThread_t* thread) { + for (uint32& reg : thread->context.gpr) + { + context->QueueResponse(fmt::format("{:08X}", CPU_swapEndianU32(reg))); + } + }); +} + +void GDBServer::CMDWriteRegisters(std::unique_ptr& context) const +{ + selectThread(m_activeThreadSelector, [&context](OSThread_t* thread) { + auto& registers = context->GetArgs()[1]; + for (uint32 i = 0; i < 32; i++) + { + thread->context.gpr[i] = CPU_swapEndianU32(std::stoi(registers.substr(i * 2, 2), nullptr, 16)); + } + }); +} + +void GDBServer::CMDReadMemory(std::unique_ptr& context) +{ + sint64 addr = std::stoul(context->GetArgs()[1], nullptr, 16); + sint64 length = std::stoul(context->GetArgs()[2], nullptr, 16); + + // todo: handle cross-mmu-range memory requests + if (!memory_isAddressRangeAccessible(addr, length)) + return context->QueueResponse(RESPONSE_ERROR); + + std::string memoryRepr; + uint8* values = memory_getPointerFromVirtualOffset(addr); + for (sint64 i = 0; i < length; i++) + { + memoryRepr += fmt::format("{:02X}", values[i]); + } + + auto patchesRange = m_patchedInstructions.lower_bound(addr); + while (patchesRange != m_patchedInstructions.end() && patchesRange->first < (addr + length)) + { + auto replStr = fmt::format("{:02X}", patchesRange->second.GetVisibleOpCode()); + memoryRepr[(patchesRange->first - addr) * 2] = replStr[0]; + memoryRepr[(patchesRange->first - addr) * 2 + 1] = replStr[1]; + patchesRange++; + } + return context->QueueResponse(memoryRepr); +} + +void GDBServer::CMDWriteMemory(std::unique_ptr& context) +{ + sint64 addr = std::stoul(context->GetArgs()[1], nullptr, 16); + sint64 length = std::stoul(context->GetArgs()[2], nullptr, 16); + auto source = context->GetArgs()[3]; + + // todo: handle cross-mmu-range memory requests + if (!memory_isAddressRangeAccessible(addr, length)) + return context->QueueResponse(RESPONSE_ERROR); + + uint8* values = memory_getPointerFromVirtualOffset(addr); + for (sint64 i = 0; i < length; i++) + { + uint8 hexValue; + const std::from_chars_result result = std::from_chars(source.data() + (i * 2), (source.data() + (i * 2) + 2), hexValue, 16); + if (result.ec == std::errc::invalid_argument || result.ec == std::errc::result_out_of_range) + return context->QueueResponse(RESPONSE_ERROR); + + if (auto it = m_patchedInstructions.find(addr + i); it != m_patchedInstructions.end()) + { + uint32 newOpCode = it->second.GetVisibleOpCode(); + uint32 byteIndex = 3 - ((addr + i) % 4); // inverted because of big endian, so address 0 is the highest byte + newOpCode &= ~(0xFF << (byteIndex * 8)); // mask out the byte + newOpCode |= ((uint32)hexValue << (byteIndex * 8)); // set new byte with OR + it->second.WriteNewOpCode(newOpCode); + } + else + { + values[i] = hexValue; + } + } + return context->QueueResponse(RESPONSE_OK); +} + +void GDBServer::CMDInsertBreakpoint(std::unique_ptr& context) +{ + auto type = std::stoul(context->GetArgs()[1], nullptr, 16); + MPTR addr = static_cast(std::stoul(context->GetArgs()[2], nullptr, 16)); + + if (type == 0 || type == 1) + { + auto bp = this->m_patchedInstructions.find(addr); + if (bp != this->m_patchedInstructions.end()) + this->m_patchedInstructions.erase(bp); + this->m_patchedInstructions.try_emplace(addr, addr, BreakpointType::BP_PERSISTENT, type == 0, type == 0 ? "swbreak:;" : "hwbreak:;"); + } + else if (type == 2 || type == 3 || type == 4) + { + if (this->m_watch_point) + return context->QueueResponse(RESPONSE_ERROR); + + this->m_watch_point = std::make_unique(addr, (AccessPointType)type); + } + + return context->QueueResponse(RESPONSE_OK); +} + +void GDBServer::CMDDeleteBreakpoint(std::unique_ptr& context) +{ + auto type = std::stoul(context->GetArgs()[1], nullptr, 16); + MPTR addr = static_cast(std::stoul(context->GetArgs()[2], nullptr, 16)); + + if (type == 0 || type == 1) + { + auto bp = this->m_patchedInstructions.find(addr); + if (bp == this->m_patchedInstructions.end() || !bp->second.ShouldBreakThreads()) + return context->QueueResponse(RESPONSE_ERROR); + else + this->m_patchedInstructions.erase(bp); + } + else if (type == 2 || type == 3 || type == 4) + { + if (!this->m_watch_point || this->m_watch_point->GetAddress() != addr) + return context->QueueResponse(RESPONSE_ERROR); + + this->m_watch_point.reset(); + } + + return context->QueueResponse(RESPONSE_OK); +} + +// Internal functions for control +void GDBServer::HandleTrapInstruction(PPCInterpreter_t* hCPU) +{ + // First, restore any removed breakpoints + for (auto& bp : m_patchedInstructions) + { + if (bp.second.IsRemoved()) + bp.second.Restore(); + } + + auto patchedBP = m_patchedInstructions.find(hCPU->instructionPointer); + if (patchedBP == m_patchedInstructions.end()) + return cemu_assert_suspicious(); + + // Secondly, delete one-shot breakpoints but also temporarily delete patched instruction to run original instruction + OSThread_t* currThread = coreinitThread_getCurrentThreadDepr(hCPU); + std::string pauseReason = fmt::format("T05thread:{:08X};core:{:02X};{}", GET_THREAD_ID(currThread), PPCInterpreter_getCoreIndex(hCPU), patchedBP->second.GetReason()); + bool pauseThreads = patchedBP->second.ShouldBreakThreads() || patchedBP->second.ShouldBreakThreadsOnNextInterrupt(); + if (patchedBP->second.IsPersistent()) + { + // Insert new restore breakpoints at next possible instructions which restores breakpoints but won't pause the CPU + std::vector nextInstructions = findNextInstruction(hCPU->instructionPointer, hCPU->spr.LR, hCPU->spr.CTR); + for (MPTR nextInstr : nextInstructions) + { + if (!m_patchedInstructions.contains(nextInstr)) + this->m_patchedInstructions.try_emplace(nextInstr, nextInstr, BreakpointType::BP_STEP_POINT, false, ""); + } + patchedBP->second.RemoveTemporarily(); + } + else + { + m_patchedInstructions.erase(patchedBP); + } + + // Thirdly, delete any instructions that were generated by a skip instruction + for (auto it = m_patchedInstructions.cbegin(), next_it = it; it != m_patchedInstructions.cend(); it = next_it) + { + ++next_it; + if (it->second.IsSkipBreakpoint()) + { + m_patchedInstructions.erase(it); + } + } + + // Fourthly, the stub can insert breakpoints that are just meant to restore patched instructions, in which case we just want to continue + if (pauseThreads) + { + cemuLog_logDebug(LogType::Force, "[GDBStub] Got trapped by a breakpoint!"); + if (m_resumed_context) + { + // Spin up thread to signal when another GDB stub trap is found + ThreadPool::FireAndForget(&waitForBrokenThreads, std::move(m_resumed_context), pauseReason); + } + + breakThreads(GET_THREAD_ID(coreinitThread_getCurrentThreadDepr(hCPU))); + cemuLog_logDebug(LogType::Force, "[GDBStub] Resumed from a breakpoint!"); + } +} + +void GDBServer::HandleAccessException(uint64 dr6) +{ + bool triggeredWrite = GetBits(dr6, 2, 1); + bool triggeredReadWrite = GetBits(dr6, 3, 1); + + std::string response; + if (m_watch_point->GetType() == AccessPointType::BP_WRITE && triggeredWrite) + response = fmt::format("watch:{:08X};", m_watch_point->GetAddress()); + else if (m_watch_point->GetType() == AccessPointType::BP_READ && triggeredReadWrite && !triggeredWrite) + response = fmt::format("rwatch:{:08X};", m_watch_point->GetAddress()); + else if (m_watch_point->GetType() == AccessPointType::BP_BOTH && triggeredReadWrite) + response = fmt::format("awatch:{:08X};", m_watch_point->GetAddress()); + + if (!response.empty()) + { + cemuLog_logDebug(LogType::Force, "Received matching breakpoint exception: {}", response); + auto nextInstructions = findNextInstruction(ppcInterpreterCurrentInstance->instructionPointer, ppcInterpreterCurrentInstance->spr.LR, ppcInterpreterCurrentInstance->spr.CTR); + for (MPTR nextInstr : nextInstructions) + { + auto bpIt = m_patchedInstructions.find(nextInstr); + if (bpIt == m_patchedInstructions.end()) + this->m_patchedInstructions.try_emplace(nextInstr, nextInstr, BreakpointType::BP_STEP_POINT, false, response); + else + bpIt->second.PauseOnNextInterrupt(); + } + } +} + +void GDBServer::HandleEntryStop(uint32 entryAddress) +{ + this->m_patchedInstructions.try_emplace(entryAddress, entryAddress, BreakpointType::BP_SINGLE, false, ""); + m_entry_point = entryAddress; +} \ No newline at end of file diff --git a/src/Cafe/HW/Espresso/Debugger/GDBStub.h b/src/Cafe/HW/Espresso/Debugger/GDBStub.h new file mode 100644 index 00000000..e198604d --- /dev/null +++ b/src/Cafe/HW/Espresso/Debugger/GDBStub.h @@ -0,0 +1,316 @@ +#pragma once + +#include "Common/precompiled.h" +#include "Common/socket.h" +#include "Cafe/OS/libs/coreinit/coreinit_Thread.h" + +#include + +class GDBServer { +public: + explicit GDBServer(uint16 port); + ~GDBServer(); + + bool Initialize(); + bool IsConnected() + { + return m_client_connected; + } + + void HandleEntryStop(uint32 entryAddress); + void HandleTrapInstruction(PPCInterpreter_t* hCPU); + void HandleAccessException(uint64 dr6); + + enum class CMDType : char + { + INVALID = '\0', + // Extended commands + QUERY_GET = 'q', + QUERY_SET = 'Q', + VCONT = 'v', + // Normal commands + CONTINUE = 'c', + IS_THREAD_RUNNING = 'T', + SET_ACTIVE_THREAD = 'H', + ACTIVE_THREAD_STATUS = '?', + ACTIVE_THREAD_STEP = 's', + REGISTER_READ = 'p', + REGISTER_SET = 'P', + REGISTERS_READ = 'g', + REGISTERS_WRITE = 'G', + MEMORY_READ = 'm', + MEMORY_WRITE = 'M', + BREAKPOINT_SET = 'Z', + BREAKPOINT_REMOVE = 'z', + }; + + class CommandContext { + public: + CommandContext(const GDBServer* server, const std::string& command) + : m_server(server), m_command(command) + { + std::smatch matches; + std::regex_match(command, matches, m_regex); + for (size_t i = 1; i < matches.size(); i++) + { + auto matchStr = matches[i].str(); + if (!matchStr.empty()) + m_args.emplace_back(std::move(matchStr)); + } + // send acknowledgement ahead of response + send(m_server->m_client_socket, RESPONSE_ACK.data(), (int)RESPONSE_ACK.size(), 0); + }; + ~CommandContext() + { + // cemuLog_logDebug(LogType::Force, "[GDBStub] Received: {}", m_command); + // cemuLog_logDebug(LogType::Force, "[GDBStub] Responded: +{}", m_response); + auto response_data = EscapeMessage(m_response); + auto response_full = fmt::format("${}#{:02x}", response_data, CalculateChecksum(response_data)); + send(m_server->m_client_socket, response_full.c_str(), (int)response_full.size(), 0); + } + CommandContext(const CommandContext&) = delete; + + [[nodiscard]] const std::string& GetCommand() const + { + return m_command; + }; + [[nodiscard]] const std::vector& GetArgs() const + { + return m_args; + }; + [[nodiscard]] bool IsValid() const + { + return !m_args.empty(); + }; + [[nodiscard]] CMDType GetType() const + { + return static_cast(m_command[0]); + }; + + // Respond Utils + static uint8 CalculateChecksum(std::string_view message_data) + { + return std::accumulate(message_data.begin(), message_data.end(), (uint8)0, std::plus<>()); + } + static std::string EscapeXMLString(std::string_view xml_data) + { + std::string escaped; + escaped.reserve(xml_data.size()); + for (char c : xml_data) + { + switch (c) + { + case '<': escaped += "<"; break; + case '>': escaped += ">"; break; + case '&': escaped += "&"; break; + case '"': escaped += """; break; + case '\'': escaped += "'"; break; + default: escaped += c; break; + } + } + return escaped; + } + static std::string EscapeMessage(std::string_view message) + { + std::string escaped; + escaped.reserve(message.size()); + for (char c : message) + { + if (c == '#' || c == '$' || c == '}' || c == '*') + { + escaped.push_back('}'); + escaped.push_back((char)(c ^ 0x20)); + } + else + escaped.push_back(c); + } + return escaped; + } + void QueueResponse(std::string_view data) + { + m_response += data; + } + + private: + const std::regex m_regex{ + R"((?:)" + R"((\?))" + R"(|(vCont\?))" + R"(|(vCont;)([a-zA-Z0-9-+=,\+:;]+))" + R"(|(qAttached))" + R"(|(qSupported):([a-zA-Z0-9-+=,\+;]+))" + R"(|(qTStatus))" + R"(|(qC))" + R"(|(qXfer):((?:features)|(?:threads)|(?:libraries)):read:([\w\.]*):([0-9a-zA-Z]+),([0-9a-zA-Z]+))" + R"(|(qfThreadInfo))" + R"(|(qsThreadInfo))" + R"(|(T)((?:-1)|(?:[0-9A-Fa-f]+)))" + R"(|(D))" // Detach + R"(|(H)(c|g)((?:-1)|(?:[0-9A-Fa-f]+)))" // Set active thread for other operations (not c) + R"(|(c)([0-9A-Fa-f]+)?)" // (Legacy, supported by vCont) Continue all for active thread + R"(|([Zz])([0-4]),([0-9A-Fa-f]+),([0-9]))" // Insert/delete breakpoints + R"(|(g))" // Read registers for active thread + R"(|(G)([0-9A-Fa-f]+))" // Write registers for active thread + R"(|(p)([0-9A-Fa-f]+))" // Read register for active thread + R"(|(P)([0-9A-Fa-f]+)=([0-9A-Fa-f]+))" // Write register for active thread + R"(|(m)([0-9A-Fa-f]+),([0-9A-Fa-f]+))" // Read memory + R"(|(M)([0-9A-Fa-f]+),([0-9A-Fa-f]+):([0-9A-Fa-f]+))" // Write memory + // R"(|(X)([0-9A-Fa-f]+),([0-9A-Fa-f]+):([0-9A-Fa-f]+))" // Write memory + R"())"}; + const GDBServer* m_server; + const std::string m_command; + std::vector m_args; + std::string m_response; + }; + + class ExecutionBreakpoint; + std::map m_patchedInstructions; + + class AccessBreakpoint; + std::unique_ptr m_watch_point; + +private: + static constexpr int s_maxGDBClients = 1; + static constexpr std::string_view s_supportedFeatures = "PacketSize=4096;qXfer:features:read+;qXfer:threads:read+;qXfer:libraries:read+;swbreak+;hwbreak+;vContSupported+"; + static constexpr size_t s_maxPacketSize = 1024 * 4; + const uint16 m_port; + + enum RegisterID + { + R0_START = 0, + R31_END = R0_START + 31, + PC = 64, + MSR = 65, + CR = 66, + LR = 67, + CTR = 68, + XER = 69, + F0_START = 71, + F31_END = F0_START + 31, + FPSCR = 103 + }; + + static constexpr std::string_view RESPONSE_EMPTY = ""; + static constexpr std::string_view RESPONSE_ACK = "+"; + static constexpr std::string_view RESPONSE_NACK = "-"; + static constexpr std::string_view RESPONSE_OK = "OK"; + static constexpr std::string_view RESPONSE_ERROR = "E01"; + + void ThreadFunc(); + std::atomic_bool m_stopRequested; + void HandleCommand(const std::string& command_str); + void HandleQuery(std::unique_ptr& context) const; + void HandleVCont(std::unique_ptr& context); + + // Commands + sint64 m_activeThreadSelector = 0; + sint64 m_activeThreadContinueSelector = 0; + void CMDContinue(std::unique_ptr& context); + void CMDNotFound(std::unique_ptr& context); + void CMDIsThreadActive(std::unique_ptr& context); + void CMDSetActiveThread(std::unique_ptr& context); + void CMDGetThreadStatus(std::unique_ptr& context); + + void CMDReadRegister(std::unique_ptr& context) const; + void CMDWriteRegister(std::unique_ptr& context) const; + void CMDReadRegisters(std::unique_ptr& context) const; + void CMDWriteRegisters(std::unique_ptr& context) const; + void CMDReadMemory(std::unique_ptr& context); + void CMDWriteMemory(std::unique_ptr& context); + void CMDInsertBreakpoint(std::unique_ptr& context); + void CMDDeleteBreakpoint(std::unique_ptr& context); + + std::thread m_thread; + std::atomic_bool m_resume_startup = false; + MPTR m_entry_point{}; + std::unique_ptr m_resumed_context; + + std::atomic_bool m_client_connected; + SOCKET m_server_socket = INVALID_SOCKET; + sockaddr_in m_server_addr{}; + SOCKET m_client_socket = INVALID_SOCKET; + sockaddr_in m_client_addr{}; +}; + +static constexpr std::string_view GDBTargetXML = R"( + + + powerpc:common + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; + +extern std::unique_ptr g_gdbstub; diff --git a/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterImpl.cpp b/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterImpl.cpp index 8cc8a6a8..1d32c47f 100644 --- a/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterImpl.cpp +++ b/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterImpl.cpp @@ -1,6 +1,7 @@ #include "PPCInterpreterInternal.h" #include "PPCInterpreterHelper.h" #include "Cafe/HW/Espresso/Debugger/Debugger.h" +#include "Cafe/HW/Espresso/Debugger/GDBStub.h" class PPCItpCafeOSUsermode { diff --git a/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterOPC.hpp b/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterOPC.hpp index 4da41590..718162be 100644 --- a/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterOPC.hpp +++ b/src/Cafe/HW/Espresso/Interpreter/PPCInterpreterOPC.hpp @@ -65,9 +65,12 @@ static void PPCInterpreter_MFTB(PPCInterpreter_t* hCPU, uint32 opcode) static void PPCInterpreter_TW(PPCInterpreter_t* hCPU, uint32 opcode) { sint32 to, rA, rB; - PPC_OPC_TEMPL_X(opcode, to, rB, rA); + PPC_OPC_TEMPL_X(opcode, to, rA, rB); cemu_assert_debug(to == 0); - debugger_enterTW(hCPU); + if (rA == DEBUGGER_BP_T_DEBUGGER) + debugger_enterTW(hCPU); + else if (rA == DEBUGGER_BP_T_GDBSTUB) + g_gdbstub->HandleTrapInstruction(hCPU); } diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp b/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp index 90ab8fb9..2762b48f 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp @@ -671,6 +671,11 @@ namespace coreinit __OSUnlockScheduler(); } + void __OSSuspendThreadNolock(OSThread_t* thread) + { + __OSSuspendThreadInternal(thread); + } + void OSSleepThread(OSThreadQueue* threadQueue) { __OSLockScheduler(); @@ -798,7 +803,18 @@ namespace coreinit return suspendCounter > 0; } - void OSCancelThread(OSThread_t* thread) + bool OSIsThreadRunning(OSThread_t* thread) + { + bool isRunning = false; + __OSLockScheduler(); + if (thread->state == OSThread_t::THREAD_STATE::STATE_RUNNING) + isRunning = true; + __OSUnlockScheduler(); + return isRunning; + } + + + void OSCancelThread(OSThread_t* thread) { __OSLockScheduler(); cemu_assert_debug(thread->requestFlags == 0 || thread->requestFlags == OSThread_t::REQUEST_FLAG_CANCEL); // todo - how to handle cases where other flags are already set? @@ -1315,6 +1331,7 @@ namespace coreinit cafeExportRegister("coreinit", OSResumeThread, LogType::CoreinitThread); cafeExportRegister("coreinit", OSContinueThread, LogType::CoreinitThread); cafeExportRegister("coreinit", OSSuspendThread, LogType::CoreinitThread); + cafeExportRegister("coreinit", __OSSuspendThreadNolock, LogType::CoreinitThread); cafeExportRegister("coreinit", OSSleepThread, LogType::CoreinitThread); cafeExportRegister("coreinit", OSWakeupThread, LogType::CoreinitThread); diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Thread.h b/src/Cafe/OS/libs/coreinit/coreinit_Thread.h index bb0aca91..f1c39bd4 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Thread.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_Thread.h @@ -461,10 +461,10 @@ struct OSThread_t /* +0x628 */ uint64 wakeTimeRelatedUkn2; // set via OSSetExceptionCallback - /* +0x630 */ MPTR ukn630Callback[Espresso::CORE_COUNT]; - /* +0x63C */ MPTR ukn63CCallback[Espresso::CORE_COUNT]; - /* +0x648 */ MPTR ukn648Callback[Espresso::CORE_COUNT]; - /* +0x654 */ MPTR ukn654Callback[Espresso::CORE_COUNT]; + /* +0x630 */ MPTR dsiCallback[Espresso::CORE_COUNT]; + /* +0x63C */ MPTR isiCallback[Espresso::CORE_COUNT]; + /* +0x648 */ MPTR programCallback[Espresso::CORE_COUNT]; + /* +0x654 */ MPTR perfMonCallback[Espresso::CORE_COUNT]; /* +0x660 */ uint32 ukn660; @@ -514,6 +514,7 @@ namespace coreinit sint32 OSResumeThread(OSThread_t* thread); void OSContinueThread(OSThread_t* thread); void __OSSuspendThreadInternal(OSThread_t* thread); + void __OSSuspendThreadNolock(OSThread_t* thread); void OSSuspendThread(OSThread_t* thread); void OSSleepThread(OSThreadQueue* threadQueue); void OSWakeupThread(OSThreadQueue* threadQueue); @@ -525,6 +526,7 @@ namespace coreinit bool OSIsThreadTerminated(OSThread_t* thread); bool OSIsThreadSuspended(OSThread_t* thread); + bool OSIsThreadRunning(OSThread_t* thread); // OSThreadQueue void OSInitThreadQueue(OSThreadQueue* threadQueue); diff --git a/src/Cafe/OS/libs/coreinit/coreinit_ThreadQueue.cpp b/src/Cafe/OS/libs/coreinit/coreinit_ThreadQueue.cpp index 79318c55..38302d5a 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_ThreadQueue.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_ThreadQueue.cpp @@ -135,7 +135,7 @@ namespace coreinit while (OSThread_t* thread = takeFirstFromQueue(offsetof(OSThread_t, waitQueueLink))) { cemu_assert_debug(thread->state == OSThread_t::THREAD_STATE::STATE_WAITING); - cemu_assert_debug(thread->suspendCounter == 0); + //cemu_assert_debug(thread->suspendCounter == 0); thread->state = OSThread_t::THREAD_STATE::STATE_READY; thread->currentWaitQueue = nullptr; coreinit::__OSAddReadyThreadToRunQueue(thread); diff --git a/src/Cafe/OS/libs/nsysnet/nsysnet.cpp b/src/Cafe/OS/libs/nsysnet/nsysnet.cpp index 5a8081e2..7fecd848 100644 --- a/src/Cafe/OS/libs/nsysnet/nsysnet.cpp +++ b/src/Cafe/OS/libs/nsysnet/nsysnet.cpp @@ -73,6 +73,15 @@ void nsysnetExport_socket_lib_init(PPCInterpreter_t* hCPU) osLib_returnFromFunction(hCPU, 0); // 0 -> Success } +void nsysnetExport_socket_lib_finish(PPCInterpreter_t* hCPU) +{ + sockLibReady = false; +#if BOOST_OS_WINDOWS + WSACleanup(); +#endif // BOOST_OS_WINDOWS + osLib_returnFromFunction(hCPU, 0); // 0 -> Success +} + uint32* __gh_errno_ptr() { OSThread_t* osThread = coreinitThread_getCurrentThreadDepr(PPCInterpreter_getCurrentInstance()); @@ -2120,6 +2129,7 @@ void nsysnet_load() { osLib_addFunction("nsysnet", "socket_lib_init", nsysnetExport_socket_lib_init); + osLib_addFunction("nsysnet", "socket_lib_finish", nsysnetExport_socket_lib_finish); // socket API osLib_addFunction("nsysnet", "socket", nsysnetExport_socket); diff --git a/src/Common/ExceptionHandler/ExceptionHandler_win32.cpp b/src/Common/ExceptionHandler/ExceptionHandler_win32.cpp index 565f4f8f..c793e434 100644 --- a/src/Common/ExceptionHandler/ExceptionHandler_win32.cpp +++ b/src/Common/ExceptionHandler/ExceptionHandler_win32.cpp @@ -9,6 +9,7 @@ #include "Config/CemuConfig.h" #include "Cafe/OS/libs/coreinit/coreinit_Thread.h" #include "Cafe/HW/Espresso/PPCState.h" +#include "Cafe/HW/Espresso/Debugger/GDBStub.h" extern uint32 currentBaseApplicationHash; extern uint32 currentUpdatedApplicationHash; @@ -378,7 +379,8 @@ int crashlogThread(void* exceptionInfoRawPtr) return 0; } -void debugger_handleSingleStepException(uint32 drMask); +void debugger_handleSingleStepException(uint64 dr6); + LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) { @@ -387,7 +389,11 @@ LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) LONG r = handleException_SINGLE_STEP(pExceptionInfo); if (r != EXCEPTION_CONTINUE_SEARCH) return r; - debugger_handleSingleStepException(pExceptionInfo->ContextRecord->Dr6 & 0xF); + + if (GetBits(pExceptionInfo->ContextRecord->Dr6, 0, 1) || GetBits(pExceptionInfo->ContextRecord->Dr6, 1, 1)) + debugger_handleSingleStepException(pExceptionInfo->ContextRecord->Dr6); + else if (GetBits(pExceptionInfo->ContextRecord->Dr6, 2, 1) || GetBits(pExceptionInfo->ContextRecord->Dr6, 3, 1)) + g_gdbstub->HandleAccessException(pExceptionInfo->ContextRecord->Dr6); return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; diff --git a/src/Common/precompiled.h b/src/Common/precompiled.h index 6183b852..939e3ec4 100644 --- a/src/Common/precompiled.h +++ b/src/Common/precompiled.h @@ -69,11 +69,12 @@ #include #include #include -#include +#include #include #include #include #include +#include #include #include @@ -208,6 +209,21 @@ typedef union _LARGE_INTEGER { inline T& operator^= (T& a, T b) { return reinterpret_cast( reinterpret_cast::type&>(a) ^= static_cast::type>(b) ); } #endif +template +inline T GetBits(T value, uint32 index, uint32 numBits) +{ + T mask = (1<>index) & mask; +} + +template +inline void SetBits(T& value, uint32 index, uint32 numBits, uint32 bitValue) +{ + T mask = (1< crash_dump{ CrashDump::Disabled }; + ConfigValue gdb_port{ 1337 }; void Load(XMLConfigParser& parser); void Save(XMLConfigParser& parser); diff --git a/src/config/LaunchSettings.cpp b/src/config/LaunchSettings.cpp index 4447f9bc..11332695 100644 --- a/src/config/LaunchSettings.cpp +++ b/src/config/LaunchSettings.cpp @@ -68,6 +68,7 @@ bool LaunchSettings::HandleCommandline(const std::vector& args) ("account,a", po::value(), "Persistent id of account") ("force-interpreter", po::value()->implicit_value(true), "Force interpreter CPU emulation, disables recompiler") + ("enable-gdbstub", po::value()->implicit_value(true), "Enable GDB stub to debug executables inside Cemu using an external debugger") ("act-url", po::value(), "URL prefix for account server") ("ecs-url", po::value(), "URL for ECS service"); @@ -162,6 +163,9 @@ bool LaunchSettings::HandleCommandline(const std::vector& args) if(vm.count("force-interpreter")) s_force_interpreter = vm["force-interpreter"].as(); + + if (vm.count("enable-gdbstub")) + s_enable_gdbstub = vm["enable-gdbstub"].as(); std::wstring extract_path, log_path; std::string output_path; diff --git a/src/config/LaunchSettings.h b/src/config/LaunchSettings.h index b42523ca..a916679c 100644 --- a/src/config/LaunchSettings.h +++ b/src/config/LaunchSettings.h @@ -21,6 +21,7 @@ public: static std::optional RenderUpsideDownEnabled() { return s_render_upside_down; } static std::optional FullscreenEnabled() { return s_fullscreen; } + static bool GDBStubEnabled() { return s_enable_gdbstub; } static bool NSightModeEnabled() { return s_nsight_mode; } static bool ForceIntelLegacyEnabled() { return s_force_intel_legacy; } @@ -39,6 +40,7 @@ private: inline static std::optional s_render_upside_down{}; inline static std::optional s_fullscreen{}; + inline static bool s_enable_gdbstub = false; inline static bool s_nsight_mode = false; inline static bool s_force_intel_legacy = false; diff --git a/src/gui/GeneralSettings2.cpp b/src/gui/GeneralSettings2.cpp index ac1d3f3b..9a110332 100644 --- a/src/gui/GeneralSettings2.cpp +++ b/src/gui/GeneralSettings2.cpp @@ -770,27 +770,42 @@ wxPanel* GeneralSettings2::AddDebugPage(wxNotebook* notebook) auto* panel = new wxPanel(notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); auto* debug_panel_sizer = new wxBoxSizer(wxVERTICAL); - auto* debug_row = new wxFlexGridSizer(0, 2, 0, 0); - debug_row->SetFlexibleDirection(wxBOTH); - debug_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); + { + auto* debug_row = new wxFlexGridSizer(0, 2, 0, 0); + debug_row->SetFlexibleDirection(wxBOTH); + debug_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - debug_row->Add(new wxStaticText(panel, wxID_ANY, _("Crash dump"), wxDefaultPosition, wxDefaultSize, 0), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); + debug_row->Add(new wxStaticText(panel, wxID_ANY, _("Crash dump"), wxDefaultPosition, wxDefaultSize, 0), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); #if BOOST_OS_WINDOWS - wxString dump_choices[] = { _("Disabled"), _("Lite"), _("Full") }; + wxString dump_choices[] = {_("Disabled"), _("Lite"), _("Full")}; #elif BOOST_OS_UNIX - wxString dump_choices[] = { _("Disabled"), _("Enabled") }; + wxString dump_choices[] = {_("Disabled"), _("Enabled")}; #endif - m_crash_dump = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(dump_choices), dump_choices); - m_crash_dump->SetSelection(0); + m_crash_dump = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(dump_choices), dump_choices); + m_crash_dump->SetSelection(0); #if BOOST_OS_WINDOWS - m_crash_dump->SetToolTip(_("Creates a dump when Cemu crashes\nOnly enable when requested by a developer!\nThe Full option will create a very large dump file (includes a full RAM dump of the Cemu process)")); + m_crash_dump->SetToolTip(_("Creates a dump when Cemu crashes\nOnly enable when requested by a developer!\nThe Full option will create a very large dump file (includes a full RAM dump of the Cemu process)")); #elif BOOST_OS_UNIX - m_crash_dump->SetToolTip(_("Creates a core dump when Cemu crashes\nOnly enable when requested by a developer!")); + m_crash_dump->SetToolTip(_("Creates a core dump when Cemu crashes\nOnly enable when requested by a developer!")); #endif - debug_row->Add(m_crash_dump, 0, wxALL | wxEXPAND, 5); + debug_row->Add(m_crash_dump, 0, wxALL | wxEXPAND, 5); + debug_panel_sizer->Add(debug_row, 0, wxALL | wxEXPAND, 5); + } - debug_panel_sizer->Add(debug_row, 0, wxALL | wxEXPAND, 5); + { + auto* debug_row = new wxFlexGridSizer(0, 2, 0, 0); + debug_row->SetFlexibleDirection(wxBOTH); + debug_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); + + debug_row->Add(new wxStaticText(panel, wxID_ANY, _("GDB Stub port"), wxDefaultPosition, wxDefaultSize, 0), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); + + m_gdb_port = new wxSpinCtrl(panel, wxID_ANY, wxT("1337"), wxDefaultPosition, wxDefaultSize, 0, 1000, 65535); + m_gdb_port->SetToolTip(_("Changes the port that the GDB stub will use, which you can use by either starting Cemu with the --enable-gdbstub option or by enabling it the Debug tab.")); + + debug_row->Add(m_gdb_port, 0, wxALL | wxEXPAND, 5); + debug_panel_sizer->Add(debug_row, 0, wxALL | wxEXPAND, 5); + } panel->SetSizerAndFit(debug_panel_sizer); @@ -1002,6 +1017,7 @@ void GeneralSettings2::StoreConfig() // debug config.crash_dump = (CrashDump)m_crash_dump->GetSelection(); + config.gdb_port = m_gdb_port->GetValue(); g_config.Save(); } @@ -1619,6 +1635,7 @@ void GeneralSettings2::ApplyConfig() // debug m_crash_dump->SetSelection((int)config.crash_dump.GetValue()); + m_gdb_port->SetValue(config.gdb_port.GetValue()); } void GeneralSettings2::OnOnlineEnable(wxCommandEvent& event) diff --git a/src/gui/GeneralSettings2.h b/src/gui/GeneralSettings2.h index 0ae732eb..e519078c 100644 --- a/src/gui/GeneralSettings2.h +++ b/src/gui/GeneralSettings2.h @@ -74,6 +74,7 @@ private: // Debug wxChoice* m_crash_dump; + wxSpinCtrl* m_gdb_port; void OnAccountCreate(wxCommandEvent& event); void OnAccountDelete(wxCommandEvent& event); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 4e82ad5d..8af36b18 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -124,6 +124,7 @@ enum // debug MAINFRAME_MENU_ID_DEBUG_RENDER_UPSIDE_DOWN = 21100, MAINFRAME_MENU_ID_DEBUG_VIEW_LOGGING_WINDOW, + MAINFRAME_MENU_ID_DEBUG_TOGGLE_GDB_STUB, MAINFRAME_MENU_ID_DEBUG_VIEW_PPC_THREADS, MAINFRAME_MENU_ID_DEBUG_VIEW_PPC_DEBUGGER, MAINFRAME_MENU_ID_DEBUG_VIEW_AUDIO_DEBUGGER, @@ -183,7 +184,7 @@ EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_INPUT, MainWindow::OnOptionsInput) EVT_MENU(MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER, MainWindow::OnToolsInput) EVT_MENU(MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER, MainWindow::OnToolsInput) EVT_MENU(MAINFRAME_MENU_ID_TOOLS_DOWNLOAD_MANAGER, MainWindow::OnToolsInput) -//// cpu menu +// cpu menu EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_8X, MainWindow::OnDebugSetting) EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_4X, MainWindow::OnDebugSetting) EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_2X, MainWindow::OnDebugSetting) @@ -209,6 +210,7 @@ EVT_MENU(MAINFRAME_MENU_ID_DEBUG_DUMP_RAM, MainWindow::OnDebugSetting) EVT_MENU(MAINFRAME_MENU_ID_DEBUG_DUMP_FST, MainWindow::OnDebugSetting) // debug -> View ... EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VIEW_LOGGING_WINDOW, MainWindow::OnLoggingWindow) +EVT_MENU(MAINFRAME_MENU_ID_DEBUG_TOGGLE_GDB_STUB, MainWindow::OnGDBStubToggle) EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VIEW_PPC_THREADS, MainWindow::OnDebugViewPPCThreads) EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VIEW_PPC_DEBUGGER, MainWindow::OnDebugViewPPCDebugger) EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VIEW_AUDIO_DEBUGGER, MainWindow::OnDebugViewAudioDebugger) @@ -344,6 +346,10 @@ MainWindow::MainWindow() { MainWindow::RequestLaunchGame(LaunchSettings::GetLoadFile().value(), wxLaunchGameEvent::INITIATED_BY::COMMAND_LINE); } + if (LaunchSettings::GDBStubEnabled()) + { + g_gdbstub = std::make_unique(config.gdb_port); + } } MainWindow::~MainWindow() @@ -1108,6 +1114,18 @@ void MainWindow::OnLoggingWindow(wxCommandEvent& event) m_logging_window->Show(true); } +void MainWindow::OnGDBStubToggle(wxCommandEvent& event) +{ + if (g_gdbstub) + { + g_gdbstub.release(); + return; + } + + const auto& config = GetConfig(); + g_gdbstub = std::make_unique(config.gdb_port); +} + void MainWindow::OnDebugViewPPCThreads(wxCommandEvent& event) { auto frame = new DebugPPCThreadsWindow(*this); @@ -2168,6 +2186,10 @@ void MainWindow::RecreateMenu() #ifdef CEMU_DEBUG_ASSERT debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_VIEW_LOGGING_WINDOW, _("&Open logging window")); #endif + m_gdbstub_toggle = debugMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_TOGGLE_GDB_STUB, _("&Launch with GDB stub"), wxEmptyString); + m_gdbstub_toggle->Check(g_gdbstub != nullptr); + m_gdbstub_toggle->Enable(!m_game_launched); + debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_VIEW_PPC_THREADS, _("&View PPC threads")); debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_VIEW_PPC_DEBUGGER, _("&View PPC debugger")); debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_VIEW_AUDIO_DEBUGGER, _("&View audio debugger")); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 9e0d47bf..735f73af 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -14,6 +14,7 @@ #include "gui/components/wxGameList.h" #include +#include "Cafe/HW/Espresso/Debugger/GDBStub.h" class DebuggerWindow2; struct GameEntry; @@ -106,6 +107,7 @@ public: void OnDebugDumpUsedTextures(wxCommandEvent& event); void OnDebugDumpUsedShaders(wxCommandEvent& event); void OnLoggingWindow(wxCommandEvent& event); + void OnGDBStubToggle(wxCommandEvent& event); void OnDebugViewPPCThreads(wxCommandEvent& event); void OnDebugViewPPCDebugger(wxCommandEvent& event); void OnDebugViewAudioDebugger(wxCommandEvent& event); @@ -171,6 +173,7 @@ private: std::string m_launched_game_name; + wxMenuItem* m_gdbstub_toggle{}; DebuggerWindow2* m_debugger_window = nullptr; LoggingWindow* m_logging_window = nullptr; diff --git a/src/gui/debugger/DebuggerWindow2.cpp b/src/gui/debugger/DebuggerWindow2.cpp index 6263f0b7..f8c5c931 100644 --- a/src/gui/debugger/DebuggerWindow2.cpp +++ b/src/gui/debugger/DebuggerWindow2.cpp @@ -66,8 +66,6 @@ wxBEGIN_EVENT_TABLE(DebuggerWindow2, wxFrame) EVT_COMMAND(wxID_ANY, wxEVT_NOTIFY_MODULE_UNLOADED, DebuggerWindow2::OnNotifyModuleUnloaded) // file menu EVT_MENU(MENU_ID_FILE_EXIT, DebuggerWindow2::OnExit) - // setting - EVT_MENU(MENU_ID_OPTIONS_PIN_TO_MAINWINDOW, DebuggerWindow2::OnOptionsInput) // window EVT_MENU_RANGE(MENU_ID_WINDOW_REGISTERS, MENU_ID_WINDOW_MODULE, DebuggerWindow2::OnWindowMenu) wxEND_EVENT_TABLE() @@ -470,7 +468,7 @@ bool DebuggerWindow2::Show(bool show) std::wstring DebuggerWindow2::GetModuleStoragePath(std::string module_name, uint32_t crc_hash) const { - if (module_name.empty() || crc_hash == 0) return std::wstring(); + if (module_name.empty() || crc_hash == 0) return {}; return ActiveSettings::GetConfigPath("debugger/{}_{:#10x}.xml", module_name, crc_hash).generic_wstring(); } @@ -529,24 +527,24 @@ void DebuggerWindow2::OnBreakpointChange(wxCommandEvent& event) void DebuggerWindow2::OnOptionsInput(wxCommandEvent& event) { - switch(event.GetId()) + switch (event.GetId()) { case MENU_ID_OPTIONS_PIN_TO_MAINWINDOW: - { - const bool value = !m_config.data().pin_to_main; - m_config.data().pin_to_main = value; - if(value) - OnParentMove(m_main_position, m_main_size); - - break; - } + { + const bool value = !m_config.data().pin_to_main; + m_config.data().pin_to_main = value; + if (value) + OnParentMove(m_main_position, m_main_size); + + break; + } case MENU_ID_OPTIONS_BREAK_ON_START: - { + { const bool value = !m_config.data().break_on_start; m_config.data().break_on_start = value; debuggerState.breakOnEntry = value; break; - } + } default: return; } diff --git a/src/gui/debugger/DebuggerWindow2.h b/src/gui/debugger/DebuggerWindow2.h index bae41196..0ca44c44 100644 --- a/src/gui/debugger/DebuggerWindow2.h +++ b/src/gui/debugger/DebuggerWindow2.h @@ -4,6 +4,7 @@ #include "config/XMLConfig.h" #include "Cafe/HW/Espresso/Debugger/Debugger.h" #include "Cafe/OS/RPL/rpl.h" +#include "Cafe/HW/Espresso/Debugger/GDBStub.h" #include #include @@ -26,10 +27,9 @@ wxDECLARE_EVENT(wxEVT_NOTIFY_MODULE_UNLOADED, wxCommandEvent); struct DebuggerConfig { DebuggerConfig() - : pin_to_main(true), break_on_start(true), show_register(true), show_dump(true), show_stack(true), show_breakpoints(true), show_modules(true) {} - + : pin_to_main(true), break_on_start(true), show_register(true), show_dump(true), show_stack(true), show_breakpoints(true), show_modules(true), show_symbols(true) {} + bool pin_to_main; - bool break_on_start; bool show_register; @@ -95,7 +95,7 @@ private: wxPoint m_main_position; wxSize m_main_size; - + RegisterWindow* m_register_window; DumpWindow* m_dump_window; BreakpointWindow* m_breakpoint_window; diff --git a/src/gui/wxgui.h b/src/gui/wxgui.h index f09ea420..098449d6 100644 --- a/src/gui/wxgui.h +++ b/src/gui/wxgui.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include