mirror of
https://github.com/cemu-project/Cemu.git
synced 2025-07-04 05:51:19 +12:00
988 lines
No EOL
30 KiB
C++
988 lines
No EOL
30 KiB
C++
#include "GDBStub.h"
|
|
#include "Debugger.h"
|
|
#include "Cafe/HW/Espresso/Recompiler/PPCRecompiler.h"
|
|
#include "GDBBreakpoints.h"
|
|
#include "util/helpers/helpers.h"
|
|
#include "util/ThreadPool/ThreadPool.h"
|
|
#include "Cafe/OS/RPL/rpl.h"
|
|
#include "Cafe/OS/RPL/rpl_structs.h"
|
|
#include "Cafe/OS/libs/coreinit/coreinit_Scheduler.h"
|
|
#include "Cafe/OS/libs/coreinit/coreinit_Thread.h"
|
|
#include "Cafe/HW/Espresso/Interpreter/PPCInterpreterInternal.h"
|
|
#include "Cafe/HW/Espresso/EspressoISA.h"
|
|
#include "Common/socket.h"
|
|
|
|
#define GET_THREAD_ID(threadPtr) memory_getVirtualOffsetFromPointer(threadPtr)
|
|
#define GET_THREAD_BY_ID(threadId) (OSThread_t*)memory_getPointerFromPhysicalOffset(threadId)
|
|
|
|
static std::vector<MPTR> findNextInstruction(MPTR currAddress, uint32 lr, uint32 ctr)
|
|
{
|
|
using namespace Espresso;
|
|
|
|
uint32 nextInstr = memory_readU32(currAddress);
|
|
if (GetPrimaryOpcode(nextInstr) == PrimaryOpcode::B)
|
|
{
|
|
uint32 LI;
|
|
bool AA, LK;
|
|
decodeOp_B(nextInstr, LI, AA, LK);
|
|
if (!AA)
|
|
LI += currAddress;
|
|
return {LI};
|
|
}
|
|
if (GetPrimaryOpcode(nextInstr) == PrimaryOpcode::BC)
|
|
{
|
|
uint32 BD, BI;
|
|
BOField BO{};
|
|
bool AA, LK;
|
|
decodeOp_BC(nextInstr, BD, BO, BI, AA, LK);
|
|
if (!LK)
|
|
BD += currAddress;
|
|
return {currAddress + 4, BD};
|
|
}
|
|
if (GetPrimaryOpcode(nextInstr) == PrimaryOpcode::GROUP_19 && GetGroup19Opcode(nextInstr) == Opcode19::BCLR)
|
|
{
|
|
return {currAddress + 4, lr};
|
|
}
|
|
if (GetPrimaryOpcode(nextInstr) == PrimaryOpcode::GROUP_19 && GetGroup19Opcode(nextInstr) == Opcode19::BCCTR)
|
|
{
|
|
return {currAddress + 4, ctr};
|
|
}
|
|
return {currAddress + 4};
|
|
}
|
|
|
|
template<typename F>
|
|
static void selectThread(sint64 selectorId, F&& action_for_thread)
|
|
{
|
|
__OSLockScheduler();
|
|
cemu_assert_debug(activeThreadCount != 0);
|
|
|
|
if (selectorId == -1)
|
|
{
|
|
for (sint32 i = 0; i < activeThreadCount; i++)
|
|
{
|
|
action_for_thread(GET_THREAD_BY_ID(activeThread[i]));
|
|
}
|
|
}
|
|
else if (selectorId == 0)
|
|
{
|
|
// Use first thread if attempted to be stopped
|
|
// todo: would this work better if it used main?
|
|
action_for_thread(coreinit::OSGetDefaultThread(1));
|
|
}
|
|
else if (selectorId > 0)
|
|
{
|
|
for (sint32 i = 0; i < activeThreadCount; i++)
|
|
{
|
|
auto* thread = GET_THREAD_BY_ID(activeThread[i]);
|
|
if (GET_THREAD_ID(thread) == selectorId)
|
|
{
|
|
action_for_thread(thread);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
__OSUnlockScheduler();
|
|
}
|
|
|
|
template<typename F>
|
|
static void selectAndBreakThread(sint64 selectorId, F&& action_for_thread)
|
|
{
|
|
__OSLockScheduler();
|
|
cemu_assert_debug(activeThreadCount != 0);
|
|
|
|
std::vector<OSThread_t*> pausedThreads;
|
|
if (selectorId == -1)
|
|
{
|
|
for (sint32 i = 0; i < activeThreadCount; i++)
|
|
{
|
|
coreinit::__OSSuspendThreadNolock(GET_THREAD_BY_ID(activeThread[i]));
|
|
pausedThreads.emplace_back(GET_THREAD_BY_ID(activeThread[i]));
|
|
}
|
|
}
|
|
else if (selectorId == 0)
|
|
{
|
|
// Use first thread if attempted to be stopped
|
|
OSThread_t* thread = GET_THREAD_BY_ID(activeThread[0]);
|
|
for (sint32 i = 0; i < activeThreadCount; i++)
|
|
{
|
|
if (GET_THREAD_ID(GET_THREAD_BY_ID(activeThread[i])) < GET_THREAD_ID(thread))
|
|
{
|
|
thread = GET_THREAD_BY_ID(activeThread[i]);
|
|
}
|
|
}
|
|
coreinit::__OSSuspendThreadNolock(thread);
|
|
pausedThreads.emplace_back(thread);
|
|
}
|
|
else if (selectorId > 0)
|
|
{
|
|
for (sint32 i = 0; i < activeThreadCount; i++)
|
|
{
|
|
auto* thread = GET_THREAD_BY_ID(activeThread[i]);
|
|
if (GET_THREAD_ID(thread) == selectorId)
|
|
{
|
|
coreinit::__OSSuspendThreadNolock(thread);
|
|
pausedThreads.emplace_back(thread);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
__OSUnlockScheduler();
|
|
|
|
for (OSThread_t* thread : pausedThreads)
|
|
{
|
|
while (coreinit::OSIsThreadRunning(thread))
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
action_for_thread(thread);
|
|
}
|
|
}
|
|
|
|
static void selectAndResumeThread(sint64 selectorId)
|
|
{
|
|
__OSLockScheduler();
|
|
cemu_assert_debug(activeThreadCount != 0);
|
|
|
|
if (selectorId == -1)
|
|
{
|
|
for (sint32 i = 0; i < activeThreadCount; i++)
|
|
{
|
|
coreinit::__OSResumeThreadInternal(GET_THREAD_BY_ID(activeThread[i]), 4);
|
|
}
|
|
}
|
|
else if (selectorId == 0)
|
|
{
|
|
// Use first thread if attempted to be stopped
|
|
coreinit::__OSResumeThreadInternal(coreinit::OSGetDefaultThread(1), 1);
|
|
}
|
|
else if (selectorId > 0)
|
|
{
|
|
for (sint32 i = 0; i < activeThreadCount; i++)
|
|
{
|
|
auto* thread = GET_THREAD_BY_ID(activeThread[i]);
|
|
if (GET_THREAD_ID(thread) == selectorId)
|
|
{
|
|
coreinit::__OSResumeThreadInternal(thread, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
__OSUnlockScheduler();
|
|
}
|
|
|
|
static void waitForBrokenThreads(std::unique_ptr<GDBServer::CommandContext> context, std::string_view reason)
|
|
{
|
|
// This should pause all threads except trapped thread
|
|
// It should however wait for the trapped thread
|
|
// The trapped thread should be paused by the trap word instruction handler (aka the running thread)
|
|
std::vector<OSThread_t*> threadsList;
|
|
__OSLockScheduler();
|
|
for (sint32 i = 0; i < activeThreadCount; i++)
|
|
{
|
|
threadsList.emplace_back(GET_THREAD_BY_ID(activeThread[i]));
|
|
}
|
|
__OSUnlockScheduler();
|
|
|
|
for (OSThread_t* thread : threadsList)
|
|
{
|
|
while (coreinit::OSIsThreadRunning(thread))
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
}
|
|
|
|
context->QueueResponse(reason);
|
|
}
|
|
|
|
static void breakThreads(sint64 trappedThread)
|
|
{
|
|
__OSLockScheduler();
|
|
cemu_assert_debug(activeThreadCount != 0);
|
|
|
|
// First, break other threads
|
|
OSThread_t* mainThread = nullptr;
|
|
for (sint32 i = 0; i < activeThreadCount; i++)
|
|
{
|
|
if (GET_THREAD_ID(GET_THREAD_BY_ID(activeThread[i])) == trappedThread)
|
|
{
|
|
mainThread = GET_THREAD_BY_ID(activeThread[i]);
|
|
}
|
|
else
|
|
{
|
|
coreinit::__OSSuspendThreadNolock(GET_THREAD_BY_ID(activeThread[i]));
|
|
}
|
|
}
|
|
|
|
// Second, break trapped thread itself which should also pause execution of this handler
|
|
// This will temporarily lift the scheduler lock until it's resumed from its suspension
|
|
coreinit::__OSSuspendThreadNolock(mainThread);
|
|
|
|
__OSUnlockScheduler();
|
|
}
|
|
|
|
std::unique_ptr<GDBServer> g_gdbstub;
|
|
|
|
GDBServer::GDBServer(uint16 port)
|
|
: m_port(port)
|
|
{
|
|
#if BOOST_OS_WINDOWS
|
|
WSADATA wsa;
|
|
WSAStartup(MAKEWORD(2, 2), &wsa);
|
|
#endif
|
|
}
|
|
|
|
GDBServer::~GDBServer()
|
|
{
|
|
if (m_client_socket != INVALID_SOCKET)
|
|
{
|
|
// close socket from other thread to forcefully stop accept() call
|
|
closesocket(m_client_socket);
|
|
m_client_socket = INVALID_SOCKET;
|
|
}
|
|
|
|
if (m_server_socket != INVALID_SOCKET)
|
|
{
|
|
closesocket(m_server_socket);
|
|
}
|
|
#if BOOST_OS_WINDOWS
|
|
WSACleanup();
|
|
#endif
|
|
|
|
m_stopRequested = false;
|
|
m_thread.join();
|
|
}
|
|
|
|
bool GDBServer::Initialize()
|
|
{
|
|
cemuLog_createLogFile(false);
|
|
|
|
if (m_server_socket = socket(PF_INET, SOCK_STREAM, 0); m_server_socket == SOCKET_ERROR)
|
|
return false;
|
|
|
|
int reuseEnabled = TRUE;
|
|
if (setsockopt(m_server_socket, SOL_SOCKET, SO_REUSEADDR, (char*)&reuseEnabled, sizeof(reuseEnabled)) == SOCKET_ERROR)
|
|
{
|
|
closesocket(m_server_socket);
|
|
m_server_socket = INVALID_SOCKET;
|
|
return false;
|
|
}
|
|
|
|
int nodelayEnabled = TRUE;
|
|
if (setsockopt(m_server_socket, IPPROTO_TCP, TCP_NODELAY, (char*)&nodelayEnabled, sizeof(nodelayEnabled)) == SOCKET_ERROR)
|
|
{
|
|
closesocket(m_server_socket);
|
|
m_server_socket = INVALID_SOCKET;
|
|
return false;
|
|
}
|
|
|
|
memset(&m_server_addr, 0, sizeof(m_server_addr));
|
|
m_server_addr.sin_family = AF_INET;
|
|
m_server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
|
m_server_addr.sin_port = htons(m_port);
|
|
|
|
if (bind(m_server_socket, (sockaddr*)&m_server_addr, sizeof(m_server_addr)) == SOCKET_ERROR)
|
|
{
|
|
closesocket(m_server_socket);
|
|
m_server_socket = INVALID_SOCKET;
|
|
return false;
|
|
}
|
|
|
|
if (listen(m_server_socket, s_maxGDBClients) == SOCKET_ERROR)
|
|
{
|
|
closesocket(m_server_socket);
|
|
m_server_socket = INVALID_SOCKET;
|
|
return false;
|
|
}
|
|
|
|
m_thread = std::thread(std::bind(&GDBServer::ThreadFunc, this));
|
|
|
|
return true;
|
|
}
|
|
|
|
void GDBServer::ThreadFunc()
|
|
{
|
|
SetThreadName("GDBServer::ThreadFunc");
|
|
|
|
while (!m_stopRequested)
|
|
{
|
|
if (!m_client_connected)
|
|
{
|
|
cemuLog_logDebug(LogType::Force, "[GDBStub] Waiting for client to connect on port {}...", m_port);
|
|
socklen_t client_addr_size = sizeof(m_client_addr);
|
|
m_client_socket = accept(m_server_socket, (struct sockaddr*)&m_client_addr, &client_addr_size);
|
|
m_client_connected = m_client_socket != SOCKET_ERROR;
|
|
}
|
|
else
|
|
{
|
|
auto receiveMessage = [&](char* buffer, const int32_t length) -> bool {
|
|
if (recv(m_client_socket, buffer, length, 0) != SOCKET_ERROR)
|
|
return false;
|
|
return true;
|
|
};
|
|
|
|
auto readChar = [&]() -> char {
|
|
char ret = 0;
|
|
recv(m_client_socket, &ret, 1, 0);
|
|
return ret;
|
|
};
|
|
|
|
char packetPrefix = readChar();
|
|
|
|
switch (packetPrefix)
|
|
{
|
|
case '+':
|
|
case '-':
|
|
break;
|
|
case '\x03':
|
|
{
|
|
cemuLog_logDebug(LogType::Force, "[GDBStub] Received interrupt (pressed CTRL+C?) from client!");
|
|
selectAndBreakThread(-1, [](OSThread_t* thread) {
|
|
});
|
|
auto thread_status = fmt::format("T05thread:{:08X};", GET_THREAD_ID(coreinit::OSGetDefaultThread(1)));
|
|
if (this->m_resumed_context)
|
|
{
|
|
this->m_resumed_context->QueueResponse(thread_status);
|
|
this->m_resumed_context.reset();
|
|
}
|
|
else
|
|
{
|
|
auto response_full = fmt::format("+${}#{:02x}", thread_status, CommandContext::CalculateChecksum(thread_status));
|
|
send(m_client_socket, response_full.c_str(), (int)response_full.size(), 0);
|
|
}
|
|
break;
|
|
}
|
|
case '$':
|
|
{
|
|
std::string message;
|
|
uint8 checkedSum = 0;
|
|
for (uint32_t i = 1;; i++)
|
|
{
|
|
char c = readChar();
|
|
if (c == '#')
|
|
break;
|
|
checkedSum += static_cast<uint8>(c);
|
|
message.push_back(c);
|
|
|
|
if (i >= s_maxPacketSize)
|
|
cemuLog_logDebug(LogType::Force, "[GDBStub] Received too big of a buffer: {}", message);
|
|
}
|
|
char checkSumStr[2];
|
|
receiveMessage(checkSumStr, 2);
|
|
uint32_t checkSum = std::stoi(std::string(checkSumStr, sizeof(checkSumStr)), nullptr, 16);
|
|
assert((checkedSum & 0xFF) == checkSum);
|
|
|
|
HandleCommand(message);
|
|
break;
|
|
}
|
|
default:
|
|
// cemuLog_logDebug(LogType::Force, "[GDBStub] Unknown packet start: {}", packetPrefix);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_client_socket != INVALID_SOCKET)
|
|
closesocket(m_client_socket);
|
|
}
|
|
|
|
void GDBServer::HandleCommand(const std::string& command_str)
|
|
{
|
|
auto context = std::make_unique<CommandContext>(this, command_str);
|
|
|
|
if (context->IsValid())
|
|
{
|
|
// cemuLog_logDebug(LogType::Force, "[GDBStub] Extracted Command {}", fmt::join(context->GetArgs(), ","));
|
|
}
|
|
|
|
switch (context->GetType())
|
|
{
|
|
// Extended commands
|
|
case CMDType::QUERY_GET:
|
|
case CMDType::QUERY_SET:
|
|
return HandleQuery(context);
|
|
case CMDType::VCONT:
|
|
return HandleVCont(context);
|
|
// Regular commands
|
|
case CMDType::IS_THREAD_RUNNING:
|
|
return CMDIsThreadActive(context);
|
|
case CMDType::SET_ACTIVE_THREAD:
|
|
return CMDSetActiveThread(context);
|
|
case CMDType::ACTIVE_THREAD_STATUS:
|
|
return CMDGetThreadStatus(context);
|
|
case CMDType::CONTINUE:
|
|
return CMDContinue(context);
|
|
case CMDType::ACTIVE_THREAD_STEP:
|
|
break;
|
|
case CMDType::REGISTER_READ:
|
|
return CMDReadRegister(context);
|
|
case CMDType::REGISTER_SET:
|
|
return CMDWriteRegister(context);
|
|
case CMDType::REGISTERS_READ:
|
|
return CMDReadRegisters(context);
|
|
case CMDType::REGISTERS_WRITE:
|
|
return CMDWriteRegisters(context);
|
|
case CMDType::MEMORY_READ:
|
|
return CMDReadMemory(context);
|
|
case CMDType::MEMORY_WRITE:
|
|
return CMDWriteMemory(context);
|
|
case CMDType::BREAKPOINT_SET:
|
|
return CMDInsertBreakpoint(context);
|
|
case CMDType::BREAKPOINT_REMOVE:
|
|
return CMDDeleteBreakpoint(context);
|
|
case CMDType::INVALID:
|
|
default:
|
|
return CMDNotFound(context);
|
|
}
|
|
|
|
CMDNotFound(context);
|
|
}
|
|
|
|
void GDBServer::HandleQuery(std::unique_ptr<CommandContext>& context) const
|
|
{
|
|
if (!context->IsValid())
|
|
return context->QueueResponse(RESPONSE_EMPTY);
|
|
|
|
const auto& query_cmd = context->GetArgs()[0];
|
|
const auto& query_args = context->GetArgs().begin() + 1;
|
|
|
|
if (query_cmd == "qSupported")
|
|
{
|
|
context->QueueResponse(s_supportedFeatures);
|
|
}
|
|
else if (query_cmd == "qAttached")
|
|
{
|
|
context->QueueResponse("1");
|
|
}
|
|
else if (query_cmd == "qRcmd")
|
|
{
|
|
}
|
|
else if (query_cmd == "qC")
|
|
{
|
|
context->QueueResponse("QC");
|
|
context->QueueResponse(std::to_string(m_activeThreadContinueSelector));
|
|
}
|
|
else if (query_cmd == "qOffsets")
|
|
{
|
|
const auto module_count = RPLLoader_GetModuleCount();
|
|
const auto module_list = RPLLoader_GetModuleList();
|
|
for (sint32 i = 0; i < module_count; i++)
|
|
{
|
|
const RPLModule* rpl = module_list[i];
|
|
if (rpl->entrypoint == m_entry_point)
|
|
{
|
|
context->QueueResponse(fmt::format("TextSeg={:08X};DataSeg={:08X}", rpl->regionMappingBase_text.GetMPTR(), rpl->regionMappingBase_data));
|
|
}
|
|
}
|
|
}
|
|
else if (query_cmd == "qfThreadInfo")
|
|
{
|
|
std::vector<std::string> threadIds;
|
|
selectThread(-1, [&threadIds](OSThread_t* thread) {
|
|
threadIds.emplace_back(fmt::format("{:08X}", memory_getVirtualOffsetFromPointer(thread)));
|
|
});
|
|
context->QueueResponse(fmt::format("m{}", fmt::join(threadIds, ",")));
|
|
}
|
|
else if (query_cmd == "qsThreadInfo")
|
|
{
|
|
context->QueueResponse("l");
|
|
}
|
|
else if (query_cmd == "qXfer")
|
|
{
|
|
auto& type = query_args[0];
|
|
|
|
if (type == "features")
|
|
{
|
|
auto& annex = query_args[1];
|
|
sint64 read_offset = std::stoul(query_args[2], nullptr, 16);
|
|
sint64 read_length = std::stoul(query_args[3], nullptr, 16);
|
|
if (annex == "target.xml")
|
|
{
|
|
if (read_offset >= GDBTargetXML.size())
|
|
context->QueueResponse("l");
|
|
else
|
|
{
|
|
auto paginated_str = GDBTargetXML.substr(read_offset, read_length);
|
|
context->QueueResponse((paginated_str.size() == read_length) ? "m" : "l");
|
|
context->QueueResponse(paginated_str);
|
|
}
|
|
}
|
|
else
|
|
cemuLog_logDebug(LogType::Force, "[GDBStub] qXfer:features:read:{} isn't a known feature document", annex);
|
|
}
|
|
else if (type == "threads")
|
|
{
|
|
sint64 read_offset = std::stoul(query_args[1], nullptr, 16);
|
|
sint64 read_length = std::stoul(query_args[2], nullptr, 16);
|
|
|
|
std::string threads_res;
|
|
threads_res += R"(<?xml version="1.0"?>)";
|
|
threads_res += "<threads>";
|
|
// note: clion seems to default to the first thread
|
|
std::map<sint64, std::string> threads_list;
|
|
selectThread(-1, [&threads_list](OSThread_t* thread) {
|
|
std::string entry;
|
|
entry += fmt::format(R"(<thread id="{:x}" core="{}")", GET_THREAD_ID(thread), thread->context.upir.value());
|
|
if (!thread->threadName.IsNull())
|
|
entry += fmt::format(R"( name="{}")", CommandContext::EscapeXMLString(thread->threadName.GetPtr()));
|
|
// todo: could add a human-readable description of the thread here
|
|
entry += fmt::format("></thread>");
|
|
threads_list.emplace(GET_THREAD_ID(thread), entry);
|
|
});
|
|
for (auto& entry : threads_list)
|
|
{
|
|
threads_res += entry.second;
|
|
}
|
|
threads_res += "</threads>";
|
|
|
|
if (read_offset >= threads_res.size())
|
|
context->QueueResponse("l");
|
|
else
|
|
{
|
|
auto paginated_str = threads_res.substr(read_offset, read_length);
|
|
context->QueueResponse((paginated_str.size() == read_length) ? "m" : "l");
|
|
context->QueueResponse(paginated_str);
|
|
}
|
|
}
|
|
else if (type == "libraries")
|
|
{
|
|
sint64 read_offset = std::stoul(query_args[1], nullptr, 16);
|
|
sint64 read_length = std::stoul(query_args[2], nullptr, 16);
|
|
|
|
std::string library_list;
|
|
library_list += R"(<?xml version="1.0"?>)";
|
|
library_list += "<library-list>";
|
|
|
|
const auto module_count = RPLLoader_GetModuleCount();
|
|
const auto module_list = RPLLoader_GetModuleList();
|
|
for (sint32 i = 0; i < module_count; i++)
|
|
{
|
|
library_list += fmt::format(R"(<library name="{}"><segment address="{:#x}"/></library>)", CommandContext::EscapeXMLString(module_list[i]->moduleName2), module_list[i]->regionMappingBase_text.GetMPTR());
|
|
}
|
|
library_list += "</library-list>";
|
|
|
|
if (read_offset >= library_list.size())
|
|
context->QueueResponse("l");
|
|
else
|
|
{
|
|
auto paginated_str = library_list.substr(read_offset, read_length);
|
|
context->QueueResponse((paginated_str.size() == read_length) ? "m" : "l");
|
|
context->QueueResponse(paginated_str);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
context->QueueResponse(RESPONSE_EMPTY);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
context->QueueResponse(RESPONSE_EMPTY);
|
|
}
|
|
}
|
|
|
|
void GDBServer::HandleVCont(std::unique_ptr<CommandContext>& context)
|
|
{
|
|
if (!context->IsValid())
|
|
{
|
|
cemuLog_logDebug(LogType::Force, "[GDBStub] Received unsupported vCont command: {}", context->GetCommand());
|
|
// cemu_assert_unimplemented();
|
|
return context->QueueResponse(RESPONSE_EMPTY);
|
|
}
|
|
|
|
const std::string& vcont_cmd = context->GetArgs()[0];
|
|
if (vcont_cmd == "vCont?")
|
|
return context->QueueResponse("vCont;c;C;s;S");
|
|
|
|
else if (vcont_cmd != "vCont;")
|
|
return context->QueueResponse(RESPONSE_EMPTY);
|
|
|
|
m_resumed_context = std::move(context);
|
|
|
|
bool resumedNoThreads = true;
|
|
for (const auto operation : TokenizeView(m_resumed_context->GetArgs()[1], ';'))
|
|
{
|
|
// todo: this might have issues with the signal versions (C/S)
|
|
// todo: test whether this works with multiple vCont;c:123123;c:123123
|
|
std::string_view operationType = operation.substr(0, operation.find(':'));
|
|
sint64 threadSelector = operationType.size() == operation.size() ? -1 : std::stoll(std::string(operation.substr(operationType.size() + 1)), nullptr, 16);
|
|
|
|
if (operationType == "c" || operationType.starts_with("C"))
|
|
{
|
|
selectAndResumeThread(threadSelector);
|
|
resumedNoThreads = false;
|
|
}
|
|
else if (operationType == "s" || operationType.starts_with("S"))
|
|
{
|
|
selectThread(threadSelector, [this](OSThread_t* thread) {
|
|
auto nextInstructions = findNextInstruction(thread->context.srr0, thread->context.lr, thread->context.ctr);
|
|
for (MPTR nextInstr : nextInstructions)
|
|
{
|
|
auto bpIt = m_patchedInstructions.find(nextInstr);
|
|
if (bpIt == m_patchedInstructions.end())
|
|
this->m_patchedInstructions.try_emplace(nextInstr, nextInstr, BreakpointType::BP_STEP_POINT, false, "swbreak:;");
|
|
else
|
|
bpIt->second.PauseOnNextInterrupt();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (resumedNoThreads)
|
|
{
|
|
selectAndResumeThread(-1);
|
|
cemuLog_logDebug(LogType::Force, "[GDBStub] Resumed all threads after skip instructions");
|
|
}
|
|
}
|
|
|
|
void GDBServer::CMDContinue(std::unique_ptr<CommandContext>& context)
|
|
{
|
|
m_resumed_context = std::move(context);
|
|
selectAndResumeThread(m_activeThreadContinueSelector);
|
|
}
|
|
|
|
void GDBServer::CMDNotFound(std::unique_ptr<CommandContext>& context)
|
|
{
|
|
return context->QueueResponse(RESPONSE_EMPTY);
|
|
}
|
|
|
|
void GDBServer::CMDIsThreadActive(std::unique_ptr<CommandContext>& context)
|
|
{
|
|
sint64 threadSelector = std::stoll(context->GetArgs()[1], nullptr, 16);
|
|
bool foundThread = false;
|
|
selectThread(threadSelector, [&foundThread](OSThread_t* thread) {
|
|
foundThread = true;
|
|
});
|
|
|
|
if (foundThread)
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
else
|
|
return context->QueueResponse(RESPONSE_ERROR);
|
|
}
|
|
|
|
void GDBServer::CMDSetActiveThread(std::unique_ptr<CommandContext>& context)
|
|
{
|
|
sint64 threadSelector = std::stoll(context->GetArgs()[2], nullptr, 16);
|
|
if (threadSelector >= 0)
|
|
{
|
|
bool foundThread = false;
|
|
selectThread(threadSelector, [&foundThread](OSThread_t* thread) {
|
|
foundThread = true;
|
|
});
|
|
if (!foundThread)
|
|
return context->QueueResponse(RESPONSE_ERROR);
|
|
}
|
|
if (context->GetArgs()[1] == "c")
|
|
m_activeThreadContinueSelector = threadSelector;
|
|
else
|
|
m_activeThreadSelector = threadSelector;
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
}
|
|
|
|
void GDBServer::CMDGetThreadStatus(std::unique_ptr<CommandContext>& context)
|
|
{
|
|
selectThread(0, [&context](OSThread_t* thread) {
|
|
context->QueueResponse(fmt::format("T05thread:{:08X};", memory_getVirtualOffsetFromPointer(thread)));
|
|
});
|
|
}
|
|
|
|
void GDBServer::CMDReadRegister(std::unique_ptr<CommandContext>& context) const
|
|
{
|
|
sint32 reg = std::stoi(context->GetArgs()[1], nullptr, 16);
|
|
selectThread(m_activeThreadSelector, [reg, &context](OSThread_t* thread) {
|
|
auto& cpu = thread->context;
|
|
if (reg >= RegisterID::R0_START && reg <= RegisterID::R31_END)
|
|
{
|
|
return context->QueueResponse(fmt::format("{:08X}", CPU_swapEndianU32(cpu.gpr[reg])));
|
|
}
|
|
else if (reg >= RegisterID::F0_START && reg <= RegisterID::F31_END)
|
|
{
|
|
return context->QueueResponse(fmt::format("{:016X}", cpu.fp_ps0[reg - RegisterID::F0_START].value()));
|
|
}
|
|
else if (reg == RegisterID::FPSCR)
|
|
{
|
|
return context->QueueResponse(fmt::format("{:08X}", cpu.fpscr.fpscr.value()));
|
|
}
|
|
else
|
|
{
|
|
switch (reg)
|
|
{
|
|
case RegisterID::PC: return context->QueueResponse(fmt::format("{:08X}", cpu.srr0));
|
|
case RegisterID::MSR: return context->QueueResponse("xxxxxxxx");
|
|
case RegisterID::CR: return context->QueueResponse(fmt::format("{:08X}", cpu.cr));
|
|
case RegisterID::LR: return context->QueueResponse(fmt::format("{:08X}", CPU_swapEndianU32(cpu.lr)));
|
|
case RegisterID::CTR: return context->QueueResponse(fmt::format("{:08X}", cpu.ctr));
|
|
case RegisterID::XER: return context->QueueResponse(fmt::format("{:08X}", cpu.xer));
|
|
default: break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void GDBServer::CMDWriteRegister(std::unique_ptr<CommandContext>& context) const
|
|
{
|
|
sint32 reg = std::stoi(context->GetArgs()[1], nullptr, 16);
|
|
uint64 value = std::stoll(context->GetArgs()[2], nullptr, 16);
|
|
selectThread(m_activeThreadSelector, [reg, value, &context](OSThread_t* thread) {
|
|
auto& cpu = thread->context;
|
|
if (reg >= RegisterID::R0_START && reg <= RegisterID::R31_END)
|
|
{
|
|
cpu.gpr[reg] = CPU_swapEndianU32(value);
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
}
|
|
else if (reg >= RegisterID::F0_START && reg <= RegisterID::F31_END)
|
|
{
|
|
// todo: figure out how to properly write to paired single registers
|
|
cpu.fp_ps0[reg - RegisterID::F0_START] = uint64be{value};
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
}
|
|
else if (reg == RegisterID::FPSCR)
|
|
{
|
|
cpu.fpscr.fpscr = uint32be{(uint32)value};
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
}
|
|
else
|
|
{
|
|
switch (reg)
|
|
{
|
|
case RegisterID::PC:
|
|
cpu.srr0 = value;
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
case RegisterID::MSR:
|
|
return context->QueueResponse(RESPONSE_ERROR);
|
|
case RegisterID::CR:
|
|
cpu.cr = value;
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
case RegisterID::LR:
|
|
cpu.lr = CPU_swapEndianU32(value);
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
case RegisterID::CTR:
|
|
cpu.ctr = value;
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
case RegisterID::XER:
|
|
cpu.xer = value;
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
default:
|
|
return context->QueueResponse(RESPONSE_ERROR);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void GDBServer::CMDReadRegisters(std::unique_ptr<CommandContext>& context) const
|
|
{
|
|
selectThread(m_activeThreadSelector, [&context](OSThread_t* thread) {
|
|
for (uint32& reg : thread->context.gpr)
|
|
{
|
|
context->QueueResponse(fmt::format("{:08X}", CPU_swapEndianU32(reg)));
|
|
}
|
|
});
|
|
}
|
|
|
|
void GDBServer::CMDWriteRegisters(std::unique_ptr<CommandContext>& context) const
|
|
{
|
|
selectThread(m_activeThreadSelector, [&context](OSThread_t* thread) {
|
|
auto& registers = context->GetArgs()[1];
|
|
for (uint32 i = 0; i < 32; i++)
|
|
{
|
|
thread->context.gpr[i] = CPU_swapEndianU32(std::stoi(registers.substr(i * 2, 2), nullptr, 16));
|
|
}
|
|
});
|
|
}
|
|
|
|
void GDBServer::CMDReadMemory(std::unique_ptr<CommandContext>& context)
|
|
{
|
|
sint64 addr = std::stoul(context->GetArgs()[1], nullptr, 16);
|
|
sint64 length = std::stoul(context->GetArgs()[2], nullptr, 16);
|
|
|
|
// todo: handle cross-mmu-range memory requests
|
|
if (!memory_isAddressRangeAccessible(addr, length))
|
|
return context->QueueResponse(RESPONSE_ERROR);
|
|
|
|
std::string memoryRepr;
|
|
uint8* values = memory_getPointerFromVirtualOffset(addr);
|
|
for (sint64 i = 0; i < length; i++)
|
|
{
|
|
memoryRepr += fmt::format("{:02X}", values[i]);
|
|
}
|
|
|
|
auto patchesRange = m_patchedInstructions.lower_bound(addr);
|
|
while (patchesRange != m_patchedInstructions.end() && patchesRange->first < (addr + length))
|
|
{
|
|
auto replStr = fmt::format("{:02X}", patchesRange->second.GetVisibleOpCode());
|
|
memoryRepr[(patchesRange->first - addr) * 2] = replStr[0];
|
|
memoryRepr[(patchesRange->first - addr) * 2 + 1] = replStr[1];
|
|
patchesRange++;
|
|
}
|
|
return context->QueueResponse(memoryRepr);
|
|
}
|
|
|
|
void GDBServer::CMDWriteMemory(std::unique_ptr<CommandContext>& context)
|
|
{
|
|
sint64 addr = std::stoul(context->GetArgs()[1], nullptr, 16);
|
|
sint64 length = std::stoul(context->GetArgs()[2], nullptr, 16);
|
|
auto source = context->GetArgs()[3];
|
|
|
|
// todo: handle cross-mmu-range memory requests
|
|
if (!memory_isAddressRangeAccessible(addr, length))
|
|
return context->QueueResponse(RESPONSE_ERROR);
|
|
|
|
uint8* values = memory_getPointerFromVirtualOffset(addr);
|
|
for (sint64 i = 0; i < length; i++)
|
|
{
|
|
uint8 hexValue;
|
|
const std::from_chars_result result = std::from_chars(source.data() + (i * 2), (source.data() + (i * 2) + 2), hexValue, 16);
|
|
if (result.ec == std::errc::invalid_argument || result.ec == std::errc::result_out_of_range)
|
|
return context->QueueResponse(RESPONSE_ERROR);
|
|
|
|
if (auto it = m_patchedInstructions.find(addr + i); it != m_patchedInstructions.end())
|
|
{
|
|
uint32 newOpCode = it->second.GetVisibleOpCode();
|
|
uint32 byteIndex = 3 - ((addr + i) % 4); // inverted because of big endian, so address 0 is the highest byte
|
|
newOpCode &= ~(0xFF << (byteIndex * 8)); // mask out the byte
|
|
newOpCode |= ((uint32)hexValue << (byteIndex * 8)); // set new byte with OR
|
|
it->second.WriteNewOpCode(newOpCode);
|
|
}
|
|
else
|
|
{
|
|
values[i] = hexValue;
|
|
}
|
|
}
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
}
|
|
|
|
void GDBServer::CMDInsertBreakpoint(std::unique_ptr<CommandContext>& context)
|
|
{
|
|
auto type = std::stoul(context->GetArgs()[1], nullptr, 16);
|
|
MPTR addr = static_cast<MPTR>(std::stoul(context->GetArgs()[2], nullptr, 16));
|
|
|
|
if (type == 0 || type == 1)
|
|
{
|
|
auto bp = this->m_patchedInstructions.find(addr);
|
|
if (bp != this->m_patchedInstructions.end())
|
|
this->m_patchedInstructions.erase(bp);
|
|
this->m_patchedInstructions.try_emplace(addr, addr, BreakpointType::BP_PERSISTENT, type == 0, type == 0 ? "swbreak:;" : "hwbreak:;");
|
|
}
|
|
else if (type == 2 || type == 3 || type == 4)
|
|
{
|
|
if (this->m_watch_point)
|
|
return context->QueueResponse(RESPONSE_ERROR);
|
|
|
|
this->m_watch_point = std::make_unique<AccessBreakpoint>(addr, (AccessPointType)type);
|
|
}
|
|
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
}
|
|
|
|
void GDBServer::CMDDeleteBreakpoint(std::unique_ptr<CommandContext>& context)
|
|
{
|
|
auto type = std::stoul(context->GetArgs()[1], nullptr, 16);
|
|
MPTR addr = static_cast<MPTR>(std::stoul(context->GetArgs()[2], nullptr, 16));
|
|
|
|
if (type == 0 || type == 1)
|
|
{
|
|
auto bp = this->m_patchedInstructions.find(addr);
|
|
if (bp == this->m_patchedInstructions.end() || !bp->second.ShouldBreakThreads())
|
|
return context->QueueResponse(RESPONSE_ERROR);
|
|
else
|
|
this->m_patchedInstructions.erase(bp);
|
|
}
|
|
else if (type == 2 || type == 3 || type == 4)
|
|
{
|
|
if (!this->m_watch_point || this->m_watch_point->GetAddress() != addr)
|
|
return context->QueueResponse(RESPONSE_ERROR);
|
|
|
|
this->m_watch_point.reset();
|
|
}
|
|
|
|
return context->QueueResponse(RESPONSE_OK);
|
|
}
|
|
|
|
// Internal functions for control
|
|
void GDBServer::HandleTrapInstruction(PPCInterpreter_t* hCPU)
|
|
{
|
|
// First, restore any removed breakpoints
|
|
for (auto& bp : m_patchedInstructions)
|
|
{
|
|
if (bp.second.IsRemoved())
|
|
bp.second.Restore();
|
|
}
|
|
|
|
auto patchedBP = m_patchedInstructions.find(hCPU->instructionPointer);
|
|
if (patchedBP == m_patchedInstructions.end())
|
|
return cemu_assert_suspicious();
|
|
|
|
// Secondly, delete one-shot breakpoints but also temporarily delete patched instruction to run original instruction
|
|
OSThread_t* currThread = coreinit::OSGetCurrentThread();
|
|
std::string pauseReason = fmt::format("T05thread:{:08X};core:{:02X};{}", GET_THREAD_ID(currThread), PPCInterpreter_getCoreIndex(hCPU), patchedBP->second.GetReason());
|
|
bool pauseThreads = patchedBP->second.ShouldBreakThreads() || patchedBP->second.ShouldBreakThreadsOnNextInterrupt();
|
|
if (patchedBP->second.IsPersistent())
|
|
{
|
|
// Insert new restore breakpoints at next possible instructions which restores breakpoints but won't pause the CPU
|
|
std::vector<MPTR> nextInstructions = findNextInstruction(hCPU->instructionPointer, hCPU->spr.LR, hCPU->spr.CTR);
|
|
for (MPTR nextInstr : nextInstructions)
|
|
{
|
|
if (!m_patchedInstructions.contains(nextInstr))
|
|
this->m_patchedInstructions.try_emplace(nextInstr, nextInstr, BreakpointType::BP_STEP_POINT, false, "");
|
|
}
|
|
patchedBP->second.RemoveTemporarily();
|
|
}
|
|
else
|
|
{
|
|
m_patchedInstructions.erase(patchedBP);
|
|
}
|
|
|
|
// Thirdly, delete any instructions that were generated by a skip instruction
|
|
for (auto it = m_patchedInstructions.cbegin(), next_it = it; it != m_patchedInstructions.cend(); it = next_it)
|
|
{
|
|
++next_it;
|
|
if (it->second.IsSkipBreakpoint())
|
|
{
|
|
m_patchedInstructions.erase(it);
|
|
}
|
|
}
|
|
|
|
// Fourthly, the stub can insert breakpoints that are just meant to restore patched instructions, in which case we just want to continue
|
|
if (pauseThreads)
|
|
{
|
|
cemuLog_logDebug(LogType::Force, "[GDBStub] Got trapped by a breakpoint!");
|
|
if (m_resumed_context)
|
|
{
|
|
// Spin up thread to signal when another GDB stub trap is found
|
|
ThreadPool::FireAndForget(&waitForBrokenThreads, std::move(m_resumed_context), pauseReason);
|
|
}
|
|
|
|
breakThreads(GET_THREAD_ID(coreinit::OSGetCurrentThread()));
|
|
cemuLog_logDebug(LogType::Force, "[GDBStub] Resumed from a breakpoint!");
|
|
}
|
|
}
|
|
|
|
void GDBServer::HandleAccessException(uint64 dr6)
|
|
{
|
|
bool triggeredWrite = GetBits(dr6, 2, 1);
|
|
bool triggeredReadWrite = GetBits(dr6, 3, 1);
|
|
|
|
std::string response;
|
|
if (m_watch_point->GetType() == AccessPointType::BP_WRITE && triggeredWrite)
|
|
response = fmt::format("watch:{:08X};", m_watch_point->GetAddress());
|
|
else if (m_watch_point->GetType() == AccessPointType::BP_READ && triggeredReadWrite && !triggeredWrite)
|
|
response = fmt::format("rwatch:{:08X};", m_watch_point->GetAddress());
|
|
else if (m_watch_point->GetType() == AccessPointType::BP_BOTH && triggeredReadWrite)
|
|
response = fmt::format("awatch:{:08X};", m_watch_point->GetAddress());
|
|
|
|
if (!response.empty())
|
|
{
|
|
PPCInterpreter_t* hCPU = PPCInterpreter_getCurrentInstance();
|
|
cemuLog_logDebug(LogType::Force, "Received matching breakpoint exception: {}", response);
|
|
auto nextInstructions = findNextInstruction(hCPU->instructionPointer, hCPU->spr.LR, hCPU->spr.CTR);
|
|
for (MPTR nextInstr : nextInstructions)
|
|
{
|
|
auto bpIt = m_patchedInstructions.find(nextInstr);
|
|
if (bpIt == m_patchedInstructions.end())
|
|
this->m_patchedInstructions.try_emplace(nextInstr, nextInstr, BreakpointType::BP_STEP_POINT, false, response);
|
|
else
|
|
bpIt->second.PauseOnNextInterrupt();
|
|
}
|
|
}
|
|
}
|
|
|
|
void GDBServer::HandleEntryStop(uint32 entryAddress)
|
|
{
|
|
this->m_patchedInstructions.try_emplace(entryAddress, entryAddress, BreakpointType::BP_SINGLE, false, "");
|
|
m_entry_point = entryAddress;
|
|
} |