Cemu/src/Cafe/HW/Espresso/Debugger/Debugger.cpp
Crementif 186e92221a
debugger: allow printing registers using logging breakpoint placeholders (#1510)
This allows a savy user, developer or modder to change the comment field of a logging breakpoint to include placeholders such as {r3} or {f3} to log the register values whenever that code is hit.
2025-03-07 23:40:17 +01:00

702 lines
No EOL
20 KiB
C++

#include "gui/guiWrapper.h"
#include "Debugger.h"
#include "Cafe/OS/RPL/rpl_structs.h"
#include "Cemu/PPCAssembler/ppcAssembler.h"
#include "Cafe/HW/Espresso/Recompiler/PPCRecompiler.h"
#include "Cemu/ExpressionParser/ExpressionParser.h"
#include "gui/debugger/DebuggerWindow2.h"
#include "Cafe/OS/libs/coreinit/coreinit.h"
#include "util/helpers/helpers.h"
#if BOOST_OS_WINDOWS
#include <Windows.h>
#endif
debuggerState_t debuggerState{ };
DebuggerBreakpoint* debugger_getFirstBP(uint32 address)
{
for (auto& it : debuggerState.breakpoints)
{
if (it->address == address)
return it;
}
return nullptr;
}
DebuggerBreakpoint* debugger_getFirstBP(uint32 address, uint8 bpType)
{
for (auto& it : debuggerState.breakpoints)
{
if (it->address == address)
{
DebuggerBreakpoint* bpItr = it;
while (bpItr)
{
if (bpItr->bpType == bpType)
return bpItr;
bpItr = bpItr->next;
}
return nullptr;
}
}
return nullptr;
}
bool debuggerBPChain_hasType(DebuggerBreakpoint* bp, uint8 bpType)
{
while (bp)
{
if (bp->bpType == bpType)
return true;
bp = bp->next;
}
return false;
}
void debuggerBPChain_add(uint32 address, DebuggerBreakpoint* bp)
{
bp->next = nullptr;
DebuggerBreakpoint* existingBP = debugger_getFirstBP(address);
if (existingBP)
{
while (existingBP->next)
existingBP = existingBP->next;
existingBP->next = bp;
return;
}
// no breakpoint chain exists for this address
debuggerState.breakpoints.push_back(bp);
}
uint32 debugger_getAddressOriginalOpcode(uint32 address)
{
auto bpItr = debugger_getFirstBP(address);
while (bpItr)
{
if (bpItr->isExecuteBP())
return bpItr->originalOpcodeValue;
bpItr = bpItr->next;
}
return memory_readU32(address);
}
void debugger_updateMemoryU32(uint32 address, uint32 newValue)
{
bool memChanged = false;
if (newValue != memory_readU32(address))
memChanged = true;
memory_writeU32(address, newValue);
if(memChanged)
PPCRecompiler_invalidateRange(address, address + 4);
}
void debugger_updateExecutionBreakpoint(uint32 address, bool forceRestore)
{
auto bpItr = debugger_getFirstBP(address);
bool hasBP = false;
uint32 originalOpcodeValue;
while (bpItr)
{
if (bpItr->isExecuteBP())
{
if (bpItr->enabled && forceRestore == false)
{
// write TW instruction to memory
debugger_updateMemoryU32(address, DEBUGGER_BP_T_DEBUGGER_TW);
return;
}
else
{
originalOpcodeValue = bpItr->originalOpcodeValue;
hasBP = true;
}
}
bpItr = bpItr->next;
}
if (hasBP)
{
// restore instruction
debugger_updateMemoryU32(address, originalOpcodeValue);
}
}
void debugger_createCodeBreakpoint(uint32 address, uint8 bpType)
{
// check if breakpoint already exists
auto existingBP = debugger_getFirstBP(address);
if (existingBP && debuggerBPChain_hasType(existingBP, bpType))
return; // breakpoint already exists
// get original opcode at address
uint32 originalOpcode = debugger_getAddressOriginalOpcode(address);
// init breakpoint object
DebuggerBreakpoint* bp = new DebuggerBreakpoint(address, originalOpcode, bpType, true);
debuggerBPChain_add(address, bp);
debugger_updateExecutionBreakpoint(address);
}
namespace coreinit
{
std::vector<std::thread::native_handle_type>& OSGetSchedulerThreads();
}
void debugger_updateMemoryBreakpoint(DebuggerBreakpoint* bp)
{
std::vector<std::thread::native_handle_type> schedulerThreadHandles = coreinit::OSGetSchedulerThreads();
#if BOOST_OS_WINDOWS
debuggerState.activeMemoryBreakpoint = bp;
for (auto& hThreadNH : schedulerThreadHandles)
{
HANDLE hThread = (HANDLE)hThreadNH;
CONTEXT ctx{};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SuspendThread(hThread);
GetThreadContext(hThread, &ctx);
if (debuggerState.activeMemoryBreakpoint)
{
ctx.Dr0 = (DWORD64)memory_getPointerFromVirtualOffset(bp->address);
ctx.Dr1 = (DWORD64)memory_getPointerFromVirtualOffset(bp->address);
// 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
{
// 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);
}
#else
cemuLog_log(LogType::Force, "Debugger breakpoints are not supported");
#endif
}
void debugger_handleSingleStepException(uint64 dr6)
{
bool triggeredDR0 = GetBits(dr6, 0, 1); // write
bool triggeredDR1 = GetBits(dr6, 1, 1); // read and write
bool catchBP = false;
if (triggeredDR0 && triggeredDR1)
{
// write (and read) access
if (debuggerState.activeMemoryBreakpoint && debuggerState.activeMemoryBreakpoint->bpType == DEBUGGER_BP_T_MEMORY_WRITE)
catchBP = true;
}
else
{
// read access
if (debuggerState.activeMemoryBreakpoint && debuggerState.activeMemoryBreakpoint->bpType == DEBUGGER_BP_T_MEMORY_READ)
catchBP = true;
}
if (catchBP)
{
PPCInterpreter_t* hCPU = PPCInterpreter_getCurrentInstance();
debugger_createCodeBreakpoint(hCPU->instructionPointer + 4, DEBUGGER_BP_T_ONE_SHOT);
}
}
void debugger_createMemoryBreakpoint(uint32 address, bool onRead, bool onWrite)
{
// init breakpoint object
uint8 bpType;
if (onRead && onWrite)
assert_dbg();
else if (onRead)
bpType = DEBUGGER_BP_T_MEMORY_READ;
else
bpType = DEBUGGER_BP_T_MEMORY_WRITE;
DebuggerBreakpoint* bp = new DebuggerBreakpoint(address, 0xFFFFFFFF, bpType, true);
debuggerBPChain_add(address, bp);
// disable any already existing memory breakpoint
if (debuggerState.activeMemoryBreakpoint)
{
debuggerState.activeMemoryBreakpoint->enabled = false;
debuggerState.activeMemoryBreakpoint = nullptr;
}
// activate new breakpoint
debugger_updateMemoryBreakpoint(bp);
}
void debugger_handleEntryBreakpoint(uint32 address)
{
if (!debuggerState.breakOnEntry)
return;
debugger_createCodeBreakpoint(address, DEBUGGER_BP_T_NORMAL);
}
void debugger_deleteBreakpoint(DebuggerBreakpoint* bp)
{
for (auto& it : debuggerState.breakpoints)
{
if (it->address == bp->address)
{
// for execution breakpoints make sure the instruction is restored
if (bp->isExecuteBP())
{
bp->enabled = false;
debugger_updateExecutionBreakpoint(bp->address);
}
// remove
if (it == bp)
{
// remove first in list
debuggerState.breakpoints.erase(std::remove(debuggerState.breakpoints.begin(), debuggerState.breakpoints.end(), bp), debuggerState.breakpoints.end());
DebuggerBreakpoint* nextBP = bp->next;
if (nextBP)
debuggerState.breakpoints.push_back(nextBP);
}
else
{
// remove from list
DebuggerBreakpoint* bpItr = it;
while (bpItr->next != bp)
{
bpItr = bpItr->next;
}
cemu_assert_debug(bpItr->next != bp);
bpItr->next = bp->next;
}
delete bp;
return;
}
}
}
void debugger_toggleExecuteBreakpoint(uint32 address)
{
auto existingBP = debugger_getFirstBP(address, DEBUGGER_BP_T_NORMAL);
if (existingBP)
{
// delete existing breakpoint
debugger_deleteBreakpoint(existingBP);
}
else
{
// create new execution breakpoint
debugger_createCodeBreakpoint(address, DEBUGGER_BP_T_NORMAL);
}
}
void debugger_toggleLoggingBreakpoint(uint32 address)
{
auto existingBP = debugger_getFirstBP(address, DEBUGGER_BP_T_LOGGING);
if (existingBP)
{
// delete existing breakpoint
debugger_deleteBreakpoint(existingBP);
}
else
{
// create new logging breakpoint
debugger_createCodeBreakpoint(address, DEBUGGER_BP_T_LOGGING);
}
}
void debugger_forceBreak()
{
debuggerState.debugSession.shouldBreak = true;
}
bool debugger_isTrapped()
{
return debuggerState.debugSession.isTrapped;
}
void debugger_resume()
{
// if there is a breakpoint on the current instruction then do a single 'step into' to skip it
debuggerState.debugSession.run = true;
}
void debugger_toggleBreakpoint(uint32 address, bool state, DebuggerBreakpoint* bp)
{
DebuggerBreakpoint* bpItr = debugger_getFirstBP(address);
while (bpItr)
{
if (bpItr == bp)
{
if (bpItr->bpType == DEBUGGER_BP_T_NORMAL || bpItr->bpType == DEBUGGER_BP_T_LOGGING)
{
bp->enabled = state;
debugger_updateExecutionBreakpoint(address);
debuggerWindow_updateViewThreadsafe2();
}
else if (bpItr->isMemBP())
{
// disable other memory breakpoints
for (auto& it : debuggerState.breakpoints)
{
DebuggerBreakpoint* bpItr2 = it;
while (bpItr2)
{
if (bpItr2->isMemBP() && bpItr2 != bp)
{
bpItr2->enabled = false;
}
bpItr2 = bpItr2->next;
}
}
bpItr->enabled = state;
if (state)
debugger_updateMemoryBreakpoint(bpItr);
else
debugger_updateMemoryBreakpoint(nullptr);
debuggerWindow_updateViewThreadsafe2();
}
return;
}
bpItr = bpItr->next;
}
}
void debugger_createPatch(uint32 address, std::span<uint8> patchData)
{
DebuggerPatch* patch = new DebuggerPatch();
patch->address = address;
patch->length = patchData.size();
patch->data.resize(4);
patch->origData.resize(4);
memcpy(&patch->data.front(), patchData.data(), patchData.size());
memcpy(&patch->origData.front(), memory_getPointerFromVirtualOffset(address), patchData.size());
// get original data from breakpoints
if ((address & 3) != 0)
cemu_assert_debug(false);
for (sint32 i = 0; i < patchData.size() / 4; i++)
{
DebuggerBreakpoint* bpItr = debugger_getFirstBP(address);
while (bpItr)
{
if (bpItr->isExecuteBP())
{
*(uint32*)(&patch->origData.front() + i * 4) = _swapEndianU32(bpItr->originalOpcodeValue);
}
bpItr = bpItr->next;
}
}
// merge with existing patches if the ranges touch
for(sint32 i=0; i<debuggerState.patches.size(); i++)
{
auto& patchItr = debuggerState.patches[i];
if (address + patchData.size() >= patchItr->address && address <= patchItr->address + patchItr->length)
{
uint32 newAddress = std::min(patch->address, patchItr->address);
uint32 newEndAddress = std::max(patch->address + patch->length, patchItr->address + patchItr->length);
uint32 newLength = newEndAddress - newAddress;
DebuggerPatch* newPatch = new DebuggerPatch();
newPatch->address = newAddress;
newPatch->length = newLength;
newPatch->data.resize(newLength);
newPatch->origData.resize(newLength);
memcpy(&newPatch->data.front() + (address - newAddress), &patch->data.front(), patch->length);
memcpy(&newPatch->data.front() + (patchItr->address - newAddress), &patchItr->data.front(), patchItr->length);
memcpy(&newPatch->origData.front() + (address - newAddress), &patch->origData.front(), patch->length);
memcpy(&newPatch->origData.front() + (patchItr->address - newAddress), &patchItr->origData.front(), patchItr->length);
delete patch;
patch = newPatch;
delete patchItr;
// remove currently iterated patch
debuggerState.patches.erase(debuggerState.patches.begin()+i);
i--;
}
}
debuggerState.patches.push_back(patch);
// apply patch (if breakpoints exist then update those instead of actual data)
if ((address & 3) != 0)
cemu_assert_debug(false);
if ((patchData.size() & 3) != 0)
cemu_assert_debug(false);
for (sint32 i = 0; i < patchData.size() / 4; i++)
{
DebuggerBreakpoint* bpItr = debugger_getFirstBP(address);
bool hasActiveExecuteBP = false;
while (bpItr)
{
if (bpItr->isExecuteBP())
{
bpItr->originalOpcodeValue = *(uint32be*)(patchData.data() + i * 4);
if (bpItr->enabled)
hasActiveExecuteBP = true;
}
bpItr = bpItr->next;
}
if (hasActiveExecuteBP == false)
{
memcpy(memory_getPointerFromVirtualOffset(address + i * 4), patchData.data() + i * 4, 4);
PPCRecompiler_invalidateRange(address, address + 4);
}
}
}
bool debugger_hasPatch(uint32 address)
{
for (auto& patch : debuggerState.patches)
{
if (address + 4 > patch->address && address < patch->address + patch->length)
return true;
}
return false;
}
void debugger_removePatch(uint32 address)
{
for (sint32 i = 0; i < debuggerState.patches.size(); i++)
{
auto& patch = debuggerState.patches[i];
if (address < patch->address || address >= (patch->address + patch->length))
continue;
MPTR startAddress = patch->address;
MPTR endAddress = patch->address + patch->length;
// remove any breakpoints overlapping with the patch
for (auto& bp : debuggerState.breakpoints)
{
if (bp->address + 4 > startAddress && bp->address < endAddress)
{
bp->enabled = false;
debugger_updateExecutionBreakpoint(bp->address);
}
}
// restore original data
memcpy(MEMPTR<void>(startAddress).GetPtr(), patch->origData.data(), patch->length);
PPCRecompiler_invalidateRange(startAddress, endAddress);
// remove patch
delete patch;
debuggerState.patches.erase(debuggerState.patches.begin() + i);
return;
}
}
void debugger_stepInto(PPCInterpreter_t* hCPU, bool updateDebuggerWindow = true)
{
bool isRecEnabled = ppcRecompilerEnabled;
ppcRecompilerEnabled = false;
uint32 initialIP = debuggerState.debugSession.instructionPointer;
debugger_updateExecutionBreakpoint(initialIP, true);
PPCInterpreterSlim_executeInstruction(hCPU);
debugger_updateExecutionBreakpoint(initialIP);
debuggerState.debugSession.instructionPointer = hCPU->instructionPointer;
if(updateDebuggerWindow)
debuggerWindow_moveIP();
ppcRecompilerEnabled = isRecEnabled;
}
bool debugger_stepOver(PPCInterpreter_t* hCPU)
{
bool isRecEnabled = ppcRecompilerEnabled;
ppcRecompilerEnabled = false;
// disassemble current instruction
PPCDisassembledInstruction disasmInstr = { 0 };
uint32 initialIP = debuggerState.debugSession.instructionPointer;
debugger_updateExecutionBreakpoint(initialIP, true);
ppcAssembler_disassemble(initialIP, memory_readU32(initialIP), &disasmInstr);
if (disasmInstr.ppcAsmCode != PPCASM_OP_BL &&
disasmInstr.ppcAsmCode != PPCASM_OP_BCTRL)
{
// nothing to skip, use step-into
debugger_stepInto(hCPU);
debugger_updateExecutionBreakpoint(initialIP);
debuggerWindow_moveIP();
ppcRecompilerEnabled = isRecEnabled;
return false;
}
// create one-shot breakpoint at next instruction
debugger_createCodeBreakpoint(initialIP + 4, DEBUGGER_BP_T_ONE_SHOT);
// step over current instruction (to avoid breakpoint)
debugger_stepInto(hCPU);
debuggerWindow_moveIP();
// restore breakpoints
debugger_updateExecutionBreakpoint(initialIP);
// run
ppcRecompilerEnabled = isRecEnabled;
return true;
}
void debugger_createPPCStateSnapshot(PPCInterpreter_t* hCPU)
{
memcpy(debuggerState.debugSession.ppcSnapshot.gpr, hCPU->gpr, sizeof(uint32) * 32);
memcpy(debuggerState.debugSession.ppcSnapshot.fpr, hCPU->fpr, sizeof(FPR_t) * 32);
debuggerState.debugSession.ppcSnapshot.spr_lr = hCPU->spr.LR;
for (uint32 i = 0; i < 32; i++)
debuggerState.debugSession.ppcSnapshot.cr[i] = hCPU->cr[i];
}
void debugger_enterTW(PPCInterpreter_t* hCPU)
{
// handle logging points
DebuggerBreakpoint* bp = debugger_getFirstBP(hCPU->instructionPointer);
bool shouldBreak = debuggerBPChain_hasType(bp, DEBUGGER_BP_T_NORMAL) || debuggerBPChain_hasType(bp, DEBUGGER_BP_T_ONE_SHOT);
while (bp)
{
if (bp->bpType == DEBUGGER_BP_T_LOGGING && bp->enabled)
{
std::string comment = !bp->comment.empty() ? boost::nowide::narrow(bp->comment) : fmt::format("Breakpoint at 0x{:08X} (no comment)", bp->address);
auto replacePlaceholders = [&](const std::string& prefix, const auto& formatFunc)
{
size_t pos = 0;
while ((pos = comment.find(prefix, pos)) != std::string::npos)
{
size_t endPos = comment.find('}', pos);
if (endPos == std::string::npos)
break;
try
{
if (int regNum = ConvertString<int>(comment.substr(pos + prefix.length(), endPos - pos - prefix.length())); regNum >= 0 && regNum < 32)
{
std::string replacement = formatFunc(regNum);
comment.replace(pos, endPos - pos + 1, replacement);
pos += replacement.length();
}
else
{
pos = endPos + 1;
}
}
catch (...)
{
pos = endPos + 1;
}
}
};
// Replace integer register placeholders {rX}
replacePlaceholders("{r", [&](int regNum) {
return fmt::format("0x{:08X}", hCPU->gpr[regNum]);
});
// Replace floating point register placeholders {fX}
replacePlaceholders("{f", [&](int regNum) {
return fmt::format("{}", hCPU->fpr[regNum].fpr);
});
std::string logName = "Breakpoint '" + comment + "'";
std::string logContext = fmt::format("Thread: {:08x} LR: 0x{:08x}", MEMPTR<OSThread_t>(coreinit::OSGetCurrentThread()).GetMPTR(), hCPU->spr.LR, cemuLog_advancedPPCLoggingEnabled() ? " Stack Trace:" : "");
cemuLog_log(LogType::Force, "[Debugger] {} was executed! {}", logName, logContext);
if (cemuLog_advancedPPCLoggingEnabled())
DebugLogStackTrace(coreinit::OSGetCurrentThread(), hCPU->gpr[1]);
break;
}
bp = bp->next;
}
// return early if it's only a non-pausing logging breakpoint to prevent a modified debugger state and GUI updates
if (!shouldBreak)
{
uint32 backupIP = debuggerState.debugSession.instructionPointer;
debuggerState.debugSession.instructionPointer = hCPU->instructionPointer;
debugger_stepInto(hCPU, false);
PPCInterpreterSlim_executeInstruction(hCPU);
debuggerState.debugSession.instructionPointer = backupIP;
return;
}
// handle breakpoints
debuggerState.debugSession.isTrapped = true;
debuggerState.debugSession.debuggedThreadMPTR = MEMPTR<OSThread_t>(coreinit::OSGetCurrentThread()).GetMPTR();
debuggerState.debugSession.instructionPointer = hCPU->instructionPointer;
debuggerState.debugSession.hCPU = hCPU;
debugger_createPPCStateSnapshot(hCPU);
// remove one-shot breakpoint if it exists
DebuggerBreakpoint* singleshotBP = debugger_getFirstBP(debuggerState.debugSession.instructionPointer, DEBUGGER_BP_T_ONE_SHOT);
if (singleshotBP)
debugger_deleteBreakpoint(singleshotBP);
debuggerWindow_notifyDebugBreakpointHit2();
debuggerWindow_updateViewThreadsafe2();
// reset step control
debuggerState.debugSession.stepInto = false;
debuggerState.debugSession.stepOver = false;
debuggerState.debugSession.run = false;
while (debuggerState.debugSession.isTrapped)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
// check for step commands
if (debuggerState.debugSession.stepOver)
{
if (debugger_stepOver(hCPU))
{
debugger_createPPCStateSnapshot(hCPU);
break; // if true is returned, continue with execution
}
debugger_createPPCStateSnapshot(hCPU);
debuggerWindow_updateViewThreadsafe2();
debuggerState.debugSession.stepOver = false;
}
if (debuggerState.debugSession.stepInto)
{
debugger_stepInto(hCPU);
debugger_createPPCStateSnapshot(hCPU);
debuggerWindow_updateViewThreadsafe2();
debuggerState.debugSession.stepInto = false;
continue;
}
if (debuggerState.debugSession.run)
{
debugger_createPPCStateSnapshot(hCPU);
debugger_stepInto(hCPU, false);
PPCInterpreterSlim_executeInstruction(hCPU);
debuggerState.debugSession.instructionPointer = hCPU->instructionPointer;
debuggerState.debugSession.run = false;
break;
}
}
debuggerState.debugSession.isTrapped = false;
debuggerState.debugSession.hCPU = nullptr;
debuggerWindow_updateViewThreadsafe2();
debuggerWindow_notifyRun();
}
void debugger_shouldBreak(PPCInterpreter_t* hCPU)
{
if(debuggerState.debugSession.shouldBreak
// exclude emulator trampoline area
&& (hCPU->instructionPointer < MEMORY_CODE_TRAMPOLINE_AREA_ADDR || hCPU->instructionPointer > MEMORY_CODE_TRAMPOLINE_AREA_ADDR + MEMORY_CODE_TRAMPOLINE_AREA_SIZE))
{
debuggerState.debugSession.shouldBreak = false;
const uint32 address = (uint32)hCPU->instructionPointer;
assert_dbg();
//debugger_createBreakpoint(address, DEBUGGER_BP_TYPE_ONE_SHOT);
}
}
void debugger_addParserSymbols(class ExpressionParser& ep)
{
const auto module_count = RPLLoader_GetModuleCount();
const auto module_list = RPLLoader_GetModuleList();
std::vector<double> module_tmp(module_count);
for (int i = 0; i < module_count; i++)
{
const auto module = module_list[i];
if (module)
{
module_tmp[i] = (double)module->regionMappingBase_text.GetMPTR();
ep.AddConstant(module->moduleName2, module_tmp[i]);
}
}
for (sint32 i = 0; i < 32; i++)
ep.AddConstant(fmt::format("r{}", i), debuggerState.debugSession.ppcSnapshot.gpr[i]);
}