diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 0a2d7f307..5b8eabe1b 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -115,6 +115,8 @@ add_library(core STATIC frontend/mic.h gdbstub/gdbstub.cpp gdbstub/gdbstub.h + gdbstub/hio.cpp + gdbstub/hio.h hle/applets/applet.cpp hle/applets/applet.h hle/applets/erreula.cpp diff --git a/src/core/gdbstub/gdbstub.cpp b/src/core/gdbstub/gdbstub.cpp index 932d1f06d..073ecdc0e 100644 --- a/src/core/gdbstub/gdbstub.cpp +++ b/src/core/gdbstub/gdbstub.cpp @@ -35,6 +35,7 @@ #include "core/arm/arm_interface.h" #include "core/core.h" #include "core/gdbstub/gdbstub.h" +#include "core/gdbstub/hio.h" #include "core/hle/kernel/process.h" #include "core/loader/loader.h" #include "core/memory.h" @@ -261,13 +262,7 @@ static u8 NibbleToHex(u8 n) { } } -/** - * Converts input hex string characters into an array of equivalent of u8 bytes. - * - * @param src Pointer to array of output hex string characters. - * @param len Length of src array. - */ -static u32 HexToInt(const u8* src, std::size_t len) { +u32 HexToInt(const u8* src, std::size_t len) { u32 output = 0; while (len-- > 0) { output = (output << 4) | HexCharToValue(src[0]); @@ -492,12 +487,7 @@ static void SendPacket(const char packet) { } } -/** - * Send reply to gdb client. - * - * @param reply Reply to be sent to client. - */ -static void SendReply(const char* reply) { +void SendReply(const char* reply) { if (!IsConnected()) { return; } @@ -653,7 +643,7 @@ static void ReadCommand() { memset(command_buffer, 0, sizeof(command_buffer)); u8 c = ReadByte(); - if (c == '+') { + if (c == GDB_STUB_ACK) { // ignore ack return; } else if (c == 0x03) { @@ -843,7 +833,7 @@ static void ReadMemory() { u32 len = HexToInt(start_offset, static_cast((command_buffer + command_length) - start_offset)); - LOG_DEBUG(Debug_GDBStub, "gdb: addr: {:08x} len: {:08x}\n", addr, len); + LOG_DEBUG(Debug_GDBStub, "ReadMemory addr: {:08x} len: {:08x}", addr, len); if (len * 2 > sizeof(reply)) { SendReply("E01"); @@ -860,7 +850,11 @@ static void ReadMemory() { MemToGdbHex(reply, data.data(), len); reply[len * 2] = '\0'; - SendReply(reinterpret_cast(reply)); + + auto reply_str = reinterpret_cast(reply); + + LOG_DEBUG(Debug_GDBStub, "ReadMemory result: {}", reply_str); + SendReply(reply_str); } /// Modify location in memory with data received from the gdb client. @@ -1050,6 +1044,11 @@ void HandlePacket() { return; } + if (HandlePendingHioRequestPacket()) { + // Don't do anything else while we wait for the client to respond + return; + } + if (!IsDataAvailable()) { return; } @@ -1059,7 +1058,7 @@ void HandlePacket() { return; } - LOG_DEBUG(Debug_GDBStub, "Packet: {}", command_buffer[0]); + LOG_DEBUG(Debug_GDBStub, "Packet: {0:d} ('{0:c}')", command_buffer[0]); switch (command_buffer[0]) { case 'q': @@ -1072,9 +1071,14 @@ void HandlePacket() { SendSignal(current_thread, latest_signal); break; case 'k': - Shutdown(); LOG_INFO(Debug_GDBStub, "killed by gdb"); + ToggleServer(false); + // Continue execution so we don't hang forever after shutting down the server + Continue(); return; + case 'F': + HandleHioReply(command_buffer, command_length); + break; case 'g': ReadRegisters(); break; @@ -1251,6 +1255,10 @@ bool GetCpuHaltFlag() { return halt_loop; } +void SetCpuHaltFlag(bool halt) { + halt_loop = halt; +} + bool GetCpuStepFlag() { return step_loop; } @@ -1265,6 +1273,7 @@ void SendTrap(Kernel::Thread* thread, int trap) { } current_thread = thread; + SendSignal(thread, trap); halt_loop = true; diff --git a/src/core/gdbstub/gdbstub.h b/src/core/gdbstub/gdbstub.h index b878b7957..33d6bd8f1 100644 --- a/src/core/gdbstub/gdbstub.h +++ b/src/core/gdbstub/gdbstub.h @@ -90,6 +90,13 @@ bool CheckBreakpoint(VAddr addr, GDBStub::BreakpointType type); // If set to true, the CPU will halt at the beginning of the next CPU loop. bool GetCpuHaltFlag(); +/** + * If set to true, the CPU will halt at the beginning of the next CPU loop. + * + * @param halt whether to halt on the next loop + */ +void SetCpuHaltFlag(bool halt); + // If set to true and the CPU is halted, the CPU will step one instruction. bool GetCpuStepFlag(); @@ -107,4 +114,19 @@ void SetCpuStepFlag(bool is_step); * @param trap Trap no. */ void SendTrap(Kernel::Thread* thread, int trap); + +/** + * Send reply to gdb client. + * + * @param reply Reply to be sent to client. + */ +void SendReply(const char* reply); + +/** + * Converts input hex string characters into an array of equivalent of u8 bytes. + * + * @param src Pointer to array of output hex string characters. + * @param len Length of src array. + */ +u32 HexToInt(const u8* src, std::size_t len); } // namespace GDBStub diff --git a/src/core/gdbstub/hio.cpp b/src/core/gdbstub/hio.cpp new file mode 100644 index 000000000..ac085d5b7 --- /dev/null +++ b/src/core/gdbstub/hio.cpp @@ -0,0 +1,261 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "common/string_util.h" +#include "core/core.h" +#include "core/gdbstub/gdbstub.h" +#include "core/gdbstub/hio.h" + +namespace GDBStub { + +namespace { + +static VAddr current_hio_request_addr; +static PackedGdbHioRequest current_hio_request; + +enum class Status { + NoRequest, + NotSent, + SentWaitingReply, +}; + +static std::atomic request_status{Status::NoRequest}; + +static std::atomic was_halted = false; +static std::atomic was_stepping = false; + +} // namespace + +/** + * @return Whether the application has requested I/O, and it has not been sent. + */ +static bool HasPendingHioRequest() { + return current_hio_request_addr != 0 && request_status == Status::NotSent; +} + +/** + * @return Whether the GDB stub is awaiting a reply from the client. + */ +static bool IsWaitingForHioReply() { + return current_hio_request_addr != 0 && request_status == Status::SentWaitingReply; +} + +/** + * Send a response indicating an error back to the application. + * + * @param error_code The error code to respond back to the app. This typically corresponds to errno. + * + * @param retval The return value of the syscall the app requested. + */ +static void SendErrorReply(int error_code, int retval = -1) { + auto packet = fmt::format("F{:x},{:x}", retval, error_code); + SendReply(packet.data()); +} + +void SetHioRequest(const VAddr addr) { + if (!IsServerEnabled()) { + LOG_WARNING(Debug_GDBStub, "HIO requested but GDB stub is not running"); + return; + } + + if (IsWaitingForHioReply()) { + LOG_WARNING(Debug_GDBStub, "HIO requested while already in progress"); + return; + } + + if (HasPendingHioRequest()) { + LOG_INFO(Debug_GDBStub, "overwriting existing HIO request that was not sent yet"); + } + + auto& memory = Core::System::GetInstance().Memory(); + const auto process = Core::System::GetInstance().Kernel().GetCurrentProcess(); + + if (!memory.IsValidVirtualAddress(*process, addr)) { + LOG_WARNING(Debug_GDBStub, "Invalid address for HIO request"); + return; + } + + memory.ReadBlock(*process, addr, ¤t_hio_request, sizeof(PackedGdbHioRequest)); + + if (current_hio_request.magic != std::array{'G', 'D', 'B', '\0'}) { + std::string_view bad_magic{ + current_hio_request.magic.data(), + current_hio_request.magic.size(), + }; + LOG_WARNING(Debug_GDBStub, "Invalid HIO request sent by application: bad magic '{}'", + bad_magic); + + current_hio_request_addr = 0; + current_hio_request = {}; + request_status = Status::NoRequest; + return; + } + + LOG_DEBUG(Debug_GDBStub, "HIO request initiated at 0x{:X}", addr); + current_hio_request_addr = addr; + request_status = Status::NotSent; + + was_halted = GetCpuHaltFlag(); + was_stepping = GetCpuStepFlag(); + + // Now halt, so that no further instructions are executed until the request + // is processed by the client. We will continue after the reply comes back + Break(); + SetCpuHaltFlag(true); + SetCpuStepFlag(false); + Core::GetRunningCore().ClearInstructionCache(); +} + +void HandleHioReply(const u8* const command_buffer, const u32 command_length) { + if (!IsWaitingForHioReply()) { + LOG_WARNING(Debug_GDBStub, "Got HIO reply but never sent a request"); + return; + } + + // Skip 'F' header + auto* command_pos = command_buffer + 1; + + if (*command_pos == 0 || *command_pos == ',') { + LOG_WARNING(Debug_GDBStub, "bad HIO packet format position 0: {}", *command_pos); + SendErrorReply(EILSEQ); + return; + } + + // Set the sign of the retval + if (*command_pos == '-') { + command_pos++; + current_hio_request.retval = -1; + } else { + if (*command_pos == '+') { + command_pos++; + } + + current_hio_request.retval = 1; + } + + const std::string command_str{command_pos, command_buffer + command_length}; + std::vector command_parts; + Common::SplitString(command_str, ',', command_parts); + + if (command_parts.empty() || command_parts.size() > 3) { + LOG_WARNING(Debug_GDBStub, "Unexpected reply packet size: {}", command_parts); + SendErrorReply(EILSEQ); + return; + } + + u64 unsigned_retval = + HexToInt(reinterpret_cast(command_parts[0].data()), command_parts[0].size()); + current_hio_request.retval *= unsigned_retval; + + if (command_parts.size() > 1) { + // Technically the errno could be signed but in practice this doesn't happen + current_hio_request.gdb_errno = + HexToInt(reinterpret_cast(command_parts[1].data()), command_parts[1].size()); + } else { + current_hio_request.gdb_errno = 0; + } + + current_hio_request.ctrl_c = false; + + if (command_parts.size() > 2 && !command_parts[2].empty()) { + if (command_parts[2][0] != 'C') { + LOG_WARNING(Debug_GDBStub, "expected ctrl-c flag got '{}'", command_parts[2][0]); + SendErrorReply(EILSEQ); + return; + } + + // for now we just ignore any trailing ";..." attachments + current_hio_request.ctrl_c = true; + } + + std::fill(std::begin(current_hio_request.param_format), + std::end(current_hio_request.param_format), 0); + + LOG_TRACE(Debug_GDBStub, "HIO reply: {{retval = {}, errno = {}, ctrl_c = {}}}", + current_hio_request.retval, current_hio_request.gdb_errno, + current_hio_request.ctrl_c); + + const auto process = Core::System::GetInstance().Kernel().GetCurrentProcess(); + auto& memory = Core::System::GetInstance().Memory(); + + // should have been checked when we first initialized the request, + // but just double check again before we write to memory + if (!memory.IsValidVirtualAddress(*process, current_hio_request_addr)) { + LOG_WARNING(Debug_GDBStub, "Invalid address {:#X} to write HIO reply", + current_hio_request_addr); + return; + } + + memory.WriteBlock(*process, current_hio_request_addr, ¤t_hio_request, + sizeof(PackedGdbHioRequest)); + + current_hio_request = {}; + current_hio_request_addr = 0; + request_status = Status::NoRequest; + + // Restore state from before the request came in + SetCpuStepFlag(was_stepping); + SetCpuHaltFlag(was_halted); + Core::GetRunningCore().ClearInstructionCache(); +} + +bool HandlePendingHioRequestPacket() { + if (!HasPendingHioRequest()) { + return false; + } + + if (IsWaitingForHioReply()) { + // We already sent it, continue waiting for a reply + return true; + } + + // We need a null-terminated string from char* instead of using + // the full length of the array (like {.begin(), .end()} constructor would) + std::string_view function_name{current_hio_request.function_name.data()}; + + std::string packet = fmt::format("F{}", function_name); + + u32 str_length_idx = 0; + + for (u32 i = 0; i < 8 && current_hio_request.param_format[i] != 0; i++) { + u64 param = current_hio_request.parameters[i]; + + // TODO: should we use the IntToGdbHex funcs instead of fmt::format_to ? + switch (current_hio_request.param_format[i]) { + case 'i': + case 'I': + case 'p': + // For pointers and 32-bit ints, truncate down to size before sending + param = static_cast(param); + [[fallthrough]]; + + case 'l': + case 'L': + fmt::format_to(std::back_inserter(packet), ",{:x}", param); + break; + + case 's': + // strings are written as {pointer}/{length} + fmt::format_to(std::back_inserter(packet), ",{:x}/{:x}", + static_cast(current_hio_request.parameters[i]), + current_hio_request.string_lengths[str_length_idx++]); + break; + + default: + LOG_WARNING(Debug_GDBStub, "unexpected hio request param format '{}'", + current_hio_request.param_format[i]); + SendErrorReply(EILSEQ); + return false; + } + } + + LOG_TRACE(Debug_GDBStub, "HIO request packet: '{}'", packet); + + SendReply(packet.data()); + request_status = Status::SentWaitingReply; + return true; +} + +} // namespace GDBStub diff --git a/src/core/gdbstub/hio.h b/src/core/gdbstub/hio.h new file mode 100644 index 000000000..d498a2737 --- /dev/null +++ b/src/core/gdbstub/hio.h @@ -0,0 +1,64 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "common/common_types.h" + +namespace GDBStub { + +/** + * A request from a debugged application to perform some I/O with the GDB client. + * This structure is also used to encode the reply back to the application. + * + * Based on the Rosalina + libctru implementations: + * https://github.com/LumaTeam/Luma3DS/blob/master/sysmodules/rosalina/include/gdb.h#L46C27-L62 + * https://github.com/devkitPro/libctru/blob/master/libctru/source/gdbhio.c#L71-L87 + */ +struct PackedGdbHioRequest { + std::array magic; // "GDB\0" + u32 version; + +private: + static inline constexpr std::size_t MAX_FUNCNAME_LEN = 16; + static inline constexpr std::size_t PARAM_COUNT = 8; + +public: + // Request. Char arrays have +1 entry for null terminator + std::array function_name; + std::array param_format; + + std::array parameters; + std::array string_lengths; + + // Return + s64 retval; + s32 gdb_errno; + bool ctrl_c; +}; + +static_assert(sizeof(PackedGdbHioRequest) == 152, + "HIO request size must match libctru implementation"); + +/** + * Set the current HIO request to the given address. This is how the debugged + * app indicates to the gdbstub that it wishes to perform a request. + * + * @param address The memory address of the \ref PackedGdbHioRequest. + */ +void SetHioRequest(const VAddr address); + +/** + * If there is a pending HIO request, send it to the client. + * + * @returns whethere any request was sent to the client. + */ +bool HandlePendingHioRequestPacket(); + +/** + * Process an HIO reply from the client. + */ +void HandleHioReply(const u8* const command_buffer, const u32 command_length); + +} // namespace GDBStub diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp index 2603bd03f..6bdc72535 100644 --- a/src/core/hle/kernel/svc.cpp +++ b/src/core/hle/kernel/svc.cpp @@ -13,6 +13,7 @@ #include "core/arm/arm_interface.h" #include "core/core.h" #include "core/core_timing.h" +#include "core/gdbstub/hio.h" #include "core/hle/kernel/address_arbiter.h" #include "core/hle/kernel/client_port.h" #include "core/hle/kernel/client_session.h" @@ -1140,9 +1141,21 @@ void SVC::Break(u8 break_reason) { system.SetStatus(Core::System::ResultStatus::ErrorUnknown); } -/// Used to output a message on a debug hardware unit - does nothing on a retail unit +/// Used to output a message on a debug hardware unit, or for the GDB file I/O +/// (HIO) protocol - does nothing on a retail unit. void SVC::OutputDebugString(VAddr address, s32 len) { - if (len <= 0) { + if (!memory.IsValidVirtualAddress(*kernel.GetCurrentProcess(), address)) { + LOG_WARNING(Kernel_SVC, "OutputDebugString called with invalid address {:X}", address); + return; + } + + if (len == 0) { + GDBStub::SetHioRequest(address); + return; + } + + if (len < 0) { + LOG_WARNING(Kernel_SVC, "OutputDebugString called with invalid length {}", len); return; }