/ src / core / gdbstub / hio.cpp
hio.cpp
  1  // Copyright 2023 Citra Emulator Project
  2  // Licensed under GPLv2 or any later version
  3  // Refer to the license.txt file included.
  4  
  5  #include <fmt/ranges.h>
  6  #include "common/string_util.h"
  7  #include "core/core.h"
  8  #include "core/gdbstub/gdbstub.h"
  9  #include "core/gdbstub/hio.h"
 10  
 11  namespace GDBStub {
 12  
 13  namespace {
 14  
 15  static VAddr current_hio_request_addr;
 16  static PackedGdbHioRequest current_hio_request;
 17  
 18  enum class Status {
 19      NoRequest,
 20      NotSent,
 21      SentWaitingReply,
 22  };
 23  
 24  static std::atomic<Status> request_status{Status::NoRequest};
 25  
 26  static std::atomic<bool> was_halted = false;
 27  static std::atomic<bool> was_stepping = false;
 28  
 29  } // namespace
 30  
 31  /**
 32   * @return Whether the application has requested I/O, and it has not been sent.
 33   */
 34  static bool HasPendingHioRequest() {
 35      return current_hio_request_addr != 0 && request_status == Status::NotSent;
 36  }
 37  
 38  /**
 39   * @return Whether the GDB stub is awaiting a reply from the client.
 40   */
 41  static bool IsWaitingForHioReply() {
 42      return current_hio_request_addr != 0 && request_status == Status::SentWaitingReply;
 43  }
 44  
 45  /**
 46   * Send a response indicating an error back to the application.
 47   *
 48   * @param error_code The error code to respond back to the app. This typically corresponds to errno.
 49   *
 50   * @param retval The return value of the syscall the app requested.
 51   */
 52  static void SendErrorReply(int error_code, int retval = -1) {
 53      auto packet = fmt::format("F{:x},{:x}", retval, error_code);
 54      SendReply(packet.data());
 55  }
 56  
 57  void SetHioRequest(Core::System& system, const VAddr addr) {
 58      if (!IsServerEnabled()) {
 59          LOG_WARNING(Debug_GDBStub, "HIO requested but GDB stub is not running");
 60          return;
 61      }
 62  
 63      if (IsWaitingForHioReply()) {
 64          LOG_WARNING(Debug_GDBStub, "HIO requested while already in progress");
 65          return;
 66      }
 67  
 68      if (HasPendingHioRequest()) {
 69          LOG_INFO(Debug_GDBStub, "overwriting existing HIO request that was not sent yet");
 70      }
 71  
 72      auto& memory = system.Memory();
 73      const auto process = system.Kernel().GetCurrentProcess();
 74  
 75      if (!memory.IsValidVirtualAddress(*process, addr)) {
 76          LOG_WARNING(Debug_GDBStub, "Invalid address for HIO request");
 77          return;
 78      }
 79  
 80      memory.ReadBlock(addr, &current_hio_request, sizeof(PackedGdbHioRequest));
 81  
 82      if (current_hio_request.magic != std::array{'G', 'D', 'B', '\0'}) {
 83          std::string_view bad_magic{
 84              current_hio_request.magic.data(),
 85              current_hio_request.magic.size(),
 86          };
 87          LOG_WARNING(Debug_GDBStub, "Invalid HIO request sent by application: bad magic '{}'",
 88                      bad_magic);
 89  
 90          current_hio_request_addr = 0;
 91          current_hio_request = {};
 92          request_status = Status::NoRequest;
 93          return;
 94      }
 95  
 96      LOG_DEBUG(Debug_GDBStub, "HIO request initiated at 0x{:X}", addr);
 97      current_hio_request_addr = addr;
 98      request_status = Status::NotSent;
 99  
100      was_halted = GetCpuHaltFlag();
101      was_stepping = GetCpuStepFlag();
102  
103      // Now halt, so that no further instructions are executed until the request
104      // is processed by the client. We will continue after the reply comes back
105      Break();
106      SetCpuHaltFlag(true);
107      SetCpuStepFlag(false);
108      system.GetRunningCore().ClearInstructionCache();
109  }
110  
111  void HandleHioReply(Core::System& system, const u8* const command_buffer,
112                      const u32 command_length) {
113      if (!IsWaitingForHioReply()) {
114          LOG_WARNING(Debug_GDBStub, "Got HIO reply but never sent a request");
115          return;
116      }
117  
118      // Skip 'F' header
119      const u8* command_pos = command_buffer + 1;
120  
121      if (*command_pos == 0 || *command_pos == ',') {
122          LOG_WARNING(Debug_GDBStub, "bad HIO packet format position 0: {}", *command_pos);
123          SendErrorReply(EILSEQ);
124          return;
125      }
126  
127      // Set the sign of the retval
128      if (*command_pos == '-') {
129          command_pos++;
130          current_hio_request.retval = -1;
131      } else {
132          if (*command_pos == '+') {
133              command_pos++;
134          }
135  
136          current_hio_request.retval = 1;
137      }
138  
139      const std::string command_str{command_pos, command_buffer + command_length};
140      const auto command_parts = Common::SplitString(command_str, ',');
141  
142      if (command_parts.empty() || command_parts.size() > 3) {
143          LOG_WARNING(Debug_GDBStub, "Unexpected reply packet size: {}", command_parts);
144          SendErrorReply(EILSEQ);
145          return;
146      }
147  
148      u64 unsigned_retval =
149          HexToInt(reinterpret_cast<const u8*>(command_parts[0].data()), command_parts[0].size());
150      current_hio_request.retval *= unsigned_retval;
151  
152      if (command_parts.size() > 1) {
153          // Technically the errno could be signed but in practice this doesn't happen
154          current_hio_request.gdb_errno =
155              HexToInt(reinterpret_cast<const u8*>(command_parts[1].data()), command_parts[1].size());
156      } else {
157          current_hio_request.gdb_errno = 0;
158      }
159  
160      current_hio_request.ctrl_c = false;
161  
162      if (command_parts.size() > 2 && !command_parts[2].empty()) {
163          if (command_parts[2][0] != 'C') {
164              LOG_WARNING(Debug_GDBStub, "expected ctrl-c flag got '{}'", command_parts[2][0]);
165              SendErrorReply(EILSEQ);
166              return;
167          }
168  
169          // for now we just ignore any trailing ";..." attachments
170          current_hio_request.ctrl_c = true;
171      }
172  
173      std::fill(std::begin(current_hio_request.param_format),
174                std::end(current_hio_request.param_format), 0);
175  
176      LOG_TRACE(Debug_GDBStub, "HIO reply: {{retval = {}, errno = {}, ctrl_c = {}}}",
177                current_hio_request.retval, current_hio_request.gdb_errno,
178                current_hio_request.ctrl_c);
179  
180      const auto process = system.Kernel().GetCurrentProcess();
181      auto& memory = system.Memory();
182  
183      // should have been checked when we first initialized the request,
184      // but just double check again before we write to memory
185      if (!memory.IsValidVirtualAddress(*process, current_hio_request_addr)) {
186          LOG_WARNING(Debug_GDBStub, "Invalid address {:#X} to write HIO reply",
187                      current_hio_request_addr);
188          return;
189      }
190  
191      memory.WriteBlock(current_hio_request_addr, &current_hio_request, sizeof(PackedGdbHioRequest));
192  
193      current_hio_request = {};
194      current_hio_request_addr = 0;
195      request_status = Status::NoRequest;
196  
197      // Restore state from before the request came in
198      SetCpuStepFlag(was_stepping);
199      SetCpuHaltFlag(was_halted);
200      system.GetRunningCore().ClearInstructionCache();
201  }
202  
203  bool HandlePendingHioRequestPacket() {
204      if (!HasPendingHioRequest()) {
205          return false;
206      }
207  
208      if (IsWaitingForHioReply()) {
209          // We already sent it, continue waiting for a reply
210          return true;
211      }
212  
213      // We need a null-terminated string from char* instead of using
214      // the full length of the array (like {.begin(), .end()} constructor would)
215      std::string_view function_name{current_hio_request.function_name.data()};
216  
217      std::string packet = fmt::format("F{}", function_name);
218  
219      u32 str_length_idx = 0;
220  
221      for (u32 i = 0; i < 8 && current_hio_request.param_format[i] != 0; i++) {
222          u64 param = current_hio_request.parameters[i];
223  
224          // TODO: should we use the IntToGdbHex funcs instead of fmt::format_to ?
225          switch (current_hio_request.param_format[i]) {
226          case 'i':
227          case 'I':
228          case 'p':
229              // For pointers and 32-bit ints, truncate down to size before sending
230              param = static_cast<u32>(param);
231              [[fallthrough]];
232  
233          case 'l':
234          case 'L':
235              fmt::format_to(std::back_inserter(packet), ",{:x}", param);
236              break;
237  
238          case 's':
239              // strings are written as {pointer}/{length}
240              fmt::format_to(std::back_inserter(packet), ",{:x}/{:x}",
241                             static_cast<u32>(current_hio_request.parameters[i]),
242                             current_hio_request.string_lengths[str_length_idx++]);
243              break;
244  
245          default:
246              LOG_WARNING(Debug_GDBStub, "unexpected hio request param format '{}'",
247                          current_hio_request.param_format[i]);
248              SendErrorReply(EILSEQ);
249              return false;
250          }
251      }
252  
253      LOG_TRACE(Debug_GDBStub, "HIO request packet: '{}'", packet);
254  
255      SendReply(packet.data());
256      request_status = Status::SentWaitingReply;
257      return true;
258  }
259  
260  } // namespace GDBStub