mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-31 17:55:23 +02:00
step-over, step-out and run-until plant a one-shot breakpoint at the address they want execution to return to. Keeping it in breakPoints_ alongside the user's own meant the two kept colliding: - Adding a log-only user breakpoint at the same address hijacked the temporary one. AddBreakPoint() didn't match across temp-ness so both existed, and then ChangeBreakPoint() looked up "the first enabled breakpoint at this address" - a log-only breakpoint isn't enabled, so the temporary one won and had its action overwritten to log-only. It lost PAUSE and the step never came back. - RemoveBreakPoint() erased up to two entries per address to catch an overlapping temporary one, so deleting either deleted both - including the interpreter's cleanup path in CheckExecBreakpoints() taking the user's breakpoint with it. - ExecBreakPoint() handled one breakpoint per address, so with both at the same address only one of them did anything: the step completed but the user's log line never printed. - Nothing dropped it when something *else* stopped us first, so an interrupted step left a breakpoint armed at an address nobody was waiting for anymore, which later fired as a phantom stop. It's a single TempBreakPoint member now, invisible to the breakpoint lists and untouched by user edits. One is enough: step over/out and cross-thread step into all require the CPU to already be stepping and resume it immediately, so only one can be in flight, and run-until now replaces rather than stacking (two pending run-untils had no coherent meaning, and the loser stayed armed). Behavior follows what other debuggers do. Both breakpoints at an address are evaluated independently and their actions combine, so a log-only breakpoint logs without stopping and still lets the step finish. Core_Break() drops the temporary breakpoint on any stop, whatever the reason - the same way gdb deletes its step-resume breakpoint and lldb discards the thread plan. Two things to be careful of, both covered by the new TempBreakpoints test: HasBreakPoints() has to account for it, or the interpreter's checked run loop and the JIT skip breakpoint checking entirely and a step with no user breakpoints set never returns; and IsAddressBreakPoint() (user-facing, for the lists and disassembly markers) is now separate from NeedsBreakCheckAt() (what the JIT frontends and interpreter ask), since only the latter should see it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
338 lines
12 KiB
C++
338 lines
12 KiB
C++
// Copyright (c) 2018- PPSSPP Project.
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, version 2.0 or later versions.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License 2.0 for more details.
|
|
|
|
// A copy of the GPL 2.0 should have been included with the program.
|
|
// If not, see http://www.gnu.org/licenses/
|
|
|
|
// Official git repository and contact information can be found at
|
|
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
|
|
|
#include "Common/StringUtils.h"
|
|
#include "Core/Debugger/Breakpoints.h"
|
|
#include "Core/Debugger/DisassemblyManager.h"
|
|
#include "Core/Debugger/WebSocket/SteppingSubscriber.h"
|
|
#include "Core/Debugger/WebSocket/WebSocketUtils.h"
|
|
#include "Core/Core.h"
|
|
#include "Core/HLE/HLE.h"
|
|
#include "Core/HLE/sceKernelThread.h"
|
|
#include "Core/MIPS/MIPSDebugInterface.h"
|
|
#include "Core/MIPS/MIPSStackWalk.h"
|
|
|
|
using namespace MIPSAnalyst;
|
|
|
|
struct WebSocketSteppingState : public DebuggerSubscriber {
|
|
WebSocketSteppingState() {
|
|
g_disassemblyManager.setCpu(currentDebugMIPS);
|
|
}
|
|
~WebSocketSteppingState() {
|
|
g_disassemblyManager.clear();
|
|
}
|
|
|
|
void Into(DebuggerRequest &req);
|
|
void Over(DebuggerRequest &req);
|
|
void Out(DebuggerRequest &req);
|
|
void RunUntil(DebuggerRequest &req);
|
|
void HLE(DebuggerRequest &req);
|
|
|
|
protected:
|
|
uint32_t GetNextAddress(DebugInterface *cpuDebug);
|
|
void PrepareResume();
|
|
void AddThreadCondition(uint32_t threadID);
|
|
};
|
|
|
|
DebuggerSubscriber *WebSocketSteppingInit(DebuggerEventHandlerMap &map) {
|
|
WebSocketSteppingState *p = new WebSocketSteppingState();
|
|
map["cpu.stepInto"] = [p](DebuggerRequest &req) { p->Into(req); };
|
|
map["cpu.stepOver"] = [p](DebuggerRequest &req) { p->Over(req); };
|
|
map["cpu.stepOut"] = [p](DebuggerRequest &req) { p->Out(req); };
|
|
map["cpu.runUntil"] = [p](DebuggerRequest &req) { p->RunUntil(req); };
|
|
map["cpu.nextHLE"] = [p](DebuggerRequest &req) { p->HLE(req); };
|
|
return p;
|
|
}
|
|
|
|
static DebugInterface *CPUFromRequest(DebuggerRequest &req, uint32_t *threadID = nullptr) {
|
|
if (!req.HasParam("thread")) {
|
|
if (threadID)
|
|
*threadID = -1;
|
|
return currentDebugMIPS;
|
|
}
|
|
|
|
uint32_t uid;
|
|
if (!req.ParamU32("thread", &uid))
|
|
return nullptr;
|
|
|
|
DebugInterface *cpuDebug = KernelDebugThread((SceUID)uid);
|
|
if (!cpuDebug)
|
|
req.Fail("Thread could not be found");
|
|
if (threadID)
|
|
*threadID = uid;
|
|
return cpuDebug;
|
|
}
|
|
|
|
// Single step into the next instruction (cpu.stepInto)
|
|
//
|
|
// Parameters:
|
|
// - thread: optional number indicating the thread id to plan stepping on.
|
|
//
|
|
// No immediate response on success. A cpu.stepping event will be sent once complete.
|
|
// May fail (same-thread case only) if another step/run request is already pending this host
|
|
// frame - safe to retry shortly after.
|
|
//
|
|
// Note: any thread can wake the cpu when it hits the next instruction currently.
|
|
void WebSocketSteppingState::Into(DebuggerRequest &req) {
|
|
if (!currentDebugMIPS->isAlive())
|
|
return req.Fail("CPU not started");
|
|
if (!Core_IsStepping()) {
|
|
// Core_Break() is explicitly free-threaded (see Core.cpp), so no need to bounce this to the CPU
|
|
// thread - and we can't anyway, since queuing to it only makes sense once the CPU actually *is*
|
|
// stepping, which this call is what triggers in the first place.
|
|
Core_Break(BreakReason::DebugStepInto, 0);
|
|
return;
|
|
}
|
|
|
|
// Route the actual breakpoint/stepping manipulation to the CPU thread instead of poking at it directly
|
|
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
|
|
Core_RunOnCPUThread([&] {
|
|
uint32_t threadID;
|
|
DebugInterface *cpuDebug = CPUFromRequest(req, &threadID);
|
|
if (!cpuDebug)
|
|
return;
|
|
|
|
if (cpuDebug == currentDebugMIPS) {
|
|
// If the current PC is on a breakpoint, the user doesn't want to do nothing.
|
|
g_breakpoints.SetSkipFirst(currentMIPS->pc);
|
|
|
|
// Core_RequestCPUStep() can fail (a step or run request is already queued this host
|
|
// frame - see its own "Can't submit two steps in one host frame" log). Previously
|
|
// unchecked here: on failure, no step ever happens and no cpu.stepping event ever
|
|
// fires, but the client got no response either (this event's contract is "no
|
|
// immediate response, a cpu.stepping event follows") - so a rejected step looked
|
|
// identical to one that's just still in flight, indefinitely. Surface it instead.
|
|
if (!Core_RequestCPUStep(CPUStepType::Into)) {
|
|
req.Fail("Could not step: a step or run request is already pending");
|
|
return;
|
|
}
|
|
} else {
|
|
uint32_t breakpointAddress = cpuDebug->GetPC();
|
|
PrepareResume();
|
|
// Could have advanced to the breakpoint already in PrepareResume().
|
|
// Note: we need to get cpuDebug again anyway (in case we ran some HLE above.)
|
|
cpuDebug = CPUFromRequest(req);
|
|
if (cpuDebug != currentDebugMIPS) {
|
|
g_breakpoints.SetTempBreakPoint(breakpointAddress);
|
|
AddThreadCondition(threadID);
|
|
Core_Resume();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Step over the next instruction (cpu.stepOver)
|
|
//
|
|
// Note: this jumps over function calls, but also delay slots.
|
|
//
|
|
// Parameters:
|
|
// - thread: optional number indicating the thread id to plan stepping on.
|
|
//
|
|
// No immediate response. A cpu.stepping event will be sent once complete.
|
|
//
|
|
// Note: any thread can wake the cpu when it hits the next instruction currently.
|
|
void WebSocketSteppingState::Over(DebuggerRequest &req) {
|
|
if (!currentDebugMIPS->isAlive())
|
|
return req.Fail("CPU not started");
|
|
if (!Core_IsStepping())
|
|
return req.Fail("CPU currently running (cpu.stepping first)");
|
|
|
|
// Route the actual breakpoint/stepping manipulation to the CPU thread instead of poking at it directly
|
|
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
|
|
Core_RunOnCPUThread([&] {
|
|
uint32_t threadID;
|
|
DebugInterface *cpuDebug = CPUFromRequest(req, &threadID);
|
|
if (!cpuDebug)
|
|
return;
|
|
|
|
MipsOpcodeInfo info = GetOpcodeInfo(cpuDebug, cpuDebug->GetPC());
|
|
uint32_t breakpointAddress = GetNextAddress(cpuDebug);
|
|
if (info.isBranch) {
|
|
if (info.isConditional && !info.isLinkedBranch) {
|
|
if (info.conditionMet) {
|
|
breakpointAddress = info.branchTarget;
|
|
} else {
|
|
// Skip over the delay slot.
|
|
breakpointAddress += 4;
|
|
}
|
|
} else {
|
|
if (info.isLinkedBranch) {
|
|
// jal or jalr - a function call. Skip the delay slot.
|
|
breakpointAddress += 4;
|
|
} else {
|
|
// j - for absolute branches, set the breakpoint at the branch target.
|
|
breakpointAddress = info.branchTarget;
|
|
}
|
|
}
|
|
}
|
|
|
|
PrepareResume();
|
|
// Could have advanced to the breakpoint already in PrepareResume().
|
|
cpuDebug = CPUFromRequest(req);
|
|
if (cpuDebug->GetPC() != breakpointAddress) {
|
|
g_breakpoints.SetTempBreakPoint(breakpointAddress);
|
|
if (cpuDebug != currentDebugMIPS)
|
|
AddThreadCondition(threadID);
|
|
Core_Resume();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Step out of a function based on a stack walk (cpu.stepOut)
|
|
//
|
|
// Parameters:
|
|
// - thread: optional number indicating the thread id to plan stepping on.
|
|
//
|
|
// No immediate response. A cpu.stepping event will be sent once complete.
|
|
//
|
|
// Note: any thread can wake the cpu when it hits the next instruction currently.
|
|
void WebSocketSteppingState::Out(DebuggerRequest &req) {
|
|
if (!currentDebugMIPS->isAlive())
|
|
return req.Fail("CPU not started");
|
|
if (!Core_IsStepping())
|
|
return req.Fail("CPU currently running (cpu.stepping first)");
|
|
|
|
// Route the actual breakpoint/stepping manipulation to the CPU thread instead of poking at it directly
|
|
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
|
|
Core_RunOnCPUThread([&] {
|
|
uint32_t threadID;
|
|
DebugInterface *cpuDebug = CPUFromRequest(req, &threadID);
|
|
if (!cpuDebug)
|
|
return;
|
|
|
|
std::vector<DebugThreadInfo> threads = GetThreadsInfo();
|
|
uint32_t entry = cpuDebug->GetPC();
|
|
uint32_t stackTop = 0;
|
|
for (const DebugThreadInfo &th : threads) {
|
|
if ((threadID == -1 && th.isCurrent) || th.id == threadID) {
|
|
entry = th.entrypoint;
|
|
stackTop = th.initialStack;
|
|
break;
|
|
}
|
|
}
|
|
|
|
uint32_t ra = cpuDebug->GetRegValue(0, MIPS_REG_RA);
|
|
uint32_t sp = cpuDebug->GetRegValue(0, MIPS_REG_SP);
|
|
std::vector<MIPSStackWalk::StackFrame> frames = MIPSStackWalk::Walk(cpuDebug->GetPC(), ra, sp, entry, stackTop);
|
|
if (frames.size() < 2) {
|
|
return req.Fail("Could not find function call to step out into");
|
|
}
|
|
|
|
uint32_t breakpointAddress = frames[1].pc;
|
|
PrepareResume();
|
|
// Could have advanced to the breakpoint already in PrepareResume().
|
|
cpuDebug = CPUFromRequest(req);
|
|
if (cpuDebug->GetPC() != breakpointAddress) {
|
|
g_breakpoints.SetTempBreakPoint(breakpointAddress);
|
|
if (cpuDebug != currentDebugMIPS)
|
|
AddThreadCondition(threadID);
|
|
Core_Resume();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Run until a certain address (cpu.runUntil)
|
|
//
|
|
// Parameters:
|
|
// - address: number parameter for destination.
|
|
//
|
|
// No immediate response. A cpu.stepping event will be sent once complete.
|
|
void WebSocketSteppingState::RunUntil(DebuggerRequest &req) {
|
|
if (!currentDebugMIPS->isAlive()) {
|
|
return req.Fail("CPU not started");
|
|
}
|
|
|
|
uint32_t address = 0;
|
|
if (!req.ParamU32("address", &address)) {
|
|
// Error already sent.
|
|
return;
|
|
}
|
|
|
|
// Route the actual breakpoint/stepping manipulation to the CPU thread instead of poking at it directly
|
|
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
|
|
Core_RunOnCPUThread([&] {
|
|
bool wasAtAddress = currentMIPS->pc == address;
|
|
PrepareResume();
|
|
// We may have arrived already if PauseResume() stepped out of a delay slot.
|
|
if (currentMIPS->pc != address || wasAtAddress) {
|
|
g_breakpoints.SetTempBreakPoint(address);
|
|
Core_Resume();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Jump after the next HLE call (cpu.nextHLE)
|
|
//
|
|
// No parameters.
|
|
//
|
|
// No immediate response. A cpu.stepping event will be sent once complete.
|
|
void WebSocketSteppingState::HLE(DebuggerRequest &req) {
|
|
if (!currentDebugMIPS->isAlive()) {
|
|
return req.Fail("CPU not started");
|
|
}
|
|
|
|
// Route the actual breakpoint/stepping manipulation to the CPU thread instead of poking at it directly
|
|
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
|
|
Core_RunOnCPUThread([&] {
|
|
PrepareResume();
|
|
hleDebugBreak();
|
|
Core_Resume();
|
|
});
|
|
}
|
|
|
|
uint32_t WebSocketSteppingState::GetNextAddress(DebugInterface *cpuDebug) {
|
|
uint32_t current = g_disassemblyManager.getStartAddress(cpuDebug->GetPC());
|
|
return g_disassemblyManager.getNthNextAddress(current, 1);
|
|
}
|
|
|
|
void WebSocketSteppingState::PrepareResume() {
|
|
if (currentMIPS->inDelaySlot) {
|
|
// Delay slot instructions are never joined, so we pass 1.
|
|
//
|
|
// This must happen synchronously, not via Core_RequestCPUStep(): that only queues the
|
|
// step for Core_ProcessStepping() to perform later (on the next iteration of the normal
|
|
// stepping-mode loop), while every caller of PrepareResume() immediately inspects
|
|
// currentMIPS->pc/inDelaySlot right after this returns to decide whether to add a
|
|
// breakpoint and call Core_Resume(). Core_Resume() itself sets coreState back to
|
|
// CORE_RUNNING_CPU, which makes Core_ProcessStepping() skip its pending-step check
|
|
// entirely - so the queued step was not just late, it was silently dropped, leaving
|
|
// g_cpuStepCommand permanently set until the next Core_Break() reset it. Any stepping
|
|
// request issued by the debugger client in that window (e.g. a script or fast-clicking
|
|
// UI immediately re-stepping instead of waiting for a fresh cpu.stepping event) hit
|
|
// Core_RequestCPUStep()'s "Can't submit two steps in one host frame" guard and got
|
|
// silently ignored - the "step-out sometimes just doesn't do anything" flakiness this
|
|
// was found while tracking down. PrepareResume() is only ever called from within a
|
|
// Core_RunOnCPUThread() callback (Into/Over/Out/RunUntil/HLE below), so it's always
|
|
// already running on the CPU thread - safe to single-step directly instead of queuing.
|
|
currentMIPS->SingleStep();
|
|
} else {
|
|
// If the current PC is on a breakpoint, the user doesn't want to do nothing.
|
|
g_breakpoints.SetSkipFirst(currentMIPS->pc);
|
|
}
|
|
}
|
|
|
|
// Restricts the temporary breakpoint a step just planted to the thread the step was requested
|
|
// for, so an unrelated thread running through the same address doesn't complete someone else's
|
|
// step. Must be called right after SetTempBreakPoint().
|
|
void WebSocketSteppingState::AddThreadCondition(uint32_t threadID) {
|
|
BreakPointCond cond;
|
|
cond.debug = currentDebugMIPS;
|
|
cond.expressionString = StringFromFormat("threadid == 0x%08x", threadID);
|
|
if (initExpression(currentDebugMIPS, cond.expressionString.c_str(), cond.expression))
|
|
g_breakpoints.SetTempBreakPointCond(cond);
|
|
}
|