From 733fe044264f03a8c41d453a0e120e00e25603f7 Mon Sep 17 00:00:00 2001 From: emoose Date: Sun, 6 Oct 2024 02:43:34 +0100 Subject: [PATCH 01/16] [Debug] Add PoC GDBStub server, fix breakpoints Fixed issue in processor.cc which prevented resuming from a breakpoint BPs would set 2 bytes of the x64 JIT opcode to an invalid opcode & then catch the exception from it - BP handler then restored the original opcode, but then advanced RIP by 2 bytes into that opcode for some reason? It would also suspend the thread just prior to this RIP change, causing some strange effects... moving suspend after it (and removing the offset) appears to work fine, though I didn't test the imgui debugger yet To use GDBStub run xenia with "--debug --gdbport 12345" args Tested with IDA 9 + idaxex loader, BPs can be set and resumed fine, suspend & resume also works - memory/registers are viewable but can't be changed yet The socket code here is very basic and only allows a single connection for the session, if you disconnect you'll need to restart emulator --- premake5.lua | 1 + src/xenia/app/premake5.lua | 1 + src/xenia/app/xenia_main.cc | 43 +- src/xenia/cpu/processor.cc | 7 +- src/xenia/debug/gdb/gdbstub.cc | 899 +++++++++++++++++++++++++++++++ src/xenia/debug/gdb/gdbstub.h | 128 +++++ src/xenia/debug/gdb/premake5.lua | 25 + 7 files changed, 1088 insertions(+), 16 deletions(-) create mode 100644 src/xenia/debug/gdb/gdbstub.cc create mode 100644 src/xenia/debug/gdb/gdbstub.h create mode 100644 src/xenia/debug/gdb/premake5.lua diff --git a/premake5.lua b/premake5.lua index 39d25db05..b508a211b 100644 --- a/premake5.lua +++ b/premake5.lua @@ -295,6 +295,7 @@ workspace("xenia") include("src/xenia/base") include("src/xenia/cpu") include("src/xenia/cpu/backend/x64") + include("src/xenia/debug/gdb") include("src/xenia/debug/ui") include("src/xenia/gpu") include("src/xenia/gpu/null") diff --git a/src/xenia/app/premake5.lua b/src/xenia/app/premake5.lua index c6499d3a4..1b47f9ea9 100644 --- a/src/xenia/app/premake5.lua +++ b/src/xenia/app/premake5.lua @@ -92,6 +92,7 @@ project("xenia-app") "xenia-app-discord", "xenia-apu-sdl", -- TODO(Triang3l): CPU debugger on Android. + "xenia-debug-gdb", "xenia-debug-ui", "xenia-helper-sdl", "xenia-hid-sdl", diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index e74264928..4cf17fdbb 100644 --- a/src/xenia/app/xenia_main.cc +++ b/src/xenia/app/xenia_main.cc @@ -25,6 +25,7 @@ #include "xenia/base/profiling.h" #include "xenia/base/threading.h" #include "xenia/config.h" +#include "xenia/debug/gdb/gdbstub.h" #include "xenia/debug/ui/debug_window.h" #include "xenia/emulator.h" #include "xenia/kernel/xam/xam_module.h" @@ -102,6 +103,8 @@ DEFINE_transient_bool(portable, true, "General"); DECLARE_bool(debug); +DEFINE_int32(gdbport, 0, "Port for GDBStub debugger to listen on, 0 = disable", + "General"); DEFINE_bool(discord, true, "Enable Discord rich presence", "General"); @@ -230,6 +233,7 @@ class EmulatorApp final : public xe::ui::WindowedApp { // Created on demand, used by the emulator. std::unique_ptr debug_window_; + std::unique_ptr debug_gdbstub_; // Refreshing the emulator - placed after its dependencies. std::atomic emulator_thread_quit_requested_; @@ -568,20 +572,33 @@ void EmulatorApp::EmulatorThread() { // Set a debug handler. // This will respond to debugging requests so we can open the debug UI. if (cvars::debug) { - emulator_->processor()->set_debug_listener_request_handler( - [this](xe::cpu::Processor* processor) { - if (debug_window_) { - return debug_window_.get(); - } - app_context().CallInUIThreadSynchronous([this]() { - debug_window_ = xe::debug::ui::DebugWindow::Create(emulator_.get(), - app_context()); - debug_window_->window()->AddListener( - &debug_window_closed_listener_); + if (cvars::gdbport > 0) { + emulator_->processor()->set_debug_listener_request_handler( + [this](xe::cpu::Processor* processor) { + if (debug_gdbstub_) { + return debug_gdbstub_.get(); + } + debug_gdbstub_ = xe::debug::gdb::GDBStub::Create(emulator_.get(), + cvars::gdbport); + return debug_gdbstub_.get(); }); - // If failed to enqueue the UI thread call, this will just be null. - return debug_window_.get(); - }); + emulator_->processor()->ShowDebugger(); + } else { + emulator_->processor()->set_debug_listener_request_handler( + [this](xe::cpu::Processor* processor) { + if (debug_window_) { + return debug_window_.get(); + } + app_context().CallInUIThreadSynchronous([this]() { + debug_window_ = xe::debug::ui::DebugWindow::Create( + emulator_.get(), app_context()); + debug_window_->window()->AddListener( + &debug_window_closed_listener_); + }); + // If failed to enqueue the UI thread call, this will just be null. + return debug_window_.get(); + }); + } } emulator_->on_launch.AddListener([&](auto title_id, const auto& game_title) { diff --git a/src/xenia/cpu/processor.cc b/src/xenia/cpu/processor.cc index ebe5403e6..2982dfc4b 100644 --- a/src/xenia/cpu/processor.cc +++ b/src/xenia/cpu/processor.cc @@ -670,18 +670,19 @@ bool Processor::OnThreadBreakpointHit(Exception* ex) { } ResumeAllThreads(); - thread_info->thread->thread()->Suspend(); // Apply thread context changes. // TODO(benvanik): apply to all threads? #if XE_ARCH_AMD64 - ex->set_resume_pc(thread_info->host_context.rip + 2); + ex->set_resume_pc(thread_info->host_context.rip); #elif XE_ARCH_ARM64 - ex->set_resume_pc(thread_info->host_context.pc + 2); + ex->set_resume_pc(thread_info->host_context.pc); #else #error Instruction pointer not specified for the target CPU architecture. #endif // XE_ARCH + thread_info->thread->thread()->Suspend(); + // Resume execution. return true; } diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc new file mode 100644 index 000000000..3cfa49019 --- /dev/null +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -0,0 +1,899 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2022 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/debug/gdb/gdbstub.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +// Link with ws2_32.lib +#pragma comment(lib, "ws2_32.lib") + +#include "xenia/base/clock.h" +#include "xenia/base/fuzzy.h" +#include "xenia/base/logging.h" +#include "xenia/base/math.h" +#include "xenia/base/platform.h" +#include "xenia/base/string_util.h" +#include "xenia/base/threading.h" +#include "xenia/cpu/breakpoint.h" +#include "xenia/cpu/ppc/ppc_opcode_info.h" +#include "xenia/cpu/stack_walker.h" +#include "xenia/kernel/xmodule.h" +#include "xenia/kernel/xthread.h" + +namespace xe { +namespace debug { +namespace gdb { + +using xe::cpu::Breakpoint; +using xe::kernel::XModule; +using xe::kernel::XObject; +using xe::kernel::XThread; + +enum class GdbStubControl : char { + Ack = '+', + Nack = '-', + PacketStart = '$', + PacketEnd = '#', + Interrupt = '\03', +}; + +constexpr const char* kGdbReplyOK = "OK"; +constexpr const char* kGdbReplyError = "E01"; + +constexpr int kSignalSigtrap = 5; + +// must start with l for debugger to accept it +// TODO: add power-altivec.xml (and update get_reg to support it) +constexpr char target_xml[] = + R"(l + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; + +std::string u64_to_padded_hex(uint64_t value) { + return fmt::format("{:016x}", value); +} + +std::string u32_to_padded_hex(uint32_t value) { + return fmt::format("{:08x}", value); +} + +template +T hex_to(std::string_view val) { + T result; + std::from_chars(val.data(), val.data() + val.size(), result, 16); + + return result; +} + +constexpr auto& hex_to_u8 = hex_to; +constexpr auto& hex_to_u32 = hex_to; +constexpr auto& hex_to_u64 = hex_to; + +std::string to_hexbyte(uint8_t i) { + std::string result = "00"; + uint8_t i1 = i & 0xF; + uint8_t i2 = i >> 4; + result[0] = i2 > 9 ? 'a' + i2 - 10 : '0' + i2; + result[1] = i1 > 9 ? 'a' + i1 - 10 : '0' + i1; + return result; +} + +// Convert a hex char (0-9, a-f, A-F) to a byte +uint8_t from_hexchar(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } else if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return 0; +} + +std::string get_reg(xe::cpu::ThreadDebugInfo* thread, uint32_t rid) { + // Send registers as 32-bit, otherwise some debuggers will switch to 64-bit + // mode (eg. IDA will switch to 64-bit and refuse to allow decompiler to work + // with it) + // + // TODO: add altivec/VMX registers here... + // + // ids from gdb/features/rs6000/powerpc-64.c + switch (rid) { + // pc + case 64: { + // Search for first frame that has guest_pc attached, GDB doesn't care + // about host + for (auto& frame : thread->frames) { + if (frame.guest_pc != 0) { + return u32_to_padded_hex((uint32_t)frame.guest_pc); + } + } + return u32_to_padded_hex(0); + } + // msr? + case 65: + return std::string(8, 'x'); + case 66: + return u32_to_padded_hex((uint32_t)thread->guest_context.cr()); + case 67: + return u32_to_padded_hex((uint32_t)thread->guest_context.lr); + case 68: + return u32_to_padded_hex((uint32_t)thread->guest_context.ctr); + // xer + case 69: + return std::string(8, 'x'); + // fpscr + case 70: + return std::string(8, 'x'); + default: + if (rid > 70) return ""; + return (rid > 31) ? u64_to_padded_hex(*(uint64_t*)&( + thread->guest_context.f[rid - 32])) // fpr + : u32_to_padded_hex( + (uint32_t)thread->guest_context.r[rid]); // gpr + } +} + +GDBStub::GDBStub(Emulator* emulator, int listen_port) + : emulator_(emulator), + processor_(emulator->processor()), + listen_port_(listen_port), + client_socket_(0), + server_socket_(0) {} + +GDBStub::~GDBStub() { + stop_thread_ = true; + if (listener_thread_.joinable()) { + listener_thread_.join(); + } + if (server_socket_ != INVALID_SOCKET) { + closesocket(server_socket_); + } + if (client_socket_ != INVALID_SOCKET) { + closesocket(client_socket_); + } + WSACleanup(); +} + +std::unique_ptr GDBStub::Create(Emulator* emulator, int listen_port) { + std::unique_ptr debugger(new GDBStub(emulator, listen_port)); + if (!debugger->Initialize()) { + xe::FatalError("GDBStub::Create: Failed to initialize GDB stub"); + return nullptr; + } + return debugger; +} + +bool GDBStub::Initialize() { + WSADATA wsaData; + int result = WSAStartup(MAKEWORD(2, 2), &wsaData); + if (result != 0) { + XELOGE("GDBStub::Initialize: WSAStartup failed with error %d", result); + return false; + } + + listener_thread_ = std::thread(&GDBStub::Listen, this); + + UpdateCache(); + return true; +} + +bool GDBStub::CreateSocket(int port) { + server_socket_ = socket(AF_INET, SOCK_STREAM, 0); + if (server_socket_ == INVALID_SOCKET) { + XELOGE("GDBStub::CreateSocket: Socket creation failed"); + return false; + } + + sockaddr_in server_addr{}; + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(port); + server_addr.sin_addr.s_addr = INADDR_ANY; + + if (bind(server_socket_, (struct sockaddr*)&server_addr, + sizeof(server_addr)) == SOCKET_ERROR) { + XELOGE("GDBStub::CreateSocket: Socket bind failed"); + return false; + } + + if (listen(server_socket_, 1) == SOCKET_ERROR) { + XELOGE("GDBStub::CreateSocket: Socket listen failed"); + return false; + } + + return true; +} + +bool GDBStub::Accept() { + client_socket_ = accept(server_socket_, nullptr, nullptr); + if (client_socket_ == INVALID_SOCKET) { + XELOGE("GDBStub::Accept: Socket accept failed"); + return false; + } + return true; +} + +void GDBStub::Listen() { + if (!CreateSocket(listen_port_)) { + return; + } + if (!Accept()) { + return; + } + + // Client is connected - pause execution + ExecutionPause(); + UpdateCache(); + + u_long mode = 1; // 1 to enable non-blocking mode + ioctlsocket(client_socket_, FIONBIO, &mode); + + while (!stop_thread_) { + if (!ProcessIncomingData()) { + // No data available, can do other work or sleep + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + // Check if we need to notify client about anything... + { + std::unique_lock lock(mtx_); + if (cache_.notify_stopped) { + if (cache_.notify_bp_thread_id != -1) + cache_.cur_thread_id = cache_.notify_bp_thread_id; + SendPacket( + GetThreadStateReply(cache_.notify_bp_thread_id, kSignalSigtrap)); + cache_.notify_bp_thread_id = -1; + cache_.notify_stopped = false; + } + } + } +} + +void GDBStub::SendPacket(const std::string& data) { + std::stringstream ss; + ss << char(GdbStubControl::PacketStart) << data + << char(GdbStubControl::PacketEnd); + + uint8_t checksum = 0; + for (char c : data) checksum += c; + + ss << std::hex << std::setw(2) << std::setfill('0') << (checksum & 0xff); + std::string packet = ss.str(); + + send(client_socket_, packet.c_str(), int(packet.size()), 0); +} + +std::string GetPacketFriendlyName(const std::string& packetCommand) { + static const std::unordered_map command_names = { + {"?", ""}, + {"!", ""}, + {"p", ""}, + {"P", ""}, + {"g", "ReadAllRegisters"}, + {"C", "Continue"}, + {"c", "continue"}, + {"s", "step"}, + {"vAttach", "vAttach"}, + {"m", "MemRead"}, + {"H", "SetThreadId"}, + {"Z", "CreateCodeBreakpoint"}, + {"z", "DeleteCodeBreakpoint"}, + {"qXfer", "Xfer"}, + {"qSupported", "Supported"}, + {"qfThreadInfo", "qfThreadInfo"}, + {"qC", "GetThreadId"}, + {"\03", "Break"}, + }; + + std::string packet_name = ""; + auto it = command_names.find(packetCommand); + if (it != command_names.end()) { + packet_name = it->second; + } + + return packet_name; +} + +bool GDBStub::ProcessIncomingData() { + char buffer[1024]; + int received = recv(client_socket_, buffer, sizeof(buffer), 0); + + if (received > 0) { + receive_buffer_.append(buffer, received); + + // Hacky interrupt '\03' packet handling, some reason checksum isn't + // attached to this? + bool isInterrupt = + buffer[0] == char(GdbStubControl::Interrupt) && received == 1; + + size_t packet_end; + while (isInterrupt || + (packet_end = receive_buffer_.find('#')) != std::string::npos) { + if (isInterrupt || packet_end + 2 < receive_buffer_.length()) { + if (isInterrupt) { + current_packet_ = char(GdbStubControl::Interrupt); + receive_buffer_ = ""; + isInterrupt = false; + } else { + current_packet_ = receive_buffer_.substr(0, packet_end + 3); + receive_buffer_ = receive_buffer_.substr(packet_end + 3); + } + + GDBCommand command; + if (ParsePacket(command)) { +#ifdef DEBUG + auto packet_name = GetPacketFriendlyName(command.cmd); + OutputDebugStringA("GDBStub: Packet "); + if (packet_name.empty()) + OutputDebugStringA(command.cmd.c_str()); + else + OutputDebugStringA(packet_name.c_str()); + + OutputDebugStringA("("); + OutputDebugStringA(command.data.c_str()); + OutputDebugStringA(")"); + OutputDebugStringA("\n"); +#endif + + GdbStubControl result = GdbStubControl::Ack; + send(client_socket_, (const char*)&result, 1, 0); + std::string response = HandleGDBCommand(command); + SendPacket(response); + } else { + GdbStubControl result = GdbStubControl::Nack; + send(client_socket_, (const char*)&result, 1, 0); + } + } else { + break; + } + } + } + + return received > 0; +} + +bool GDBStub::ParsePacket(GDBCommand& out_cmd) { + // Index to track position in current_packet_ + size_t buffer_index = 0; + + // Read a character from the buffer and increment index + auto ReadCharFromBuffer = [&]() -> char { + if (buffer_index >= current_packet_.size()) { + return '\0'; + } + return current_packet_[buffer_index++]; + }; + + // Parse two hex digits from buffer + auto ReadHexByteFromBuffer = [&]() -> char { + if (buffer_index + 2 > current_packet_.size()) { + return 0; + } + char high = current_packet_[buffer_index++]; + char low = current_packet_[buffer_index++]; + return (from_hexchar(high) << 4) | from_hexchar(low); + }; + + // Read the first character from the buffer + char c = ReadCharFromBuffer(); + + // Expecting start of packet '$' + if (c != char(GdbStubControl::PacketStart)) { + // gdb starts conversation with + for some reason + if (c == char(GdbStubControl::Ack)) { + c = ReadCharFromBuffer(); + } + // and IDA sometimes has double +, grr + if (c == char(GdbStubControl::Ack)) { + c = ReadCharFromBuffer(); + } + // Interrupt is special, handle it without checking checksum + if (c == char(GdbStubControl::Interrupt)) { + out_cmd.cmd = char(GdbStubControl::Interrupt); + out_cmd.data = ""; + out_cmd.checksum = 0; + return true; + } + if (c != char(GdbStubControl::PacketStart)) { + return false; + } + } + + // Clear packet data + out_cmd.cmd = ""; + out_cmd.data = ""; + out_cmd.checksum = 0; + bool cmd_part = true; + uint8_t checksum = 0; + + // Parse packet content + while (true) { + c = ReadCharFromBuffer(); + + // If we reach the end of the buffer or hit '#', stop + if (c == '\0' || c == char(GdbStubControl::PacketEnd)) { + break; + } + + checksum = (checksum + static_cast(c)) % 256; + + // Handle escaped characters + if (c == '}') { + c = ReadCharFromBuffer() ^ 0x20; // Read next char and XOR with 0x20 + checksum = (checksum + static_cast(c)) % 256; + } + + // Command-data splitters: check for ':', '.', or ';' + if (cmd_part && (c == ':' || c == '.' || c == ';')) { + cmd_part = false; + } + + if (cmd_part) { + out_cmd.cmd += c; + + // Only 'q' and 'v' commands can have multi-char commands + if (out_cmd.cmd.length() == 1 && c != 'q' && c != 'v') { + cmd_part = false; + } + } else { + out_cmd.data += c; + } + } + + // Now read & compare the checksum + out_cmd.checksum = ReadHexByteFromBuffer(); + return out_cmd.checksum == checksum; +} + +void GDBStub::UpdateCache() { + auto kernel_state = emulator_->kernel_state(); + auto object_table = kernel_state->object_table(); + + std::unique_lock lock(mtx_); + + cache_.is_stopped = + processor_->execution_state() != cpu::ExecutionState::kRunning; + cache_.notify_stopped = cache_.is_stopped; + if (!cache_.is_stopped) { + // Early exit - the rest of the data is kept stale on purpose. + return; + } + + // Fetch module listing. + // We hold refs so that none are unloaded. + cache_.modules = + object_table->GetObjectsByType(XObject::Type::Module); + + cache_.thread_debug_infos = processor_->QueryThreadDebugInfos(); + cache_.cur_thread_id = cache_.thread_debug_infos[0]->thread_id; +} + +std::string GDBStub::ReadRegister(const std::string& data) { + uint32_t rid = hex_to_u32(data); + std::string result = get_reg(cache_.cur_thread_info(), rid); + if (result.empty()) { + return kGdbReplyError; // TODO: is this error correct? + } + return result; +} + +std::string GDBStub::ReadRegisters() { + std::string result; + result.reserve(68 * 16 + 3 * 8); + for (int i = 0; i < 71; ++i) { + result += get_reg(cache_.cur_thread_info(), i); + } + return result; +} + +std::string GDBStub::ExecutionPause() { +#ifdef DEBUG + OutputDebugStringA("GDBStub: ExecutionPause\n"); +#endif + processor_->Pause(); + return kGdbReplyOK; +} + +std::string GDBStub::ExecutionContinue() { +#ifdef DEBUG + OutputDebugStringA("GDBStub: ExecutionContinue\n"); +#endif + processor_->Continue(); + return kGdbReplyOK; +} + +std::string GDBStub::ExecutionStep() { +#ifdef DEBUG + OutputDebugStringA("GDBStub: ExecutionStep "); + OutputDebugStringA(std::to_string(cache_.last_bp_thread_id).c_str()); + OutputDebugStringA("\n"); +#endif + + if (cache_.last_bp_thread_id != -1) + processor_->StepGuestInstruction(cache_.last_bp_thread_id); + + return kGdbReplyOK; +} + +std::string GDBStub::ReadMemory(const std::string& data) { + auto s = data.find(','); + uint32_t addr = hex_to_u32(data.substr(0, s)); + uint32_t len = hex_to_u32(data.substr(s + 1)); + std::string result; + result.reserve(len * 2); + + // TODO: is there a better way to check if addr is valid? + auto* heap = processor_->memory()->LookupHeap(addr); + if (!heap) { + return kGdbReplyError; + } + uint32_t protect = 0; + if (!heap->QueryProtect(addr, &protect) || + (protect & kMemoryProtectRead) != kMemoryProtectRead) { + return kGdbReplyError; + } + + auto* mem = processor_->memory()->TranslateVirtual(addr); + for (uint32_t i = 0; i < len; ++i) { + result += to_hexbyte(*mem); + mem++; + } + + if (len && result.empty()) { + return kGdbReplyError; // nothing read + } + + return result; +} + +std::string GDBStub::BuildTargetXml() { return target_xml; } + +std::string GDBStub::BuildThreadList() { + std::string buffer; + buffer += "l"; + buffer += ""; + + for (int i = 0; i < cache_.thread_debug_infos.size(); i++) { + auto& thread = cache_.thread_debug_infos[i]; + buffer += fmt::format(R"*()*", + thread->thread_id, thread->thread->thread_name()); + } + + buffer += ""; + return buffer; +} + +std::string GDBStub::GetThreadStateReply(uint32_t thread_id, uint8_t signal) { + constexpr int PC_REGISTER = 64; + constexpr int LR_REGISTER = 67; + + auto* thread = cache_.thread_info(thread_id); + + if (thread_id != -1 && thread) { + uint64_t pc_value = 0; + for (auto& frame : thread->frames) { + if (frame.guest_pc != 0) { + pc_value = frame.guest_pc; + break; + } + } + + return fmt::format( + "T{:02x}{:02x}:{};{:02x}:{};thread:{:x};", signal, PC_REGISTER, + u32_to_padded_hex((uint32_t)pc_value), LR_REGISTER, + u32_to_padded_hex((uint32_t)thread->guest_context.lr), thread_id); + } + return "S05"; +} + +void GDBStub::CreateCodeBreakpoint(uint64_t address) { +#ifdef DEBUG + OutputDebugStringA("GDBStub: Adding breakpoint: "); + OutputDebugStringA(u64_to_padded_hex(address).c_str()); + OutputDebugStringA("\n"); +#endif + auto& state = cache_.breakpoints; + auto breakpoint = std::make_unique( + processor_, Breakpoint::AddressType::kGuest, address, + [this](Breakpoint* breakpoint, cpu::ThreadDebugInfo* thread_info, + uint64_t host_address) { + OnBreakpointHit(breakpoint, thread_info); + }); + + auto& map = state.code_breakpoints_by_guest_address; + auto it = map.find(breakpoint->guest_address()); + if (it != map.end()) { + // Already exists! + return; + } + map.emplace(breakpoint->guest_address(), breakpoint.get()); + + processor_->AddBreakpoint(breakpoint.get()); + state.all_breakpoints.emplace_back(std::move(breakpoint)); +} + +void GDBStub::DeleteCodeBreakpoint(uint64_t address) { +#ifdef DEBUG + OutputDebugStringA("GDBStub: Deleting breakpoint: "); + OutputDebugStringA(u64_to_padded_hex(address).c_str()); + OutputDebugStringA("\n"); +#endif + auto* breakpoint = LookupBreakpointAtAddress(address); + if (!breakpoint) { + return; + } + DeleteCodeBreakpoint(breakpoint); +} + +void GDBStub::DeleteCodeBreakpoint(Breakpoint* breakpoint) { + auto& state = cache_.breakpoints; + for (size_t i = 0; i < state.all_breakpoints.size(); ++i) { + if (state.all_breakpoints[i].get() != breakpoint) { + continue; + } + processor_->RemoveBreakpoint(breakpoint); + + auto& map = state.code_breakpoints_by_guest_address; + auto it = map.find(breakpoint->guest_address()); + if (it != map.end()) { + map.erase(it); + } + + state.all_breakpoints.erase(state.all_breakpoints.begin() + i); + break; + } +} + +Breakpoint* GDBStub::LookupBreakpointAtAddress(uint64_t address) { + auto& state = cache_.breakpoints; + auto& map = state.code_breakpoints_by_guest_address; + auto it = map.find(static_cast(address)); + return it == map.end() ? nullptr : it->second; +} + +void GDBStub::OnFocus() {} + +void GDBStub::OnDetached() { + UpdateCache(); + + // Remove all breakpoints. + while (!cache_.breakpoints.all_breakpoints.empty()) { + DeleteCodeBreakpoint(cache_.breakpoints.all_breakpoints.front().get()); + } +} + +void GDBStub::OnExecutionPaused() { +#ifdef DEBUG + OutputDebugStringA("GDBStub: OnExecutionPaused\n"); +#endif + UpdateCache(); +} + +void GDBStub::OnExecutionContinued() { +#ifdef DEBUG + OutputDebugStringA("GDBStub: OnExecutionContinued\n"); +#endif + UpdateCache(); +} + +void GDBStub::OnExecutionEnded() { +#ifdef DEBUG + OutputDebugStringA("GDBStub: OnExecutionEnded\n"); +#endif + UpdateCache(); +} + +void GDBStub::OnStepCompleted(cpu::ThreadDebugInfo* thread_info) { +#ifdef DEBUG + OutputDebugStringA("GDBStub: OnStepCompleted\n"); +#endif + // Some debuggers like IDA will remove the current breakpoint & step into next + // instruction, only re-adding BP after it's told about the step + cache_.notify_bp_thread_id = thread_info->thread_id; + cache_.last_bp_thread_id = thread_info->thread_id; + UpdateCache(); +} + +void GDBStub::OnBreakpointHit(Breakpoint* breakpoint, + cpu::ThreadDebugInfo* thread_info) { +#ifdef DEBUG + OutputDebugStringA("GDBStub: Breakpoint: "); + OutputDebugStringA(u64_to_padded_hex(breakpoint->address()).c_str()); + OutputDebugStringA(" thread "); + OutputDebugStringA(std::to_string(thread_info->thread_id).c_str()); + OutputDebugStringA("\n"); +#endif + + cache_.notify_bp_thread_id = thread_info->thread_id; + cache_.last_bp_thread_id = thread_info->thread_id; + UpdateCache(); +} + +std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { + static const std::unordered_map> + command_map = { + {"?", + [&](const GDBCommand& cmd) { + return "S05"; // tell debugger we're currently stopped + }}, + {"!", [&](const GDBCommand& cmd) { return kGdbReplyOK; }}, + {"p", [&](const GDBCommand& cmd) { return ReadRegister(cmd.data); }}, + {"P", [&](const GDBCommand& cmd) { return kGdbReplyOK; }}, + {"g", [&](const GDBCommand& cmd) { return ReadRegisters(); }}, + {"C", [&](const GDBCommand& cmd) { return ExecutionContinue(); }}, + {"c", [&](const GDBCommand& cmd) { return ExecutionContinue(); }}, + {"s", [&](const GDBCommand& cmd) { return ExecutionStep(); }}, + {"vAttach", + [&](const GDBCommand& cmd) { + ExecutionPause(); + return "S05"; + }}, + {"m", [&](const GDBCommand& cmd) { return ReadMemory(cmd.data); }}, + {"H", + [&](const GDBCommand& cmd) { + // Set current debugger thread ID + int threadId = std::stol(cmd.data.substr(1), 0, 16); + cache_.cur_thread_id = cache_.thread_debug_infos[0]->thread_id; + for (auto& thread : cache_.thread_debug_infos) { + if (thread->thread_id == threadId) { + cache_.cur_thread_id = threadId; + break; + } + } + return kGdbReplyOK; + }}, + {"Z", + [&](const GDBCommand& cmd) { + auto& hex_addr = cmd.data.substr(2); + uint64_t addr = std::stoull(hex_addr.substr(0, hex_addr.find(',')), + nullptr, 16); + CreateCodeBreakpoint(addr); + return kGdbReplyOK; + }}, + {"z", + [&](const GDBCommand& cmd) { + auto& hex_addr = cmd.data.substr(2); + uint64_t addr = std::stoull(hex_addr.substr(0, hex_addr.find(',')), + nullptr, 16); + DeleteCodeBreakpoint(addr); + return kGdbReplyOK; + }}, + {"qXfer", + [&](const GDBCommand& cmd) { + auto param = cmd.data; + if (param.length() > 0 && param[0] == ':') { + param = param.substr(1); + } + auto sub_cmd = param.substr(0, param.find(':')); + if (sub_cmd == "features") { + return BuildTargetXml(); + } else if (sub_cmd == "threads") { + return BuildThreadList(); + } + return std::string(kGdbReplyError); + }}, + {"qSupported", + [&](const GDBCommand& cmd) { + return "PacketSize=1024;qXfer:features:read+;qXfer:threads:read+"; + }}, + {"qfThreadInfo", + [&](const GDBCommand& cmd) { + std::string result; + for (auto& thread : cache_.thread_debug_infos) { + if (!result.empty()) result += ","; + result += std::to_string(thread->thread_id); + } + return "m" + result; + }}, + {"qC", + [&](const GDBCommand& cmd) { + return "QC" + std::to_string(cache_.cur_thread_info()->thread_id); + }}, + {"\03", [&](const GDBCommand& cmd) { return ExecutionPause(); }}, + }; + + auto it = command_map.find(command.cmd); + if (it != command_map.end()) { + return it->second(command); + } + + return ""; +} + +} // namespace gdb +} // namespace debug +} // namespace xe diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h new file mode 100644 index 000000000..0763899f2 --- /dev/null +++ b/src/xenia/debug/gdb/gdbstub.h @@ -0,0 +1,128 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2022 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_DEBUG_GDB_GDBSTUB_H_ +#define XENIA_DEBUG_GDB_GDBSTUB_H_ + +#include +#include + +#include "xenia/base/host_thread_context.h" +#include "xenia/cpu/breakpoint.h" +#include "xenia/cpu/debug_listener.h" +#include "xenia/cpu/processor.h" +#include "xenia/emulator.h" +#include "xenia/xbox.h" + +namespace xe { +namespace debug { +namespace gdb { + +class GDBStub : public cpu::DebugListener { + public: + virtual ~GDBStub(); + + static std::unique_ptr Create(Emulator* emulator, int listen_port); + + Emulator* emulator() const { return emulator_; } + + void OnFocus() override; + void OnDetached() override; + void OnExecutionPaused() override; + void OnExecutionContinued() override; + void OnExecutionEnded() override; + void OnStepCompleted(cpu::ThreadDebugInfo* thread_info) override; + void OnBreakpointHit(cpu::Breakpoint* breakpoint, + cpu::ThreadDebugInfo* thread_info) override; + + private: + struct GDBCommand { + std::string cmd{}; + std::string data{}; + uint8_t checksum{}; + }; + + explicit GDBStub(Emulator* emulator, int listen_port); + bool Initialize(); + + bool CreateSocket(int port); + bool Accept(); + void Listen(); + void SendPacket(const std::string& data); + bool ProcessIncomingData(); + bool ParsePacket(GDBCommand& out_cmd); + std::string HandleGDBCommand(const GDBCommand& command); + + void UpdateCache(); + + std::string ReadRegister(const std::string& data); + std::string ReadRegisters(); + std::string ExecutionPause(); + std::string ExecutionContinue(); + std::string ExecutionStep(); + std::string ReadMemory(const std::string& data); + std::string BuildTargetXml(); + std::string BuildThreadList(); + + std::string GetThreadStateReply(uint32_t thread_id, uint8_t signal); + + void CreateCodeBreakpoint(uint64_t address); + void DeleteCodeBreakpoint(uint64_t address); + void DeleteCodeBreakpoint(cpu::Breakpoint* breakpoint); + cpu::Breakpoint* LookupBreakpointAtAddress(uint64_t address); + + Emulator* emulator_ = nullptr; + cpu::Processor* processor_ = nullptr; + + int listen_port_; + std::thread listener_thread_; + uint64_t server_socket_, client_socket_; + std::mutex mtx_; + std::condition_variable cv_; + bool stop_thread_ = false; + std::string receive_buffer_; + std::string current_packet_; + + struct EmulatorStateCache { + uint32_t cur_thread_id = -1; + uint32_t last_bp_thread_id = -1; + + uint32_t notify_bp_thread_id = -1; + bool notify_stopped = false; + + bool is_stopped = false; + std::vector> modules; + std::vector thread_debug_infos; + + struct { + char kernel_call_filter[64] = {0}; + std::vector> all_breakpoints; + std::unordered_map + code_breakpoints_by_guest_address; + } breakpoints; + + cpu::ThreadDebugInfo* thread_info(int threadId) { + for (auto& thread : thread_debug_infos) { + if (thread->thread_id == threadId) { + return thread; + } + } + return nullptr; + } + cpu::ThreadDebugInfo* cur_thread_info() { + return thread_info(cur_thread_id); + } + } cache_; +}; + +} // namespace gdb +} // namespace debug +} // namespace xe + +#endif // XENIA_DEBUG_UI_DEBUG_WINDOW_H_ diff --git a/src/xenia/debug/gdb/premake5.lua b/src/xenia/debug/gdb/premake5.lua new file mode 100644 index 000000000..9e6db656c --- /dev/null +++ b/src/xenia/debug/gdb/premake5.lua @@ -0,0 +1,25 @@ +project_root = "../../../.." +include(project_root.."/tools/build") + +group("src") +project("xenia-debug-gdb") + uuid("9193a274-f4c2-4746-bd85-93fcfc5c3e39") + kind("StaticLib") + language("C++") + links({ + "imgui", + "xenia-base", + "xenia-cpu", + "xenia-ui", + }) + filter({"configurations:Release", "platforms:Windows"}) + buildoptions({ + "/Os", + "/O1" + }) + filter{} + defines({ + }) + includedirs({ + }) + local_platform_files() From 2204259781d25816a8f172633379c3fcba464fe0 Mon Sep 17 00:00:00 2001 From: emoose Date: Sun, 6 Oct 2024 04:06:11 +0100 Subject: [PATCH 02/16] [Debug] GDBStub: add info for each command --- src/xenia/debug/gdb/gdbstub.cc | 119 +++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 49 deletions(-) diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index 3cfa49019..3dba51f5b 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -22,6 +22,7 @@ #pragma comment(lib, "ws2_32.lib") #include "xenia/base/clock.h" +#include "xenia/base/debugging.h" #include "xenia/base/fuzzy.h" #include "xenia/base/logging.h" #include "xenia/base/math.h" @@ -356,12 +357,13 @@ void GDBStub::SendPacket(const std::string& data) { send(client_socket_, packet.c_str(), int(packet.size()), 0); } +#ifdef DEBUG std::string GetPacketFriendlyName(const std::string& packetCommand) { static const std::unordered_map command_names = { - {"?", ""}, - {"!", ""}, - {"p", ""}, - {"P", ""}, + {"?", "StartupQuery"}, + {"!", "EnableExtendedMode"}, + {"p", "ReadRegister"}, + {"P", "WriteRegister"}, {"g", "ReadAllRegisters"}, {"C", "Continue"}, {"c", "continue"}, @@ -386,6 +388,7 @@ std::string GetPacketFriendlyName(const std::string& packetCommand) { return packet_name; } +#endif bool GDBStub::ProcessIncomingData() { char buffer[1024]; @@ -416,16 +419,10 @@ bool GDBStub::ProcessIncomingData() { if (ParsePacket(command)) { #ifdef DEBUG auto packet_name = GetPacketFriendlyName(command.cmd); - OutputDebugStringA("GDBStub: Packet "); - if (packet_name.empty()) - OutputDebugStringA(command.cmd.c_str()); - else - OutputDebugStringA(packet_name.c_str()); - OutputDebugStringA("("); - OutputDebugStringA(command.data.c_str()); - OutputDebugStringA(")"); - OutputDebugStringA("\n"); + debugging::DebugPrint("GDBStub: Packet {}({})\n", + packet_name.empty() ? command.cmd : packet_name, + command.data); #endif GdbStubControl result = GdbStubControl::Ack; @@ -581,7 +578,7 @@ std::string GDBStub::ReadRegisters() { std::string GDBStub::ExecutionPause() { #ifdef DEBUG - OutputDebugStringA("GDBStub: ExecutionPause\n"); + debugging::DebugPrint("GDBStub: ExecutionPause\n"); #endif processor_->Pause(); return kGdbReplyOK; @@ -589,7 +586,7 @@ std::string GDBStub::ExecutionPause() { std::string GDBStub::ExecutionContinue() { #ifdef DEBUG - OutputDebugStringA("GDBStub: ExecutionContinue\n"); + debugging::DebugPrint("GDBStub: ExecutionContinue\n"); #endif processor_->Continue(); return kGdbReplyOK; @@ -597,9 +594,7 @@ std::string GDBStub::ExecutionContinue() { std::string GDBStub::ExecutionStep() { #ifdef DEBUG - OutputDebugStringA("GDBStub: ExecutionStep "); - OutputDebugStringA(std::to_string(cache_.last_bp_thread_id).c_str()); - OutputDebugStringA("\n"); + debugging::DebugPrint("GDBStub: ExecutionStep (thread {})\n", cache_.last_bp_thread_id); #endif if (cache_.last_bp_thread_id != -1) @@ -681,10 +676,9 @@ std::string GDBStub::GetThreadStateReply(uint32_t thread_id, uint8_t signal) { void GDBStub::CreateCodeBreakpoint(uint64_t address) { #ifdef DEBUG - OutputDebugStringA("GDBStub: Adding breakpoint: "); - OutputDebugStringA(u64_to_padded_hex(address).c_str()); - OutputDebugStringA("\n"); + debugging::DebugPrint("GDBStub: Adding breakpoint: {:X}\n", address); #endif + auto& state = cache_.breakpoints; auto breakpoint = std::make_unique( processor_, Breakpoint::AddressType::kGuest, address, @@ -707,9 +701,7 @@ void GDBStub::CreateCodeBreakpoint(uint64_t address) { void GDBStub::DeleteCodeBreakpoint(uint64_t address) { #ifdef DEBUG - OutputDebugStringA("GDBStub: Deleting breakpoint: "); - OutputDebugStringA(u64_to_padded_hex(address).c_str()); - OutputDebugStringA("\n"); + debugging::DebugPrint("GDBStub: Deleting breakpoint: {:X}\n", address); #endif auto* breakpoint = LookupBreakpointAtAddress(address); if (!breakpoint) { @@ -757,28 +749,28 @@ void GDBStub::OnDetached() { void GDBStub::OnExecutionPaused() { #ifdef DEBUG - OutputDebugStringA("GDBStub: OnExecutionPaused\n"); + debugging::DebugPrint("GDBStub: OnExecutionPaused\n"); #endif UpdateCache(); } void GDBStub::OnExecutionContinued() { #ifdef DEBUG - OutputDebugStringA("GDBStub: OnExecutionContinued\n"); + debugging::DebugPrint("GDBStub: OnExecutionContinued\n"); #endif UpdateCache(); } void GDBStub::OnExecutionEnded() { #ifdef DEBUG - OutputDebugStringA("GDBStub: OnExecutionEnded\n"); + debugging::DebugPrint("GDBStub: OnExecutionEnded\n"); #endif UpdateCache(); } void GDBStub::OnStepCompleted(cpu::ThreadDebugInfo* thread_info) { #ifdef DEBUG - OutputDebugStringA("GDBStub: OnStepCompleted\n"); + debugging::DebugPrint("GDBStub: OnStepCompleted\n"); #endif // Some debuggers like IDA will remove the current breakpoint & step into next // instruction, only re-adding BP after it's told about the step @@ -790,11 +782,8 @@ void GDBStub::OnStepCompleted(cpu::ThreadDebugInfo* thread_info) { void GDBStub::OnBreakpointHit(Breakpoint* breakpoint, cpu::ThreadDebugInfo* thread_info) { #ifdef DEBUG - OutputDebugStringA("GDBStub: Breakpoint: "); - OutputDebugStringA(u64_to_padded_hex(breakpoint->address()).c_str()); - OutputDebugStringA(" thread "); - OutputDebugStringA(std::to_string(thread_info->thread_id).c_str()); - OutputDebugStringA("\n"); + debugging::DebugPrint("GDBStub: Breakpoint hit at {:X} (thread {})\n", + breakpoint->address(), thread_info->thread_id); #endif cache_.notify_bp_thread_id = thread_info->thread_id; @@ -806,36 +795,68 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { static const std::unordered_map> command_map = { + // "sent when connection is first established to query the reason the + // target halted" {"?", [&](const GDBCommand& cmd) { - return "S05"; // tell debugger we're currently stopped + return "S05"; // tell debugger we're currently paused }}, + + // Enable extended mode {"!", [&](const GDBCommand& cmd) { return kGdbReplyOK; }}, - {"p", [&](const GDBCommand& cmd) { return ReadRegister(cmd.data); }}, - {"P", [&](const GDBCommand& cmd) { return kGdbReplyOK; }}, - {"g", [&](const GDBCommand& cmd) { return ReadRegisters(); }}, + + // Execution continue {"C", [&](const GDBCommand& cmd) { return ExecutionContinue(); }}, + // Execution continue {"c", [&](const GDBCommand& cmd) { return ExecutionContinue(); }}, + // Execution step {"s", [&](const GDBCommand& cmd) { return ExecutionStep(); }}, - {"vAttach", - [&](const GDBCommand& cmd) { - ExecutionPause(); - return "S05"; - }}, + // Execution interrupt + {"\03", [&](const GDBCommand& cmd) { return ExecutionPause(); }}, + + // Read memory {"m", [&](const GDBCommand& cmd) { return ReadMemory(cmd.data); }}, + // Read register + {"p", [&](const GDBCommand& cmd) { return ReadRegister(cmd.data); }}, + // Write register + {"P", + [&](const GDBCommand& cmd) { + return kGdbReplyOK; // TODO: we'll just tell it write was fine + }}, + // Read all registers + {"g", [&](const GDBCommand& cmd) { return ReadRegisters(); }}, + + // Attach to specific process ID - IDA used to send this, but doesn't + // after some changes? + {"vAttach", [&](const GDBCommand& cmd) { return "S05"; }}, + + // Get current debugger thread ID + {"qC", + [&](const GDBCommand& cmd) { + return "QC" + std::to_string(cache_.cur_thread_info()->thread_id); + }}, + // Set current debugger thread ID {"H", [&](const GDBCommand& cmd) { - // Set current debugger thread ID + // Reset to known good ID + cache_.cur_thread_id = + cache_.thread_debug_infos.size() + ? cache_.thread_debug_infos[0]->thread_id + : -1; + + // Check if the desired thread ID exists int threadId = std::stol(cmd.data.substr(1), 0, 16); - cache_.cur_thread_id = cache_.thread_debug_infos[0]->thread_id; for (auto& thread : cache_.thread_debug_infos) { if (thread->thread_id == threadId) { cache_.cur_thread_id = threadId; break; } } + return kGdbReplyOK; }}, + + // Create breakpoint {"Z", [&](const GDBCommand& cmd) { auto& hex_addr = cmd.data.substr(2); @@ -844,6 +865,7 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { CreateCodeBreakpoint(addr); return kGdbReplyOK; }}, + // Delete breakpoint {"z", [&](const GDBCommand& cmd) { auto& hex_addr = cmd.data.substr(2); @@ -852,6 +874,8 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { DeleteCodeBreakpoint(addr); return kGdbReplyOK; }}, + + // Data transfer {"qXfer", [&](const GDBCommand& cmd) { auto param = cmd.data; @@ -866,10 +890,12 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { } return std::string(kGdbReplyError); }}, + // Supported features (TODO: memory map) {"qSupported", [&](const GDBCommand& cmd) { return "PacketSize=1024;qXfer:features:read+;qXfer:threads:read+"; }}, + // Thread list (IDA requests this but ignores in favor of qXfer?) {"qfThreadInfo", [&](const GDBCommand& cmd) { std::string result; @@ -879,11 +905,6 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { } return "m" + result; }}, - {"qC", - [&](const GDBCommand& cmd) { - return "QC" + std::to_string(cache_.cur_thread_info()->thread_id); - }}, - {"\03", [&](const GDBCommand& cmd) { return ExecutionPause(); }}, }; auto it = command_map.find(command.cmd); From bfa6eee936440ba82245239112fa569b24c334ab Mon Sep 17 00:00:00 2001 From: emoose Date: Sun, 6 Oct 2024 04:24:24 +0100 Subject: [PATCH 03/16] [Debug] Processor: breakpoint address fixes - Update OnThreadBreakpointHit to use bp->ContainsHostAddress Sometimes guest PC doesn't map to the x64 exactly, so ResolveStack may return a guest PC different to the breakpoint that triggered it Processor code would just skip the breakpoint entirely if PC didn't match so any debugger wouldn't know what BP caused it Luckily BP does have a ContainsHostAddress func which seems perfect, but was somehow left unused Also added a hack to nudge the PC we tell GDB about to the BP's PC - Update StepGuest/HostInstruction to add step BP to front of map Fixes an issue where a different BP might get triggered before the step BP which could cause debugger to step again, causing assert error as a step BP already existed I noticed one more issue, if BPs are set on instructions very close together it might act on the same x64 instruction, causing assert when a BP is already placed and another BP tries to set on the same addr... Not really sure what best way to fix that is yet, for now just don't place BPs too close together! --- src/xenia/cpu/processor.cc | 13 ++++++++++--- src/xenia/debug/gdb/gdbstub.cc | 27 +++++++++++++++++++++++---- src/xenia/debug/gdb/gdbstub.h | 2 ++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/xenia/cpu/processor.cc b/src/xenia/cpu/processor.cc index 2982dfc4b..ee47e835b 100644 --- a/src/xenia/cpu/processor.cc +++ b/src/xenia/cpu/processor.cc @@ -644,7 +644,8 @@ bool Processor::OnThreadBreakpointHit(Exception* ex) { if ((scan_breakpoint->address_type() == Breakpoint::AddressType::kGuest && scan_breakpoint->guest_address() == frame.guest_pc) || (scan_breakpoint->address_type() == Breakpoint::AddressType::kHost && - scan_breakpoint->host_address() == frame.host_pc)) { + scan_breakpoint->host_address() == frame.host_pc) || + scan_breakpoint->ContainsHostAddress(frame.host_pc)) { breakpoint = scan_breakpoint; break; } @@ -937,7 +938,10 @@ void Processor::StepHostInstruction(uint32_t thread_id) { thread_info->step_breakpoint.reset(); OnStepCompleted(thread_info); })); - AddBreakpoint(thread_info->step_breakpoint.get()); + + // Add to front of breakpoints map, so this should get evaluated first + breakpoints_.insert(breakpoints_.begin(), thread_info->step_breakpoint.get()); + thread_info->step_breakpoint->Resume(); // ResumeAllBreakpoints(); @@ -970,7 +974,10 @@ void Processor::StepGuestInstruction(uint32_t thread_id) { thread_info->step_breakpoint.reset(); OnStepCompleted(thread_info); })); - AddBreakpoint(thread_info->step_breakpoint.get()); + + // Add to front of breakpoints map, so this should get evaluated first + breakpoints_.insert(breakpoints_.begin(), thread_info->step_breakpoint.get()); + thread_info->step_breakpoint->Resume(); // ResumeAllBreakpoints(); diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index 3dba51f5b..a0c8af71e 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -184,7 +184,8 @@ uint8_t from_hexchar(char c) { return 0; } -std::string get_reg(xe::cpu::ThreadDebugInfo* thread, uint32_t rid) { +std::string GDBStub::ReadRegister(xe::cpu::ThreadDebugInfo* thread, + uint32_t rid) { // Send registers as 32-bit, otherwise some debuggers will switch to 64-bit // mode (eg. IDA will switch to 64-bit and refuse to allow decompiler to work // with it) @@ -195,6 +196,16 @@ std::string get_reg(xe::cpu::ThreadDebugInfo* thread, uint32_t rid) { switch (rid) { // pc case 64: { + // If we recently hit a BP then debugger is likely asking for registers + // for it + // + // Lie about the PC and say it's the BP address, since PC might not always + // match + if (cache_.notify_bp_guest_address != -1) { + auto ret = u32_to_padded_hex((uint32_t)cache_.notify_bp_guest_address); + cache_.notify_bp_guest_address = -1; + return ret; + } // Search for first frame that has guest_pc attached, GDB doesn't care // about host for (auto& frame : thread->frames) { @@ -560,7 +571,7 @@ void GDBStub::UpdateCache() { std::string GDBStub::ReadRegister(const std::string& data) { uint32_t rid = hex_to_u32(data); - std::string result = get_reg(cache_.cur_thread_info(), rid); + std::string result = ReadRegister(cache_.cur_thread_info(), rid); if (result.empty()) { return kGdbReplyError; // TODO: is this error correct? } @@ -571,7 +582,7 @@ std::string GDBStub::ReadRegisters() { std::string result; result.reserve(68 * 16 + 3 * 8); for (int i = 0; i < 71; ++i) { - result += get_reg(cache_.cur_thread_info(), i); + result += ReadRegister(cache_.cur_thread_info(), i); } return result; } @@ -594,7 +605,8 @@ std::string GDBStub::ExecutionContinue() { std::string GDBStub::ExecutionStep() { #ifdef DEBUG - debugging::DebugPrint("GDBStub: ExecutionStep (thread {})\n", cache_.last_bp_thread_id); + debugging::DebugPrint("GDBStub: ExecutionStep (thread {})\n", + cache_.last_bp_thread_id); #endif if (cache_.last_bp_thread_id != -1) @@ -666,6 +678,12 @@ std::string GDBStub::GetThreadStateReply(uint32_t thread_id, uint8_t signal) { } } + // If BP was hit use the address of it, so debugger can match it up to its + // BP list + if (cache_.notify_bp_guest_address != -1) { + pc_value = cache_.notify_bp_guest_address; + } + return fmt::format( "T{:02x}{:02x}:{};{:02x}:{};thread:{:x};", signal, PC_REGISTER, u32_to_padded_hex((uint32_t)pc_value), LR_REGISTER, @@ -786,6 +804,7 @@ void GDBStub::OnBreakpointHit(Breakpoint* breakpoint, breakpoint->address(), thread_info->thread_id); #endif + cache_.notify_bp_guest_address = breakpoint->address(); cache_.notify_bp_thread_id = thread_info->thread_id; cache_.last_bp_thread_id = thread_info->thread_id; UpdateCache(); diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h index 0763899f2..dc19e83b6 100644 --- a/src/xenia/debug/gdb/gdbstub.h +++ b/src/xenia/debug/gdb/gdbstub.h @@ -61,6 +61,7 @@ class GDBStub : public cpu::DebugListener { void UpdateCache(); + std::string ReadRegister(xe::cpu::ThreadDebugInfo* thread, uint32_t rid); std::string ReadRegister(const std::string& data); std::string ReadRegisters(); std::string ExecutionPause(); @@ -93,6 +94,7 @@ class GDBStub : public cpu::DebugListener { uint32_t cur_thread_id = -1; uint32_t last_bp_thread_id = -1; + uint64_t notify_bp_guest_address = -1; uint32_t notify_bp_thread_id = -1; bool notify_stopped = false; From b7ab7e966f7d45823d7c14fb4ea0b23249c2d1d1 Mon Sep 17 00:00:00 2001 From: emoose Date: Sun, 6 Oct 2024 04:47:59 +0100 Subject: [PATCH 04/16] [GDBStub] Prevent adding BP if other BP shares same host addr Fixes assert error with BPs set very close to each other (in IDA the failed BP will also show different color) --- src/xenia/debug/gdb/gdbstub.cc | 28 +++++++++++++++++++++------- src/xenia/debug/gdb/gdbstub.h | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index a0c8af71e..310a30439 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -692,7 +692,7 @@ std::string GDBStub::GetThreadStateReply(uint32_t thread_id, uint8_t signal) { return "S05"; } -void GDBStub::CreateCodeBreakpoint(uint64_t address) { +bool GDBStub::CreateCodeBreakpoint(uint64_t address) { #ifdef DEBUG debugging::DebugPrint("GDBStub: Adding breakpoint: {:X}\n", address); #endif @@ -705,16 +705,30 @@ void GDBStub::CreateCodeBreakpoint(uint64_t address) { OnBreakpointHit(breakpoint, thread_info); }); + // Fetch list of host addrs used by the new BP + std::vector host_addresses; + breakpoint->ForEachHostAddress([&host_addresses](uintptr_t host_address) { + host_addresses.push_back(host_address); + }); + auto& map = state.code_breakpoints_by_guest_address; - auto it = map.find(breakpoint->guest_address()); - if (it != map.end()) { - // Already exists! - return; + for (auto& kvp : map) { + if (kvp.first == breakpoint->guest_address()) { + return false; // Already exists! + } + for (auto& host_address : host_addresses) { + if (kvp.second->ContainsHostAddress(host_address)) { + return false; // Host addr is in use by another BP already + } + } } + map.emplace(breakpoint->guest_address(), breakpoint.get()); processor_->AddBreakpoint(breakpoint.get()); state.all_breakpoints.emplace_back(std::move(breakpoint)); + + return true; } void GDBStub::DeleteCodeBreakpoint(uint64_t address) { @@ -881,8 +895,8 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { auto& hex_addr = cmd.data.substr(2); uint64_t addr = std::stoull(hex_addr.substr(0, hex_addr.find(',')), nullptr, 16); - CreateCodeBreakpoint(addr); - return kGdbReplyOK; + + return CreateCodeBreakpoint(addr) ? kGdbReplyOK : kGdbReplyError; }}, // Delete breakpoint {"z", diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h index dc19e83b6..92394c613 100644 --- a/src/xenia/debug/gdb/gdbstub.h +++ b/src/xenia/debug/gdb/gdbstub.h @@ -73,7 +73,7 @@ class GDBStub : public cpu::DebugListener { std::string GetThreadStateReply(uint32_t thread_id, uint8_t signal); - void CreateCodeBreakpoint(uint64_t address); + bool CreateCodeBreakpoint(uint64_t address); void DeleteCodeBreakpoint(uint64_t address); void DeleteCodeBreakpoint(cpu::Breakpoint* breakpoint); cpu::Breakpoint* LookupBreakpointAtAddress(uint64_t address); From 0469ae2aeee3c2d2a862bc1d6cb019443a1bbb69 Mon Sep 17 00:00:00 2001 From: emoose Date: Sun, 6 Oct 2024 05:56:29 +0100 Subject: [PATCH 05/16] [GDBStub] Use xe::SocketServer instead of winsock, allow detaching Detach will clear breakpoints and resume execution Reconnecting should now work fine, could probably have multiple connections too (may be some threading issues with that though...) --- src/xenia/base/socket.h | 3 + src/xenia/base/socket_win.cc | 9 ++ src/xenia/debug/gdb/gdbstub.cc | 215 ++++++++++++++------------------- src/xenia/debug/gdb/gdbstub.h | 19 ++- 4 files changed, 113 insertions(+), 133 deletions(-) diff --git a/src/xenia/base/socket.h b/src/xenia/base/socket.h index 0c300ef24..40c120623 100644 --- a/src/xenia/base/socket.h +++ b/src/xenia/base/socket.h @@ -46,6 +46,9 @@ class Socket { // Returns true if the client is connected and can send/receive data. virtual bool is_connected() = 0; + // Sets socket non-blocking mode + virtual void set_nonblocking(bool nonblocking) = 0; + // Closes the socket. // This will signal the wait handle. virtual void Close() = 0; diff --git a/src/xenia/base/socket_win.cc b/src/xenia/base/socket_win.cc index 730b2b8ab..f3c560cbd 100644 --- a/src/xenia/base/socket_win.cc +++ b/src/xenia/base/socket_win.cc @@ -113,6 +113,15 @@ class Win32Socket : public Socket { return socket_ != INVALID_SOCKET; } + void set_nonblocking(bool nonblocking) override { + std::lock_guard lock(mutex_); + if (socket_ == INVALID_SOCKET) { + return; + } + u_long val = nonblocking ? 1 : 0; + ioctlsocket(socket_, FIONBIO, &val); + } + void Close() override { std::lock_guard lock(mutex_); diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index 310a30439..7ee83fea4 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -9,8 +9,6 @@ #include "xenia/debug/gdb/gdbstub.h" -#include -#include #include #include #include @@ -18,9 +16,6 @@ #include #include -// Link with ws2_32.lib -#pragma comment(lib, "ws2_32.lib") - #include "xenia/base/clock.h" #include "xenia/base/debugging.h" #include "xenia/base/fuzzy.h" @@ -58,7 +53,7 @@ constexpr const char* kGdbReplyError = "E01"; constexpr int kSignalSigtrap = 5; // must start with l for debugger to accept it -// TODO: add power-altivec.xml (and update get_reg to support it) +// TODO: add power-altivec.xml (and update ReadRegister to support it) constexpr char target_xml[] = R"(l @@ -184,6 +179,24 @@ uint8_t from_hexchar(char c) { return 0; } +std::string GDBStub::DebuggerDetached() { + // Delete all breakpoints + auto& state = cache_.breakpoints; + + for (auto& breakpoint : state.all_breakpoints) { + processor_->RemoveBreakpoint(breakpoint.get()); + } + + state.code_breakpoints_by_guest_address.clear(); + state.all_breakpoints.clear(); + + if (processor_->execution_state() == cpu::ExecutionState::kPaused) { + ExecutionContinue(); + } + + return kGdbReplyOK; +} + std::string GDBStub::ReadRegister(xe::cpu::ThreadDebugInfo* thread, uint32_t rid) { // Send registers as 32-bit, otherwise some debuggers will switch to 64-bit @@ -242,23 +255,9 @@ std::string GDBStub::ReadRegister(xe::cpu::ThreadDebugInfo* thread, GDBStub::GDBStub(Emulator* emulator, int listen_port) : emulator_(emulator), processor_(emulator->processor()), - listen_port_(listen_port), - client_socket_(0), - server_socket_(0) {} + listen_port_(listen_port) {} -GDBStub::~GDBStub() { - stop_thread_ = true; - if (listener_thread_.joinable()) { - listener_thread_.join(); - } - if (server_socket_ != INVALID_SOCKET) { - closesocket(server_socket_); - } - if (client_socket_ != INVALID_SOCKET) { - closesocket(client_socket_); - } - WSACleanup(); -} +GDBStub::~GDBStub() { stop_thread_ = true; } std::unique_ptr GDBStub::Create(Emulator* emulator, int listen_port) { std::unique_ptr debugger(new GDBStub(emulator, listen_port)); @@ -270,71 +269,31 @@ std::unique_ptr GDBStub::Create(Emulator* emulator, int listen_port) { } bool GDBStub::Initialize() { - WSADATA wsaData; - int result = WSAStartup(MAKEWORD(2, 2), &wsaData); - if (result != 0) { - XELOGE("GDBStub::Initialize: WSAStartup failed with error %d", result); - return false; - } - - listener_thread_ = std::thread(&GDBStub::Listen, this); + socket_ = xe::SocketServer::Create( + listen_port_, [this](std::unique_ptr client) { Listen(client); }); UpdateCache(); return true; } -bool GDBStub::CreateSocket(int port) { - server_socket_ = socket(AF_INET, SOCK_STREAM, 0); - if (server_socket_ == INVALID_SOCKET) { - XELOGE("GDBStub::CreateSocket: Socket creation failed"); - return false; - } - - sockaddr_in server_addr{}; - server_addr.sin_family = AF_INET; - server_addr.sin_port = htons(port); - server_addr.sin_addr.s_addr = INADDR_ANY; - - if (bind(server_socket_, (struct sockaddr*)&server_addr, - sizeof(server_addr)) == SOCKET_ERROR) { - XELOGE("GDBStub::CreateSocket: Socket bind failed"); - return false; - } - - if (listen(server_socket_, 1) == SOCKET_ERROR) { - XELOGE("GDBStub::CreateSocket: Socket listen failed"); - return false; - } - - return true; -} - -bool GDBStub::Accept() { - client_socket_ = accept(server_socket_, nullptr, nullptr); - if (client_socket_ == INVALID_SOCKET) { - XELOGE("GDBStub::Accept: Socket accept failed"); - return false; - } - return true; -} - -void GDBStub::Listen() { - if (!CreateSocket(listen_port_)) { - return; - } - if (!Accept()) { - return; - } - +void GDBStub::Listen(std::unique_ptr& client) { // Client is connected - pause execution ExecutionPause(); UpdateCache(); - u_long mode = 1; // 1 to enable non-blocking mode - ioctlsocket(client_socket_, FIONBIO, &mode); + client->set_nonblocking(true); + + std::string receive_buffer; while (!stop_thread_) { - if (!ProcessIncomingData()) { + if (!client->is_connected()) { + break; + } + + if (!ProcessIncomingData(client, receive_buffer)) { + if (!client->is_connected()) { + break; + } // No data available, can do other work or sleep std::this_thread::sleep_for(std::chrono::milliseconds(10)); } @@ -345,8 +304,8 @@ void GDBStub::Listen() { if (cache_.notify_stopped) { if (cache_.notify_bp_thread_id != -1) cache_.cur_thread_id = cache_.notify_bp_thread_id; - SendPacket( - GetThreadStateReply(cache_.notify_bp_thread_id, kSignalSigtrap)); + SendPacket(client, GetThreadStateReply(cache_.notify_bp_thread_id, + kSignalSigtrap)); cache_.notify_bp_thread_id = -1; cache_.notify_stopped = false; } @@ -354,7 +313,8 @@ void GDBStub::Listen() { } } -void GDBStub::SendPacket(const std::string& data) { +void GDBStub::SendPacket(std::unique_ptr& client, + const std::string& data) { std::stringstream ss; ss << char(GdbStubControl::PacketStart) << data << char(GdbStubControl::PacketEnd); @@ -365,7 +325,7 @@ void GDBStub::SendPacket(const std::string& data) { ss << std::hex << std::setw(2) << std::setfill('0') << (checksum & 0xff); std::string packet = ss.str(); - send(client_socket_, packet.c_str(), int(packet.size()), 0); + client->Send(packet.c_str(), packet.size()); } #ifdef DEBUG @@ -388,6 +348,7 @@ std::string GetPacketFriendlyName(const std::string& packetCommand) { {"qSupported", "Supported"}, {"qfThreadInfo", "qfThreadInfo"}, {"qC", "GetThreadId"}, + {"D", "Detach"}, {"\03", "Break"}, }; @@ -401,77 +362,82 @@ std::string GetPacketFriendlyName(const std::string& packetCommand) { } #endif -bool GDBStub::ProcessIncomingData() { +bool GDBStub::ProcessIncomingData(std::unique_ptr& client, + std::string& receive_buffer) { char buffer[1024]; - int received = recv(client_socket_, buffer, sizeof(buffer), 0); + size_t received = client->Receive(buffer, sizeof(buffer)); + if (received == -1 || received == 0) { + return false; + } - if (received > 0) { - receive_buffer_.append(buffer, received); + receive_buffer.append(buffer, received); - // Hacky interrupt '\03' packet handling, some reason checksum isn't - // attached to this? - bool isInterrupt = - buffer[0] == char(GdbStubControl::Interrupt) && received == 1; + // Hacky interrupt '\03' packet handling, some reason checksum isn't + // attached to this? + bool isInterrupt = + buffer[0] == char(GdbStubControl::Interrupt) && received == 1; - size_t packet_end; - while (isInterrupt || - (packet_end = receive_buffer_.find('#')) != std::string::npos) { - if (isInterrupt || packet_end + 2 < receive_buffer_.length()) { - if (isInterrupt) { - current_packet_ = char(GdbStubControl::Interrupt); - receive_buffer_ = ""; - isInterrupt = false; - } else { - current_packet_ = receive_buffer_.substr(0, packet_end + 3); - receive_buffer_ = receive_buffer_.substr(packet_end + 3); - } + // Check if we've received a full packet yet, if not exit and allow caller + // to try again + size_t packet_end; + while (isInterrupt || + (packet_end = receive_buffer.find('#')) != std::string::npos) { + if (isInterrupt || packet_end + 2 < receive_buffer.length()) { + std::string current_packet; + if (isInterrupt) { + current_packet = char(GdbStubControl::Interrupt); + receive_buffer = ""; + isInterrupt = false; + } else { + current_packet = receive_buffer.substr(0, packet_end + 3); + receive_buffer = receive_buffer.substr(packet_end + 3); + } - GDBCommand command; - if (ParsePacket(command)) { + GDBCommand command; + if (ParsePacket(current_packet, command)) { #ifdef DEBUG - auto packet_name = GetPacketFriendlyName(command.cmd); + auto packet_name = GetPacketFriendlyName(command.cmd); - debugging::DebugPrint("GDBStub: Packet {}({})\n", - packet_name.empty() ? command.cmd : packet_name, - command.data); + debugging::DebugPrint("GDBStub: Packet {}({})\n", + packet_name.empty() ? command.cmd : packet_name, + command.data); #endif - GdbStubControl result = GdbStubControl::Ack; - send(client_socket_, (const char*)&result, 1, 0); - std::string response = HandleGDBCommand(command); - SendPacket(response); - } else { - GdbStubControl result = GdbStubControl::Nack; - send(client_socket_, (const char*)&result, 1, 0); - } + GdbStubControl result = GdbStubControl::Ack; + client->Send(&result, 1); + std::string response = HandleGDBCommand(command); + SendPacket(client, response); } else { - break; + GdbStubControl result = GdbStubControl::Nack; + client->Send(&result, 1); } + } else { + break; } } - return received > 0; + return true; } -bool GDBStub::ParsePacket(GDBCommand& out_cmd) { - // Index to track position in current_packet_ +bool GDBStub::ParsePacket(const std::string& packet, GDBCommand& out_cmd) { + // Index to track position in packet size_t buffer_index = 0; // Read a character from the buffer and increment index auto ReadCharFromBuffer = [&]() -> char { - if (buffer_index >= current_packet_.size()) { + if (buffer_index >= packet.size()) { return '\0'; } - return current_packet_[buffer_index++]; + return packet[buffer_index++]; }; // Parse two hex digits from buffer auto ReadHexByteFromBuffer = [&]() -> char { - if (buffer_index + 2 > current_packet_.size()) { + if (buffer_index + 2 > packet.size()) { return 0; } - char high = current_packet_[buffer_index++]; - char low = current_packet_[buffer_index++]; + char high = packet[buffer_index++]; + char low = packet[buffer_index++]; return (from_hexchar(high) << 4) | from_hexchar(low); }; @@ -835,6 +801,9 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { return "S05"; // tell debugger we're currently paused }}, + // Detach + {"D", [&](const GDBCommand& cmd) { return DebuggerDetached(); }}, + // Enable extended mode {"!", [&](const GDBCommand& cmd) { return kGdbReplyOK; }}, diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h index 92394c613..ca598f268 100644 --- a/src/xenia/debug/gdb/gdbstub.h +++ b/src/xenia/debug/gdb/gdbstub.h @@ -14,6 +14,7 @@ #include #include "xenia/base/host_thread_context.h" +#include "xenia/base/socket.h" #include "xenia/cpu/breakpoint.h" #include "xenia/cpu/debug_listener.h" #include "xenia/cpu/processor.h" @@ -51,16 +52,16 @@ class GDBStub : public cpu::DebugListener { explicit GDBStub(Emulator* emulator, int listen_port); bool Initialize(); - bool CreateSocket(int port); - bool Accept(); - void Listen(); - void SendPacket(const std::string& data); - bool ProcessIncomingData(); - bool ParsePacket(GDBCommand& out_cmd); + void Listen(std::unique_ptr& client); + void SendPacket(std::unique_ptr& client, const std::string& data); + bool ProcessIncomingData(std::unique_ptr& client, + std::string& receive_buffer); + bool ParsePacket(const std::string& packet, GDBCommand& out_cmd); std::string HandleGDBCommand(const GDBCommand& command); void UpdateCache(); + std::string DebuggerDetached(); std::string ReadRegister(xe::cpu::ThreadDebugInfo* thread, uint32_t rid); std::string ReadRegister(const std::string& data); std::string ReadRegisters(); @@ -82,13 +83,11 @@ class GDBStub : public cpu::DebugListener { cpu::Processor* processor_ = nullptr; int listen_port_; - std::thread listener_thread_; - uint64_t server_socket_, client_socket_; + std::unique_ptr socket_; + std::mutex mtx_; std::condition_variable cv_; bool stop_thread_ = false; - std::string receive_buffer_; - std::string current_packet_; struct EmulatorStateCache { uint32_t cur_thread_id = -1; From 26cfa694976c11d36dcdb3a78361dd18b95cf0a9 Mon Sep 17 00:00:00 2001 From: emoose Date: Sun, 6 Oct 2024 06:22:52 +0100 Subject: [PATCH 06/16] [GDBStub] Add memory map, MSR/FPSCR, handle Kill request IDA seems to ignore the map though, sad! --- src/xenia/debug/gdb/gdbstub.cc | 35 +++++++++++++++++++++++++++++----- src/xenia/debug/gdb/gdbstub.h | 1 + 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index 7ee83fea4..55fc849ae 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -52,6 +52,20 @@ constexpr const char* kGdbReplyError = "E01"; constexpr int kSignalSigtrap = 5; +constexpr char memory_map[] = + R"(l + + + + + + + + + + +)"; + // must start with l for debugger to accept it // TODO: add power-altivec.xml (and update ReadRegister to support it) constexpr char target_xml[] = @@ -228,9 +242,8 @@ std::string GDBStub::ReadRegister(xe::cpu::ThreadDebugInfo* thread, } return u32_to_padded_hex(0); } - // msr? case 65: - return std::string(8, 'x'); + return u32_to_padded_hex((uint32_t)thread->guest_context.msr); case 66: return u32_to_padded_hex((uint32_t)thread->guest_context.cr()); case 67: @@ -242,7 +255,7 @@ std::string GDBStub::ReadRegister(xe::cpu::ThreadDebugInfo* thread, return std::string(8, 'x'); // fpscr case 70: - return std::string(8, 'x'); + return u32_to_padded_hex(thread->guest_context.fpscr.value); default: if (rid > 70) return ""; return (rid > 31) ? u64_to_padded_hex(*(uint64_t*)&( @@ -298,7 +311,7 @@ void GDBStub::Listen(std::unique_ptr& client) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } - // Check if we need to notify client about anything... + // Check if we need to notify client about anything { std::unique_lock lock(mtx_); if (cache_.notify_stopped) { @@ -349,6 +362,7 @@ std::string GetPacketFriendlyName(const std::string& packetCommand) { {"qfThreadInfo", "qfThreadInfo"}, {"qC", "GetThreadId"}, {"D", "Detach"}, + {"k", "KillRequest"}, {"\03", "Break"}, }; @@ -557,6 +571,9 @@ std::string GDBStub::ExecutionPause() { #ifdef DEBUG debugging::DebugPrint("GDBStub: ExecutionPause\n"); #endif + if (processor_->execution_state() != cpu::ExecutionState::kRunning) { + return kGdbReplyError; + } processor_->Pause(); return kGdbReplyOK; } @@ -612,6 +629,8 @@ std::string GDBStub::ReadMemory(const std::string& data) { return result; } +std::string GDBStub::BuildMemoryMap() { return memory_map; } + std::string GDBStub::BuildTargetXml() { return target_xml; } std::string GDBStub::BuildThreadList() { @@ -804,6 +823,9 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { // Detach {"D", [&](const GDBCommand& cmd) { return DebuggerDetached(); }}, + // Kill request (just treat as detach for now) + {"k", [&](const GDBCommand& cmd) { return DebuggerDetached(); }}, + // Enable extended mode {"!", [&](const GDBCommand& cmd) { return kGdbReplyOK; }}, @@ -887,6 +909,8 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { auto sub_cmd = param.substr(0, param.find(':')); if (sub_cmd == "features") { return BuildTargetXml(); + } else if (sub_cmd == "memory-map") { + return BuildMemoryMap(); } else if (sub_cmd == "threads") { return BuildThreadList(); } @@ -895,7 +919,8 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { // Supported features (TODO: memory map) {"qSupported", [&](const GDBCommand& cmd) { - return "PacketSize=1024;qXfer:features:read+;qXfer:threads:read+"; + return "PacketSize=1024;qXfer:features:read+;qXfer:threads:read+;" + "qXfer:memory-map:read+"; }}, // Thread list (IDA requests this but ignores in favor of qXfer?) {"qfThreadInfo", diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h index ca598f268..cf35c6ea6 100644 --- a/src/xenia/debug/gdb/gdbstub.h +++ b/src/xenia/debug/gdb/gdbstub.h @@ -69,6 +69,7 @@ class GDBStub : public cpu::DebugListener { std::string ExecutionContinue(); std::string ExecutionStep(); std::string ReadMemory(const std::string& data); + std::string BuildMemoryMap(); std::string BuildTargetXml(); std::string BuildThreadList(); From c9778a4032e855ce8534c0921b5cac19be63a548 Mon Sep 17 00:00:00 2001 From: emoose Date: Sun, 6 Oct 2024 09:05:42 +0100 Subject: [PATCH 07/16] [GDBStub] Use main thread as default thread ID --- src/xenia/debug/gdb/gdbstub.cc | 93 ++++++++++++++++++++-------------- src/xenia/emulator.cc | 4 ++ src/xenia/emulator.h | 2 + 3 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index 55fc849ae..930a77998 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -193,24 +193,6 @@ uint8_t from_hexchar(char c) { return 0; } -std::string GDBStub::DebuggerDetached() { - // Delete all breakpoints - auto& state = cache_.breakpoints; - - for (auto& breakpoint : state.all_breakpoints) { - processor_->RemoveBreakpoint(breakpoint.get()); - } - - state.code_breakpoints_by_guest_address.clear(); - state.all_breakpoints.clear(); - - if (processor_->execution_state() == cpu::ExecutionState::kPaused) { - ExecutionContinue(); - } - - return kGdbReplyOK; -} - std::string GDBStub::ReadRegister(xe::cpu::ThreadDebugInfo* thread, uint32_t rid) { // Send registers as 32-bit, otherwise some debuggers will switch to 64-bit @@ -546,12 +528,18 @@ void GDBStub::UpdateCache() { object_table->GetObjectsByType(XObject::Type::Module); cache_.thread_debug_infos = processor_->QueryThreadDebugInfos(); - cache_.cur_thread_id = cache_.thread_debug_infos[0]->thread_id; + if (cache_.cur_thread_id == -1) { + cache_.cur_thread_id = emulator_->main_thread_id(); + } } std::string GDBStub::ReadRegister(const std::string& data) { + auto* thread = cache_.cur_thread_info(); + if (!thread) { + return kGdbReplyError; + } uint32_t rid = hex_to_u32(data); - std::string result = ReadRegister(cache_.cur_thread_info(), rid); + std::string result = ReadRegister(thread, rid); if (result.empty()) { return kGdbReplyError; // TODO: is this error correct? } @@ -559,10 +547,14 @@ std::string GDBStub::ReadRegister(const std::string& data) { } std::string GDBStub::ReadRegisters() { + auto* thread = cache_.cur_thread_info(); + if (!thread) { + return kGdbReplyError; + } std::string result; result.reserve(68 * 16 + 3 * 8); for (int i = 0; i < 71; ++i) { - result += ReadRegister(cache_.cur_thread_info(), i); + result += ReadRegister(thread, i); } return result; } @@ -758,9 +750,18 @@ void GDBStub::OnFocus() {} void GDBStub::OnDetached() { UpdateCache(); - // Remove all breakpoints. - while (!cache_.breakpoints.all_breakpoints.empty()) { - DeleteCodeBreakpoint(cache_.breakpoints.all_breakpoints.front().get()); + // Delete all breakpoints + auto& state = cache_.breakpoints; + + for (auto& breakpoint : state.all_breakpoints) { + processor_->RemoveBreakpoint(breakpoint.get()); + } + + state.code_breakpoints_by_guest_address.clear(); + state.all_breakpoints.clear(); + + if (processor_->execution_state() == cpu::ExecutionState::kPaused) { + ExecutionContinue(); } } @@ -821,10 +822,18 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { }}, // Detach - {"D", [&](const GDBCommand& cmd) { return DebuggerDetached(); }}, + {"D", + [&](const GDBCommand& cmd) { + OnDetached(); + return kGdbReplyOK; + }}, // Kill request (just treat as detach for now) - {"k", [&](const GDBCommand& cmd) { return DebuggerDetached(); }}, + {"k", + [&](const GDBCommand& cmd) { + OnDetached(); + return kGdbReplyOK; + }}, // Enable extended mode {"!", [&](const GDBCommand& cmd) { return kGdbReplyOK; }}, @@ -857,24 +866,32 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { // Get current debugger thread ID {"qC", [&](const GDBCommand& cmd) { - return "QC" + std::to_string(cache_.cur_thread_info()->thread_id); + auto* thread = cache_.cur_thread_info(); + if (!thread) { + return std::string(kGdbReplyError); + } + return "QC" + std::to_string(thread->thread_id); }}, // Set current debugger thread ID {"H", [&](const GDBCommand& cmd) { - // Reset to known good ID - cache_.cur_thread_id = - cache_.thread_debug_infos.size() - ? cache_.thread_debug_infos[0]->thread_id - : -1; - - // Check if the desired thread ID exists int threadId = std::stol(cmd.data.substr(1), 0, 16); - for (auto& thread : cache_.thread_debug_infos) { - if (thread->thread_id == threadId) { - cache_.cur_thread_id = threadId; - break; + + if (!threadId) { + // Treat Thread 0 as main thread, seems to work for IDA + cache_.cur_thread_id = emulator_->main_thread_id(); + } else { + uint32_t thread_id = -1; + + // Check if the desired thread ID exists + for (auto& thread : cache_.thread_debug_infos) { + if (thread->thread_id == threadId) { + thread_id = threadId; + break; + } } + + cache_.cur_thread_id = thread_id; } return kGdbReplyOK; diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 952e28de1..82d76c397 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -174,6 +174,10 @@ Emulator::~Emulator() { ExceptionHandler::Uninstall(Emulator::ExceptionCallbackThunk, this); } +uint32_t Emulator::main_thread_id() { + return main_thread_ ? main_thread_->thread_id() : 0; +} + X_STATUS Emulator::Setup( ui::Window* display_window, ui::ImGuiDrawer* imgui_drawer, bool require_cpu_backend, diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index ccf37d7f9..5d2db462f 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -124,6 +124,8 @@ class Emulator { // Are we currently running a title? bool is_title_open() const { return title_id_.has_value(); } + uint32_t main_thread_id(); + // Window used for displaying graphical output. Can be null. ui::Window* display_window() const { return display_window_; } From 2ee35a20540ba84e5b80d45d3a3edf8331283100 Mon Sep 17 00:00:00 2001 From: emoose Date: Mon, 7 Oct 2024 13:27:18 +0100 Subject: [PATCH 08/16] [GDBStub] Remove duplicated code, minor fixups --- src/xenia/debug/gdb/gdbstub.cc | 170 +++++++++++++++------------------ src/xenia/debug/gdb/gdbstub.h | 4 +- 2 files changed, 77 insertions(+), 97 deletions(-) diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index 930a77998..49488bc07 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -2,7 +2,7 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2022 Ben Vanik. All rights reserved. * + * Copyright 2024 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ @@ -52,7 +52,8 @@ constexpr const char* kGdbReplyError = "E01"; constexpr int kSignalSigtrap = 5; -constexpr char memory_map[] = +// must start with l for debugger to accept it +constexpr char kMemoryMapXml[] = R"(l @@ -66,9 +67,8 @@ constexpr char memory_map[] = )"; -// must start with l for debugger to accept it // TODO: add power-altivec.xml (and update ReadRegister to support it) -constexpr char target_xml[] = +constexpr char kTargetXml[] = R"(l @@ -152,26 +152,6 @@ constexpr char target_xml[] = )"; -std::string u64_to_padded_hex(uint64_t value) { - return fmt::format("{:016x}", value); -} - -std::string u32_to_padded_hex(uint32_t value) { - return fmt::format("{:08x}", value); -} - -template -T hex_to(std::string_view val) { - T result; - std::from_chars(val.data(), val.data() + val.size(), result, 16); - - return result; -} - -constexpr auto& hex_to_u8 = hex_to; -constexpr auto& hex_to_u32 = hex_to; -constexpr auto& hex_to_u64 = hex_to; - std::string to_hexbyte(uint8_t i) { std::string result = "00"; uint8_t i1 = i & 0xF; @@ -193,60 +173,6 @@ uint8_t from_hexchar(char c) { return 0; } -std::string GDBStub::ReadRegister(xe::cpu::ThreadDebugInfo* thread, - uint32_t rid) { - // Send registers as 32-bit, otherwise some debuggers will switch to 64-bit - // mode (eg. IDA will switch to 64-bit and refuse to allow decompiler to work - // with it) - // - // TODO: add altivec/VMX registers here... - // - // ids from gdb/features/rs6000/powerpc-64.c - switch (rid) { - // pc - case 64: { - // If we recently hit a BP then debugger is likely asking for registers - // for it - // - // Lie about the PC and say it's the BP address, since PC might not always - // match - if (cache_.notify_bp_guest_address != -1) { - auto ret = u32_to_padded_hex((uint32_t)cache_.notify_bp_guest_address); - cache_.notify_bp_guest_address = -1; - return ret; - } - // Search for first frame that has guest_pc attached, GDB doesn't care - // about host - for (auto& frame : thread->frames) { - if (frame.guest_pc != 0) { - return u32_to_padded_hex((uint32_t)frame.guest_pc); - } - } - return u32_to_padded_hex(0); - } - case 65: - return u32_to_padded_hex((uint32_t)thread->guest_context.msr); - case 66: - return u32_to_padded_hex((uint32_t)thread->guest_context.cr()); - case 67: - return u32_to_padded_hex((uint32_t)thread->guest_context.lr); - case 68: - return u32_to_padded_hex((uint32_t)thread->guest_context.ctr); - // xer - case 69: - return std::string(8, 'x'); - // fpscr - case 70: - return u32_to_padded_hex(thread->guest_context.fpscr.value); - default: - if (rid > 70) return ""; - return (rid > 31) ? u64_to_padded_hex(*(uint64_t*)&( - thread->guest_context.f[rid - 32])) // fpr - : u32_to_padded_hex( - (uint32_t)thread->guest_context.r[rid]); // gpr - } -} - GDBStub::GDBStub(Emulator* emulator, int listen_port) : emulator_(emulator), processor_(emulator->processor()), @@ -362,7 +288,8 @@ bool GDBStub::ProcessIncomingData(std::unique_ptr& client, std::string& receive_buffer) { char buffer[1024]; size_t received = client->Receive(buffer, sizeof(buffer)); - if (received == -1 || received == 0) { + if (received == uint64_t(-1) || // disconnected + received == 0) { return false; } @@ -533,15 +460,75 @@ void GDBStub::UpdateCache() { } } +std::string GDBStub::ReadRegister(xe::cpu::ThreadDebugInfo* thread, + uint32_t rid) { + // Send registers as 32-bit, otherwise some debuggers will switch to 64-bit + // mode (eg. IDA will switch to 64-bit and refuse to allow decompiler to work + // with it) + // + // TODO: add altivec/VMX registers here... + // + // ids from gdb/features/rs6000/powerpc-64.c + switch (rid) { + // pc + case 64: { + // If we recently hit a BP then debugger is likely asking for registers + // for it + // + // Lie about the PC and say it's the BP addr, since the PC (derived from + // X64 host addr) might not match expected BP addr + if (cache_.notify_bp_guest_address != -1) { + auto ret = string_util::to_hex_string( + (uint32_t)cache_.notify_bp_guest_address); + cache_.notify_bp_guest_address = -1; + return ret; + } + // Search for first frame that has guest_pc attached, GDB doesn't care + // about host + for (auto& frame : thread->frames) { + if (frame.guest_pc != 0) { + return string_util::to_hex_string((uint32_t)frame.guest_pc); + } + } + return string_util::to_hex_string((uint32_t)0); + } + case 65: + return string_util::to_hex_string((uint32_t)thread->guest_context.msr); + case 66: + return string_util::to_hex_string((uint32_t)thread->guest_context.cr()); + case 67: + return string_util::to_hex_string((uint32_t)thread->guest_context.lr); + case 68: + return string_util::to_hex_string((uint32_t)thread->guest_context.ctr); + // xer + case 69: + return std::string(8, 'x'); + case 70: + return string_util::to_hex_string(thread->guest_context.fpscr.value); + } + + if (rid > 70) { + return ""; + } + + // fpr + if (rid > 31) { + return string_util::to_hex_string(thread->guest_context.f[rid - 32]); + } + + // gpr + return string_util::to_hex_string((uint32_t)thread->guest_context.r[rid]); +} + std::string GDBStub::ReadRegister(const std::string& data) { auto* thread = cache_.cur_thread_info(); if (!thread) { return kGdbReplyError; } - uint32_t rid = hex_to_u32(data); + uint32_t rid = string_util::from_string(data, true); std::string result = ReadRegister(thread, rid); if (result.empty()) { - return kGdbReplyError; // TODO: is this error correct? + return kGdbReplyError; } return result; } @@ -592,8 +579,8 @@ std::string GDBStub::ExecutionStep() { std::string GDBStub::ReadMemory(const std::string& data) { auto s = data.find(','); - uint32_t addr = hex_to_u32(data.substr(0, s)); - uint32_t len = hex_to_u32(data.substr(s + 1)); + uint32_t addr = string_util::from_string(data.substr(0, s), true); + uint32_t len = string_util::from_string(data.substr(s + 1), true); std::string result; result.reserve(len * 2); @@ -621,10 +608,6 @@ std::string GDBStub::ReadMemory(const std::string& data) { return result; } -std::string GDBStub::BuildMemoryMap() { return memory_map; } - -std::string GDBStub::BuildTargetXml() { return target_xml; } - std::string GDBStub::BuildThreadList() { std::string buffer; buffer += "l"; @@ -661,10 +644,9 @@ std::string GDBStub::GetThreadStateReply(uint32_t thread_id, uint8_t signal) { pc_value = cache_.notify_bp_guest_address; } - return fmt::format( - "T{:02x}{:02x}:{};{:02x}:{};thread:{:x};", signal, PC_REGISTER, - u32_to_padded_hex((uint32_t)pc_value), LR_REGISTER, - u32_to_padded_hex((uint32_t)thread->guest_context.lr), thread_id); + return fmt::format("T{:02x}{:02x}:{:08x};{:02x}:{:08x};thread:{:x};", + signal, PC_REGISTER, uint32_t(pc_value), LR_REGISTER, + uint32_t(thread->guest_context.lr), thread_id); } return "S05"; } @@ -925,15 +907,15 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { } auto sub_cmd = param.substr(0, param.find(':')); if (sub_cmd == "features") { - return BuildTargetXml(); + return std::string(kTargetXml); } else if (sub_cmd == "memory-map") { - return BuildMemoryMap(); + return std::string(kMemoryMapXml); } else if (sub_cmd == "threads") { return BuildThreadList(); } return std::string(kGdbReplyError); }}, - // Supported features (TODO: memory map) + // Supported features {"qSupported", [&](const GDBCommand& cmd) { return "PacketSize=1024;qXfer:features:read+;qXfer:threads:read+;" diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h index cf35c6ea6..c803421dd 100644 --- a/src/xenia/debug/gdb/gdbstub.h +++ b/src/xenia/debug/gdb/gdbstub.h @@ -2,7 +2,7 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2022 Ben Vanik. All rights reserved. * + * Copyright 2024 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ @@ -69,8 +69,6 @@ class GDBStub : public cpu::DebugListener { std::string ExecutionContinue(); std::string ExecutionStep(); std::string ReadMemory(const std::string& data); - std::string BuildMemoryMap(); - std::string BuildTargetXml(); std::string BuildThreadList(); std::string GetThreadStateReply(uint32_t thread_id, uint8_t signal); From 0acc46e52e191a817bc551c870684a101b5d27ae Mon Sep 17 00:00:00 2001 From: emoose Date: Mon, 7 Oct 2024 20:52:20 +0100 Subject: [PATCH 09/16] [GDBStub] Add MemoryWrite, extra checks in memory cmds --- src/xenia/debug/gdb/gdbstub.cc | 127 +++++++++++++++++++++++++-------- src/xenia/debug/gdb/gdbstub.h | 12 ++-- 2 files changed, 105 insertions(+), 34 deletions(-) diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index 49488bc07..448835088 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -217,18 +217,19 @@ void GDBStub::Listen(std::unique_ptr& client) { } // No data available, can do other work or sleep std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - // Check if we need to notify client about anything - { - std::unique_lock lock(mtx_); - if (cache_.notify_stopped) { - if (cache_.notify_bp_thread_id != -1) - cache_.cur_thread_id = cache_.notify_bp_thread_id; - SendPacket(client, GetThreadStateReply(cache_.notify_bp_thread_id, - kSignalSigtrap)); - cache_.notify_bp_thread_id = -1; - cache_.notify_stopped = false; + // Check if we need to notify client about anything + { + std::unique_lock lock(mtx_); + if (cache_.notify_stopped) { + if (cache_.notify_bp_thread_id != -1) { + cache_.cur_thread_id = cache_.notify_bp_thread_id; + } + SendPacket(client, GetThreadStateReply(cache_.notify_bp_thread_id, + kSignalSigtrap)); + cache_.notify_bp_thread_id = -1; + cache_.notify_stopped = false; + } } } } @@ -241,7 +242,9 @@ void GDBStub::SendPacket(std::unique_ptr& client, << char(GdbStubControl::PacketEnd); uint8_t checksum = 0; - for (char c : data) checksum += c; + for (char c : data) { + checksum += c; + } ss << std::hex << std::setw(2) << std::setfill('0') << (checksum & 0xff); std::string packet = ss.str(); @@ -254,17 +257,18 @@ std::string GetPacketFriendlyName(const std::string& packetCommand) { static const std::unordered_map command_names = { {"?", "StartupQuery"}, {"!", "EnableExtendedMode"}, - {"p", "ReadRegister"}, - {"P", "WriteRegister"}, - {"g", "ReadAllRegisters"}, + {"p", "RegRead"}, + {"P", "RegWrite"}, + {"g", "RegReadAll"}, {"C", "Continue"}, {"c", "continue"}, {"s", "step"}, {"vAttach", "vAttach"}, {"m", "MemRead"}, + {"M", "MemWrite"}, {"H", "SetThreadId"}, - {"Z", "CreateCodeBreakpoint"}, - {"z", "DeleteCodeBreakpoint"}, + {"Z", "BreakpointCreate"}, + {"z", "BreakpointDelete"}, {"qXfer", "Xfer"}, {"qSupported", "Supported"}, {"qfThreadInfo", "qfThreadInfo"}, @@ -460,7 +464,7 @@ void GDBStub::UpdateCache() { } } -std::string GDBStub::ReadRegister(xe::cpu::ThreadDebugInfo* thread, +std::string GDBStub::RegisterRead(xe::cpu::ThreadDebugInfo* thread, uint32_t rid) { // Send registers as 32-bit, otherwise some debuggers will switch to 64-bit // mode (eg. IDA will switch to 64-bit and refuse to allow decompiler to work @@ -520,20 +524,20 @@ std::string GDBStub::ReadRegister(xe::cpu::ThreadDebugInfo* thread, return string_util::to_hex_string((uint32_t)thread->guest_context.r[rid]); } -std::string GDBStub::ReadRegister(const std::string& data) { +std::string GDBStub::RegisterRead(const std::string& data) { auto* thread = cache_.cur_thread_info(); if (!thread) { return kGdbReplyError; } uint32_t rid = string_util::from_string(data, true); - std::string result = ReadRegister(thread, rid); + std::string result = RegisterRead(thread, rid); if (result.empty()) { return kGdbReplyError; } return result; } -std::string GDBStub::ReadRegisters() { +std::string GDBStub::RegisterReadAll() { auto* thread = cache_.cur_thread_info(); if (!thread) { return kGdbReplyError; @@ -541,7 +545,7 @@ std::string GDBStub::ReadRegisters() { std::string result; result.reserve(68 * 16 + 3 * 8); for (int i = 0; i < 71; ++i) { - result += ReadRegister(thread, i); + result += RegisterRead(thread, i); } return result; } @@ -571,19 +575,30 @@ std::string GDBStub::ExecutionStep() { cache_.last_bp_thread_id); #endif - if (cache_.last_bp_thread_id != -1) + if (cache_.last_bp_thread_id != -1) { processor_->StepGuestInstruction(cache_.last_bp_thread_id); + } return kGdbReplyOK; } -std::string GDBStub::ReadMemory(const std::string& data) { - auto s = data.find(','); - uint32_t addr = string_util::from_string(data.substr(0, s), true); - uint32_t len = string_util::from_string(data.substr(s + 1), true); +std::string GDBStub::MemoryRead(const std::string& data) { + auto len_sep = data.find(','); + + if (len_sep == std::string::npos) { + return kGdbReplyError; + } + + uint32_t addr = + string_util::from_string(data.substr(0, len_sep), true); + uint32_t len = + string_util::from_string(data.substr(len_sep + 1), true); + std::string result; result.reserve(len * 2); + auto global_lock = global_critical_region_.Acquire(); + // TODO: is there a better way to check if addr is valid? auto* heap = processor_->memory()->LookupHeap(addr); if (!heap) { @@ -608,6 +623,55 @@ std::string GDBStub::ReadMemory(const std::string& data) { return result; } +std::string GDBStub::MemoryWrite(const std::string& data) { + auto len_sep = data.find(','); + auto mem_sep = data.find(':'); + + if (len_sep == std::string::npos || mem_sep == std::string::npos) { + return kGdbReplyError; + } + + uint32_t addr = + string_util::from_string(data.substr(0, len_sep), true); + uint32_t len = string_util::from_string( + data.substr(len_sep + 1, mem_sep - (len_sep + 1)), true); + + auto global_lock = global_critical_region_.Acquire(); + + auto* heap = processor_->memory()->LookupHeap(addr); + if (!heap) { + return kGdbReplyError; + } + uint32_t protect = 0; + if (!heap->QueryProtect(addr, &protect) || + (protect & kMemoryProtectRead) != kMemoryProtectRead) { + return kGdbReplyError; + } + + if (len == 0) { + return kGdbReplyOK; + } + + uint32_t old_protect = 0; + bool mem_unprotected = + heap->Protect(addr, len, protect | kMemoryProtectWrite, &old_protect); + if (!mem_unprotected) { + return kGdbReplyError; + } + + std::vector mem_data; + string_util::hex_string_to_array(mem_data, + std::string_view(data.data() + mem_sep + 1)); + auto* mem = processor_->memory()->TranslateVirtual(addr); + for (uint32_t i = 0; i < len; ++i) { + mem[i] = mem_data[i]; + } + + heap->Protect(addr, len, old_protect); + + return kGdbReplyOK; +} + std::string GDBStub::BuildThreadList() { std::string buffer; buffer += "l"; @@ -830,16 +894,19 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { {"\03", [&](const GDBCommand& cmd) { return ExecutionPause(); }}, // Read memory - {"m", [&](const GDBCommand& cmd) { return ReadMemory(cmd.data); }}, + {"m", [&](const GDBCommand& cmd) { return MemoryRead(cmd.data); }}, + // Write memory + {"M", [&](const GDBCommand& cmd) { return MemoryWrite(cmd.data); }}, + // Read register - {"p", [&](const GDBCommand& cmd) { return ReadRegister(cmd.data); }}, + {"p", [&](const GDBCommand& cmd) { return RegisterRead(cmd.data); }}, // Write register {"P", [&](const GDBCommand& cmd) { return kGdbReplyOK; // TODO: we'll just tell it write was fine }}, // Read all registers - {"g", [&](const GDBCommand& cmd) { return ReadRegisters(); }}, + {"g", [&](const GDBCommand& cmd) { return RegisterReadAll(); }}, // Attach to specific process ID - IDA used to send this, but doesn't // after some changes? diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h index c803421dd..3d0ed97dd 100644 --- a/src/xenia/debug/gdb/gdbstub.h +++ b/src/xenia/debug/gdb/gdbstub.h @@ -62,13 +62,14 @@ class GDBStub : public cpu::DebugListener { void UpdateCache(); std::string DebuggerDetached(); - std::string ReadRegister(xe::cpu::ThreadDebugInfo* thread, uint32_t rid); - std::string ReadRegister(const std::string& data); - std::string ReadRegisters(); + std::string RegisterRead(xe::cpu::ThreadDebugInfo* thread, uint32_t rid); + std::string RegisterRead(const std::string& data); + std::string RegisterReadAll(); std::string ExecutionPause(); std::string ExecutionContinue(); std::string ExecutionStep(); - std::string ReadMemory(const std::string& data); + std::string MemoryRead(const std::string& data); + std::string MemoryWrite(const std::string& data); std::string BuildThreadList(); std::string GetThreadStateReply(uint32_t thread_id, uint8_t signal); @@ -88,12 +89,15 @@ class GDBStub : public cpu::DebugListener { std::condition_variable cv_; bool stop_thread_ = false; + xe::global_critical_region global_critical_region_; + struct EmulatorStateCache { uint32_t cur_thread_id = -1; uint32_t last_bp_thread_id = -1; uint64_t notify_bp_guest_address = -1; uint32_t notify_bp_thread_id = -1; + std::vector notify_debug_messages; bool notify_stopped = false; bool is_stopped = false; From ea9cf0c8f90c12685e1d240cf2bd248db6b58cc1 Mon Sep 17 00:00:00 2001 From: emoose Date: Mon, 7 Oct 2024 22:26:41 +0100 Subject: [PATCH 10/16] [GDBStub] Allow register writes, disable ctx promotion for --debug Register changes won't always register if context promotion pass is enabled, fortunately this was noted by benvanik at https://github.com/xenia-project/xenia/blob/3d30b2eec3ab1f83140b09745bee881fb5d5dde2/src/xenia/debug/ui/debug_window.cc#L1004 Disabling context promotion pass if --debug is set seems to allow register updates fine, without too much performance loss on my end. Still need to implement PC and CR reg writes, and the WriteRegistersAll cmd Not completely sure if all register writes actually take effect yet, seems GPR are fine at least but unsure about others --- src/xenia/cpu/ppc/ppc_translator.cc | 7 ++- src/xenia/debug/gdb/gdbstub.cc | 69 +++++++++++++++++++++++++++-- src/xenia/debug/gdb/gdbstub.h | 3 ++ 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/xenia/cpu/ppc/ppc_translator.cc b/src/xenia/cpu/ppc/ppc_translator.cc index c559abe89..09753066c 100644 --- a/src/xenia/cpu/ppc/ppc_translator.cc +++ b/src/xenia/cpu/ppc/ppc_translator.cc @@ -33,6 +33,8 @@ DEFINE_bool(disable_context_promotion, false, "some sports games, but will reduce performance.", "CPU"); +DECLARE_bool(debug); + namespace xe { namespace cpu { namespace ppc { @@ -59,7 +61,10 @@ PPCTranslator::PPCTranslator(PPCFrontend* frontend) : frontend_(frontend) { // Passes are executed in the order they are added. Multiple of the same // pass type may be used. - if (!cvars::disable_context_promotion) { + + // Disable context promotion for debug, otherwise register changes won't apply + // correctly + if (!cvars::disable_context_promotion && !cvars::debug) { if (validate) { compiler_->AddPass(std::make_unique()); } diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index 448835088..7121d0668 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -523,6 +523,54 @@ std::string GDBStub::RegisterRead(xe::cpu::ThreadDebugInfo* thread, // gpr return string_util::to_hex_string((uint32_t)thread->guest_context.r[rid]); } +std::string GDBStub::RegisterWrite(xe::cpu::ThreadDebugInfo* thread, + uint32_t rid, const std::string_view value) { + auto* guest_context = thread->thread->thread_state()->context(); + switch (rid) { + // pc + case 64: + return kGdbReplyOK; // TODO: figure a way to change this + case 65: + guest_context->msr = string_util::from_string(value, true); + thread->guest_context.msr = guest_context->msr; + return kGdbReplyOK; + case 66: + // CR + return kGdbReplyOK; // TODO: figure a way to change this + case 67: + guest_context->lr = string_util::from_string(value, true); + thread->guest_context.lr = guest_context->lr; + return kGdbReplyOK; + case 68: + guest_context->ctr = string_util::from_string(value, true); + thread->guest_context.ctr = guest_context->ctr; + return kGdbReplyOK; + // xer + case 69: + return kGdbReplyOK; + case 70: + guest_context->fpscr.value = + string_util::from_string(value, true); + thread->guest_context.fpscr.value = guest_context->fpscr.value; + return kGdbReplyOK; + } + + if (rid > 70) { + return kGdbReplyError; + } + + // fpr + if (rid > 31) { + guest_context->f[rid - 32] = string_util::from_string(value, true); + thread->guest_context.f[rid - 32] = guest_context->f[rid - 32]; + return kGdbReplyOK; + } + + // gpr + guest_context->r[rid] = string_util::from_string(value, true); + thread->guest_context.r[rid] = guest_context->r[rid]; + return kGdbReplyOK; +} std::string GDBStub::RegisterRead(const std::string& data) { auto* thread = cache_.cur_thread_info(); @@ -537,6 +585,22 @@ std::string GDBStub::RegisterRead(const std::string& data) { return result; } +std::string GDBStub::RegisterWrite(const std::string& data) { + auto* thread = cache_.cur_thread_info(); + if (!thread) { + return kGdbReplyError; + } + + auto value_sep = data.find('='); + if (value_sep == std::string::npos) { + return kGdbReplyError; + } + + uint32_t rid = + string_util::from_string(data.substr(0, value_sep), true); + return RegisterWrite(thread, rid, data.data() + value_sep + 1); +} + std::string GDBStub::RegisterReadAll() { auto* thread = cache_.cur_thread_info(); if (!thread) { @@ -901,10 +965,7 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { // Read register {"p", [&](const GDBCommand& cmd) { return RegisterRead(cmd.data); }}, // Write register - {"P", - [&](const GDBCommand& cmd) { - return kGdbReplyOK; // TODO: we'll just tell it write was fine - }}, + {"P", [&](const GDBCommand& cmd) { return RegisterWrite(cmd.data); }}, // Read all registers {"g", [&](const GDBCommand& cmd) { return RegisterReadAll(); }}, diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h index 3d0ed97dd..027953805 100644 --- a/src/xenia/debug/gdb/gdbstub.h +++ b/src/xenia/debug/gdb/gdbstub.h @@ -63,7 +63,10 @@ class GDBStub : public cpu::DebugListener { std::string DebuggerDetached(); std::string RegisterRead(xe::cpu::ThreadDebugInfo* thread, uint32_t rid); + std::string RegisterWrite(xe::cpu::ThreadDebugInfo* thread, uint32_t rid, + const std::string_view value); std::string RegisterRead(const std::string& data); + std::string RegisterWrite(const std::string& data); std::string RegisterReadAll(); std::string ExecutionPause(); std::string ExecutionContinue(); From a4187736ad9d9c39b3cea4d3aa2b4afdcf03f446 Mon Sep 17 00:00:00 2001 From: emoose Date: Mon, 7 Oct 2024 22:56:26 +0100 Subject: [PATCH 11/16] [GDBStub] Add RegisterWriteAll --- src/xenia/debug/gdb/gdbstub.cc | 26 ++++++++++++++++++++++++++ src/xenia/debug/gdb/gdbstub.h | 1 + 2 files changed, 27 insertions(+) diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index 7121d0668..ae058b4b0 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -260,6 +260,7 @@ std::string GetPacketFriendlyName(const std::string& packetCommand) { {"p", "RegRead"}, {"P", "RegWrite"}, {"g", "RegReadAll"}, + {"G", "RegWriteAll"}, {"C", "Continue"}, {"c", "continue"}, {"s", "step"}, @@ -614,6 +615,28 @@ std::string GDBStub::RegisterReadAll() { return result; } +std::string GDBStub::RegisterWriteAll(const std::string& data) { + auto* thread = cache_.cur_thread_info(); + if (!thread) { + return kGdbReplyError; + } + + int string_offset = 0; + for (int i = 0; i < 71; ++i) { + int reg_size = 8; // 8 hex-nibbles per 32-bit register + if (i > 31 && i < 64) { + reg_size = 16; // 16 hex-nibbles for 64-bit FPR registers + } + + std::string_view reg_data(data.data() + string_offset, reg_size); + RegisterWrite(thread, i, reg_data); // TODO: check return value + + string_offset += reg_size; + } + + return kGdbReplyOK; +} + std::string GDBStub::ExecutionPause() { #ifdef DEBUG debugging::DebugPrint("GDBStub: ExecutionPause\n"); @@ -968,6 +991,9 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { {"P", [&](const GDBCommand& cmd) { return RegisterWrite(cmd.data); }}, // Read all registers {"g", [&](const GDBCommand& cmd) { return RegisterReadAll(); }}, + // Write all registers + {"G", + [&](const GDBCommand& cmd) { return RegisterWriteAll(cmd.data); }}, // Attach to specific process ID - IDA used to send this, but doesn't // after some changes? diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h index 027953805..d8e4b8a97 100644 --- a/src/xenia/debug/gdb/gdbstub.h +++ b/src/xenia/debug/gdb/gdbstub.h @@ -68,6 +68,7 @@ class GDBStub : public cpu::DebugListener { std::string RegisterRead(const std::string& data); std::string RegisterWrite(const std::string& data); std::string RegisterReadAll(); + std::string RegisterWriteAll(const std::string& data); std::string ExecutionPause(); std::string ExecutionContinue(); std::string ExecutionStep(); From e260d0a110b3014e3ac690b03304114bb23cfbcf Mon Sep 17 00:00:00 2001 From: emoose Date: Tue, 8 Oct 2024 00:04:46 +0100 Subject: [PATCH 12/16] [GDBStub] Provide exception type to debugger --- src/xenia/cpu/debug_listener.h | 3 ++ src/xenia/cpu/processor.cc | 2 +- src/xenia/debug/gdb/gdbstub.cc | 44 ++++++++++++++++++++++++------ src/xenia/debug/gdb/gdbstub.h | 7 +++-- src/xenia/debug/ui/debug_window.cc | 2 ++ src/xenia/debug/ui/debug_window.h | 1 + 6 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/xenia/cpu/debug_listener.h b/src/xenia/cpu/debug_listener.h index 3e10321db..6233e87fe 100644 --- a/src/xenia/cpu/debug_listener.h +++ b/src/xenia/cpu/debug_listener.h @@ -30,6 +30,9 @@ class DebugListener { // end. virtual void OnDetached() = 0; + // Handles exception info (will be followed by OnExecutionPaused) + virtual void OnUnhandledException(Exception* ex) = 0; + // Handles execution being interrupted and transitioning to // ExceutionState::kPaused. virtual void OnExecutionPaused() = 0; diff --git a/src/xenia/cpu/processor.cc b/src/xenia/cpu/processor.cc index ee47e835b..ae428ee92 100644 --- a/src/xenia/cpu/processor.cc +++ b/src/xenia/cpu/processor.cc @@ -724,7 +724,7 @@ bool Processor::OnUnhandledException(Exception* ex) { execution_state_ = ExecutionState::kPaused; // Notify debugger that exceution stopped. - // debug_listener_->OnException(info); + debug_listener_->OnUnhandledException(ex); debug_listener_->OnExecutionPaused(); // Suspend self. diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index ae058b4b0..7798c6b7e 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -27,6 +27,7 @@ #include "xenia/cpu/breakpoint.h" #include "xenia/cpu/ppc/ppc_opcode_info.h" #include "xenia/cpu/stack_walker.h" +#include "xenia/cpu/thread.h" #include "xenia/kernel/xmodule.h" #include "xenia/kernel/xthread.h" @@ -50,7 +51,9 @@ enum class GdbStubControl : char { constexpr const char* kGdbReplyOK = "OK"; constexpr const char* kGdbReplyError = "E01"; -constexpr int kSignalSigtrap = 5; +constexpr int kSignalSigill = 4; // Illegal instruction +constexpr int kSignalSigtrap = 5; // Trace trap +constexpr int kSignalSigsegv = 11; // Segmentation fault // must start with l for debugger to accept it constexpr char kMemoryMapXml[] = @@ -222,12 +225,26 @@ void GDBStub::Listen(std::unique_ptr& client) { { std::unique_lock lock(mtx_); if (cache_.notify_stopped) { - if (cache_.notify_bp_thread_id != -1) { - cache_.cur_thread_id = cache_.notify_bp_thread_id; + if (cache_.notify_thread_id != -1) { + cache_.cur_thread_id = cache_.notify_thread_id; } - SendPacket(client, GetThreadStateReply(cache_.notify_bp_thread_id, - kSignalSigtrap)); - cache_.notify_bp_thread_id = -1; + + int sig_num = kSignalSigtrap; + if (cache_.notify_exception_code.has_value()) { + if (cache_.notify_exception_code == + xe::Exception::Code::kIllegalInstruction) { + sig_num = kSignalSigill; + } else { + sig_num = kSignalSigsegv; + } + + cache_.notify_exception_code.reset(); + cache_.notify_exception_access_violation.reset(); + } + + SendPacket(client, + GetThreadStateReply(cache_.notify_thread_id, sig_num)); + cache_.notify_thread_id = -1; cache_.notify_stopped = false; } } @@ -898,6 +915,17 @@ void GDBStub::OnDetached() { } } +void GDBStub::OnUnhandledException(Exception* ex) { +#ifdef DEBUG + debugging::DebugPrint("GDBStub: OnUnhandledException {} {}\n", + int(ex->code()), int(ex->access_violation_operation())); +#endif + std::unique_lock lock(mtx_); + cache_.notify_exception_code = ex->code(); + cache_.notify_exception_access_violation = ex->access_violation_operation(); + cache_.notify_thread_id = cpu::Thread::GetCurrentThreadId(); +} + void GDBStub::OnExecutionPaused() { #ifdef DEBUG debugging::DebugPrint("GDBStub: OnExecutionPaused\n"); @@ -925,7 +953,7 @@ void GDBStub::OnStepCompleted(cpu::ThreadDebugInfo* thread_info) { #endif // Some debuggers like IDA will remove the current breakpoint & step into next // instruction, only re-adding BP after it's told about the step - cache_.notify_bp_thread_id = thread_info->thread_id; + cache_.notify_thread_id = thread_info->thread_id; cache_.last_bp_thread_id = thread_info->thread_id; UpdateCache(); } @@ -938,7 +966,7 @@ void GDBStub::OnBreakpointHit(Breakpoint* breakpoint, #endif cache_.notify_bp_guest_address = breakpoint->address(); - cache_.notify_bp_thread_id = thread_info->thread_id; + cache_.notify_thread_id = thread_info->thread_id; cache_.last_bp_thread_id = thread_info->thread_id; UpdateCache(); } diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h index d8e4b8a97..2e1d7f096 100644 --- a/src/xenia/debug/gdb/gdbstub.h +++ b/src/xenia/debug/gdb/gdbstub.h @@ -35,6 +35,7 @@ class GDBStub : public cpu::DebugListener { void OnFocus() override; void OnDetached() override; + void OnUnhandledException(Exception* ex) override; void OnExecutionPaused() override; void OnExecutionContinued() override; void OnExecutionEnded() override; @@ -100,9 +101,11 @@ class GDBStub : public cpu::DebugListener { uint32_t last_bp_thread_id = -1; uint64_t notify_bp_guest_address = -1; - uint32_t notify_bp_thread_id = -1; - std::vector notify_debug_messages; + uint32_t notify_thread_id = -1; bool notify_stopped = false; + std::optional + notify_exception_access_violation; + std::optional notify_exception_code; bool is_stopped = false; std::vector> modules; diff --git a/src/xenia/debug/ui/debug_window.cc b/src/xenia/debug/ui/debug_window.cc index d926874c5..83b076c46 100644 --- a/src/xenia/debug/ui/debug_window.cc +++ b/src/xenia/debug/ui/debug_window.cc @@ -1576,6 +1576,8 @@ void DebugWindow::OnDetached() { } } +void DebugWindow::OnUnhandledException(Exception* ex) {} + void DebugWindow::OnExecutionPaused() { UpdateCache(); Focus(); diff --git a/src/xenia/debug/ui/debug_window.h b/src/xenia/debug/ui/debug_window.h index d40ce484f..c58a3d448 100644 --- a/src/xenia/debug/ui/debug_window.h +++ b/src/xenia/debug/ui/debug_window.h @@ -44,6 +44,7 @@ class DebugWindow : public cpu::DebugListener { void OnFocus() override; void OnDetached() override; + void OnUnhandledException(Exception* ex) override; void OnExecutionPaused() override; void OnExecutionContinued() override; void OnExecutionEnded() override; From c17261abf7a988e1cb38495c6f0ab50e20832733 Mon Sep 17 00:00:00 2001 From: emoose Date: Tue, 8 Oct 2024 22:10:57 +0100 Subject: [PATCH 13/16] [GDBStub] Return error for code / unimpl. reg writes, small fixups --- src/xenia/debug/gdb/gdbstub.cc | 136 ++++++++++++++++----------------- src/xenia/debug/gdb/gdbstub.h | 25 +++++- 2 files changed, 90 insertions(+), 71 deletions(-) diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index 7798c6b7e..106ded38b 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -40,21 +40,9 @@ using xe::kernel::XModule; using xe::kernel::XObject; using xe::kernel::XThread; -enum class GdbStubControl : char { - Ack = '+', - Nack = '-', - PacketStart = '$', - PacketEnd = '#', - Interrupt = '\03', -}; - constexpr const char* kGdbReplyOK = "OK"; constexpr const char* kGdbReplyError = "E01"; -constexpr int kSignalSigill = 4; // Illegal instruction -constexpr int kSignalSigtrap = 5; // Trace trap -constexpr int kSignalSigsegv = 11; // Segmentation fault - // must start with l for debugger to accept it constexpr char kMemoryMapXml[] = R"(l @@ -70,7 +58,7 @@ constexpr char kMemoryMapXml[] = )"; -// TODO: add power-altivec.xml (and update ReadRegister to support it) +// TODO: add power-altivec.xml (and update RegisterRead to support it) constexpr char kTargetXml[] = R"(l @@ -229,13 +217,13 @@ void GDBStub::Listen(std::unique_ptr& client) { cache_.cur_thread_id = cache_.notify_thread_id; } - int sig_num = kSignalSigtrap; + SignalCode signal = SignalCode::SIGTRAP; if (cache_.notify_exception_code.has_value()) { if (cache_.notify_exception_code == xe::Exception::Code::kIllegalInstruction) { - sig_num = kSignalSigill; + signal = SignalCode::SIGILL; } else { - sig_num = kSignalSigsegv; + signal = SignalCode::SIGSEGV; } cache_.notify_exception_code.reset(); @@ -243,7 +231,7 @@ void GDBStub::Listen(std::unique_ptr& client) { } SendPacket(client, - GetThreadStateReply(cache_.notify_thread_id, sig_num)); + GetThreadStateReply(cache_.notify_thread_id, signal)); cache_.notify_thread_id = -1; cache_.notify_stopped = false; } @@ -255,8 +243,7 @@ void GDBStub::Listen(std::unique_ptr& client) { void GDBStub::SendPacket(std::unique_ptr& client, const std::string& data) { std::stringstream ss; - ss << char(GdbStubControl::PacketStart) << data - << char(GdbStubControl::PacketEnd); + ss << char(ControlCode::PacketStart) << data << char(ControlCode::PacketEnd); uint8_t checksum = 0; for (char c : data) { @@ -271,7 +258,7 @@ void GDBStub::SendPacket(std::unique_ptr& client, #ifdef DEBUG std::string GetPacketFriendlyName(const std::string& packetCommand) { - static const std::unordered_map command_names = { + static const std::unordered_map kGdbCommandNames = { {"?", "StartupQuery"}, {"!", "EnableExtendedMode"}, {"p", "RegRead"}, @@ -297,8 +284,8 @@ std::string GetPacketFriendlyName(const std::string& packetCommand) { }; std::string packet_name = ""; - auto it = command_names.find(packetCommand); - if (it != command_names.end()) { + auto it = kGdbCommandNames.find(packetCommand); + if (it != kGdbCommandNames.end()) { packet_name = it->second; } @@ -319,8 +306,7 @@ bool GDBStub::ProcessIncomingData(std::unique_ptr& client, // Hacky interrupt '\03' packet handling, some reason checksum isn't // attached to this? - bool isInterrupt = - buffer[0] == char(GdbStubControl::Interrupt) && received == 1; + bool isInterrupt = buffer[0] == char(ControlCode::Interrupt) && received == 1; // Check if we've received a full packet yet, if not exit and allow caller // to try again @@ -330,7 +316,7 @@ bool GDBStub::ProcessIncomingData(std::unique_ptr& client, if (isInterrupt || packet_end + 2 < receive_buffer.length()) { std::string current_packet; if (isInterrupt) { - current_packet = char(GdbStubControl::Interrupt); + current_packet = char(ControlCode::Interrupt); receive_buffer = ""; isInterrupt = false; } else { @@ -348,12 +334,12 @@ bool GDBStub::ProcessIncomingData(std::unique_ptr& client, command.data); #endif - GdbStubControl result = GdbStubControl::Ack; + ControlCode result = ControlCode::Ack; client->Send(&result, 1); std::string response = HandleGDBCommand(command); SendPacket(client, response); } else { - GdbStubControl result = GdbStubControl::Nack; + ControlCode result = ControlCode::Nack; client->Send(&result, 1); } } else { @@ -390,23 +376,23 @@ bool GDBStub::ParsePacket(const std::string& packet, GDBCommand& out_cmd) { char c = ReadCharFromBuffer(); // Expecting start of packet '$' - if (c != char(GdbStubControl::PacketStart)) { + if (c != char(ControlCode::PacketStart)) { // gdb starts conversation with + for some reason - if (c == char(GdbStubControl::Ack)) { + if (c == char(ControlCode::Ack)) { c = ReadCharFromBuffer(); } // and IDA sometimes has double +, grr - if (c == char(GdbStubControl::Ack)) { + if (c == char(ControlCode::Ack)) { c = ReadCharFromBuffer(); } // Interrupt is special, handle it without checking checksum - if (c == char(GdbStubControl::Interrupt)) { - out_cmd.cmd = char(GdbStubControl::Interrupt); + if (c == char(ControlCode::Interrupt)) { + out_cmd.cmd = char(ControlCode::Interrupt); out_cmd.data = ""; out_cmd.checksum = 0; return true; } - if (c != char(GdbStubControl::PacketStart)) { + if (c != char(ControlCode::PacketStart)) { return false; } } @@ -423,7 +409,7 @@ bool GDBStub::ParsePacket(const std::string& packet, GDBCommand& out_cmd) { c = ReadCharFromBuffer(); // If we reach the end of the buffer or hit '#', stop - if (c == '\0' || c == char(GdbStubControl::PacketEnd)) { + if (c == '\0' || c == char(ControlCode::PacketEnd)) { break; } @@ -488,12 +474,14 @@ std::string GDBStub::RegisterRead(xe::cpu::ThreadDebugInfo* thread, // mode (eg. IDA will switch to 64-bit and refuse to allow decompiler to work // with it) // - // TODO: add altivec/VMX registers here... + // TODO: add altivec/VMX registers here + // TODO: add cvar to allow switch to 64-bit mode? (unsure if any x360 opcodes + // use the upper 32-bits?) // // ids from gdb/features/rs6000/powerpc-64.c - switch (rid) { - // pc - case 64: { + switch (RegisterIndex(rid)) { + case RegisterIndex::PC: { + // // If we recently hit a BP then debugger is likely asking for registers // for it // @@ -514,71 +502,69 @@ std::string GDBStub::RegisterRead(xe::cpu::ThreadDebugInfo* thread, } return string_util::to_hex_string((uint32_t)0); } - case 65: + case RegisterIndex::MSR: return string_util::to_hex_string((uint32_t)thread->guest_context.msr); - case 66: + case RegisterIndex::CR: return string_util::to_hex_string((uint32_t)thread->guest_context.cr()); - case 67: + case RegisterIndex::LR: return string_util::to_hex_string((uint32_t)thread->guest_context.lr); - case 68: + case RegisterIndex::CTR: return string_util::to_hex_string((uint32_t)thread->guest_context.ctr); - // xer - case 69: + case RegisterIndex::XER: return std::string(8, 'x'); - case 70: + case RegisterIndex::FPSCR: return string_util::to_hex_string(thread->guest_context.fpscr.value); } - if (rid > 70) { + if (rid >= int(RegisterIndex::PC)) { return ""; } // fpr - if (rid > 31) { + if (rid >= int(RegisterIndex::FPR0)) { return string_util::to_hex_string(thread->guest_context.f[rid - 32]); } // gpr return string_util::to_hex_string((uint32_t)thread->guest_context.r[rid]); } + std::string GDBStub::RegisterWrite(xe::cpu::ThreadDebugInfo* thread, uint32_t rid, const std::string_view value) { + // Have to update the main thread context, and the ThreadDebugInfo context auto* guest_context = thread->thread->thread_state()->context(); - switch (rid) { - // pc - case 64: - return kGdbReplyOK; // TODO: figure a way to change this - case 65: + switch (RegisterIndex(rid)) { + case RegisterIndex::PC: + return kGdbReplyError; // TODO: figure a way to change this + case RegisterIndex::MSR: guest_context->msr = string_util::from_string(value, true); thread->guest_context.msr = guest_context->msr; return kGdbReplyOK; - case 66: - // CR - return kGdbReplyOK; // TODO: figure a way to change this - case 67: + case RegisterIndex::CR: + return kGdbReplyError; // TODO: figure a way to change this + case RegisterIndex::LR: guest_context->lr = string_util::from_string(value, true); thread->guest_context.lr = guest_context->lr; return kGdbReplyOK; - case 68: + case RegisterIndex::CTR: guest_context->ctr = string_util::from_string(value, true); thread->guest_context.ctr = guest_context->ctr; return kGdbReplyOK; - // xer - case 69: - return kGdbReplyOK; - case 70: + case RegisterIndex::XER: + return kGdbReplyError; + case RegisterIndex::FPSCR: guest_context->fpscr.value = string_util::from_string(value, true); thread->guest_context.fpscr.value = guest_context->fpscr.value; return kGdbReplyOK; } - if (rid > 70) { + if (rid >= int(RegisterIndex::PC)) { return kGdbReplyError; } // fpr - if (rid > 31) { + if (rid >= int(RegisterIndex::FPR0)) { guest_context->f[rid - 32] = string_util::from_string(value, true); thread->guest_context.f[rid - 32] = guest_context->f[rid - 32]; return kGdbReplyOK; @@ -625,7 +611,7 @@ std::string GDBStub::RegisterReadAll() { return kGdbReplyError; } std::string result; - result.reserve(68 * 16 + 3 * 8); + result.reserve((39 * 8) + (32 * 16)); for (int i = 0; i < 71; ++i) { result += RegisterRead(thread, i); } @@ -638,6 +624,11 @@ std::string GDBStub::RegisterWriteAll(const std::string& data) { return kGdbReplyError; } + int expected_size = (39 * 8) + (32 * 16); + if (data.length() != expected_size) { + return kGdbReplyError; + } + int string_offset = 0; for (int i = 0; i < 71; ++i) { int reg_size = 8; // 8 hex-nibbles per 32-bit register @@ -746,6 +737,14 @@ std::string GDBStub::MemoryWrite(const std::string& data) { if (!heap) { return kGdbReplyError; } + + // Check if they're trying to write to an executable function + if (auto* exe_addr = processor_->LookupFunction(addr)) { + // TODO: allow the write and ask processor to recompile if no breakpoints + // are set there? + return kGdbReplyError; // error for now as writes here won't have an effect + } + uint32_t protect = 0; if (!heap->QueryProtect(addr, &protect) || (protect & kMemoryProtectRead) != kMemoryProtectRead) { @@ -791,10 +790,8 @@ std::string GDBStub::BuildThreadList() { return buffer; } -std::string GDBStub::GetThreadStateReply(uint32_t thread_id, uint8_t signal) { - constexpr int PC_REGISTER = 64; - constexpr int LR_REGISTER = 67; - +std::string GDBStub::GetThreadStateReply(uint32_t thread_id, + SignalCode signal) { auto* thread = cache_.thread_info(thread_id); if (thread_id != -1 && thread) { @@ -813,7 +810,8 @@ std::string GDBStub::GetThreadStateReply(uint32_t thread_id, uint8_t signal) { } return fmt::format("T{:02x}{:02x}:{:08x};{:02x}:{:08x};thread:{:x};", - signal, PC_REGISTER, uint32_t(pc_value), LR_REGISTER, + uint8_t(signal), int(RegisterIndex::PC), + uint32_t(pc_value), int(RegisterIndex::LR), uint32_t(thread->guest_context.lr), thread_id); } return "S05"; diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h index 2e1d7f096..b8b71e02c 100644 --- a/src/xenia/debug/gdb/gdbstub.h +++ b/src/xenia/debug/gdb/gdbstub.h @@ -26,6 +26,28 @@ namespace debug { namespace gdb { class GDBStub : public cpu::DebugListener { + enum class ControlCode : char { + Ack = '+', + Nack = '-', + PacketStart = '$', + PacketEnd = '#', + Interrupt = '\03', + }; + + enum class SignalCode : uint8_t { SIGILL = 4, SIGTRAP = 5, SIGSEGV = 11 }; + + enum class RegisterIndex : int { + GPR0 = 0, + FPR0 = 32, + PC = 64, + MSR = 65, + CR = 66, + LR = 67, + CTR = 68, + XER = 69, + FPSCR = 70 + }; + public: virtual ~GDBStub(); @@ -77,7 +99,7 @@ class GDBStub : public cpu::DebugListener { std::string MemoryWrite(const std::string& data); std::string BuildThreadList(); - std::string GetThreadStateReply(uint32_t thread_id, uint8_t signal); + std::string GetThreadStateReply(uint32_t thread_id, SignalCode signal); bool CreateCodeBreakpoint(uint64_t address); void DeleteCodeBreakpoint(uint64_t address); @@ -112,7 +134,6 @@ class GDBStub : public cpu::DebugListener { std::vector thread_debug_infos; struct { - char kernel_call_filter[64] = {0}; std::vector> all_breakpoints; std::unordered_map code_breakpoints_by_guest_address; From 220324b4631d48bce8460e5c10b662af4834435e Mon Sep 17 00:00:00 2001 From: emoose Date: Wed, 9 Oct 2024 01:14:40 +0100 Subject: [PATCH 14/16] [GDBStub] Fix debugger showing invalid symbols after pausing/continuing --- src/xenia/debug/gdb/gdbstub.cc | 87 ++++++++++++++++++++++------------ src/xenia/debug/gdb/gdbstub.h | 16 +++++-- 2 files changed, 68 insertions(+), 35 deletions(-) diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index 106ded38b..469efcd31 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -181,29 +181,33 @@ std::unique_ptr GDBStub::Create(Emulator* emulator, int listen_port) { } bool GDBStub::Initialize() { - socket_ = xe::SocketServer::Create( - listen_port_, [this](std::unique_ptr client) { Listen(client); }); + socket_ = xe::SocketServer::Create(listen_port_, + [this](std::unique_ptr socket) { + GDBClient client; + client.socket = std::move(socket); + Listen(client); + }); UpdateCache(); return true; } -void GDBStub::Listen(std::unique_ptr& client) { +void GDBStub::Listen(GDBClient& client) { // Client is connected - pause execution ExecutionPause(); UpdateCache(); - client->set_nonblocking(true); + client.socket->set_nonblocking(true); std::string receive_buffer; while (!stop_thread_) { - if (!client->is_connected()) { + if (!client.socket->is_connected()) { break; } - if (!ProcessIncomingData(client, receive_buffer)) { - if (!client->is_connected()) { + if (!ProcessIncomingData(client)) { + if (!client.socket->is_connected()) { break; } // No data available, can do other work or sleep @@ -240,8 +244,7 @@ void GDBStub::Listen(std::unique_ptr& client) { } } -void GDBStub::SendPacket(std::unique_ptr& client, - const std::string& data) { +void GDBStub::SendPacket(GDBClient& client, const std::string& data) { std::stringstream ss; ss << char(ControlCode::PacketStart) << data << char(ControlCode::PacketEnd); @@ -253,7 +256,7 @@ void GDBStub::SendPacket(std::unique_ptr& client, ss << std::hex << std::setw(2) << std::setfill('0') << (checksum & 0xff); std::string packet = ss.str(); - client->Send(packet.c_str(), packet.size()); + client.socket->Send(packet.c_str(), packet.size()); } #ifdef DEBUG @@ -293,16 +296,15 @@ std::string GetPacketFriendlyName(const std::string& packetCommand) { } #endif -bool GDBStub::ProcessIncomingData(std::unique_ptr& client, - std::string& receive_buffer) { +bool GDBStub::ProcessIncomingData(GDBClient& client) { char buffer[1024]; - size_t received = client->Receive(buffer, sizeof(buffer)); + size_t received = client.socket->Receive(buffer, sizeof(buffer)); if (received == uint64_t(-1) || // disconnected received == 0) { return false; } - receive_buffer.append(buffer, received); + client.receive_buffer.append(buffer, received); // Hacky interrupt '\03' packet handling, some reason checksum isn't // attached to this? @@ -312,16 +314,16 @@ bool GDBStub::ProcessIncomingData(std::unique_ptr& client, // to try again size_t packet_end; while (isInterrupt || - (packet_end = receive_buffer.find('#')) != std::string::npos) { - if (isInterrupt || packet_end + 2 < receive_buffer.length()) { + (packet_end = client.receive_buffer.find('#')) != std::string::npos) { + if (isInterrupt || packet_end + 2 < client.receive_buffer.length()) { std::string current_packet; if (isInterrupt) { current_packet = char(ControlCode::Interrupt); - receive_buffer = ""; + client.receive_buffer = ""; isInterrupt = false; } else { - current_packet = receive_buffer.substr(0, packet_end + 3); - receive_buffer = receive_buffer.substr(packet_end + 3); + current_packet = client.receive_buffer.substr(0, packet_end + 3); + client.receive_buffer = client.receive_buffer.substr(packet_end + 3); } GDBCommand command; @@ -334,13 +336,18 @@ bool GDBStub::ProcessIncomingData(std::unique_ptr& client, command.data); #endif - ControlCode result = ControlCode::Ack; - client->Send(&result, 1); - std::string response = HandleGDBCommand(command); + if (!client.no_ack_mode) { + ControlCode result = ControlCode::Ack; + client.socket->Send(&result, 1); + } + + std::string response = HandleGDBCommand(client, command); SendPacket(client, response); } else { - ControlCode result = ControlCode::Nack; - client->Send(&result, 1); + if (!client.no_ack_mode) { + ControlCode result = ControlCode::Nack; + client.socket->Send(&result, 1); + } } } else { break; @@ -653,7 +660,7 @@ std::string GDBStub::ExecutionPause() { return kGdbReplyError; } processor_->Pause(); - return kGdbReplyOK; + return ""; } std::string GDBStub::ExecutionContinue() { @@ -661,7 +668,7 @@ std::string GDBStub::ExecutionContinue() { debugging::DebugPrint("GDBStub: ExecutionContinue\n"); #endif processor_->Continue(); - return kGdbReplyOK; + return ""; } std::string GDBStub::ExecutionStep() { @@ -674,7 +681,7 @@ std::string GDBStub::ExecutionStep() { processor_->StepGuestInstruction(cache_.last_bp_thread_id); } - return kGdbReplyOK; + return ""; } std::string GDBStub::MemoryRead(const std::string& data) { @@ -739,7 +746,7 @@ std::string GDBStub::MemoryWrite(const std::string& data) { } // Check if they're trying to write to an executable function - if (auto* exe_addr = processor_->LookupFunction(addr)) { + if (processor_->LookupFunction(addr) != nullptr) { // TODO: allow the write and ask processor to recompile if no breakpoints // are set there? return kGdbReplyError; // error for now as writes here won't have an effect @@ -790,6 +797,14 @@ std::string GDBStub::BuildThreadList() { return buffer; } +std::string GDBStub::QueryPacket(GDBClient& client, const std::string& data) { + if (data == "StartNoAckMode") { + client.no_ack_mode = true; + return kGdbReplyOK; + } + return kGdbReplyError; +} + std::string GDBStub::GetThreadStateReply(uint32_t thread_id, SignalCode signal) { auto* thread = cache_.thread_info(thread_id); @@ -822,6 +837,11 @@ bool GDBStub::CreateCodeBreakpoint(uint64_t address) { debugging::DebugPrint("GDBStub: Adding breakpoint: {:X}\n", address); #endif + auto* exe_addr = processor_->LookupFunction((uint32_t)address); + if (!exe_addr) { + return false; // TODO: move this check to Breakpoint? + } + auto& state = cache_.breakpoints; auto breakpoint = std::make_unique( processor_, Breakpoint::AddressType::kGuest, address, @@ -969,7 +989,8 @@ void GDBStub::OnBreakpointHit(Breakpoint* breakpoint, UpdateCache(); } -std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { +std::string GDBStub::HandleGDBCommand(GDBClient& client, + const GDBCommand& command) { static const std::unordered_map> command_map = { @@ -1021,6 +1042,12 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { {"G", [&](const GDBCommand& cmd) { return RegisterWriteAll(cmd.data); }}, + // Query / setting change + {"Q", + [&](const GDBCommand& cmd) { + return QueryPacket(client, cmd.data); + }}, + // Attach to specific process ID - IDA used to send this, but doesn't // after some changes? {"vAttach", [&](const GDBCommand& cmd) { return "S05"; }}, @@ -1099,7 +1126,7 @@ std::string GDBStub::HandleGDBCommand(const GDBCommand& command) { {"qSupported", [&](const GDBCommand& cmd) { return "PacketSize=1024;qXfer:features:read+;qXfer:threads:read+;" - "qXfer:memory-map:read+"; + "qXfer:memory-map:read+;QStartNoAckMode+"; }}, // Thread list (IDA requests this but ignores in favor of qXfer?) {"qfThreadInfo", diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h index b8b71e02c..39c80cb5b 100644 --- a/src/xenia/debug/gdb/gdbstub.h +++ b/src/xenia/debug/gdb/gdbstub.h @@ -72,15 +72,20 @@ class GDBStub : public cpu::DebugListener { uint8_t checksum{}; }; + struct GDBClient { + std::unique_ptr socket; + bool no_ack_mode = false; + std::string receive_buffer; + }; + explicit GDBStub(Emulator* emulator, int listen_port); bool Initialize(); - void Listen(std::unique_ptr& client); - void SendPacket(std::unique_ptr& client, const std::string& data); - bool ProcessIncomingData(std::unique_ptr& client, - std::string& receive_buffer); + void Listen(GDBClient& client); + void SendPacket(GDBClient& client, const std::string& data); + bool ProcessIncomingData(GDBClient& client); bool ParsePacket(const std::string& packet, GDBCommand& out_cmd); - std::string HandleGDBCommand(const GDBCommand& command); + std::string HandleGDBCommand(GDBClient& client, const GDBCommand& command); void UpdateCache(); @@ -98,6 +103,7 @@ class GDBStub : public cpu::DebugListener { std::string MemoryRead(const std::string& data); std::string MemoryWrite(const std::string& data); std::string BuildThreadList(); + std::string QueryPacket(GDBClient& client, const std::string& data); std::string GetThreadStateReply(uint32_t thread_id, SignalCode signal); From 2e9c9f403d01d48a093fbc0fad22ab81ba74612a Mon Sep 17 00:00:00 2001 From: emoose Date: Thu, 24 Oct 2024 00:27:59 +0100 Subject: [PATCH 15/16] [GDBStub] Forward `DbgPrint` & `DmSendNotificationString` messages TODO: see if there's any other debug-string-output functions that we can pass to debugger? Might be worth adding a custom GDB command for toggling these too, some games could be spammy --- src/xenia/cpu/debug_listener.h | 3 ++ src/xenia/debug/gdb/gdbstub.cc | 47 ++++++++++++++++++- src/xenia/debug/gdb/gdbstub.h | 5 +- src/xenia/debug/ui/debug_window.cc | 2 + src/xenia/debug/ui/debug_window.h | 1 + src/xenia/kernel/xbdm/xbdm_misc.cc | 10 +++- src/xenia/kernel/xboxkrnl/xboxkrnl_strings.cc | 5 ++ 7 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/xenia/cpu/debug_listener.h b/src/xenia/cpu/debug_listener.h index 6233e87fe..6a9f4864e 100644 --- a/src/xenia/cpu/debug_listener.h +++ b/src/xenia/cpu/debug_listener.h @@ -52,6 +52,9 @@ class DebugListener { // Breakpoints may be hit during stepping. virtual void OnBreakpointHit(Breakpoint* breakpoint, ThreadDebugInfo* thread_info) = 0; + + // Handles any debug messages from the guest + virtual void OnDebugPrint(const std::string_view message) = 0; }; } // namespace cpu diff --git a/src/xenia/debug/gdb/gdbstub.cc b/src/xenia/debug/gdb/gdbstub.cc index 469efcd31..407577a55 100644 --- a/src/xenia/debug/gdb/gdbstub.cc +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -143,6 +143,7 @@ constexpr char kTargetXml[] = )"; +// TODO: move these to string_util.h? std::string to_hexbyte(uint8_t i) { std::string result = "00"; uint8_t i1 = i & 0xF; @@ -164,6 +165,23 @@ uint8_t from_hexchar(char c) { return 0; } +template +inline std::string to_hex_string(const T* data, size_t count) { + // Ensure that T is trivially copyable + static_assert(std::is_trivially_copyable::value, + "T must be a trivially copyable type"); + + static const char hex_chars[] = "0123456789ABCDEF"; + std::string result; + result.reserve(sizeof(T) * count * 2); + const auto* bytes = reinterpret_cast(data); + for (size_t i = 0; i < sizeof(T) * count; ++i) { + result += hex_chars[bytes[i] >> 4]; + result += hex_chars[bytes[i] & 0x0F]; + } + return result; +} + GDBStub::GDBStub(Emulator* emulator, int listen_port) : emulator_(emulator), processor_(emulator->processor()), @@ -211,7 +229,7 @@ void GDBStub::Listen(GDBClient& client) { break; } // No data available, can do other work or sleep - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Check if we need to notify client about anything { @@ -238,6 +256,13 @@ void GDBStub::Listen(GDBClient& client) { GetThreadStateReply(cache_.notify_thread_id, signal)); cache_.notify_thread_id = -1; cache_.notify_stopped = false; + } else if (cache_.notify_debug_prints.size()) { + // Send the oldest message in our queue, only send 1 per loop to + // reduce spamming the client & process any incoming packets + std::string& message = cache_.notify_debug_prints.front(); + SendPacket(client, + "O" + to_hex_string(message.c_str(), message.length())); + cache_.notify_debug_prints.pop(); } } } @@ -676,6 +701,7 @@ std::string GDBStub::ExecutionStep() { debugging::DebugPrint("GDBStub: ExecutionStep (thread {})\n", cache_.last_bp_thread_id); #endif + std::unique_lock lock(mtx_); if (cache_.last_bp_thread_id != -1) { processor_->StepGuestInstruction(cache_.last_bp_thread_id); @@ -783,6 +809,8 @@ std::string GDBStub::MemoryWrite(const std::string& data) { } std::string GDBStub::BuildThreadList() { + std::unique_lock lock(mtx_); + std::string buffer; buffer += "l"; buffer += ""; @@ -836,6 +864,7 @@ bool GDBStub::CreateCodeBreakpoint(uint64_t address) { #ifdef DEBUG debugging::DebugPrint("GDBStub: Adding breakpoint: {:X}\n", address); #endif + std::unique_lock lock(mtx_); auto* exe_addr = processor_->LookupFunction((uint32_t)address); if (!exe_addr) { @@ -888,6 +917,7 @@ void GDBStub::DeleteCodeBreakpoint(uint64_t address) { } void GDBStub::DeleteCodeBreakpoint(Breakpoint* breakpoint) { + std::unique_lock lock(mtx_); auto& state = cache_.breakpoints; for (size_t i = 0; i < state.all_breakpoints.size(); ++i) { if (state.all_breakpoints[i].get() != breakpoint) { @@ -918,6 +948,7 @@ void GDBStub::OnFocus() {} void GDBStub::OnDetached() { UpdateCache(); + std::unique_lock lock(mtx_); // Delete all breakpoints auto& state = cache_.breakpoints; @@ -969,10 +1000,13 @@ void GDBStub::OnStepCompleted(cpu::ThreadDebugInfo* thread_info) { #ifdef DEBUG debugging::DebugPrint("GDBStub: OnStepCompleted\n"); #endif + std::unique_lock lock(mtx_); + // Some debuggers like IDA will remove the current breakpoint & step into next // instruction, only re-adding BP after it's told about the step cache_.notify_thread_id = thread_info->thread_id; cache_.last_bp_thread_id = thread_info->thread_id; + UpdateCache(); } @@ -983,12 +1017,20 @@ void GDBStub::OnBreakpointHit(Breakpoint* breakpoint, breakpoint->address(), thread_info->thread_id); #endif + std::unique_lock lock(mtx_); + cache_.notify_bp_guest_address = breakpoint->address(); cache_.notify_thread_id = thread_info->thread_id; cache_.last_bp_thread_id = thread_info->thread_id; + UpdateCache(); } +void GDBStub::OnDebugPrint(const std::string_view message) { + std::unique_lock lock(mtx_); + cache_.notify_debug_prints.push(std::string(message)); +} + std::string GDBStub::HandleGDBCommand(GDBClient& client, const GDBCommand& command) { static const std::unordered_map lock(mtx_); auto* thread = cache_.cur_thread_info(); if (!thread) { return std::string(kGdbReplyError); @@ -1064,6 +1107,7 @@ std::string GDBStub::HandleGDBCommand(GDBClient& client, // Set current debugger thread ID {"H", [&](const GDBCommand& cmd) { + std::unique_lock lock(mtx_); int threadId = std::stol(cmd.data.substr(1), 0, 16); if (!threadId) { @@ -1131,6 +1175,7 @@ std::string GDBStub::HandleGDBCommand(GDBClient& client, // Thread list (IDA requests this but ignores in favor of qXfer?) {"qfThreadInfo", [&](const GDBCommand& cmd) { + std::unique_lock lock(mtx_); std::string result; for (auto& thread : cache_.thread_debug_infos) { if (!result.empty()) result += ","; diff --git a/src/xenia/debug/gdb/gdbstub.h b/src/xenia/debug/gdb/gdbstub.h index 39c80cb5b..e4ed4c16c 100644 --- a/src/xenia/debug/gdb/gdbstub.h +++ b/src/xenia/debug/gdb/gdbstub.h @@ -11,6 +11,7 @@ #define XENIA_DEBUG_GDB_GDBSTUB_H_ #include +#include #include #include "xenia/base/host_thread_context.h" @@ -64,6 +65,7 @@ class GDBStub : public cpu::DebugListener { void OnStepCompleted(cpu::ThreadDebugInfo* thread_info) override; void OnBreakpointHit(cpu::Breakpoint* breakpoint, cpu::ThreadDebugInfo* thread_info) override; + void OnDebugPrint(const std::string_view message) override; private: struct GDBCommand { @@ -119,7 +121,6 @@ class GDBStub : public cpu::DebugListener { std::unique_ptr socket_; std::mutex mtx_; - std::condition_variable cv_; bool stop_thread_ = false; xe::global_critical_region global_critical_region_; @@ -135,6 +136,8 @@ class GDBStub : public cpu::DebugListener { notify_exception_access_violation; std::optional notify_exception_code; + std::queue notify_debug_prints; + bool is_stopped = false; std::vector> modules; std::vector thread_debug_infos; diff --git a/src/xenia/debug/ui/debug_window.cc b/src/xenia/debug/ui/debug_window.cc index 83b076c46..3e7dc0f05 100644 --- a/src/xenia/debug/ui/debug_window.cc +++ b/src/xenia/debug/ui/debug_window.cc @@ -1606,6 +1606,8 @@ void DebugWindow::OnBreakpointHit(Breakpoint* breakpoint, Focus(); } +void DebugWindow::OnDebugPrint(const std::string_view message) {} + void DebugWindow::Focus() const { app_context_.CallInUIThread([this]() { window_->Focus(); }); } diff --git a/src/xenia/debug/ui/debug_window.h b/src/xenia/debug/ui/debug_window.h index c58a3d448..7d6039440 100644 --- a/src/xenia/debug/ui/debug_window.h +++ b/src/xenia/debug/ui/debug_window.h @@ -51,6 +51,7 @@ class DebugWindow : public cpu::DebugListener { void OnStepCompleted(cpu::ThreadDebugInfo* thread_info) override; void OnBreakpointHit(cpu::Breakpoint* breakpoint, cpu::ThreadDebugInfo* thread_info) override; + void OnDebugPrint(const std::string_view message) override; private: class DebugDialog final : public xe::ui::ImGuiDialog { diff --git a/src/xenia/kernel/xbdm/xbdm_misc.cc b/src/xenia/kernel/xbdm/xbdm_misc.cc index 7adc3072e..b31fc2d68 100644 --- a/src/xenia/kernel/xbdm/xbdm_misc.cc +++ b/src/xenia/kernel/xbdm/xbdm_misc.cc @@ -8,6 +8,7 @@ */ #include "xenia/base/logging.h" +#include "xenia/cpu/processor.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xbdm/xbdm_private.h" @@ -69,7 +70,14 @@ DECLARE_XBDM_EXPORT1(DmGetXboxName, kDebug, kImplemented) dword_result_t DmIsDebuggerPresent_entry() { return 0; } DECLARE_XBDM_EXPORT1(DmIsDebuggerPresent, kDebug, kStub); -void DmSendNotificationString_entry(lpdword_t unk0_ptr) {} +void DmSendNotificationString_entry(lpstring_t message) { + XELOGI("(DmSendNotificationString) {}", message.value()); + + if (cpu::DebugListener* listener = + kernel_state()->processor()->debug_listener()) { + listener->OnDebugPrint(message.value()); + } +} DECLARE_XBDM_EXPORT1(DmSendNotificationString, kDebug, kStub); dword_result_t DmRegisterCommandProcessor_entry(lpdword_t name_ptr, diff --git a/src/xenia/kernel/xboxkrnl/xboxkrnl_strings.cc b/src/xenia/kernel/xboxkrnl/xboxkrnl_strings.cc index 6da1b5380..3ec34cee9 100644 --- a/src/xenia/kernel/xboxkrnl/xboxkrnl_strings.cc +++ b/src/xenia/kernel/xboxkrnl/xboxkrnl_strings.cc @@ -13,6 +13,7 @@ #include #include "xenia/base/logging.h" +#include "xenia/cpu/processor.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/user_module.h" #include "xenia/kernel/util/shim_utils.h" @@ -850,6 +851,10 @@ SHIM_CALL DbgPrint_entry(PPCContext* ppc_context) { XELOGI("(DbgPrint) {}", str); + if (cpu::DebugListener* listener = ppc_context->processor->debug_listener()) { + listener->OnDebugPrint(str); + } + SHIM_SET_RETURN_32(X_STATUS_SUCCESS); } From 591755432430ddf7751dc02f50deadde0c3ee011 Mon Sep 17 00:00:00 2001 From: emoose Date: Thu, 24 Oct 2024 00:28:38 +0100 Subject: [PATCH 16/16] [App] Update gdbstub setting description --- src/xenia/app/xenia_main.cc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index 4cf17fdbb..9f62e8a4c 100644 --- a/src/xenia/app/xenia_main.cc +++ b/src/xenia/app/xenia_main.cc @@ -103,8 +103,10 @@ DEFINE_transient_bool(portable, true, "General"); DECLARE_bool(debug); -DEFINE_int32(gdbport, 0, "Port for GDBStub debugger to listen on, 0 = disable", - "General"); +DEFINE_int32( + gdbport, 0, + "Port for GDBStub debugger to listen on, requires --debug (0 = disable)", + "General"); DEFINE_bool(discord, true, "Enable Discord rich presence", "General");