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()