diff --git a/premake5.lua b/premake5.lua index bf143ba6d..44f69f17b 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 11a7a0100..2895e551e 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 0a2dcdb26..9b18b04f4 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" @@ -100,6 +101,10 @@ DEFINE_transient_bool(portable, true, "General"); DECLARE_bool(debug); +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"); @@ -228,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_; @@ -566,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/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/cpu/debug_listener.h b/src/xenia/cpu/debug_listener.h index 3e10321db..6a9f4864e 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; @@ -49,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/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/cpu/processor.cc b/src/xenia/cpu/processor.cc index ebe5403e6..ae428ee92 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; } @@ -670,18 +671,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; } @@ -722,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. @@ -936,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(); @@ -969,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 new file mode 100644 index 000000000..407577a55 --- /dev/null +++ b/src/xenia/debug/gdb/gdbstub.cc @@ -0,0 +1,1198 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2024 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 "xenia/base/clock.h" +#include "xenia/base/debugging.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/cpu/thread.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; + +constexpr const char* kGdbReplyOK = "OK"; +constexpr const char* kGdbReplyError = "E01"; + +// must start with l for debugger to accept it +constexpr char kMemoryMapXml[] = + R"(l + + + + + + + + + + +)"; + +// TODO: add power-altivec.xml (and update RegisterRead to support it) +constexpr char kTargetXml[] = + R"(l + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; + +// TODO: move these to string_util.h? +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; +} + +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()), + listen_port_(listen_port) {} + +GDBStub::~GDBStub() { stop_thread_ = true; } + +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() { + 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(GDBClient& client) { + // Client is connected - pause execution + ExecutionPause(); + UpdateCache(); + + client.socket->set_nonblocking(true); + + std::string receive_buffer; + + while (!stop_thread_) { + if (!client.socket->is_connected()) { + break; + } + + if (!ProcessIncomingData(client)) { + if (!client.socket->is_connected()) { + break; + } + // No data available, can do other work or sleep + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Check if we need to notify client about anything + { + std::unique_lock lock(mtx_); + if (cache_.notify_stopped) { + if (cache_.notify_thread_id != -1) { + cache_.cur_thread_id = cache_.notify_thread_id; + } + + SignalCode signal = SignalCode::SIGTRAP; + if (cache_.notify_exception_code.has_value()) { + if (cache_.notify_exception_code == + xe::Exception::Code::kIllegalInstruction) { + signal = SignalCode::SIGILL; + } else { + signal = SignalCode::SIGSEGV; + } + + cache_.notify_exception_code.reset(); + cache_.notify_exception_access_violation.reset(); + } + + SendPacket(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(); + } + } + } + } +} + +void GDBStub::SendPacket(GDBClient& client, const std::string& data) { + std::stringstream ss; + ss << char(ControlCode::PacketStart) << data << char(ControlCode::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(); + + client.socket->Send(packet.c_str(), packet.size()); +} + +#ifdef DEBUG +std::string GetPacketFriendlyName(const std::string& packetCommand) { + static const std::unordered_map kGdbCommandNames = { + {"?", "StartupQuery"}, + {"!", "EnableExtendedMode"}, + {"p", "RegRead"}, + {"P", "RegWrite"}, + {"g", "RegReadAll"}, + {"G", "RegWriteAll"}, + {"C", "Continue"}, + {"c", "continue"}, + {"s", "step"}, + {"vAttach", "vAttach"}, + {"m", "MemRead"}, + {"M", "MemWrite"}, + {"H", "SetThreadId"}, + {"Z", "BreakpointCreate"}, + {"z", "BreakpointDelete"}, + {"qXfer", "Xfer"}, + {"qSupported", "Supported"}, + {"qfThreadInfo", "qfThreadInfo"}, + {"qC", "GetThreadId"}, + {"D", "Detach"}, + {"k", "KillRequest"}, + {"\03", "Break"}, + }; + + std::string packet_name = ""; + auto it = kGdbCommandNames.find(packetCommand); + if (it != kGdbCommandNames.end()) { + packet_name = it->second; + } + + return packet_name; +} +#endif + +bool GDBStub::ProcessIncomingData(GDBClient& client) { + char buffer[1024]; + size_t received = client.socket->Receive(buffer, sizeof(buffer)); + if (received == uint64_t(-1) || // disconnected + received == 0) { + return false; + } + + client.receive_buffer.append(buffer, received); + + // Hacky interrupt '\03' packet handling, some reason checksum isn't + // attached to this? + 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 + size_t packet_end; + while (isInterrupt || + (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); + client.receive_buffer = ""; + isInterrupt = false; + } else { + current_packet = client.receive_buffer.substr(0, packet_end + 3); + client.receive_buffer = client.receive_buffer.substr(packet_end + 3); + } + + GDBCommand command; + if (ParsePacket(current_packet, command)) { +#ifdef DEBUG + auto packet_name = GetPacketFriendlyName(command.cmd); + + debugging::DebugPrint("GDBStub: Packet {}({})\n", + packet_name.empty() ? command.cmd : packet_name, + command.data); +#endif + + if (!client.no_ack_mode) { + ControlCode result = ControlCode::Ack; + client.socket->Send(&result, 1); + } + + std::string response = HandleGDBCommand(client, command); + SendPacket(client, response); + } else { + if (!client.no_ack_mode) { + ControlCode result = ControlCode::Nack; + client.socket->Send(&result, 1); + } + } + } else { + break; + } + } + + return true; +} + +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 >= packet.size()) { + return '\0'; + } + return packet[buffer_index++]; + }; + + // Parse two hex digits from buffer + auto ReadHexByteFromBuffer = [&]() -> char { + if (buffer_index + 2 > packet.size()) { + return 0; + } + char high = packet[buffer_index++]; + char low = 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(ControlCode::PacketStart)) { + // gdb starts conversation with + for some reason + if (c == char(ControlCode::Ack)) { + c = ReadCharFromBuffer(); + } + // and IDA sometimes has double +, grr + if (c == char(ControlCode::Ack)) { + c = ReadCharFromBuffer(); + } + // Interrupt is special, handle it without checking checksum + if (c == char(ControlCode::Interrupt)) { + out_cmd.cmd = char(ControlCode::Interrupt); + out_cmd.data = ""; + out_cmd.checksum = 0; + return true; + } + if (c != char(ControlCode::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(ControlCode::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(); + if (cache_.cur_thread_id == -1) { + cache_.cur_thread_id = emulator_->main_thread_id(); + } +} + +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 + // with it) + // + // 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 (RegisterIndex(rid)) { + case RegisterIndex::PC: { + // + // 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 RegisterIndex::MSR: + return string_util::to_hex_string((uint32_t)thread->guest_context.msr); + case RegisterIndex::CR: + return string_util::to_hex_string((uint32_t)thread->guest_context.cr()); + case RegisterIndex::LR: + return string_util::to_hex_string((uint32_t)thread->guest_context.lr); + case RegisterIndex::CTR: + return string_util::to_hex_string((uint32_t)thread->guest_context.ctr); + case RegisterIndex::XER: + return std::string(8, 'x'); + case RegisterIndex::FPSCR: + return string_util::to_hex_string(thread->guest_context.fpscr.value); + } + + if (rid >= int(RegisterIndex::PC)) { + return ""; + } + + // fpr + 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 (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 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 RegisterIndex::CTR: + guest_context->ctr = string_util::from_string(value, true); + thread->guest_context.ctr = guest_context->ctr; + return kGdbReplyOK; + 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 >= int(RegisterIndex::PC)) { + return kGdbReplyError; + } + + // fpr + 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; + } + + // 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(); + if (!thread) { + return kGdbReplyError; + } + uint32_t rid = string_util::from_string(data, true); + std::string result = RegisterRead(thread, rid); + if (result.empty()) { + return kGdbReplyError; + } + 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) { + return kGdbReplyError; + } + std::string result; + result.reserve((39 * 8) + (32 * 16)); + for (int i = 0; i < 71; ++i) { + result += RegisterRead(thread, i); + } + return result; +} + +std::string GDBStub::RegisterWriteAll(const std::string& data) { + auto* thread = cache_.cur_thread_info(); + if (!thread) { + 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 + 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"); +#endif + if (processor_->execution_state() != cpu::ExecutionState::kRunning) { + return kGdbReplyError; + } + processor_->Pause(); + return ""; +} + +std::string GDBStub::ExecutionContinue() { +#ifdef DEBUG + debugging::DebugPrint("GDBStub: ExecutionContinue\n"); +#endif + processor_->Continue(); + return ""; +} + +std::string GDBStub::ExecutionStep() { +#ifdef DEBUG + 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); + } + + return ""; +} + +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) { + 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::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; + } + + // Check if they're trying to write to an executable function + 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 + } + + 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::unique_lock lock(mtx_); + + 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::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); + + 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; + } + } + + // 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}:{:08x};{:02x}:{:08x};thread:{:x};", + uint8_t(signal), int(RegisterIndex::PC), + uint32_t(pc_value), int(RegisterIndex::LR), + uint32_t(thread->guest_context.lr), thread_id); + } + return "S05"; +} + +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) { + return false; // TODO: move this check to Breakpoint? + } + + 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); + }); + + // 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; + 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) { +#ifdef DEBUG + debugging::DebugPrint("GDBStub: Deleting breakpoint: {:X}\n", address); +#endif + auto* breakpoint = LookupBreakpointAtAddress(address); + if (!breakpoint) { + return; + } + DeleteCodeBreakpoint(breakpoint); +} + +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) { + 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(); + + std::unique_lock lock(mtx_); + // 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(); + } +} + +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"); +#endif + UpdateCache(); +} + +void GDBStub::OnExecutionContinued() { +#ifdef DEBUG + debugging::DebugPrint("GDBStub: OnExecutionContinued\n"); +#endif + UpdateCache(); +} + +void GDBStub::OnExecutionEnded() { +#ifdef DEBUG + debugging::DebugPrint("GDBStub: OnExecutionEnded\n"); +#endif + UpdateCache(); +} + +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(); +} + +void GDBStub::OnBreakpointHit(Breakpoint* breakpoint, + cpu::ThreadDebugInfo* thread_info) { +#ifdef DEBUG + debugging::DebugPrint("GDBStub: Breakpoint hit at {:X} (thread {})\n", + 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> + 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 paused + }}, + + // Detach + {"D", + [&](const GDBCommand& cmd) { + OnDetached(); + return kGdbReplyOK; + }}, + + // Kill request (just treat as detach for now) + {"k", + [&](const GDBCommand& cmd) { + OnDetached(); + return kGdbReplyOK; + }}, + + // Enable extended mode + {"!", [&](const GDBCommand& cmd) { return kGdbReplyOK; }}, + + // Execution continue + {"C", [&](const GDBCommand& cmd) { return ExecutionContinue(); }}, + // Execution continue + {"c", [&](const GDBCommand& cmd) { return ExecutionContinue(); }}, + // Execution step + {"s", [&](const GDBCommand& cmd) { return ExecutionStep(); }}, + // Execution interrupt + {"\03", [&](const GDBCommand& cmd) { return ExecutionPause(); }}, + + // Read memory + {"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 RegisterRead(cmd.data); }}, + // Write register + {"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); }}, + + // 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"; }}, + + // Get current debugger thread ID + {"qC", + [&](const GDBCommand& cmd) { + std::unique_lock lock(mtx_); + 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) { + std::unique_lock lock(mtx_); + int threadId = std::stol(cmd.data.substr(1), 0, 16); + + 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; + }}, + + // Create breakpoint + {"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); + + return CreateCodeBreakpoint(addr) ? kGdbReplyOK : kGdbReplyError; + }}, + // Delete breakpoint + {"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; + }}, + + // Data transfer + {"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 std::string(kTargetXml); + } else if (sub_cmd == "memory-map") { + return std::string(kMemoryMapXml); + } else if (sub_cmd == "threads") { + return BuildThreadList(); + } + return std::string(kGdbReplyError); + }}, + // Supported features + {"qSupported", + [&](const GDBCommand& cmd) { + return "PacketSize=1024;qXfer:features:read+;qXfer:threads:read+;" + "qXfer:memory-map:read+;QStartNoAckMode+"; + }}, + // 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 += ","; + result += std::to_string(thread->thread_id); + } + return "m" + result; + }}, + }; + + 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..e4ed4c16c --- /dev/null +++ b/src/xenia/debug/gdb/gdbstub.h @@ -0,0 +1,169 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2024 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 + +#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" +#include "xenia/emulator.h" +#include "xenia/xbox.h" + +namespace xe { +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(); + + static std::unique_ptr Create(Emulator* emulator, int listen_port); + + Emulator* emulator() const { return emulator_; } + + void OnFocus() override; + void OnDetached() override; + void OnUnhandledException(Exception* ex) 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; + void OnDebugPrint(const std::string_view message) override; + + private: + struct GDBCommand { + std::string cmd{}; + std::string data{}; + 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(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(GDBClient& client, const GDBCommand& command); + + void UpdateCache(); + + 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 RegisterWriteAll(const std::string& data); + std::string ExecutionPause(); + std::string ExecutionContinue(); + std::string ExecutionStep(); + 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); + + bool 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::unique_ptr socket_; + + std::mutex mtx_; + 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_thread_id = -1; + bool notify_stopped = false; + std::optional + 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; + + struct { + 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() diff --git a/src/xenia/debug/ui/debug_window.cc b/src/xenia/debug/ui/debug_window.cc index 2e1055cec..81ea528cd 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(); @@ -1604,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 d40ce484f..7d6039440 100644 --- a/src/xenia/debug/ui/debug_window.h +++ b/src/xenia/debug/ui/debug_window.h @@ -44,12 +44,14 @@ 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; 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/emulator.cc b/src/xenia/emulator.cc index 2dad04bd9..b1ce39531 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -189,6 +189,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 6bc037759..50fa5c17d 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -125,6 +125,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_; } 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); }