mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-31 09:45:24 +02:00
Only one step can be carried out per pass through Core_ProcessStepping(), so roughly one per host frame. A second request arriving before that was rejected outright - "Can't submit two steps in one host frame" - with no step performed, which put the burden on every caller to notice and retry. A script firing five cpu.stepInto in a row advanced one instruction and logged four errors. They queue now, up to 8 deep; past that something is looping and it says so rather than growing without bound. Five stepIntos advance five instructions. The queue is deliberately *not* cleared by Core_Break(). That looks like the obvious place for it - stopping for another reason should abandon a pending plan, the way the temporary breakpoint and the runUntilTime deadline are dropped there - but completing a step-over or step-out goes *through* Core_Break(), since their temporary breakpoint is what stops us. Clearing there would throw away everything after the first entry of any sequence. It's cleared on CoreLifecycle::STARTING instead, so a step queued against the game that just went away can't run against the new one. g_cpuStepCommand keeps its existing double duty as both "the step in flight" and "why we're stopped" (reason/relatedAddr, read by Core_GetSteppingReason), so Core_Break()'s override check for an in-progress Over/Out is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
396 lines
15 KiB
C++
396 lines
15 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/CoreTiming.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 RunUntilTime(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.runUntilTime"] = [p](DebuggerRequest &req) { p->RunUntilTime(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.
|
|
//
|
|
// Response (same event name) with no extra data on success. A cpu.stepping event follows once
|
|
// the step completes.
|
|
// May fail (same-thread case only) if too many steps are already queued, which means a client is
|
|
// firing them faster than they can possibly be carried out.
|
|
//
|
|
// 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);
|
|
|
|
// Steps queue up now, so this only fails once the queue is full - a client stepping
|
|
// far faster than frames go by. No step happens and no cpu.stepping event fires in
|
|
// that case, which would otherwise be indistinguishable from one still in flight, so
|
|
// surface it as an error rather than leaving the caller waiting.
|
|
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.
|
|
//
|
|
// Response (same event name) with no extra data. A cpu.stepping event follows 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.
|
|
//
|
|
// Response (same event name) with no extra data. A cpu.stepping event follows 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.
|
|
//
|
|
// Response (same event name) with no extra data. A cpu.stepping event follows 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();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Run until a point in emulated time (cpu.runUntilTime)
|
|
//
|
|
// The counterpart to cpu.runUntil for "let the game get N seconds in", which is what lining a
|
|
// scripted repro up with a wall-clock description of a bug needs. Polling cpu.status in a loop
|
|
// does the same job far more slowly and lands somewhere different every run; this stops on the
|
|
// requested tick, so the same script reaches the same place every time.
|
|
//
|
|
// Parameters (exactly one of):
|
|
// - us: absolute emulated microseconds to run until, as reported by cpu.status.
|
|
// - relativeUs: microseconds to run for, measured from now.
|
|
//
|
|
// Response (same event name):
|
|
// - targetUs: the absolute emulated time it will stop at.
|
|
// - us: emulated time right now.
|
|
// A cpu.stepping event follows once it gets there. Note that anything else that stops the CPU
|
|
// first - a breakpoint, an exception - cancels the deadline, same as it cancels a step.
|
|
void WebSocketSteppingState::RunUntilTime(DebuggerRequest &req) {
|
|
if (!currentDebugMIPS->isAlive()) {
|
|
return req.Fail("CPU not started");
|
|
}
|
|
|
|
const bool absolute = req.HasParam("us");
|
|
if (absolute == req.HasParam("relativeUs")) {
|
|
return req.Fail("Pass exactly one of 'us' or 'relativeUs'");
|
|
}
|
|
|
|
double requested = 0.0;
|
|
if (!req.ParamF64(absolute ? "us" : "relativeUs", &requested)) {
|
|
// Error already sent.
|
|
return;
|
|
}
|
|
if (requested < 0.0) {
|
|
return req.Fail("Time must not be negative");
|
|
}
|
|
|
|
// Route the actual 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([&] {
|
|
const u64 nowUs = CoreTiming::GetGlobalTimeUs();
|
|
const u64 targetUs = absolute ? (u64)requested : nowUs + (u64)requested;
|
|
if (targetUs <= nowUs) {
|
|
req.Fail("Target time has already passed");
|
|
return;
|
|
}
|
|
|
|
CoreTiming::SetBreakDeadlineUs(targetUs);
|
|
|
|
PrepareResume();
|
|
Core_Resume();
|
|
|
|
JsonWriter &json = req.Respond();
|
|
json.writeFloat("targetUs", (double)targetUs);
|
|
json.writeFloat("us", (double)nowUs);
|
|
});
|
|
}
|
|
|
|
// Jump after the next HLE call (cpu.nextHLE)
|
|
//
|
|
// No parameters.
|
|
//
|
|
// Response (same event name) with no extra data. A cpu.stepping event follows 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);
|
|
}
|