Merge pull request #22051 from hrydgard/web-debugger-access

Refactor the websocket debug interface to be thread-safe
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-08-08 13:26:38 +02:00
commit 1f6f663e2a
20 files changed
+1296 -1110

No files matched your search

+70 -5
View File
@@ -17,9 +17,13 @@
#include "ppsspp_config.h"
#include <atomic>
#include <cstdint>
#include <mutex>
#include <memory>
#include <set>
#include <thread>
#include <vector>
#include <condition_variable>
#include "Common/System/System.h"
@@ -67,6 +71,64 @@ struct CPUStepCommand {
static CPUStepCommand g_cpuStepCommand;
// Task queue for Core_RunOnCPUThread(), see Core.h for the rationale. Drained from Core_RunLoopUntil()
// below, so at least once per call to it (i.e. about once per host frame) even while the CPU is fully
// running, and continuously (in a tight spin) while it's stepping/paused.
struct CPUThreadTask {
std::function<void()> func;
bool done = false;
};
static std::mutex g_cpuQueueMutex;
static std::condition_variable g_cpuQueueCond;
static std::vector<std::shared_ptr<CPUThreadTask>> g_cpuQueue;
static std::once_flag g_cpuThreadIdOnce;
static std::thread::id g_cpuThreadId;
// Published via release/acquire around g_cpuThreadIdOnce, so it's safe to check from other threads
// without taking g_cpuQueueMutex - g_cpuThreadId itself never changes once this becomes true.
static std::atomic<bool> g_cpuThreadIdValid{ false };
void Core_RunOnCPUThread(std::function<void()> func) {
if (g_cpuThreadIdValid.load(std::memory_order_acquire) && std::this_thread::get_id() == g_cpuThreadId) {
// Already on the CPU thread (or called before it's ever run) - just do it now, avoids deadlock.
func();
return;
}
auto task = std::make_shared<CPUThreadTask>();
task->func = std::move(func);
std::unique_lock<std::mutex> guard(g_cpuQueueMutex);
g_cpuQueue.push_back(task);
g_cpuQueueCond.wait(guard, [&] { return task->done; });
}
// Called from the CPU thread only.
static void Core_ProcessCPUQueue() {
std::call_once(g_cpuThreadIdOnce, [] {
g_cpuThreadId = std::this_thread::get_id();
g_cpuThreadIdValid.store(true, std::memory_order_release);
});
std::vector<std::shared_ptr<CPUThreadTask>> tasks;
{
std::lock_guard<std::mutex> guard(g_cpuQueueMutex);
if (g_cpuQueue.empty())
return;
tasks = std::move(g_cpuQueue);
g_cpuQueue.clear();
}
for (auto &task : tasks)
task->func();
{
std::lock_guard<std::mutex> guard(g_cpuQueueMutex);
for (auto &task : tasks)
task->done = true;
}
g_cpuQueueCond.notify_all();
}
// This is so that external threads can wait for the CPU to become inactive.
static std::condition_variable m_InactiveCond;
static std::mutex m_hInactiveMutex;
@@ -206,6 +268,11 @@ bool Core_GetPowerSaving() {
void Core_RunLoopUntil(u64 globalticks) {
while (true) {
// Drain any functions queued up by Core_RunOnCPUThread() from other threads. Doing this at the
// top of this loop means it's reached at least once per call (i.e. about once per host frame)
// even while the CPU is fully running, and continuously (in a tight spin) while it's stepping/paused.
Core_ProcessCPUQueue();
switch (coreState) {
case CORE_POWERDOWN:
case CORE_RUNTIME_ERROR:
@@ -274,8 +341,7 @@ bool Core_RequestCPUStep(CPUStepType type, int stepSize) {
}
// Handles more advanced step types (used by the debugger).
// stepSize is to support stepping through compound instructions like fused lui+ladd (li).
// Yes, our disassembler does support those.
// stepSize is always in instructions (4 bytes each), never bytes.
// Doesn't return the new address, as that's just mips->getPC().
// Internal use.
static void Core_PerformCPUStep(MIPSDebugInterface *cpu, CPUStepType stepType, int stepSize) {
@@ -283,10 +349,9 @@ static void Core_PerformCPUStep(MIPSDebugInterface *cpu, CPUStepType stepType, i
case CPUStepType::Into:
{
u32 currentPc = cpu->GetPC();
u32 newAddress = currentPc + stepSize;
// If the current PC is on a breakpoint, the user still wants the step to happen.
g_breakpoints.SetSkipFirst(currentPc);
for (int i = 0; i < (int)(newAddress - currentPc) / 4; i++) {
for (int i = 0; i < stepSize; i++) {
currentMIPS->SingleStep();
}
break;
@@ -294,7 +359,7 @@ static void Core_PerformCPUStep(MIPSDebugInterface *cpu, CPUStepType stepType, i
case CPUStepType::Over:
{
u32 currentPc = cpu->GetPC();
u32 breakpointAddress = currentPc + stepSize;
u32 breakpointAddress = currentPc + stepSize * 4;
g_breakpoints.SetSkipFirst(currentPc);
MIPSAnalyst::MipsOpcodeInfo info = MIPSAnalyst::GetOpcodeInfo(cpu, cpu->GetPC());
+16
View File
@@ -18,6 +18,7 @@
#pragma once
#include <cstdint>
#include <functional>
#include <string>
#include <string_view>
@@ -84,6 +85,7 @@ BreakReason Core_BreakReason();
// This should be called externally.
// Can fail if another step type was requested this frame.
// stepSize is always in instructions (4 bytes each), never bytes - see Core_PerformCPUStep in Core.cpp.
bool Core_RequestCPUStep(CPUStepType stepType, int stepSize);
bool Core_NextFrame();
@@ -148,6 +150,20 @@ bool Core_GetPowerSaving();
void Core_RunLoopUntil(u64 globalticks);
// Runs a function on the CPU thread - the thread that calls Core_RunLoopUntil (and thus, indirectly,
// NativeFrame). Useful for code running on unrelated threads (like the WebSocket debugger) that needs to
// safely touch state that's otherwise only ever touched from that thread (breakpoints, stepping, etc.),
// instead of poking at it directly from wherever the call happens to come from.
//
// Safe to call from any thread, including the CPU thread itself (in which case func just runs immediately).
// Blocks the calling thread until func has actually run, so don't call this from the CPU thread with
// something that would itself try to wait on the CPU thread - that'll deadlock.
//
// Drained at the top of every Core_RunLoopUntil() iteration, so it's reached continuously (in a tight
// spin) while the CPU is stepping/paused, and at least once per call (i.e. about once per host frame)
// even while it's fully running.
void Core_RunOnCPUThread(std::function<void()> func);
extern volatile CoreState coreState;
extern volatile bool coreStatePending;
-155
View File
@@ -595,26 +595,6 @@ void DisassemblyFunction::load()
{
generateBranchLines();
// gather all branch targets
std::set<u32> branchTargets;
{
std::lock_guard<std::recursive_mutex> guard(lock_);
for (size_t i = 0; i < lines.size(); i++)
{
switch (lines[i].type)
{
case LINE_DOWN:
branchTargets.insert(lines[i].second);
break;
case LINE_UP:
branchTargets.insert(lines[i].first);
break;
default:
break;
}
}
}
DebugInterface *cpu = g_disassemblyManager.getCpu();
u32 funcPos = address;
u32 funcEnd = address+size;
@@ -655,7 +635,6 @@ void DisassemblyFunction::load()
}
MIPSAnalyst::MipsOpcodeInfo opInfo = MIPSAnalyst::GetOpcodeInfo(cpu,funcPos);
u32 opAddress = funcPos;
funcPos += 4;
// skip branches and their delay slots
@@ -665,70 +644,6 @@ void DisassemblyFunction::load()
continue;
}
// lui
if (MIPS_GET_OP(opInfo.encodedOpcode) == 0x0F && funcPos < funcEnd && funcPos != nextData)
{
MIPSOpcode next = Memory::Read_Instruction(funcPos);
MIPSInfo nextInfo = MIPSGetInfo(next);
u32 immediate = ((opInfo.encodedOpcode & 0xFFFF) << 16) + (s16)(next.encoding & 0xFFFF);
int rt = MIPS_GET_RT(opInfo.encodedOpcode);
int nextRs = MIPS_GET_RS(next.encoding);
int nextRt = MIPS_GET_RT(next.encoding);
// both rs and rt of the second op have to match rt of the first,
// otherwise there may be hidden consequences if the macro is displayed.
// also, don't create a macro if something branches into the middle of it
if (nextRs == rt && nextRt == rt && branchTargets.find(funcPos) == branchTargets.end())
{
DisassemblyMacro* macro = NULL;
switch (MIPS_GET_OP(next.encoding))
{
case 0x09: // addiu
macro = new DisassemblyMacro(opAddress);
macro->setMacroLi(immediate,rt);
funcPos += 4;
break;
case 0x20: // lb
case 0x21: // lh
case 0x23: // lw
case 0x24: // lbu
case 0x25: // lhu
case 0x28: // sb
case 0x29: // sh
case 0x2B: // sw
macro = new DisassemblyMacro(opAddress);
int dataSize = MIPSGetMemoryAccessSize(next);
if (dataSize == 0) {
delete macro;
return;
}
macro->setMacroMemory(MIPSGetName(next),immediate,rt,dataSize);
funcPos += 4;
break;
}
if (macro != NULL)
{
if (opcodeSequenceStart != opAddress)
addOpcodeSequence(opcodeSequenceStart,opAddress);
std::lock_guard<std::recursive_mutex> guard(lock_);
entries[opAddress] = macro;
for (int i = 0; i < macro->getNumLines(); i++)
{
lineAddresses.push_back(macro->getLineAddress(i));
}
opcodeSequenceStart = funcPos;
continue;
}
}
}
// just a normal opcode
}
@@ -801,76 +716,6 @@ void DisassemblyOpcode::getBranchLines(u32 start, u32 size, std::vector<BranchLi
}
void DisassemblyMacro::setMacroLi(u32 _immediate, u8 _rt)
{
type = MACRO_LI;
name = "li";
immediate = _immediate;
rt = _rt;
numOpcodes = 2;
}
void DisassemblyMacro::setMacroMemory(std::string_view _name, u32 _immediate, u8 _rt, int _dataSize)
{
type = MACRO_MEMORYIMM;
name = _name;
immediate = _immediate;
rt = _rt;
dataSize = _dataSize;
numOpcodes = 2;
}
bool DisassemblyMacro::disassemble(u32 address, DisassemblyLineInfo &dest, bool insertSymbols, DebugInterface *cpuDebug)
{
char buffer[64];
dest.type = DISTYPE_MACRO;
dest.info = MIPSAnalyst::GetOpcodeInfo(cpuDebug, address);
std::string addressSymbol;
switch (type)
{
case MACRO_LI:
dest.name = name;
addressSymbol = g_symbolMap->GetLabelString(immediate);
if (!addressSymbol.empty() && insertSymbols) {
snprintf(buffer, sizeof(buffer), "%s,%s", MIPSDebugInterface::GetRegName(0, rt).c_str(), addressSymbol.c_str());
} else {
snprintf(buffer, sizeof(buffer), "%s,0x%08X", MIPSDebugInterface::GetRegName(0, rt).c_str(), immediate);
}
dest.params = buffer;
dest.info.hasRelevantAddress = true;
dest.info.relevantAddress = immediate;
break;
case MACRO_MEMORYIMM:
dest.name = name;
addressSymbol = g_symbolMap->GetLabelString(immediate);
if (!addressSymbol.empty() && insertSymbols) {
snprintf(buffer, sizeof(buffer), "%s,%s", MIPSDebugInterface::GetRegName(0, rt).c_str(), addressSymbol.c_str());
} else {
snprintf(buffer, sizeof(buffer), "%s,0x%08X", MIPSDebugInterface::GetRegName(0, rt).c_str(), immediate);
}
dest.params = buffer;
dest.info.isDataAccess = true;
dest.info.dataAddress = immediate;
dest.info.dataSize = dataSize;
dest.info.hasRelevantAddress = true;
dest.info.relevantAddress = immediate;
break;
default:
return false;
}
dest.totalSize = getTotalSize();
return true;
}
DisassemblyData::DisassemblyData(u32 _address, u32 _size, DataType _type): address(_address), size(_size), type(_type)
{
_dbg_assert_(PSP_GetBootState() == BootState::Complete);
+1 -28
View File
@@ -32,7 +32,7 @@ typedef u64 HashType;
typedef u32 HashType;
#endif
enum DisassemblyLineType { DISTYPE_OPCODE, DISTYPE_MACRO, DISTYPE_DATA, DISTYPE_OTHER };
enum DisassemblyLineType { DISTYPE_OPCODE, DISTYPE_DATA, DISTYPE_OTHER };
struct DisassemblyLineInfo
{
@@ -117,33 +117,6 @@ private:
};
class DisassemblyMacro: public DisassemblyEntry
{
public:
DisassemblyMacro(u32 _address): address(_address) { }
void setMacroLi(u32 _immediate, u8 _rt);
void setMacroMemory(std::string_view _name, u32 _immediate, u8 _rt, int _dataSize);
void recheck() override { };
int getNumLines() override { return 1; };
int getLineNum(u32 address, bool findStart) override { return 0; };
u32 getLineAddress(int line) override { return address; };
u32 getTotalSize() override { return numOpcodes * 4; };
bool disassemble(u32 address, DisassemblyLineInfo& dest, bool insertSymbols, DebugInterface *cpuDebug) override;
private:
enum MacroType { MACRO_LI, MACRO_MEMORYIMM };
MacroType type;
std::string name;
u32 immediate;
u32 address;
u32 numOpcodes;
u8 rt;
int dataSize;
};
class DisassemblyData: public DisassemblyEntry
{
public:
+11 -5
View File
@@ -128,11 +128,13 @@ static void SetupDebuggerLock() {
}
void HandleDebuggerRequest(const http::ServerRequest &request) {
net::WebSocketServer *ws = net::WebSocketServer::CreateAsUpgrade(request, "debugger.ppsspp.org");
if (!ws)
return;
SetCurrentThreadName("WebSocketDebugger");
net::WebSocketServer *ws = net::WebSocketServer::CreateAsUpgrade(request, "debugger.ppsspp.org");
if (!ws) {
return;
}
UpdateConnected(1);
SetupDebuggerLock();
@@ -188,7 +190,10 @@ void HandleDebuggerRequest(const http::ServerRequest &request) {
ws->Send(DebuggerErrorEvent("Bad message: binary WebSocket frames are not supported", LogLevel::LERROR));
});
while (ws->Process(highActivity ? 1.0f / 1000.0f : 1.0f / 60.0f)) {
// Don't out-line the highActivity check, it needs to recompute on every lap.
constexpr float lowActivityPollTimeStep = 1.0f / 60.0f;
constexpr float highActivityPollTimeStep = 1.0f / 1000.0f;
while (ws->Process(highActivity ? highActivityPollTimeStep : lowActivityPollTimeStep)) {
std::lock_guard<std::mutex> guard(lifecycleLock);
// These send events that aren't just responses to requests
@@ -212,6 +217,7 @@ void HandleDebuggerRequest(const http::ServerRequest &request) {
if (stopRequested) {
ws->Close(net::WebSocketClose::GOING_AWAY);
}
if (highActivity > 0) {
highActivity--;
}
+112 -71
View File
@@ -16,6 +16,7 @@
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include "Common/StringUtils.h"
#include "Core/Core.h"
#include "Core/Debugger/Breakpoints.h"
#include "Core/Debugger/DisassemblyManager.h"
#include "Core/Debugger/SymbolMap.h"
@@ -138,8 +139,12 @@ void WebSocketCPUBreakpointAdd(DebuggerRequest &req) {
if (!params.Parse(req))
return;
g_breakpoints.AddBreakPoint(params.address);
params.Apply();
// Route the actual breakpoint manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
g_breakpoints.AddBreakPoint(params.address);
params.Apply();
});
req.Respond();
}
@@ -157,11 +162,19 @@ void WebSocketCPUBreakpointUpdate(DebuggerRequest &req) {
WebSocketCPUBreakpointParams params;
if (!params.Parse(req))
return;
bool enabled;
if (!g_breakpoints.IsAddressBreakPoint(params.address, &enabled))
return req.Fail("Breakpoint not found");
params.Apply();
// Route the actual breakpoint manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
bool found = false;
Core_RunOnCPUThread([&] {
bool enabled;
found = g_breakpoints.IsAddressBreakPoint(params.address, &enabled);
if (found)
params.Apply();
});
if (!found)
return req.Fail("Breakpoint not found");
req.Respond();
}
@@ -180,7 +193,11 @@ void WebSocketCPUBreakpointRemove(DebuggerRequest &req) {
if (!req.ParamU32("address", &address))
return;
g_breakpoints.RemoveBreakPoint(address);
// Route the actual breakpoint manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
g_breakpoints.RemoveBreakPoint(address);
});
req.Respond();
}
@@ -202,38 +219,42 @@ void WebSocketCPUBreakpointList(DebuggerRequest &req) {
return req.Fail("CPU not started");
}
JsonWriter &json = req.Respond();
json.pushArray("breakpoints");
auto bps = g_breakpoints.GetBreakpoints();
for (const auto &bp : bps) {
if (bp.temporary)
continue;
// Route the breakpoint/symbol/disassembly reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
JsonWriter &json = req.Respond();
json.pushArray("breakpoints");
std::vector<BreakPoint> bps = g_breakpoints.GetBreakpoints();
for (const BreakPoint &bp : bps) {
if (bp.temporary)
continue;
json.pushDict();
json.writeUint("address", bp.addr);
json.writeBool("enabled", bp.IsEnabled());
json.writeBool("log", (bp.result & BREAK_ACTION_LOG) != 0);
if (bp.hasCond)
json.writeString("condition", bp.cond.expressionString);
else
json.writeNull("condition");
if (!bp.logFormat.empty())
json.writeString("logFormat", bp.logFormat);
else
json.writeNull("logFormat");
std::string symbol = g_symbolMap->GetLabelString(bp.addr);
if (symbol.empty())
json.writeNull("symbol");
else
json.writeString("symbol", symbol);
json.pushDict();
json.writeUint("address", bp.addr);
json.writeBool("enabled", bp.IsEnabled());
json.writeBool("log", (bp.result & BREAK_ACTION_LOG) != 0);
if (bp.hasCond)
json.writeString("condition", bp.cond.expressionString);
else
json.writeNull("condition");
if (!bp.logFormat.empty())
json.writeString("logFormat", bp.logFormat);
else
json.writeNull("logFormat");
std::string symbol = g_symbolMap->GetLabelString(bp.addr);
if (symbol.empty())
json.writeNull("symbol");
else
json.writeString("symbol", symbol);
DisassemblyLineInfo line;
g_disassemblyManager.getLine(g_disassemblyManager.getStartAddress(bp.addr), true, line, currentDebugMIPS);
json.writeString("code", line.name + " " + line.params);
DisassemblyLineInfo line;
g_disassemblyManager.getLine(g_disassemblyManager.getStartAddress(bp.addr), true, line, currentDebugMIPS);
json.writeString("code", line.name + " " + line.params);
json.pop();
}
json.pop();
}
json.pop();
});
}
struct WebSocketMemoryBreakpointParams {
@@ -361,8 +382,12 @@ void WebSocketMemoryBreakpointAdd(DebuggerRequest &req) {
if (!params.Parse(req))
return;
g_breakpoints.AddMemCheck(params.address, params.end, params.cond, params.Result(true));
params.Apply();
// Route the actual breakpoint manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
g_breakpoints.AddMemCheck(params.address, params.end, params.cond, params.Result(true));
params.Apply();
});
req.Respond();
}
@@ -386,12 +411,20 @@ void WebSocketMemoryBreakpointUpdate(DebuggerRequest &req) {
if (!params.Parse(req))
return;
MemCheck mc;
if (!g_breakpoints.GetMemCheck(params.address, params.end, &mc))
return req.Fail("Breakpoint not found");
// Route the actual breakpoint manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
bool found = false;
Core_RunOnCPUThread([&] {
MemCheck mc;
found = g_breakpoints.GetMemCheck(params.address, params.end, &mc);
if (found) {
g_breakpoints.ChangeMemCheck(params.address, params.end, params.cond, params.Result(true));
params.Apply();
}
});
g_breakpoints.ChangeMemCheck(params.address, params.end, params.cond, params.Result(true));
params.Apply();
if (!found)
return req.Fail("Breakpoint not found");
req.Respond();
}
@@ -414,7 +447,11 @@ void WebSocketMemoryBreakpointRemove(DebuggerRequest &req) {
if (!req.ParamU32("size", &size))
return;
g_breakpoints.RemoveMemCheck(address, size == 0 ? 0 : address + size);
// Route the actual breakpoint manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
g_breakpoints.RemoveMemCheck(address, size == 0 ? 0 : address + size);
});
req.Respond();
}
@@ -440,34 +477,38 @@ void WebSocketMemoryBreakpointList(DebuggerRequest &req) {
return req.Fail("CPU not started");
}
JsonWriter &json = req.Respond();
json.pushArray("breakpoints");
auto mcs = g_breakpoints.GetMemChecks();
for (const auto &mc : mcs) {
json.pushDict();
json.writeUint("address", mc.start);
json.writeUint("size", mc.end == 0 ? 0 : mc.end - mc.start);
json.writeBool("enabled", mc.IsEnabled());
json.writeBool("log", (mc.result & BREAK_ACTION_LOG) != 0);
json.writeBool("read", (mc.cond & MEMCHECK_READ) != 0);
json.writeBool("write", (mc.cond & MEMCHECK_WRITE) != 0);
json.writeBool("change", (mc.cond & MEMCHECK_WRITE_ONCHANGE) != 0);
json.writeUint("hits", mc.numHits);
if (mc.hasCondition)
json.writeString("condition", mc.condition.expressionString);
else
json.writeNull("condition");
if (!mc.logFormat.empty())
json.writeString("logFormat", mc.logFormat);
else
json.writeNull("logFormat");
std::string symbol = g_symbolMap->GetLabelString(mc.start);
if (symbol.empty())
json.writeNull("symbol");
else
json.writeString("symbol", symbol);
// Route the breakpoint/symbol reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
JsonWriter &json = req.Respond();
json.pushArray("breakpoints");
std::vector<MemCheck> mcs = g_breakpoints.GetMemChecks();
for (const MemCheck &mc : mcs) {
json.pushDict();
json.writeUint("address", mc.start);
json.writeUint("size", mc.end == 0 ? 0 : mc.end - mc.start);
json.writeBool("enabled", mc.IsEnabled());
json.writeBool("log", (mc.result & BREAK_ACTION_LOG) != 0);
json.writeBool("read", (mc.cond & MEMCHECK_READ) != 0);
json.writeBool("write", (mc.cond & MEMCHECK_WRITE) != 0);
json.writeBool("change", (mc.cond & MEMCHECK_WRITE_ONCHANGE) != 0);
json.writeUint("hits", mc.numHits);
if (mc.hasCondition)
json.writeString("condition", mc.condition.expressionString);
else
json.writeNull("condition");
if (!mc.logFormat.empty())
json.writeString("logFormat", mc.logFormat);
else
json.writeNull("logFormat");
std::string symbol = g_symbolMap->GetLabelString(mc.start);
if (symbol.empty())
json.writeNull("symbol");
else
json.writeString("symbol", symbol);
json.pop();
}
json.pop();
}
json.pop();
});
}
+162 -132
View File
@@ -72,6 +72,9 @@ void WebSocketCPUStepping(DebuggerRequest &req) {
return req.Fail("CPU not started");
}
if (!Core_IsStepping() && Core_IsActive()) {
// 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::DebugStep, 0);
}
}
@@ -89,11 +92,15 @@ void WebSocketCPUResume(DebuggerRequest &req) {
return req.Fail("CPU not stepping");
}
g_breakpoints.SetSkipFirst(currentMIPS->pc);
if (currentMIPS->inDelaySlot) {
Core_RequestCPUStep(CPUStepType::Into, 1);
}
Core_Resume();
// 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([&] {
g_breakpoints.SetSkipFirst(currentMIPS->pc);
if (currentMIPS->inDelaySlot) {
Core_RequestCPUStep(CPUStepType::Into, 1);
}
Core_Resume();
});
}
// Request the current CPU status (cpu.status)
@@ -105,6 +112,10 @@ void WebSocketCPUResume(DebuggerRequest &req) {
// - paused: boolean, CPU paused or not started yet.
// - pc: number value of PC register (inaccurate unless stepping.)
// - ticks: number of CPU cycles into emulation.
// Deliberately not routed through Core_RunOnCPUThread(): this is meant to be a cheap, frequently-pollable
// status check, and the "pc" field is already documented as inaccurate unless stepping - queuing it would
// add up to a frame of latency to every poll for no real accuracy benefit. Matches how SteppingBroadcaster
// already reads this same state directly from the WebSocket thread.
void WebSocketCPUStatus(DebuggerRequest &req) {
JsonWriter &json = req.Respond();
@@ -131,55 +142,59 @@ void WebSocketCPUStatus(DebuggerRequest &req) {
// - uintValues: array of unsigned integer values for the registers.
// - floatValues: array of strings showing float representation. May be "nan", "inf", or "-inf".
void WebSocketCPUGetAllRegs(DebuggerRequest &req) {
auto cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
// Route the actual register reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
JsonWriter &json = req.Respond();
JsonWriter &json = req.Respond();
json.pushArray("categories");
for (int c = 0; c < MIPSDebugInterface::GetNumCategories(); ++c) {
json.pushDict();
json.writeInt("id", c);
json.writeString("name", MIPSDebugInterface::GetCategoryName(c));
json.pushArray("categories");
for (int c = 0; c < MIPSDebugInterface::GetNumCategories(); ++c) {
json.pushDict();
json.writeInt("id", c);
json.writeString("name", MIPSDebugInterface::GetCategoryName(c));
int total = MIPSDebugInterface::GetNumRegsInCategory(c);
int total = MIPSDebugInterface::GetNumRegsInCategory(c);
json.pushArray("registerNames");
for (int r = 0; r < total; ++r)
json.writeString(MIPSDebugInterface::GetRegName(c, r));
if (c == 0) {
json.writeString("pc");
json.writeString("hi");
json.writeString("lo");
json.pushArray("registerNames");
for (int r = 0; r < total; ++r)
json.writeString(MIPSDebugInterface::GetRegName(c, r));
if (c == 0) {
json.writeString("pc");
json.writeString("hi");
json.writeString("lo");
}
json.pop();
json.pushArray("uintValues");
// Writing as floating point to avoid negatives. Actually double, so safe.
for (int r = 0; r < total; ++r)
json.writeUint(cpuDebug->GetRegValue(c, r));
if (c == 0) {
json.writeUint(cpuDebug->GetPC());
json.writeUint(cpuDebug->GetHi());
json.writeUint(cpuDebug->GetLo());
}
json.pop();
json.pushArray("floatValues");
// Note: String so it can have Infinity and NaN.
for (int r = 0; r < total; ++r)
json.writeString(RegValueAsFloat(cpuDebug->GetRegValue(c, r)));
if (c == 0) {
json.writeString(RegValueAsFloat(cpuDebug->GetPC()));
json.writeString(RegValueAsFloat(cpuDebug->GetHi()));
json.writeString(RegValueAsFloat(cpuDebug->GetLo()));
}
json.pop();
json.pop();
}
json.pop();
json.pushArray("uintValues");
// Writing as floating point to avoid negatives. Actually double, so safe.
for (int r = 0; r < total; ++r)
json.writeUint(cpuDebug->GetRegValue(c, r));
if (c == 0) {
json.writeUint(cpuDebug->GetPC());
json.writeUint(cpuDebug->GetHi());
json.writeUint(cpuDebug->GetLo());
}
json.pop();
json.pushArray("floatValues");
// Note: String so it can have Infinity and NaN.
for (int r = 0; r < total; ++r)
json.writeString(RegValueAsFloat(cpuDebug->GetRegValue(c, r)));
if (c == 0) {
json.writeString(RegValueAsFloat(cpuDebug->GetPC()));
json.writeString(RegValueAsFloat(cpuDebug->GetHi()));
json.writeString(RegValueAsFloat(cpuDebug->GetLo()));
}
json.pop();
json.pop();
}
json.pop();
});
}
enum class DebuggerRegType {
@@ -271,37 +286,41 @@ static DebuggerRegType ValidateCatReg(DebuggerRequest &req, int *cat, int *reg)
// - uintValue: value in register.
// - floatValue: string showing float representation. May be "nan", "inf", or "-inf".
void WebSocketCPUGetReg(DebuggerRequest &req) {
auto cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
// Route the actual register read to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
int cat, reg;
uint32_t val = 0;
switch (ValidateCatReg(req, &cat, &reg)) {
case DebuggerRegType::NORMAL:
val = cpuDebug->GetRegValue(cat, reg);
break;
int cat, reg;
uint32_t val = 0;
switch (ValidateCatReg(req, &cat, &reg)) {
case DebuggerRegType::NORMAL:
val = cpuDebug->GetRegValue(cat, reg);
break;
case DebuggerRegType::PC:
val = cpuDebug->GetPC();
break;
case DebuggerRegType::HI:
val = cpuDebug->GetHi();
break;
case DebuggerRegType::LO:
val = cpuDebug->GetLo();
break;
case DebuggerRegType::PC:
val = cpuDebug->GetPC();
break;
case DebuggerRegType::HI:
val = cpuDebug->GetHi();
break;
case DebuggerRegType::LO:
val = cpuDebug->GetLo();
break;
case DebuggerRegType::INVALID:
// Error response already sent.
return;
}
case DebuggerRegType::INVALID:
// Error response already sent.
return;
}
JsonWriter &json = req.Respond();
json.writeInt("category", cat);
json.writeInt("register", reg);
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
JsonWriter &json = req.Respond();
json.writeInt("category", cat);
json.writeInt("register", reg);
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
});
}
// Update the value of a single register (cpu.setReg)
@@ -334,50 +353,55 @@ void WebSocketCPUSetReg(DebuggerRequest &req) {
return req.Fail("CPU currently running (cpu.stepping first)");
}
auto cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
// Route the actual register write to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
uint32_t val;
if (!req.ParamU32("value", &val, true)) {
// Already sent error.
return;
}
int cat, reg;
switch (ValidateCatReg(req, &cat, &reg)) {
case DebuggerRegType::NORMAL:
if (cat == 0 && reg == 0 && val != 0) {
return req.Fail("Cannot change reg zero");
uint32_t val;
if (!req.ParamU32("value", &val, true)) {
// Already sent error.
return;
}
cpuDebug->SetRegValue(cat, reg, val);
// In case part of it was ignored (e.g. flags reg.)
val = cpuDebug->GetRegValue(cat, reg);
break;
case DebuggerRegType::PC:
cpuDebug->SetPC(val);
break;
case DebuggerRegType::HI:
cpuDebug->SetHi(val);
break;
case DebuggerRegType::LO:
cpuDebug->SetLo(val);
break;
int cat, reg;
switch (ValidateCatReg(req, &cat, &reg)) {
case DebuggerRegType::NORMAL:
if (cat == 0 && reg == 0 && val != 0) {
req.Fail("Cannot change reg zero");
return;
}
cpuDebug->SetRegValue(cat, reg, val);
// In case part of it was ignored (e.g. flags reg.)
val = cpuDebug->GetRegValue(cat, reg);
break;
case DebuggerRegType::INVALID:
// Error response already sent.
return;
}
case DebuggerRegType::PC:
cpuDebug->SetPC(val);
break;
case DebuggerRegType::HI:
cpuDebug->SetHi(val);
break;
case DebuggerRegType::LO:
cpuDebug->SetLo(val);
break;
Reporting::NotifyDebugger();
case DebuggerRegType::INVALID:
// Error response already sent.
return;
}
JsonWriter &json = req.Respond();
// Repeat it back just to avoid confusion on how it parsed.
json.writeInt("category", cat);
json.writeInt("register", reg);
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
Reporting::NotifyDebugger();
JsonWriter &json = req.Respond();
// Repeat it back just to avoid confusion on how it parsed.
json.writeInt("category", cat);
json.writeInt("register", reg);
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
});
}
// Evaluate an expression (cpu.evaluate)
@@ -394,26 +418,32 @@ void WebSocketCPUEvaluate(DebuggerRequest &req) {
return req.Fail("CPU not started");
}
auto cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
// Route the actual register/symbol reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
std::string exp;
if (!req.ParamString("expression", &exp)) {
// Already sent error.
return;
}
std::string exp;
if (!req.ParamString("expression", &exp)) {
// Already sent error.
return;
}
u32 val;
PostfixExpression postfix;
if (!initExpression(cpuDebug, exp.c_str(), postfix)) {
return req.Fail(StringFromFormat("Could not parse expression syntax: %s", getExpressionError()));
}
if (!parseExpression(cpuDebug, postfix, val)) {
return req.Fail(StringFromFormat("Could not evaluate expression: %s", getExpressionError()));
}
u32 val;
PostfixExpression postfix;
if (!initExpression(cpuDebug, exp.c_str(), postfix)) {
req.Fail(StringFromFormat("Could not parse expression syntax: %s", getExpressionError()));
return;
}
if (!parseExpression(cpuDebug, postfix, val)) {
req.Fail(StringFromFormat("Could not evaluate expression: %s", getExpressionError()));
return;
}
JsonWriter &json = req.Respond();
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
JsonWriter &json = req.Respond();
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
});
}
+127 -106
View File
@@ -21,6 +21,7 @@
#include "Common/Data/Encoding/Utf8.h"
#include "Common/StringUtils.h"
#include "Core/Core.h"
#include "Core/Debugger/Breakpoints.h"
#include "Core/Debugger/DisassemblyManager.h"
#include "Core/Debugger/WebSocket/DisasmSubscriber.h"
@@ -51,7 +52,7 @@ protected:
};
DebuggerSubscriber *WebSocketDisasmInit(DebuggerEventHandlerMap &map) {
auto p = new WebSocketDisasmState();
WebSocketDisasmState *p = new WebSocketDisasmState();
map["memory.base"] = [p](DebuggerRequest &req) { p->Base(req); };
map["memory.disasm"] = [p](DebuggerRequest &req) { p->Disasm(req); };
map["memory.searchDisasm"] = [p](DebuggerRequest &req) { p->SearchDisasm(req); };
@@ -78,8 +79,6 @@ void WebSocketDisasmState::WriteDisasmLine(JsonWriter &json, const DisassemblyLi
json.pushDict();
if (l.type == DISTYPE_OPCODE)
json.writeString("type", "opcode");
else if (l.type == DISTYPE_MACRO)
json.writeString("type", "macro");
else if (l.type == DISTYPE_DATA)
json.writeString("type", "data");
else if (l.type == DISTYPE_OTHER)
@@ -145,7 +144,7 @@ void WebSocketDisasmState::WriteDisasmLine(JsonWriter &json, const DisassemblyLi
json.pushDict("breakpoint");
json.writeBool("enabled", enabled);
json.writeUint("address", addr + breakpointOffset);
auto cond = g_breakpoints.GetBreakPointCondition(addr + breakpointOffset);
BreakPointCond *cond = g_breakpoints.GetBreakPointCondition(addr + breakpointOffset);
if (cond)
json.writeString("condition", cond->expressionString);
else
@@ -297,83 +296,88 @@ void WebSocketDisasmState::Base(DebuggerRequest &req) {
void WebSocketDisasmState::Disasm(DebuggerRequest &req) {
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
auto cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
// In case of client errors, we limit the range to something that won't make us crash.
static const uint32_t MAX_RANGE = 10000;
uint32_t start, end;
if (!req.ParamU32("address", &start))
return;
uint32_t count = 0;
if (!req.ParamU32("count", &count, false, DebuggerParamType::OPTIONAL))
return;
if (count != 0) {
count = std::min(count, MAX_RANGE);
// Let's assume everything is two instructions.
g_disassemblyManager.analyze(start - 4, count * 8 + 8);
start = g_disassemblyManager.getStartAddress(start);
if (start == -1)
req.ParamU32("address", &start);
end = g_disassemblyManager.getNthNextAddress(start, count);
} else if (req.ParamU32("end", &end)) {
end = std::max(start, end);
if (end - start > MAX_RANGE * 4)
end = start + MAX_RANGE * 4;
// Let's assume everything is two instructions at most.
g_disassemblyManager.analyze(start - 4, end - start + 8);
start = g_disassemblyManager.getStartAddress(start);
if (start == -1)
req.ParamU32("address", &start);
// Correct end and calculate count based on it.
// This accounts for macros as one line, although two instructions.
u32 stop = end;
u32 next = start;
count = 0;
if (stop < start) {
for (next = start; next > stop; next = g_disassemblyManager.getNthNextAddress(next, 1)) {
// Route the disassembly manager/symbol reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
uint32_t start, end;
if (!req.ParamU32("address", &start))
return;
uint32_t count = 0;
if (!req.ParamU32("count", &count, false, DebuggerParamType::OPTIONAL))
return;
if (count != 0) {
count = std::min(count, MAX_RANGE);
// Let's assume everything is two instructions.
g_disassemblyManager.analyze(start - 4, count * 8 + 8);
start = g_disassemblyManager.getStartAddress(start);
if (start == -1)
req.ParamU32("address", &start);
end = g_disassemblyManager.getNthNextAddress(start, count);
} else if (req.ParamU32("end", &end)) {
end = std::max(start, end);
if (end - start > MAX_RANGE * 4)
end = start + MAX_RANGE * 4;
// Let's assume everything is two instructions at most.
g_disassemblyManager.analyze(start - 4, end - start + 8);
start = g_disassemblyManager.getStartAddress(start);
if (start == -1)
req.ParamU32("address", &start);
// Correct end and calculate count based on it.
// This accounts for macros as one line, although two instructions.
u32 stop = end;
u32 next = start;
count = 0;
if (stop < start) {
for (next = start; next > stop; next = g_disassemblyManager.getNthNextAddress(next, 1)) {
count++;
}
}
for (end = next; end < stop && end >= next; end = g_disassemblyManager.getNthNextAddress(end, 1)) {
count++;
}
} else {
// Error message already sent.
return;
}
for (end = next; end < stop && end >= next; end = g_disassemblyManager.getNthNextAddress(end, 1)) {
count++;
bool displaySymbols = true;
if (!req.ParamBool("displaySymbols", &displaySymbols, DebuggerParamType::OPTIONAL))
return;
JsonWriter &json = req.Respond();
json.pushDict("range");
json.writeUint("start", start);
json.writeUint("end", end);
json.pop();
json.pushArray("lines");
DisassemblyLineInfo line;
uint32_t addr = start;
for (uint32_t i = 0; i < count; ++i) {
g_disassemblyManager.getLine(addr, displaySymbols, line, cpuDebug);
WriteDisasmLine(json, line);
addr += line.totalSize;
// These are pretty long, so let's grease the wheels a bit.
if (i % 50 == 0)
req.Flush();
}
} else {
// Error message already sent.
return;
}
json.pop();
bool displaySymbols = true;
if (!req.ParamBool("displaySymbols", &displaySymbols, DebuggerParamType::OPTIONAL))
return;
JsonWriter &json = req.Respond();
json.pushDict("range");
json.writeUint("start", start);
json.writeUint("end", end);
json.pop();
json.pushArray("lines");
DisassemblyLineInfo line;
uint32_t addr = start;
for (uint32_t i = 0; i < count; ++i) {
g_disassemblyManager.getLine(addr, displaySymbols, line, cpuDebug);
WriteDisasmLine(json, line);
addr += line.totalSize;
// These are pretty long, so let's grease the wheels a bit.
if (i % 50 == 0)
req.Flush();
}
json.pop();
json.pushArray("branchGuides");
auto branchGuides = g_disassemblyManager.getBranchLines(start, end - start);
for (auto bl : branchGuides)
WriteBranchGuide(json, bl);
json.pop();
json.pushArray("branchGuides");
std::vector<BranchLine> branchGuides = g_disassemblyManager.getBranchLines(start, end - start);
for (const BranchLine &bl : branchGuides)
WriteBranchGuide(json, bl);
json.pop();
});
}
// Search disassembly for some text (memory.searchDisasm)
@@ -390,9 +394,6 @@ void WebSocketDisasmState::Disasm(DebuggerRequest &req) {
void WebSocketDisasmState::SearchDisasm(DebuggerRequest &req) {
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
auto cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
uint32_t start;
if (!req.ParamU32("address", &start))
@@ -421,41 +422,51 @@ void WebSocketDisasmState::SearchDisasm(DebuggerRequest &req) {
std::transform(match.begin(), match.end(), match.begin(), ::tolower);
DisassemblyLineInfo line;
bool found = false;
uint32_t addr = start;
do {
g_disassemblyManager.getLine(addr, displaySymbols, line, cpuDebug);
const std::string addressSymbol = g_symbolMap->GetLabelString(addr);
// Route the disassembly manager/symbol reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
// Note: for a very large [start, end) range, this scan itself can take a while - unlike memory.search
// there's no size cap here, so a huge range will block the CPU thread's own frame pump for its duration.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
std::string mergeForSearch;
// Address+space (9) + symbol + colon+space (2) + name + space(1) + params = 12 fixed size worst case.
mergeForSearch.resize(12 + addressSymbol.size() + line.name.size() + line.params.size());
DisassemblyLineInfo line;
bool found = false;
uint32_t addr = start;
do {
g_disassemblyManager.getLine(addr, displaySymbols, line, cpuDebug);
const std::string addressSymbol = g_symbolMap->GetLabelString(addr);
sprintf(&mergeForSearch[0], "%08x ", addr);
auto inserter = mergeForSearch.begin() + 9;
if (!addressSymbol.empty()) {
inserter = std::transform(addressSymbol.begin(), addressSymbol.end(), inserter, ::tolower);
*inserter++ = ':';
std::string mergeForSearch;
// Address+space (9) + symbol + colon+space (2) + name + space(1) + params = 12 fixed size worst case.
mergeForSearch.resize(12 + addressSymbol.size() + line.name.size() + line.params.size());
sprintf(&mergeForSearch[0], "%08x ", addr);
std::string::iterator inserter = mergeForSearch.begin() + 9;
if (!addressSymbol.empty()) {
inserter = std::transform(addressSymbol.begin(), addressSymbol.end(), inserter, ::tolower);
*inserter++ = ':';
*inserter++ = ' ';
}
inserter = std::transform(line.name.begin(), line.name.end(), inserter, ::tolower);
*inserter++ = ' ';
}
inserter = std::transform(line.name.begin(), line.name.end(), inserter, ::tolower);
*inserter++ = ' ';
inserter = std::transform(line.params.begin(), line.params.end(), inserter, ::tolower);
inserter = std::transform(line.params.begin(), line.params.end(), inserter, ::tolower);
if (mergeForSearch.find(match) != mergeForSearch.npos) {
found = true;
break;
}
if (mergeForSearch.find(match) != mergeForSearch.npos) {
found = true;
break;
}
addr = RoundMemAddressUp(addr + line.totalSize);
} while (addr != end);
addr = RoundMemAddressUp(addr + line.totalSize);
} while (addr != end);
JsonWriter &json = req.Respond();
if (found)
json.writeUint("address", addr);
else
json.writeNull("address");
JsonWriter &json = req.Respond();
if (found)
json.writeUint("address", addr);
else
json.writeNull("address");
});
}
// Assemble an instruction (memory.assemble)
@@ -478,12 +489,22 @@ void WebSocketDisasmState::Assemble(DebuggerRequest &req) {
if (!req.ParamString("code", &code))
return;
// Route the actual code assembly to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
std::string error;
if (!MipsAssembleOpcode(code, currentDebugMIPS, address, &error)) {
uint32_t encoding = 0;
bool ok = false;
Core_RunOnCPUThread([&] {
ok = MipsAssembleOpcode(code, currentDebugMIPS, address, &error);
if (ok)
encoding = Memory::Read_Instruction(address).encoding;
});
if (!ok) {
return req.Fail(StringFromFormat("Could not assemble: %s", error.c_str()));
}
JsonWriter &json = req.Respond();
Reporting::NotifyDebugger();
json.writeUint("encoding", Memory::Read_Instruction(address).encoding);
json.writeUint("encoding", encoding);
}
+347 -260
View File
@@ -98,40 +98,44 @@ static bool DataTypeFromString(const std::string &s, DataType *out) {
// - waitType: numeric wait type, if the thread is waiting, or 0 if not waiting.
// - isCurrent: boolean, true for the currently executing thread.
void WebSocketHLEThreadList(DebuggerRequest &req) {
// Will just return none of the CPU isn't ready yet.
auto threads = GetThreadsInfo();
// Route the actual kernel thread reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
// Will just return none of the CPU isn't ready yet.
std::vector<DebugThreadInfo> threads = GetThreadsInfo();
JsonWriter &json = req.Respond();
json.pushArray("threads");
for (const auto &th : threads) {
json.pushDict();
json.writeUint("id", th.id);
json.writeString("name", th.name);
json.writeInt("status", th.status);
json.pushArray("statuses");
if (th.status & THREADSTATUS_RUNNING)
json.writeString("running");
if (th.status & THREADSTATUS_READY)
json.writeString("ready");
if (th.status & THREADSTATUS_WAIT)
json.writeString("wait");
if (th.status & THREADSTATUS_SUSPEND)
json.writeString("suspend");
if (th.status & THREADSTATUS_DORMANT)
json.writeString("dormant");
if (th.status & THREADSTATUS_DEAD)
json.writeString("dead");
JsonWriter &json = req.Respond();
json.pushArray("threads");
for (const DebugThreadInfo &th : threads) {
json.pushDict();
json.writeUint("id", th.id);
json.writeString("name", th.name);
json.writeInt("status", th.status);
json.pushArray("statuses");
if (th.status & THREADSTATUS_RUNNING)
json.writeString("running");
if (th.status & THREADSTATUS_READY)
json.writeString("ready");
if (th.status & THREADSTATUS_WAIT)
json.writeString("wait");
if (th.status & THREADSTATUS_SUSPEND)
json.writeString("suspend");
if (th.status & THREADSTATUS_DORMANT)
json.writeString("dormant");
if (th.status & THREADSTATUS_DEAD)
json.writeString("dead");
json.pop();
json.writeUint("pc", th.curPC);
json.writeUint("entry", th.entrypoint);
json.writeUint("initialStackSize", th.initialStack);
json.writeUint("currentStackSize", th.stackSize);
json.writeInt("priority", th.priority);
json.writeInt("waitType", (int)th.waitType);
json.writeBool("isCurrent", th.isCurrent);
json.pop();
}
json.pop();
json.writeUint("pc", th.curPC);
json.writeUint("entry", th.entrypoint);
json.writeUint("initialStackSize", th.initialStack);
json.writeUint("currentStackSize", th.stackSize);
json.writeInt("priority", th.priority);
json.writeInt("waitType", (int)th.waitType);
json.writeBool("isCurrent", th.isCurrent);
json.pop();
}
json.pop();
});
}
static bool ThreadInfoForStatus(DebuggerRequest &req, DebugThreadInfo *result) {
@@ -148,8 +152,8 @@ static bool ThreadInfoForStatus(DebuggerRequest &req, DebugThreadInfo *result) {
if (!req.ParamU32("thread", &threadID))
return false;
auto threads = GetThreadsInfo();
for (const auto &t : threads) {
std::vector<DebugThreadInfo> threads = GetThreadsInfo();
for (const DebugThreadInfo &t : threads) {
if (t.id == threadID) {
*result = t;
return true;
@@ -169,27 +173,34 @@ static bool ThreadInfoForStatus(DebuggerRequest &req, DebugThreadInfo *result) {
// - thread: id repeated back.
// - status: string 'ready'.
void WebSocketHLEThreadWake(DebuggerRequest &req) {
DebugThreadInfo threadInfo{ -1 };
if (!ThreadInfoForStatus(req, &threadInfo))
return;
// Route the actual kernel thread manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugThreadInfo threadInfo{ -1 };
if (!ThreadInfoForStatus(req, &threadInfo))
return;
switch (threadInfo.status) {
case THREADSTATUS_SUSPEND:
case THREADSTATUS_WAIT:
case THREADSTATUS_WAITSUSPEND:
if (__KernelResumeThreadFromWait(threadInfo.id, 0) != 0)
return req.Fail("Failed to resume thread");
break;
switch (threadInfo.status) {
case THREADSTATUS_SUSPEND:
case THREADSTATUS_WAIT:
case THREADSTATUS_WAITSUSPEND:
if (__KernelResumeThreadFromWait(threadInfo.id, 0) != 0) {
req.Fail("Failed to resume thread");
return;
}
break;
default:
return req.Fail("Cannot force run thread based on current status");
}
default:
req.Fail("Cannot force run thread based on current status");
return;
}
Reporting::NotifyDebugger();
Reporting::NotifyDebugger();
JsonWriter &json = req.Respond();
json.writeUint("thread", threadInfo.id);
json.writeString("status", "ready");
JsonWriter &json = req.Respond();
json.writeUint("thread", threadInfo.id);
json.writeString("status", "ready");
});
}
// Force stop a thread (hle.thread.stop)
@@ -201,33 +212,40 @@ void WebSocketHLEThreadWake(DebuggerRequest &req) {
// - thread: id repeated back.
// - status: string 'dormant'.
void WebSocketHLEThreadStop(DebuggerRequest &req) {
DebugThreadInfo threadInfo{ -1 };
if (!ThreadInfoForStatus(req, &threadInfo))
return;
// Route the actual kernel thread manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugThreadInfo threadInfo{ -1 };
if (!ThreadInfoForStatus(req, &threadInfo))
return;
switch (threadInfo.status) {
case THREADSTATUS_SUSPEND:
case THREADSTATUS_WAIT:
case THREADSTATUS_WAITSUSPEND:
case THREADSTATUS_READY:
__KernelStopThread(threadInfo.id, 0, "stopped from debugger");
break;
switch (threadInfo.status) {
case THREADSTATUS_SUSPEND:
case THREADSTATUS_WAIT:
case THREADSTATUS_WAITSUSPEND:
case THREADSTATUS_READY:
__KernelStopThread(threadInfo.id, 0, "stopped from debugger");
break;
default:
return req.Fail("Cannot force stop thread based on current status");
}
default:
req.Fail("Cannot force stop thread based on current status");
return;
}
// Get it again to verify.
if (!ThreadInfoForStatus(req, &threadInfo))
return;
if ((threadInfo.status & THREADSTATUS_DORMANT) == 0)
return req.Fail("Failed to stop thread");
// Get it again to verify.
if (!ThreadInfoForStatus(req, &threadInfo))
return;
if ((threadInfo.status & THREADSTATUS_DORMANT) == 0) {
req.Fail("Failed to stop thread");
return;
}
Reporting::NotifyDebugger();
Reporting::NotifyDebugger();
JsonWriter &json = req.Respond();
json.writeUint("thread", threadInfo.id);
json.writeString("status", "dormant");
JsonWriter &json = req.Respond();
json.writeUint("thread", threadInfo.id);
json.writeString("status", "dormant");
});
}
// List all current known function symbols (hle.func.list)
@@ -243,18 +261,22 @@ void WebSocketHLEFuncList(DebuggerRequest &req) {
if (!g_symbolMap)
return req.Fail("CPU not active");
auto functions = g_symbolMap->GetAllActiveSymbols(ST_FUNCTION);
// Route the actual symbol reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
std::vector<SymbolEntry> functions = g_symbolMap->GetAllActiveSymbols(ST_FUNCTION);
JsonWriter &json = req.Respond();
json.pushArray("functions");
for (auto f : functions) {
json.pushDict();
json.writeString("name", f.name);
json.writeUint("address", f.address);
json.writeUint("size", f.size);
JsonWriter &json = req.Respond();
json.pushArray("functions");
for (const SymbolEntry &f : functions) {
json.pushDict();
json.writeString("name", f.name);
json.writeUint("address", f.address);
json.writeUint("size", f.size);
json.pop();
}
json.pop();
}
json.pop();
});
}
// Add a new function symbols (hle.func.add)
@@ -297,51 +319,57 @@ void WebSocketHLEFuncAdd(DebuggerRequest &req) {
if (name.empty())
name = StringFromFormat("z_un_%08x", addr);
u32 prevBegin = g_symbolMap->GetFunctionStart(addr);
u32 endBegin = size == -1 ? prevBegin : g_symbolMap->GetFunctionStart(addr + size - 4);
if (prevBegin == addr) {
return req.Fail("Function already exists at 'address'");
} else if (endBegin != prevBegin) {
return req.Fail("Function already exists between 'address' and 'address' + 'size'");
} else if (prevBegin != -1) {
std::string prevName = g_symbolMap->GetLabelString(prevBegin);
u32 prevSize = g_symbolMap->GetFunctionSize(prevBegin);
u32 newPrevSize = addr - prevBegin;
// Route the actual symbol manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
u32 prevBegin = g_symbolMap->GetFunctionStart(addr);
u32 endBegin = size == -1 ? prevBegin : g_symbolMap->GetFunctionStart(addr + size - 4);
if (prevBegin == addr) {
req.Fail("Function already exists at 'address'");
return;
} else if (endBegin != prevBegin) {
req.Fail("Function already exists between 'address' and 'address' + 'size'");
return;
} else if (prevBegin != -1) {
std::string prevName = g_symbolMap->GetLabelString(prevBegin);
u32 prevSize = g_symbolMap->GetFunctionSize(prevBegin);
u32 newPrevSize = addr - prevBegin;
// The new function will be the remainder, unless otherwise specified.
if (size == -1)
size = prevSize - newPrevSize;
// The new function will be the remainder, unless otherwise specified.
if (size == -1)
size = prevSize - newPrevSize;
// Make sure we register the new length for replacements too.
MIPSAnalyst::ForgetFunctions(prevBegin, prevBegin + newPrevSize);
g_symbolMap->SetFunctionSize(prevBegin, newPrevSize);
MIPSAnalyst::RegisterFunction(prevBegin, newPrevSize, prevName.c_str());
} else {
// There was no function there, so hopefully they specified a size.
if (size == -1)
size = 4;
}
// Make sure we register the new length for replacements too.
MIPSAnalyst::ForgetFunctions(prevBegin, prevBegin + newPrevSize);
g_symbolMap->SetFunctionSize(prevBegin, newPrevSize);
MIPSAnalyst::RegisterFunction(prevBegin, newPrevSize, prevName.c_str());
} else {
// There was no function there, so hopefully they specified a size.
if (size == -1)
size = 4;
}
// To ensure we restore replacements.
MIPSAnalyst::ForgetFunctions(addr, addr + size);
g_symbolMap->AddFunction(name.c_str(), addr, size);
g_symbolMap->SortSymbols();
MIPSAnalyst::RegisterFunction(addr, size, name.c_str());
// To ensure we restore replacements.
MIPSAnalyst::ForgetFunctions(addr, addr + size);
g_symbolMap->AddFunction(name.c_str(), addr, size);
g_symbolMap->SortSymbols();
MIPSAnalyst::RegisterFunction(addr, size, name.c_str());
MIPSAnalyst::UpdateHashMap();
MIPSAnalyst::ApplyHashMap();
MIPSAnalyst::UpdateHashMap();
MIPSAnalyst::ApplyHashMap();
if (g_Config.bFuncReplacements) {
MIPSAnalyst::ReplaceFunctions();
}
if (g_Config.bFuncReplacements) {
MIPSAnalyst::ReplaceFunctions();
}
// Clear cache for branch lines and such.
g_disassemblyManager.clear();
// Clear cache for branch lines and such.
g_disassemblyManager.clear();
JsonWriter &json = req.Respond();
json.writeUint("address", addr);
json.writeUint("size", size);
json.writeString("name", name);
JsonWriter &json = req.Respond();
json.writeUint("address", addr);
json.writeUint("size", size);
json.writeString("name", name);
});
}
// Remove a function symbol (hle.func.remove)
@@ -366,39 +394,45 @@ void WebSocketHLEFuncRemove(DebuggerRequest &req) {
addr = RoundDownToMultipleOf(addr, 4);
u32 funcBegin = g_symbolMap->GetFunctionStart(addr);
if (funcBegin == -1)
return req.Fail("No function found at 'address'");
u32 funcSize = g_symbolMap->GetFunctionSize(funcBegin);
// Route the actual symbol manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
u32 funcBegin = g_symbolMap->GetFunctionStart(addr);
if (funcBegin == -1) {
req.Fail("No function found at 'address'");
return;
}
u32 funcSize = g_symbolMap->GetFunctionSize(funcBegin);
// Expand the previous function.
u32 prevBegin = g_symbolMap->GetFunctionStart(funcBegin - 1);
if (prevBegin != -1) {
std::string prevName = g_symbolMap->GetLabelString(prevBegin);
u32 expandedSize = g_symbolMap->GetFunctionSize(prevBegin) + funcSize;
g_symbolMap->SetFunctionSize(prevBegin, expandedSize);
MIPSAnalyst::ForgetFunctions(prevBegin, prevBegin + expandedSize);
MIPSAnalyst::RegisterFunction(prevBegin, expandedSize, prevName.c_str());
} else {
MIPSAnalyst::ForgetFunctions(funcBegin, funcBegin + funcSize);
}
// Expand the previous function.
u32 prevBegin = g_symbolMap->GetFunctionStart(funcBegin - 1);
if (prevBegin != -1) {
std::string prevName = g_symbolMap->GetLabelString(prevBegin);
u32 expandedSize = g_symbolMap->GetFunctionSize(prevBegin) + funcSize;
g_symbolMap->SetFunctionSize(prevBegin, expandedSize);
MIPSAnalyst::ForgetFunctions(prevBegin, prevBegin + expandedSize);
MIPSAnalyst::RegisterFunction(prevBegin, expandedSize, prevName.c_str());
} else {
MIPSAnalyst::ForgetFunctions(funcBegin, funcBegin + funcSize);
}
g_symbolMap->RemoveFunction(funcBegin, true);
g_symbolMap->SortSymbols();
g_symbolMap->RemoveFunction(funcBegin, true);
g_symbolMap->SortSymbols();
MIPSAnalyst::UpdateHashMap();
MIPSAnalyst::ApplyHashMap();
MIPSAnalyst::UpdateHashMap();
MIPSAnalyst::ApplyHashMap();
if (g_Config.bFuncReplacements) {
MIPSAnalyst::ReplaceFunctions();
}
if (g_Config.bFuncReplacements) {
MIPSAnalyst::ReplaceFunctions();
}
// Clear cache for branch lines and such.
g_disassemblyManager.clear();
// Clear cache for branch lines and such.
g_disassemblyManager.clear();
JsonWriter &json = req.Respond();
json.writeUint("address", funcBegin);
json.writeUint("size", funcSize);
JsonWriter &json = req.Respond();
json.writeUint("address", funcBegin);
json.writeUint("size", funcSize);
});
}
// This function removes function symbols that intersect or lie inside the range
@@ -461,13 +495,19 @@ void WebSocketHLEFuncRemoveRange(DebuggerRequest &req) {
addr = RoundDownToMultipleOf(addr, 4);
size = RoundUpToMultipleOf(size, 4);
// This only depends on addr/size, not on anything CPU-thread-owned, so fail fast here rather than
// making a round trip through the queue for a request we already know is invalid.
if (!Memory::IsValidRange(addr, size))
return req.Fail("Address or size outside valid memory");
u32 count = RemoveFuncSymbolsInRange(addr, size);
// Route the actual symbol manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
u32 count = RemoveFuncSymbolsInRange(addr, size);
JsonWriter &json = req.Respond();
json.writeUint("count", count);
JsonWriter &json = req.Respond();
json.writeUint("count", count);
});
}
// Rename a function symbol (hle.func.rename)
@@ -495,25 +535,31 @@ void WebSocketHLEFuncRename(DebuggerRequest &req) {
addr = RoundDownToMultipleOf(addr, 4);
u32 funcBegin = g_symbolMap->GetFunctionStart(addr);
if (funcBegin == -1)
return req.Fail("No function found at 'address'");
u32 funcSize = g_symbolMap->GetFunctionSize(funcBegin);
// Route the actual symbol manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
u32 funcBegin = g_symbolMap->GetFunctionStart(addr);
if (funcBegin == -1) {
req.Fail("No function found at 'address'");
return;
}
u32 funcSize = g_symbolMap->GetFunctionSize(funcBegin);
g_symbolMap->SetLabelName(name.c_str(), funcBegin);
// To ensure we reapply replacements (in case we check name there.)
MIPSAnalyst::ForgetFunctions(funcBegin, funcBegin + funcSize);
MIPSAnalyst::RegisterFunction(funcBegin, funcSize, name.c_str());
MIPSAnalyst::UpdateHashMap();
MIPSAnalyst::ApplyHashMap();
if (g_Config.bFuncReplacements) {
MIPSAnalyst::ReplaceFunctions();
}
g_symbolMap->SetLabelName(name.c_str(), funcBegin);
// To ensure we reapply replacements (in case we check name there.)
MIPSAnalyst::ForgetFunctions(funcBegin, funcBegin + funcSize);
MIPSAnalyst::RegisterFunction(funcBegin, funcSize, name.c_str());
MIPSAnalyst::UpdateHashMap();
MIPSAnalyst::ApplyHashMap();
if (g_Config.bFuncReplacements) {
MIPSAnalyst::ReplaceFunctions();
}
JsonWriter &json = req.Respond();
json.writeUint("address", funcBegin);
json.writeUint("size", funcSize);
json.writeString("name", name);
JsonWriter &json = req.Respond();
json.writeUint("address", funcBegin);
json.writeUint("size", funcSize);
json.writeString("name", name);
});
}
// Auto-detect functions in a memory range (hle.func.scan)
@@ -546,17 +592,26 @@ void WebSocketHLEFuncScan(DebuggerRequest &req) {
if (!req.ParamBool("remove", &remove, DebuggerParamType::OPTIONAL))
return;
// This only depends on addr/size, not on anything CPU-thread-owned, so fail fast here rather than
// making a round trip through the queue for a request we already know is invalid.
if (!Memory::IsValidRange(addr, size))
return req.Fail("Address or size outside valid memory");
if (remove) {
RemoveFuncSymbolsInRange(addr, size);
}
// Route the actual symbol scan/manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
// Note: like memory.search, 'size' has no cap beyond valid memory range, so a very large scan
// will block the CPU thread's own frame pump for its duration.
Core_RunOnCPUThread([&] {
bool insertSymbols = MIPSAnalyst::ScanForFunctions(addr, addr + size, true);
MIPSAnalyst::FinalizeScan(insertSymbols);
if (remove) {
RemoveFuncSymbolsInRange(addr, size);
}
req.Respond();
bool insertSymbols = MIPSAnalyst::ScanForFunctions(addr, addr + size, true);
MIPSAnalyst::FinalizeScan(insertSymbols);
req.Respond();
});
}
// List all known user modules (hle.module.list)
@@ -573,19 +628,23 @@ void WebSocketHLEModuleList(DebuggerRequest &req) {
if (!g_symbolMap)
return req.Fail("CPU not active");
auto modules = g_symbolMap->getAllModules();
// Route the actual symbol reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
std::vector<LoadedModuleInfo> modules = g_symbolMap->getAllModules();
JsonWriter &json = req.Respond();
json.pushArray("modules");
for (auto m : modules) {
json.pushDict();
json.writeString("name", m.name);
json.writeUint("address", m.address);
json.writeUint("size", m.size);
json.writeBool("isActive", m.active);
JsonWriter &json = req.Respond();
json.pushArray("modules");
for (const LoadedModuleInfo &m : modules) {
json.pushDict();
json.writeString("name", m.name);
json.writeUint("address", m.address);
json.writeUint("size", m.size);
json.writeBool("isActive", m.active);
json.pop();
}
json.pop();
}
json.pop();
});
}
// Walk the stack and list stack frames (hle.backtrace)
@@ -606,48 +665,54 @@ void WebSocketHLEBacktrace(DebuggerRequest &req) {
if (!Core_IsStepping())
return req.Fail("CPU currently running (cpu.stepping first)");
uint32_t threadID = -1;
DebugInterface *cpuDebug = currentDebugMIPS;
if (req.HasParam("thread")) {
if (!req.ParamU32("thread", &threadID))
return;
// Route the actual CPU/symbol/disassembly reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
uint32_t threadID = -1;
DebugInterface *cpuDebug = currentDebugMIPS;
if (req.HasParam("thread")) {
if (!req.ParamU32("thread", &threadID))
return;
cpuDebug = KernelDebugThread((SceUID)threadID);
if (!cpuDebug)
return req.Fail("Thread could not be found");
}
auto 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;
cpuDebug = KernelDebugThread((SceUID)threadID);
if (!cpuDebug) {
req.Fail("Thread could not be found");
return;
}
}
}
uint32_t ra = cpuDebug->GetRegValue(0, MIPS_REG_RA);
uint32_t sp = cpuDebug->GetRegValue(0, MIPS_REG_SP);
auto frames = MIPSStackWalk::Walk(cpuDebug->GetPC(), ra, sp, entry, stackTop);
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;
}
}
JsonWriter &json = req.Respond();
json.pushArray("frames");
for (auto f : frames) {
json.pushDict();
json.writeUint("entry", f.entry);
json.writeUint("pc", f.pc);
json.writeUint("sp", f.sp);
json.writeUint("stackSize", f.stackSize);
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);
DisassemblyLineInfo line;
g_disassemblyManager.getLine(g_disassemblyManager.getStartAddress(f.pc), true, line, cpuDebug);
json.writeString("code", line.name + " " + line.params);
JsonWriter &json = req.Respond();
json.pushArray("frames");
for (const MIPSStackWalk::StackFrame &f : frames) {
json.pushDict();
json.writeUint("entry", f.entry);
json.writeUint("pc", f.pc);
json.writeUint("sp", f.sp);
json.writeUint("stackSize", f.stackSize);
DisassemblyLineInfo line;
g_disassemblyManager.getLine(g_disassemblyManager.getStartAddress(f.pc), true, line, cpuDebug);
json.writeString("code", line.name + " " + line.params);
json.pop();
}
json.pop();
}
json.pop();
});
}
// List all current known data symbols (hle.data.list)
@@ -664,19 +729,23 @@ void WebSocketHLEDataList(DebuggerRequest &req) {
if (!g_symbolMap)
return req.Fail("CPU not active");
auto entries = g_symbolMap->GetAllActiveSymbols(ST_DATA);
// Route the actual symbol reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
std::vector<SymbolEntry> entries = g_symbolMap->GetAllActiveSymbols(ST_DATA);
JsonWriter &json = req.Respond();
json.pushArray("data");
for (auto &d : entries) {
json.pushDict();
json.writeString("name", d.name);
json.writeUint("address", d.address);
json.writeUint("size", d.size);
json.writeString("type", DataTypeToString(g_symbolMap->GetDataType(d.address)));
JsonWriter &json = req.Respond();
json.pushArray("data");
for (const SymbolEntry &d : entries) {
json.pushDict();
json.writeString("name", d.name);
json.writeUint("address", d.address);
json.writeUint("size", d.size);
json.writeString("type", DataTypeToString(g_symbolMap->GetDataType(d.address)));
json.pop();
}
json.pop();
}
json.pop();
});
}
// Add a new data symbol (hle.data.add)
@@ -717,6 +786,8 @@ void WebSocketHLEDataAdd(DebuggerRequest &req) {
if (!DataTypeFromString(typeStr, &type))
return req.Fail("Invalid 'type', must be byte, halfword, word, or ascii");
// This only depends on addr/size, not on anything CPU-thread-owned, so fail fast here rather than
// making a round trip through the queue for a request we already know is invalid.
if (!Memory::IsValidRange(addr, size))
return req.Fail("Address or size outside valid memory");
@@ -726,18 +797,22 @@ void WebSocketHLEDataAdd(DebuggerRequest &req) {
if (name.empty())
name = StringFromFormat("data_%08x", addr);
g_symbolMap->AddData(addr, size, type);
g_symbolMap->AddLabel(name.c_str(), addr);
g_symbolMap->SortSymbols();
// Route the actual symbol manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
g_symbolMap->AddData(addr, size, type);
g_symbolMap->AddLabel(name.c_str(), addr);
g_symbolMap->SortSymbols();
// Clear cache so the disassembly view picks up the new annotation.
g_disassemblyManager.clear();
// Clear cache so the disassembly view picks up the new annotation.
g_disassemblyManager.clear();
JsonWriter &json = req.Respond();
json.writeUint("address", addr);
json.writeUint("size", size);
json.writeString("type", typeStr);
json.writeString("name", name);
JsonWriter &json = req.Respond();
json.writeUint("address", addr);
json.writeUint("size", size);
json.writeString("type", typeStr);
json.writeString("name", name);
});
}
// Remove a data symbol (hle.data.remove)
@@ -758,18 +833,24 @@ void WebSocketHLEDataRemove(DebuggerRequest &req) {
if (!req.ParamU32("address", &addr))
return;
u32 dataBegin = g_symbolMap->GetDataStart(addr);
if (dataBegin == -1)
return req.Fail("No data symbol found at 'address'");
u32 dataSize = g_symbolMap->GetDataSize(dataBegin);
// Route the actual symbol manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
u32 dataBegin = g_symbolMap->GetDataStart(addr);
if (dataBegin == -1) {
req.Fail("No data symbol found at 'address'");
return;
}
u32 dataSize = g_symbolMap->GetDataSize(dataBegin);
g_symbolMap->RemoveData(dataBegin, true);
g_symbolMap->SortSymbols();
g_disassemblyManager.clear();
g_symbolMap->RemoveData(dataBegin, true);
g_symbolMap->SortSymbols();
g_disassemblyManager.clear();
JsonWriter &json = req.Respond();
json.writeUint("address", dataBegin);
json.writeUint("size", dataSize);
JsonWriter &json = req.Respond();
json.writeUint("address", dataBegin);
json.writeUint("size", dataSize);
});
}
// Rename a data symbol (hle.data.rename)
@@ -795,15 +876,21 @@ void WebSocketHLEDataRename(DebuggerRequest &req) {
if (!req.ParamString("name", &name))
return;
u32 dataBegin = g_symbolMap->GetDataStart(addr);
if (dataBegin == -1)
return req.Fail("No data symbol found at 'address'");
u32 dataSize = g_symbolMap->GetDataSize(dataBegin);
// Route the actual symbol manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
u32 dataBegin = g_symbolMap->GetDataStart(addr);
if (dataBegin == -1) {
req.Fail("No data symbol found at 'address'");
return;
}
u32 dataSize = g_symbolMap->GetDataSize(dataBegin);
g_symbolMap->SetLabelName(name.c_str(), dataBegin);
g_symbolMap->SetLabelName(name.c_str(), dataBegin);
JsonWriter &json = req.Respond();
json.writeUint("address", dataBegin);
json.writeUint("size", dataSize);
json.writeString("name", name);
JsonWriter &json = req.Respond();
json.writeUint("address", dataBegin);
json.writeUint("size", dataSize);
json.writeString("name", name);
});
}
+300 -223
View File
@@ -56,23 +56,18 @@ struct AutoDisabledReplacements {
std::map<u32, u32> replacements;
std::vector<u32> emuhacks;
bool saved = false;
bool wasStepping = false;
};
// Important: Only use keepReplacements when reading, not writing.
static AutoDisabledReplacements LockMemoryAndCPU(uint32_t addr, bool keepReplacements) {
// Call this from within a Core_RunOnCPUThread() callback - see Core_RunOnCPUThread() in Core.h.
// No longer needs to pause a running CPU to do this safely: we're already running on the CPU thread
// by the time this is called, so nothing else can be concurrently executing MIPS code or touching the
// JIT's emuhack ops on this thread while we hold onto them below.
//
// Important: Only use keepReplacements=false when reading, not writing.
static AutoDisabledReplacements LockMemory(bool keepReplacements) {
AutoDisabledReplacements result;
CoreState state = coreState;
if (Core_IsStepping()) {
result.wasStepping = true;
} else {
while (state != CoreState::CORE_RUNNING_CPU) {
state = coreState;
}
Core_Break(BreakReason::MemoryAccess, addr);
Core_WaitInactive();
}
// This still guards against a *different* thread (not the CPU thread) tearing down or
// reinitializing the memory system underneath us, e.g. during shutdown.
result.lock = new Memory::MemoryInitedLock();
if (!keepReplacements) {
result.saved = true;
@@ -92,8 +87,6 @@ AutoDisabledReplacements::AutoDisabledReplacements(AutoDisabledReplacements &&ot
emuhacks = std::move(other.emuhacks);
saved = other.saved;
other.saved = false;
wasStepping = other.wasStepping;
other.wasStepping = true;
}
AutoDisabledReplacements::~AutoDisabledReplacements() {
@@ -103,8 +96,6 @@ AutoDisabledReplacements::~AutoDisabledReplacements() {
MIPSComp::jit->RestoreSavedEmuHackOps(emuhacks);
RestoreSavedReplacements(replacements);
}
if (!wasStepping)
Core_Resume();
delete lock;
}
@@ -121,17 +112,23 @@ void WebSocketMemoryReadU8(DebuggerRequest &req) {
return;
}
auto memLock = LockMemoryAndCPU(addr, true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
// Route the actual memory read to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
AutoDisabledReplacements memLock = LockMemory(true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive()) {
req.Fail("CPU not started");
return;
}
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
JsonWriter &json = req.Respond();
json.writeUint("value", Memory::Read_U8(addr));
JsonWriter &json = req.Respond();
json.writeUint("value", Memory::Read_U8(addr));
});
}
// Read two bytes from memory (memory.read_u16)
@@ -147,17 +144,23 @@ void WebSocketMemoryReadU16(DebuggerRequest &req) {
return;
}
auto memLock = LockMemoryAndCPU(addr, true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
// Route the actual memory read to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
AutoDisabledReplacements memLock = LockMemory(true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive()) {
req.Fail("CPU not started");
return;
}
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
JsonWriter &json = req.Respond();
json.writeUint("value", Memory::Read_U16(addr));
JsonWriter &json = req.Respond();
json.writeUint("value", Memory::Read_U16(addr));
});
}
// Read four bytes from memory (memory.read_u32)
@@ -173,17 +176,23 @@ void WebSocketMemoryReadU32(DebuggerRequest &req) {
return;
}
auto memLock = LockMemoryAndCPU(addr, true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
// Route the actual memory read to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
AutoDisabledReplacements memLock = LockMemory(true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive()) {
req.Fail("CPU not started");
return;
}
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
JsonWriter &json = req.Respond();
json.writeUint("value", Memory::Read_U32(addr));
JsonWriter &json = req.Respond();
json.writeUint("value", Memory::Read_U32(addr));
});
}
// Read bytes from memory (memory.read)
@@ -206,29 +215,40 @@ void WebSocketMemoryRead(DebuggerRequest &req) {
if (!req.ParamBool("replacements", &replacements, DebuggerParamType::OPTIONAL))
return;
auto memLock = LockMemoryAndCPU(addr, replacements);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
// Route the actual memory read to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
// Note: for a very large 'size', base64-encoding it is real CPU work that will now happen on the
// CPU thread itself, blocking its own frame pump for the duration - same caveat as memory.search.
Core_RunOnCPUThread([&] {
AutoDisabledReplacements memLock = LockMemory(replacements);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive()) {
req.Fail("CPU not started");
return;
}
if (!Memory::IsValidAddress(addr))
return req.Fail("Invalid address");
else if (!Memory::IsValidRange(addr, size))
return req.Fail("Invalid size");
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
} else if (!Memory::IsValidRange(addr, size)) {
req.Fail("Invalid size");
return;
}
JsonWriter &json = req.Respond();
// Start a value without any actual data yet...
json.writeRaw("base64", "");
req.Flush();
JsonWriter &json = req.Respond();
// Start a value without any actual data yet...
json.writeRaw("base64", "");
req.Flush();
// Now we'll write it directly to the stream.
req.ws->AddFragment(false, "\"");
// 65535 is an "even" number of base64 characters.
static const size_t CHUNK_SIZE = 65535;
for (size_t i = 0; i < size; i += CHUNK_SIZE) {
size_t left = std::min(size - i, CHUNK_SIZE);
req.ws->AddFragment(false, Base64Encode(Memory::GetPointerUnchecked(addr) + i, left));
}
req.ws->AddFragment(false, "\"");
// Now we'll write it directly to the stream.
req.ws->AddFragment(false, "\"");
// 65535 is an "even" number of base64 characters.
static const size_t CHUNK_SIZE = 65535;
for (size_t i = 0; i < size; i += CHUNK_SIZE) {
size_t left = std::min(size - i, CHUNK_SIZE);
req.ws->AddFragment(false, Base64Encode(Memory::GetPointerUnchecked(addr) + i, left));
}
req.ws->AddFragment(false, "\"");
});
}
// Read a NUL terminated string from memory (memory.readString)
@@ -247,30 +267,40 @@ void WebSocketMemoryReadString(DebuggerRequest &req) {
if (!req.ParamU32("address", &addr))
return;
auto memLock = LockMemoryAndCPU(addr, true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
// Route the actual memory read to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
AutoDisabledReplacements memLock = LockMemory(true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive()) {
req.Fail("CPU not started");
return;
}
std::string type = "utf-8";
if (!req.ParamString("type", &type, DebuggerParamType::OPTIONAL))
return;
if (type != "utf-8" && type != "base64")
return req.Fail("Invalid type, must be either utf-8 or base64");
std::string type = "utf-8";
if (!req.ParamString("type", &type, DebuggerParamType::OPTIONAL))
return;
if (type != "utf-8" && type != "base64") {
req.Fail("Invalid type, must be either utf-8 or base64");
return;
}
if (!Memory::IsValidAddress(addr))
return req.Fail("Invalid address");
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
// Let's try to avoid crashing and get a safe length.
const uint8_t *p = Memory::GetPointerUnchecked(addr);
size_t longest = Memory::ClampValidSizeAt(addr, Memory::g_MemorySize);
size_t len = strnlen((const char *)p, longest);
// Let's try to avoid crashing and get a safe length.
const uint8_t *p = Memory::GetPointerUnchecked(addr);
size_t longest = Memory::ClampValidSizeAt(addr, Memory::g_MemorySize);
size_t len = strnlen((const char *)p, longest);
JsonWriter &json = req.Respond();
if (type == "utf-8") {
json.writeString("value", std::string((const char *)p, len));
} else if (type == "base64") {
json.writeString("base64", Base64Encode(p, len));
}
JsonWriter &json = req.Respond();
if (type == "utf-8") {
json.writeString("value", std::string((const char *)p, len));
} else if (type == "base64") {
json.writeString("base64", Base64Encode(p, len));
}
});
}
// Write a byte to memory (memory.write_u8)
@@ -290,20 +320,26 @@ void WebSocketMemoryWriteU8(DebuggerRequest &req) {
return;
}
auto memLock = LockMemoryAndCPU(addr, true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
// Route the actual memory write to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
AutoDisabledReplacements memLock = LockMemory(true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive()) {
req.Fail("CPU not started");
return;
}
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
currentMIPS->InvalidateICache(addr, 1);
Memory::Write_U8(val, addr);
Reporting::NotifyDebugger();
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
currentMIPS->InvalidateICache(addr, 1);
Memory::Write_U8(val, addr);
Reporting::NotifyDebugger();
JsonWriter &json = req.Respond();
json.writeUint("value", Memory::Read_U8(addr));
JsonWriter &json = req.Respond();
json.writeUint("value", Memory::Read_U8(addr));
});
}
// Write two bytes to memory (memory.write_u16)
@@ -323,20 +359,26 @@ void WebSocketMemoryWriteU16(DebuggerRequest &req) {
return;
}
auto memLock = LockMemoryAndCPU(addr, true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
// Route the actual memory write to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
AutoDisabledReplacements memLock = LockMemory(true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive()) {
req.Fail("CPU not started");
return;
}
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
currentMIPS->InvalidateICache(addr, 2);
Memory::Write_U16(val, addr);
Reporting::NotifyDebugger();
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
currentMIPS->InvalidateICache(addr, 2);
Memory::Write_U16(val, addr);
Reporting::NotifyDebugger();
JsonWriter &json = req.Respond();
json.writeUint("value", Memory::Read_U16(addr));
JsonWriter &json = req.Respond();
json.writeUint("value", Memory::Read_U16(addr));
});
}
// Write four bytes to memory (memory.write_u32)
@@ -356,20 +398,26 @@ void WebSocketMemoryWriteU32(DebuggerRequest &req) {
return;
}
auto memLock = LockMemoryAndCPU(addr, true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
// Route the actual memory write to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
AutoDisabledReplacements memLock = LockMemory(true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive()) {
req.Fail("CPU not started");
return;
}
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
currentMIPS->InvalidateICache(addr, 4);
Memory::Write_U32(val, addr);
Reporting::NotifyDebugger();
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
}
currentMIPS->InvalidateICache(addr, 4);
Memory::Write_U32(val, addr);
Reporting::NotifyDebugger();
JsonWriter &json = req.Respond();
json.writeUint("value", Memory::Read_U32(addr));
JsonWriter &json = req.Respond();
json.writeUint("value", Memory::Read_U32(addr));
});
}
// Write bytes to memory (memory.write)
@@ -387,22 +435,31 @@ void WebSocketMemoryWrite(DebuggerRequest &req) {
if (!req.ParamString("base64", &encoded))
return;
auto memLock = LockMemoryAndCPU(addr, true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
// Route the actual memory write to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
AutoDisabledReplacements memLock = LockMemory(true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive()) {
req.Fail("CPU not started");
return;
}
std::vector<uint8_t> value = Base64Decode(&encoded[0], encoded.size());
uint32_t size = (uint32_t)value.size();
std::vector<uint8_t> value = Base64Decode(&encoded[0], encoded.size());
uint32_t size = (uint32_t)value.size();
if (!Memory::IsValidAddress(addr))
return req.Fail("Invalid address");
else if (value.size() != (size_t)size || !Memory::IsValidRange(addr, size))
return req.Fail("Invalid size");
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
} else if (value.size() != (size_t)size || !Memory::IsValidRange(addr, size)) {
req.Fail("Invalid size");
return;
}
currentMIPS->InvalidateICache(addr, size);
Memory::MemcpyUnchecked(addr, &value[0], size);
Reporting::NotifyDebugger();
req.Respond();
currentMIPS->InvalidateICache(addr, size);
Memory::MemcpyUnchecked(addr, &value[0], size);
Reporting::NotifyDebugger();
req.Respond();
});
}
// Search memory for a value or byte pattern (memory.search)
@@ -441,105 +498,125 @@ void WebSocketMemorySearch(DebuggerRequest &req) {
if (!req.ParamString("type", &type))
return;
auto memLock = LockMemoryAndCPU(addr, true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
if (!Memory::IsValidAddress(addr))
return req.Fail("Invalid address");
else if (!Memory::IsValidRange(addr, size))
return req.Fail("Invalid size");
uint32_t align = 1;
uint32_t needleSize = 0;
uint32_t needleValue = 0;
std::vector<uint8_t> needleBytes;
std::vector<uint8_t> maskBytes;
if (type == "u8" || type == "u16" || type == "u32") {
needleSize = type == "u8" ? 1 : type == "u16" ? 2 : 4;
align = needleSize;
if (!req.ParamU32("value", &needleValue, false))
// Route the actual memory scan to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
// Note: 'size' has no cap beyond valid memory range, so a very large scan will now block the CPU
// thread's own frame pump for its duration, rather than running unqueued on this thread as before.
Core_RunOnCPUThread([&] {
AutoDisabledReplacements memLock = LockMemory(true);
if (!currentDebugMIPS->isAlive() || !Memory::IsActive()) {
req.Fail("CPU not started");
return;
}
if (!Memory::IsValidAddress(addr)) {
req.Fail("Invalid address");
return;
} else if (!Memory::IsValidRange(addr, size)) {
req.Fail("Invalid size");
return;
} else if (type == "float") {
needleSize = 4;
align = 4;
// allowFloatBits: accepts a string like "1.5" and gives us its raw bit pattern.
if (!req.ParamU32("value", &needleValue, true))
return;
} else if (type == "bytes") {
std::string encoded;
if (!req.ParamString("base64", &encoded))
return;
needleBytes = Base64Decode(&encoded[0], encoded.size());
if (needleBytes.empty())
return req.Fail("'base64' must decode to at least one byte");
needleSize = (uint32_t)needleBytes.size();
align = 1;
if (req.HasParam("maskBase64")) {
std::string maskEncoded;
if (!req.ParamString("maskBase64", &maskEncoded))
return;
maskBytes = Base64Decode(&maskEncoded[0], maskEncoded.size());
if (maskBytes.size() != needleBytes.size())
return req.Fail("'maskBase64' must decode to the same length as 'base64'");
}
} else {
return req.Fail("Invalid 'type', must be u8, u16, u32, float, or bytes");
}
if (needleSize > size)
return req.Fail("'size' is smaller than the pattern/value being searched for");
uint32_t align = 1;
uint32_t needleSize = 0;
uint32_t needleValue = 0;
std::vector<uint8_t> needleBytes;
std::vector<uint8_t> maskBytes;
if (!req.ParamU32("align", &align, false, DebuggerParamType::OPTIONAL))
return;
if (align == 0)
return req.Fail("'align' must not be zero");
if (type == "u8" || type == "u16" || type == "u32") {
needleSize = type == "u8" ? 1 : type == "u16" ? 2 : 4;
align = needleSize;
if (!req.ParamU32("value", &needleValue, false))
return;
} else if (type == "float") {
needleSize = 4;
align = 4;
// allowFloatBits: accepts a string like "1.5" and gives us its raw bit pattern.
if (!req.ParamU32("value", &needleValue, true))
return;
} else if (type == "bytes") {
std::string encoded;
if (!req.ParamString("base64", &encoded))
return;
needleBytes = Base64Decode(&encoded[0], encoded.size());
if (needleBytes.empty()) {
req.Fail("'base64' must decode to at least one byte");
return;
}
needleSize = (uint32_t)needleBytes.size();
align = 1;
uint32_t maxResults = 1000;
if (!req.ParamU32("maxResults", &maxResults, false, DebuggerParamType::OPTIONAL))
return;
if (maxResults == 0)
maxResults = 1000;
else if (maxResults > 100000)
maxResults = 100000;
const uint8_t *base = Memory::GetPointerUnchecked(addr);
std::vector<uint32_t> matches;
bool truncated = false;
for (uint32_t offset = 0; offset + needleSize <= size; offset += align) {
bool match;
if (!needleBytes.empty()) {
match = true;
for (uint32_t i = 0; i < needleSize; ++i) {
uint8_t mask = maskBytes.empty() ? 0xFF : maskBytes[i];
if ((base[offset + i] & mask) != (needleBytes[i] & mask)) {
match = false;
break;
if (req.HasParam("maskBase64")) {
std::string maskEncoded;
if (!req.ParamString("maskBase64", &maskEncoded))
return;
maskBytes = Base64Decode(&maskEncoded[0], maskEncoded.size());
if (maskBytes.size() != needleBytes.size()) {
req.Fail("'maskBase64' must decode to the same length as 'base64'");
return;
}
}
} else {
uint32_t actual = base[offset];
if (needleSize >= 2)
actual |= base[offset + 1] << 8;
if (needleSize >= 4)
actual |= (base[offset + 2] << 16) | (base[offset + 3] << 24);
match = actual == needleValue;
req.Fail("Invalid 'type', must be u8, u16, u32, float, or bytes");
return;
}
if (match) {
if (matches.size() >= maxResults) {
truncated = true;
break;
if (needleSize > size) {
req.Fail("'size' is smaller than the pattern/value being searched for");
return;
}
if (!req.ParamU32("align", &align, false, DebuggerParamType::OPTIONAL))
return;
if (align == 0) {
req.Fail("'align' must not be zero");
return;
}
uint32_t maxResults = 1000;
if (!req.ParamU32("maxResults", &maxResults, false, DebuggerParamType::OPTIONAL))
return;
if (maxResults == 0)
maxResults = 1000;
else if (maxResults > 100000)
maxResults = 100000;
const uint8_t *base = Memory::GetPointerUnchecked(addr);
std::vector<uint32_t> matches;
bool truncated = false;
for (uint32_t offset = 0; offset + needleSize <= size; offset += align) {
bool match;
if (!needleBytes.empty()) {
match = true;
for (uint32_t i = 0; i < needleSize; ++i) {
uint8_t mask = maskBytes.empty() ? 0xFF : maskBytes[i];
if ((base[offset + i] & mask) != (needleBytes[i] & mask)) {
match = false;
break;
}
}
} else {
uint32_t actual = base[offset];
if (needleSize >= 2)
actual |= base[offset + 1] << 8;
if (needleSize >= 4)
actual |= (base[offset + 2] << 16) | (base[offset + 3] << 24);
match = actual == needleValue;
}
matches.push_back(addr + offset);
}
}
JsonWriter &json = req.Respond();
json.pushArray("matches");
for (uint32_t m : matches)
json.writeUint(m);
json.pop();
json.writeBool("truncated", truncated);
if (match) {
if (matches.size() >= maxResults) {
truncated = true;
break;
}
matches.push_back(addr + offset);
}
}
JsonWriter &json = req.Respond();
json.pushArray("matches");
for (uint32_t m : matches)
json.writeUint(m);
json.pop();
json.writeBool("truncated", truncated);
});
}
+112 -95
View File
@@ -44,13 +44,12 @@ struct WebSocketSteppingState : public DebuggerSubscriber {
protected:
uint32_t GetNextAddress(DebugInterface *cpuDebug);
int GetNextInstructionCount(DebugInterface *cpuDebug);
void PrepareResume();
void AddThreadCondition(uint32_t breakpointAddress, uint32_t threadID);
};
DebuggerSubscriber *WebSocketSteppingInit(DebuggerEventHandlerMap &map) {
auto p = new WebSocketSteppingState();
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); };
@@ -90,33 +89,39 @@ 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;
}
uint32_t threadID;
auto cpuDebug = CPUFromRequest(req, &threadID);
if (!cpuDebug)
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);
if (cpuDebug == currentDebugMIPS) {
// If the current PC is on a breakpoint, the user doesn't want to do nothing.
g_breakpoints.SetSkipFirst(currentMIPS->pc);
int c = GetNextInstructionCount(cpuDebug);
Core_RequestCPUStep(CPUStepType::Into, c);
} 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.AddBreakPoint(breakpointAddress, true);
AddThreadCondition(breakpointAddress, threadID);
Core_Resume();
Core_RequestCPUStep(CPUStepType::Into, 1);
} 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.AddBreakPoint(breakpointAddress, true);
AddThreadCondition(breakpointAddress, threadID);
Core_Resume();
}
}
}
});
}
// Step over the next instruction (cpu.stepOver)
@@ -135,41 +140,45 @@ void WebSocketSteppingState::Over(DebuggerRequest &req) {
if (!Core_IsStepping())
return req.Fail("CPU currently running (cpu.stepping first)");
uint32_t threadID;
auto cpuDebug = CPUFromRequest(req, &threadID);
if (!cpuDebug)
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;
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;
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 {
// 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;
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.AddBreakPoint(breakpointAddress, true);
if (cpuDebug != currentDebugMIPS)
AddThreadCondition(breakpointAddress, threadID);
Core_Resume();
}
PrepareResume();
// Could have advanced to the breakpoint already in PrepareResume().
cpuDebug = CPUFromRequest(req);
if (cpuDebug->GetPC() != breakpointAddress) {
g_breakpoints.AddBreakPoint(breakpointAddress, true);
if (cpuDebug != currentDebugMIPS)
AddThreadCondition(breakpointAddress, threadID);
Core_Resume();
}
});
}
// Step out of a function based on a stack walk (cpu.stepOut)
@@ -186,39 +195,43 @@ void WebSocketSteppingState::Out(DebuggerRequest &req) {
if (!Core_IsStepping())
return req.Fail("CPU currently running (cpu.stepping first)");
uint32_t threadID;
auto cpuDebug = CPUFromRequest(req, &threadID);
if (!cpuDebug)
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;
auto 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;
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);
auto 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 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.AddBreakPoint(breakpointAddress, true);
if (cpuDebug != currentDebugMIPS)
AddThreadCondition(breakpointAddress, threadID);
Core_Resume();
}
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.AddBreakPoint(breakpointAddress, true);
if (cpuDebug != currentDebugMIPS)
AddThreadCondition(breakpointAddress, threadID);
Core_Resume();
}
});
}
// Run until a certain address (cpu.runUntil)
@@ -238,13 +251,17 @@ void WebSocketSteppingState::RunUntil(DebuggerRequest &req) {
return;
}
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.AddBreakPoint(address, true);
Core_Resume();
}
// 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.AddBreakPoint(address, true);
Core_Resume();
}
});
}
// Jump after the next HLE call (cpu.nextHLE)
@@ -257,9 +274,13 @@ void WebSocketSteppingState::HLE(DebuggerRequest &req) {
return req.Fail("CPU not started");
}
PrepareResume();
hleDebugBreak();
Core_Resume();
// 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) {
@@ -267,10 +288,6 @@ uint32_t WebSocketSteppingState::GetNextAddress(DebugInterface *cpuDebug) {
return g_disassemblyManager.getNthNextAddress(current, 1);
}
int WebSocketSteppingState::GetNextInstructionCount(DebugInterface *cpuDebug) {
return (GetNextAddress(cpuDebug) - cpuDebug->GetPC()) / 4;
}
void WebSocketSteppingState::PrepareResume() {
if (currentMIPS->inDelaySlot) {
// Delay slot instructions are never joined, so we pass 1.
+20 -1
View File
@@ -38,8 +38,8 @@
#include "Core/Util/RecentFiles.h"
#include "Core/Util/PathUtil.h"
#include "Core/Config.h"
#include "Core/Debugger/WebSocket.h"
#include "Core/WebServer.h"
#include "Core/Debugger/WebSocket.h"
enum class ServerStatus {
STOPPED,
@@ -760,6 +760,17 @@ void WebServerSetUploadPath(const Path &path) {
g_uploadPath = path;
}
WebServerFlags GetServerFlags() {
return serverFlags;
}
void OpenWebDebugger() {
if (!WebServerRunning(WebServerFlags::DEBUGGER)) {
StartWebServer(WebServerFlags::DEBUGGER);
}
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "http://localhost:" + std::to_string(g_Config.iRemoteISOPort) + "/debugger/index.html");
}
static void WebServerThread() {
SetCurrentThreadName("HTTPServer");
@@ -787,6 +798,14 @@ static void WebServerThread() {
double lastRegister = time_now_d();
INFO_LOG(Log::HTTP, "Entering web server loop. Listening on port %d", g_Config.iRemoteISOPort);
if (serverFlags & WebServerFlags::DEBUGGER) {
g_OSD.Show(OSDType::MESSAGE_SUCCESS, "Debugger web server running on port " + std::to_string(g_Config.iRemoteISOPort), 5.0f, "debugger");
g_OSD.SetClickCallback("debugger", []() {
OpenWebDebugger();
});
}
while (RetrieveStatus() == ServerStatus::RUNNING) {
constexpr double webServerSliceSeconds = 0.2f;
http->RunSlice(webServerSliceSeconds);
+3
View File
@@ -43,6 +43,9 @@ bool RemoteISOFileSupported(const std::string &filename);
void WebServerSetUploadPath(const Path &path);
int WebServerPort();
// Will start the webserver if not running.
void OpenWebDebugger();
struct UploadProgress {
s64 totalBytes = 0;
s64 uploadedBytes = 0;
+5
View File
@@ -14,6 +14,7 @@
#include "Core/Config.h"
#include "Core/System.h"
#include "Core/SaveState.h"
#include "Core/WebServer.h"
#include "Core/Debugger/MemBlockInfo.h"
#include "Core/RetroAchievements.h"
#include "Core/Core.h"
@@ -2392,6 +2393,10 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug, GPUCommon *gpuDebug, Draw:
});
}
ImGui::Separator();
if (ImGui::MenuItem("Open web debugger")) {
OpenWebDebugger();
}
ImGui::Separator();
if (ImGui::MenuItem("Exit")) {
System_ExitApp();
}
+6 -17
View File
@@ -465,7 +465,7 @@ void ImDisasmView::FollowBranch() {
DisassemblyLineInfo line;
g_disassemblyManager.getLine(curAddress_, true, line, debugger_);
if (line.type == DISTYPE_OPCODE || line.type == DISTYPE_MACRO) {
if (line.type == DISTYPE_OPCODE) {
if (line.info.isBranch) {
jumpStack_.push_back(curAddress_);
gotoAddr(line.info.branchTarget);
@@ -890,7 +890,7 @@ void ImDisasmView::updateStatusBarText() {
g_disassemblyManager.getLine(curAddress_, true, line, debugger_);
text[0] = 0;
if (line.type == DISTYPE_OPCODE || line.type == DISTYPE_MACRO) {
if (line.type == DISTYPE_OPCODE) {
if (line.info.hasRelevantAddress && IsLikelyStringAt(line.info.relevantAddress)) {
snprintf(text, sizeof(text), "[%08X] = \"%s\"", line.info.relevantAddress, Memory::GetCharPointer(line.info.relevantAddress));
}
@@ -1162,13 +1162,6 @@ void ImDisasmView::scrollStepping(u32 newPc) {
}
}
u32 ImDisasmView::getInstructionSizeAt(u32 address) {
u32 start = g_disassemblyManager.getStartAddress(address);
u32 next = g_disassemblyManager.getNthNextAddress(start, 1);
return next - address;
}
void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, ImConfig &cfg, ImControl &control, CoreState coreState) {
disasmView_.setDebugger(mipsDebug);
@@ -1181,12 +1174,10 @@ void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, ImConfig &cfg, ImContro
if (ImGui::IsWindowFocused()) {
// Process stepping keyboard shortcuts.
if (ImGui::IsKeyPressed(ImGuiKey_F10)) {
u32 stepSize = disasmView_.getInstructionSizeAt(mipsDebug->GetPC());
Core_RequestCPUStep(CPUStepType::Over, stepSize);
Core_RequestCPUStep(CPUStepType::Over, 1);
}
if (ImGui::IsKeyPressed(ImGuiKey_F11)) {
u32 stepSize = disasmView_.getInstructionSizeAt(mipsDebug->GetPC());
Core_RequestCPUStep(CPUStepType::Into, stepSize);
Core_RequestCPUStep(CPUStepType::Into, 1);
}
}
@@ -1219,8 +1210,7 @@ void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, ImConfig &cfg, ImContro
ImGui::SameLine();
if (ImGui::RepeatButtonShift("Into")) {
u32 stepSize = disasmView_.getInstructionSizeAt(mipsDebug->GetPC());
Core_RequestCPUStep(CPUStepType::Into, stepSize);
Core_RequestCPUStep(CPUStepType::Into, 1);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("F11");
@@ -1228,8 +1218,7 @@ void ImDisasmWindow::Draw(MIPSDebugInterface *mipsDebug, ImConfig &cfg, ImContro
ImGui::SameLine();
if (ImGui::SmallButton("Over")) {
u32 stepSize = disasmView_.getInstructionSizeAt(mipsDebug->GetPC());
Core_RequestCPUStep(CPUStepType::Over, stepSize);
Core_RequestCPUStep(CPUStepType::Over, 1);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("F10");
-1
View File
@@ -58,7 +58,6 @@ public:
}
void scrollStepping(u32 newPc);
u32 getInstructionSizeAt(u32 address); // not const because it might have to analyze.
void gotoAddr(unsigned int addr) {
if (positionLocked_ != 0)
+2 -8
View File
@@ -602,7 +602,7 @@ void CtrlDisAsmView::followBranch()
DisassemblyLineInfo line;
g_disassemblyManager.getLine(curAddress, true, line, debugger);
if (line.type == DISTYPE_OPCODE || line.type == DISTYPE_MACRO)
if (line.type == DISTYPE_OPCODE)
{
if (line.info.isBranch)
{
@@ -1116,7 +1116,7 @@ void CtrlDisAsmView::updateStatusBarText()
g_disassemblyManager.getLine(curAddress,true,line, debugger);
text[0] = 0;
if (line.type == DISTYPE_OPCODE || line.type == DISTYPE_MACRO)
if (line.type == DISTYPE_OPCODE)
{
if (line.info.hasRelevantAddress && IsLikelyStringAt(line.info.relevantAddress)) {
snprintf(text, sizeof(text), "[%08X] = \"%s\"", line.info.relevantAddress, Memory::GetCharPointer(line.info.relevantAddress));
@@ -1350,9 +1350,3 @@ void CtrlDisAsmView::scrollStepping(u32 newPc)
}
}
u32 CtrlDisAsmView::getInstructionSizeAt(u32 address)
{
u32 start = g_disassemblyManager.getStartAddress(address);
u32 next = g_disassemblyManager.getNthNextAddress(start,1);
return next - address;
}
-1
View File
@@ -119,7 +119,6 @@ public:
}
void scrollStepping(u32 newPc);
u32 getInstructionSizeAt(u32 address);
void gotoAddr(unsigned int addr)
{
+1 -2
View File
@@ -206,8 +206,7 @@ void CDisasm::step(CPUStepType stepType) {
ptr->setDontRedraw(true);
lastTicks_ = CoreTiming::GetTicks();
u32 stepSize = ptr->getInstructionSizeAt(cpu->GetPC());
Core_RequestCPUStep(stepType, stepSize);
Core_RequestCPUStep(stepType, 1);
}
void CDisasm::runToLine() {
+1
View File
@@ -92,6 +92,7 @@ bool System_AudioRecordingState() { return false; }
void NativeFrame(GraphicsContext *graphicsContext) { }
void NativeResized() { }
void System_LaunchUrl(LaunchUrlType urlType, std::string_view url) {}
std::string System_GetProperty(SystemProperty prop) { return ""; }
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) { return std::vector<std::string>(); }
int64_t System_GetPropertyInt(SystemProperty prop) {