mirror of
https://github.com/cemu-project/Cemu.git
synced 2024-11-28 03:54:18 +01:00
Add GDB stub for debugging (#657)
* Implement GDB stub debugger Can be enabled by using the "--enable-gdbstub" option (and the debugger GUI, although that's untested) which'll pause any game you launch at start-up. Will start at port 1337 although it'll eventually be user-editable. The code is a bit weirdly sorted and also just needs a general cleanup, so expect that eventually too. And uses egyptian braces but formatting was easier to do at the end, so that's also something to do. It has been tested to work with IDA Pro, Clion and the standalone interface for now, but I plan on writing some instructions in the PR to follow for people who want to use this. Memory breakpoints aren't possible yet, only execution breakpoints. This code was aimed to be decoupled from the existing debugger to be able to be ported to the Wii U for an equal debugging experience. That's also why it uses the Cafe OS's thread sleep and resuming functions whenever possible instead of using recompiler/interpreter controls. * Add memory writing and floating point registers support * Reformat code a bit * Format code to adhere to Cemu's coding style * Rework GDB Stub settings in GUI * Small styling fixes * Rework execution breakpoints Should work better in some edge cases now. But this should also allow for adding access breakpoints since it's now more separated. * Implement access breakpoints * Fix some issues with breakpoints * Fix includes for Linux * Fix unnecessary include * Tweaks for Linux compatibility * Use std::thread instead of std::jthread to fix MacOS support * Enable GDB read/write breakpoints on x86 only * Fix compilation for GCC compilers at least The thread type varies on some platforms, so supporting this is hell... but let's get it to compile on MacOS first. * Disable them for MacOS due to lack of ptrace --------- Co-authored-by: Exzap <13877693+Exzap@users.noreply.github.com>
This commit is contained in:
parent
05d82b09e9
commit
6d75776b28
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,6 +17,7 @@
|
||||
.idea/
|
||||
|
||||
build/
|
||||
cmake-build-*-*/
|
||||
out/
|
||||
.cache/
|
||||
bin/Cemu_*
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -30,6 +30,7 @@
|
||||
#include "GamePatch.h"
|
||||
|
||||
#include <time.h>
|
||||
#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 -------");
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -3,11 +3,6 @@
|
||||
#include <set>
|
||||
#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
|
||||
{
|
||||
|
286
src/Cafe/HW/Espresso/Debugger/GDBBreakpoints.h
Normal file
286
src/Cafe/HW/Espresso/Debugger/GDBBreakpoints.h
Normal file
@ -0,0 +1,286 @@
|
||||
#include <utility>
|
||||
|
||||
#if BOOST_OS_LINUX
|
||||
#include <sys/ptrace.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/user.h>
|
||||
|
||||
// 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<std::thread::native_handle_type>& 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;
|
||||
};
|
979
src/Cafe/HW/Espresso/Debugger/GDBStub.cpp
Normal file
979
src/Cafe/HW/Espresso/Debugger/GDBStub.cpp
Normal file
@ -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<MPTR> 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<typename F>
|
||||
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<typename F>
|
||||
static void selectAndBreakThread(sint64 selectorId, F&& action_for_thread)
|
||||
{
|
||||
__OSLockScheduler();
|
||||
cemu_assert_debug(activeThreadCount != 0);
|
||||
|
||||
std::vector<OSThread_t*> 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<GDBServer::CommandContext> 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<OSThread_t*> 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<GDBServer> 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<uint8>(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<CommandContext>(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<CommandContext>& 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<std::string> 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"(<?xml version="1.0"?>)";
|
||||
threads_res += "<threads>";
|
||||
// note: clion seems to default to the first thread
|
||||
std::map<sint64, std::string> threads_list;
|
||||
selectThread(-1, [&threads_list](OSThread_t* thread) {
|
||||
std::string entry;
|
||||
entry += fmt::format(R"(<thread id="{:x}" core="{}")", GET_THREAD_ID(thread), thread->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("></thread>");
|
||||
threads_list.emplace(GET_THREAD_ID(thread), entry);
|
||||
});
|
||||
for (auto& entry : threads_list)
|
||||
{
|
||||
threads_res += entry.second;
|
||||
}
|
||||
threads_res += "</threads>";
|
||||
|
||||
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"(<?xml version="1.0"?>)";
|
||||
library_list += "<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"(<library name="{}"><segment address="{:#x}"/></library>)", CommandContext::EscapeXMLString(module_list[i]->moduleName2), module_list[i]->regionMappingBase_text.GetMPTR());
|
||||
}
|
||||
library_list += "</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<CommandContext>& 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<CommandContext>& context)
|
||||
{
|
||||
m_resumed_context = std::move(context);
|
||||
selectAndResumeThread(m_activeThreadContinueSelector);
|
||||
}
|
||||
|
||||
void GDBServer::CMDNotFound(std::unique_ptr<CommandContext>& context)
|
||||
{
|
||||
return context->QueueResponse(RESPONSE_EMPTY);
|
||||
}
|
||||
|
||||
void GDBServer::CMDIsThreadActive(std::unique_ptr<CommandContext>& 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<CommandContext>& 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<CommandContext>& context)
|
||||
{
|
||||
selectThread(0, [&context](OSThread_t* thread) {
|
||||
context->QueueResponse(fmt::format("T05thread:{:08X};", memory_getVirtualOffsetFromPointer(thread)));
|
||||
});
|
||||
}
|
||||
|
||||
void GDBServer::CMDReadRegister(std::unique_ptr<CommandContext>& 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<CommandContext>& 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<CommandContext>& 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<CommandContext>& 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<CommandContext>& 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<CommandContext>& 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<CommandContext>& context)
|
||||
{
|
||||
auto type = std::stoul(context->GetArgs()[1], nullptr, 16);
|
||||
MPTR addr = static_cast<MPTR>(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<AccessBreakpoint>(addr, (AccessPointType)type);
|
||||
}
|
||||
|
||||
return context->QueueResponse(RESPONSE_OK);
|
||||
}
|
||||
|
||||
void GDBServer::CMDDeleteBreakpoint(std::unique_ptr<CommandContext>& context)
|
||||
{
|
||||
auto type = std::stoul(context->GetArgs()[1], nullptr, 16);
|
||||
MPTR addr = static_cast<MPTR>(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<MPTR> 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;
|
||||
}
|
316
src/Cafe/HW/Espresso/Debugger/GDBStub.h
Normal file
316
src/Cafe/HW/Espresso/Debugger/GDBStub.h
Normal file
@ -0,0 +1,316 @@
|
||||
#pragma once
|
||||
|
||||
#include "Common/precompiled.h"
|
||||
#include "Common/socket.h"
|
||||
#include "Cafe/OS/libs/coreinit/coreinit_Thread.h"
|
||||
|
||||
#include <numeric>
|
||||
|
||||
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<std::string>& GetArgs() const
|
||||
{
|
||||
return m_args;
|
||||
};
|
||||
[[nodiscard]] bool IsValid() const
|
||||
{
|
||||
return !m_args.empty();
|
||||
};
|
||||
[[nodiscard]] CMDType GetType() const
|
||||
{
|
||||
return static_cast<CMDType>(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<std::string> m_args;
|
||||
std::string m_response;
|
||||
};
|
||||
|
||||
class ExecutionBreakpoint;
|
||||
std::map<MPTR, ExecutionBreakpoint> m_patchedInstructions;
|
||||
|
||||
class AccessBreakpoint;
|
||||
std::unique_ptr<AccessBreakpoint> 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<CommandContext>& context) const;
|
||||
void HandleVCont(std::unique_ptr<CommandContext>& context);
|
||||
|
||||
// Commands
|
||||
sint64 m_activeThreadSelector = 0;
|
||||
sint64 m_activeThreadContinueSelector = 0;
|
||||
void CMDContinue(std::unique_ptr<CommandContext>& context);
|
||||
void CMDNotFound(std::unique_ptr<CommandContext>& context);
|
||||
void CMDIsThreadActive(std::unique_ptr<CommandContext>& context);
|
||||
void CMDSetActiveThread(std::unique_ptr<CommandContext>& context);
|
||||
void CMDGetThreadStatus(std::unique_ptr<CommandContext>& context);
|
||||
|
||||
void CMDReadRegister(std::unique_ptr<CommandContext>& context) const;
|
||||
void CMDWriteRegister(std::unique_ptr<CommandContext>& context) const;
|
||||
void CMDReadRegisters(std::unique_ptr<CommandContext>& context) const;
|
||||
void CMDWriteRegisters(std::unique_ptr<CommandContext>& context) const;
|
||||
void CMDReadMemory(std::unique_ptr<CommandContext>& context);
|
||||
void CMDWriteMemory(std::unique_ptr<CommandContext>& context);
|
||||
void CMDInsertBreakpoint(std::unique_ptr<CommandContext>& context);
|
||||
void CMDDeleteBreakpoint(std::unique_ptr<CommandContext>& context);
|
||||
|
||||
std::thread m_thread;
|
||||
std::atomic_bool m_resume_startup = false;
|
||||
MPTR m_entry_point{};
|
||||
std::unique_ptr<CommandContext> 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"(<?xml version="1.0"?>
|
||||
<!DOCTYPE target SYSTEM "gdb-target.dtd">
|
||||
<target version="1.0">
|
||||
<architecture>powerpc:common</architecture>
|
||||
<feature name="org.gnu.gdb.power.core">
|
||||
<reg name="r0" bitsize="32" type="uint32"/>
|
||||
<reg name="r1" bitsize="32" type="uint32"/>
|
||||
<reg name="r2" bitsize="32" type="uint32"/>
|
||||
<reg name="r3" bitsize="32" type="uint32"/>
|
||||
<reg name="r4" bitsize="32" type="uint32"/>
|
||||
<reg name="r5" bitsize="32" type="uint32"/>
|
||||
<reg name="r6" bitsize="32" type="uint32"/>
|
||||
<reg name="r7" bitsize="32" type="uint32"/>
|
||||
<reg name="r8" bitsize="32" type="uint32"/>
|
||||
<reg name="r9" bitsize="32" type="uint32"/>
|
||||
<reg name="r10" bitsize="32" type="uint32"/>
|
||||
<reg name="r11" bitsize="32" type="uint32"/>
|
||||
<reg name="r12" bitsize="32" type="uint32"/>
|
||||
<reg name="r13" bitsize="32" type="uint32"/>
|
||||
<reg name="r14" bitsize="32" type="uint32"/>
|
||||
<reg name="r15" bitsize="32" type="uint32"/>
|
||||
<reg name="r16" bitsize="32" type="uint32"/>
|
||||
<reg name="r17" bitsize="32" type="uint32"/>
|
||||
<reg name="r18" bitsize="32" type="uint32"/>
|
||||
<reg name="r19" bitsize="32" type="uint32"/>
|
||||
<reg name="r20" bitsize="32" type="uint32"/>
|
||||
<reg name="r21" bitsize="32" type="uint32"/>
|
||||
<reg name="r22" bitsize="32" type="uint32"/>
|
||||
<reg name="r23" bitsize="32" type="uint32"/>
|
||||
<reg name="r24" bitsize="32" type="uint32"/>
|
||||
<reg name="r25" bitsize="32" type="uint32"/>
|
||||
<reg name="r26" bitsize="32" type="uint32"/>
|
||||
<reg name="r27" bitsize="32" type="uint32"/>
|
||||
<reg name="r28" bitsize="32" type="uint32"/>
|
||||
<reg name="r29" bitsize="32" type="uint32"/>
|
||||
<reg name="r30" bitsize="32" type="uint32"/>
|
||||
<reg name="r31" bitsize="32" type="uint32"/>
|
||||
<reg name="pc" bitsize="32" type="code_ptr" regnum="64"/>
|
||||
<reg name="msr" bitsize="32" type="uint32"/>
|
||||
<reg name="cr" bitsize="32" type="uint32"/>
|
||||
<reg name="lr" bitsize="32" type="code_ptr"/>
|
||||
<reg name="ctr" bitsize="32" type="uint32"/>
|
||||
<reg name="xer" bitsize="32" type="uint32"/>
|
||||
</feature>
|
||||
<feature name="org.gnu.gdb.power.fpu">
|
||||
<reg name="f0" bitsize="64" type="ieee_double" regnum="71"/>
|
||||
<reg name="f1" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f2" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f3" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f4" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f5" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f6" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f7" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f8" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f9" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f10" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f11" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f12" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f13" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f14" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f15" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f16" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f17" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f18" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f19" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f20" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f21" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f22" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f23" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f24" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f25" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f26" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f27" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f28" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f29" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f30" bitsize="64" type="ieee_double"/>
|
||||
<reg name="f31" bitsize="64" type="ieee_double"/>
|
||||
<reg name="fpscr" bitsize="32" group="float"/>
|
||||
</feature>
|
||||
</target>)";
|
||||
|
||||
extern std::unique_ptr<GDBServer> g_gdbstub;
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -69,11 +69,12 @@
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <time.h>
|
||||
#include <ctime>
|
||||
#include <regex>
|
||||
#include <type_traits>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <ranges>
|
||||
|
||||
#include <boost/predef.h>
|
||||
#include <boost/nowide/convert.hpp>
|
||||
@ -208,6 +209,21 @@ typedef union _LARGE_INTEGER {
|
||||
inline T& operator^= (T& a, T b) { return reinterpret_cast<T&>( reinterpret_cast<std::underlying_type<T>::type&>(a) ^= static_cast<std::underlying_type<T>::type>(b) ); }
|
||||
#endif
|
||||
|
||||
template<typename T>
|
||||
inline T GetBits(T value, uint32 index, uint32 numBits)
|
||||
{
|
||||
T mask = (1<<numBits)-1;
|
||||
return (value>>index) & mask;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline void SetBits(T& value, uint32 index, uint32 numBits, uint32 bitValue)
|
||||
{
|
||||
T mask = (1<<numBits)-1;
|
||||
value &= ~(mask << index);
|
||||
value |= (bitValue << index);
|
||||
}
|
||||
|
||||
#if !defined(_MSC_VER) || defined(__clang__) // clang-cl does not have built-in _udiv128
|
||||
inline uint64 _udiv128(uint64 highDividend, uint64 lowDividend, uint64 divisor, uint64 *remainder)
|
||||
{
|
||||
|
@ -336,6 +336,7 @@ void CemuConfig::Load(XMLConfigParser& parser)
|
||||
#elif BOOST_OS_UNIX
|
||||
crash_dump = debug.get("CrashDumpUnix", crash_dump);
|
||||
#endif
|
||||
gdb_port = debug.get("GDBPort", 1337);
|
||||
|
||||
// input
|
||||
auto input = parser.get("Input");
|
||||
@ -515,6 +516,7 @@ void CemuConfig::Save(XMLConfigParser& parser)
|
||||
#elif BOOST_OS_UNIX
|
||||
debug.set("CrashDumpUnix", crash_dump.GetValue());
|
||||
#endif
|
||||
debug.set("GDBPort", gdb_port);
|
||||
|
||||
// input
|
||||
auto input = config.set("Input");
|
||||
|
@ -484,6 +484,7 @@ struct CemuConfig
|
||||
|
||||
// debug
|
||||
ConfigValueBounds<CrashDump> crash_dump{ CrashDump::Disabled };
|
||||
ConfigValue<uint16> gdb_port{ 1337 };
|
||||
|
||||
void Load(XMLConfigParser& parser);
|
||||
void Save(XMLConfigParser& parser);
|
||||
|
@ -68,6 +68,7 @@ bool LaunchSettings::HandleCommandline(const std::vector<std::wstring>& args)
|
||||
("account,a", po::value<std::string>(), "Persistent id of account")
|
||||
|
||||
("force-interpreter", po::value<bool>()->implicit_value(true), "Force interpreter CPU emulation, disables recompiler")
|
||||
("enable-gdbstub", po::value<bool>()->implicit_value(true), "Enable GDB stub to debug executables inside Cemu using an external debugger")
|
||||
|
||||
("act-url", po::value<std::string>(), "URL prefix for account server")
|
||||
("ecs-url", po::value<std::string>(), "URL for ECS service");
|
||||
@ -163,6 +164,9 @@ bool LaunchSettings::HandleCommandline(const std::vector<std::wstring>& args)
|
||||
if(vm.count("force-interpreter"))
|
||||
s_force_interpreter = vm["force-interpreter"].as<bool>();
|
||||
|
||||
if (vm.count("enable-gdbstub"))
|
||||
s_enable_gdbstub = vm["enable-gdbstub"].as<bool>();
|
||||
|
||||
std::wstring extract_path, log_path;
|
||||
std::string output_path;
|
||||
if (vm.count("extract"))
|
||||
|
@ -21,6 +21,7 @@ public:
|
||||
static std::optional<bool> RenderUpsideDownEnabled() { return s_render_upside_down; }
|
||||
static std::optional<bool> 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<bool> s_render_upside_down{};
|
||||
inline static std::optional<bool> s_fullscreen{};
|
||||
|
||||
inline static bool s_enable_gdbstub = false;
|
||||
inline static bool s_nsight_mode = false;
|
||||
inline static bool s_force_intel_legacy = false;
|
||||
|
||||
|
@ -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)
|
||||
|
@ -74,6 +74,7 @@ private:
|
||||
|
||||
// Debug
|
||||
wxChoice* m_crash_dump;
|
||||
wxSpinCtrl* m_gdb_port;
|
||||
|
||||
void OnAccountCreate(wxCommandEvent& event);
|
||||
void OnAccountDelete(wxCommandEvent& event);
|
||||
|
@ -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<GDBServer>(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<GDBServer>(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"));
|
||||
|
@ -14,6 +14,7 @@
|
||||
#include "gui/components/wxGameList.h"
|
||||
|
||||
#include <future>
|
||||
#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;
|
||||
|
||||
|
@ -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);
|
||||
{
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
@ -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 <wx/bitmap.h>
|
||||
#include <wx/frame.h>
|
||||
@ -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;
|
||||
|
@ -12,6 +12,7 @@
|
||||
#include <wx/slider.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/spinctrl.h>
|
||||
#include <wx/listbase.h>
|
||||
#include <wx/display.h>
|
||||
#include <wx/aboutdlg.h>
|
||||
|
Loading…
Reference in New Issue
Block a user