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:
Crementif 2023-02-19 15:41:49 +01:00 committed by GitHub
parent 05d82b09e9
commit 6d75776b28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1765 additions and 59 deletions

View 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;
};