Initial commit for adding gdbserver to mpact-sim and vxc_sim

PiperOrigin-RevId: 886288912
Change-Id: I258a6ed1a2ca4df8746936d8fe9e43a9a23c2da5
diff --git a/mpact/sim/generic/BUILD b/mpact/sim/generic/BUILD
index f1b3884..f3f419d 100644
--- a/mpact/sim/generic/BUILD
+++ b/mpact/sim/generic/BUILD
@@ -127,10 +127,12 @@
     srcs = [],
     hdrs = [
         "core_debug_interface.h",
+        "debug_info.h",
     ],
     deps = [
         ":core",
         ":instruction",
+        "@com_google_absl//absl/container:flat_hash_map",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/status:statusor",
     ],
@@ -216,7 +218,6 @@
         "//mpact/sim/proto:component_data_cc_proto",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/strings",
-        "@com_google_absl//absl/types:variant",
     ],
 )
 
diff --git a/mpact/sim/generic/core_debug_interface.h b/mpact/sim/generic/core_debug_interface.h
index 81d3762..0071b6a 100644
--- a/mpact/sim/generic/core_debug_interface.h
+++ b/mpact/sim/generic/core_debug_interface.h
@@ -122,10 +122,26 @@
   // Remove all software breakpoints.
   virtual absl::Status ClearAllSwBreakpoints() = 0;
 
+  // Set a data watchpoint for the given memory range. Any access matching the
+  // given access type (load/store) will halt execution following the completion
+  // of that access.
+  virtual absl::Status SetDataWatchpoint(uint64_t address, size_t length,
+                                         AccessType access_type) {
+    return absl::UnimplementedError("Not implemented");
+  }
+  // Clear data watchpoint for the given memory address and access type.
+  virtual absl::Status ClearDataWatchpoint(uint64_t address,
+                                           AccessType access_type) {
+    return absl::UnimplementedError("Not implemented");
+  }
+
   // Return the instruction object for the instruction at the given address.
   virtual absl::StatusOr<Instruction*> GetInstruction(uint64_t address) = 0;
   // Return the string representation for the instruction at the given address.
   virtual absl::StatusOr<std::string> GetDisassembly(uint64_t address) = 0;
+
+  // Returns the executable file name.
+  virtual std::string GetExecutableFileName() { return ""; };
 };
 
 }  // namespace generic
diff --git a/mpact/sim/generic/debug_info.h b/mpact/sim/generic/debug_info.h
new file mode 100644
index 0000000..9b7d607
--- /dev/null
+++ b/mpact/sim/generic/debug_info.h
@@ -0,0 +1,42 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file contains the definition of the DebugInfo base class.
+
+#ifndef MPACT_SIM_GENERIC_DEBUG_INFO_H_
+#define MPACT_SIM_GENERIC_DEBUG_INFO_H_
+
+#include <cstdint>
+#include <string>
+
+#include "absl/container/flat_hash_map.h"
+
+namespace mpact::sim::generic {
+
+class DebugInfo {
+ public:
+  using DebugRegisterMap = absl::flat_hash_map<uint64_t, std::string>;
+
+  virtual ~DebugInfo() = default;
+
+  virtual const DebugRegisterMap& debug_register_map() const = 0;
+
+  virtual int GetFirstGpr() const = 0;
+  virtual int GetLastGpr() const = 0;
+  virtual int GetGprWidth() const = 0;
+};
+
+}  // namespace mpact::sim::generic
+
+#endif  // MPACT_SIM_GENERIC_DEBUG_INFO_H_
diff --git a/mpact/sim/util/gdbserver/BUILD b/mpact/sim/util/gdbserver/BUILD
new file mode 100644
index 0000000..fdfbe85
--- /dev/null
+++ b/mpact/sim/util/gdbserver/BUILD
@@ -0,0 +1,39 @@
+# Copyright 2026 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This file contains the build rules for the gdbserver utility.
+
+load("@rules_cc//cc:cc_library.bzl", "cc_library")
+
+package(
+    default_applicable_licenses = ["//:license"],
+    default_visibility = ["//visibility:public"],
+)
+
+cc_library(
+    name = "gdbserver",
+    srcs = ["gdbserver.cpp"],
+    hdrs = ["gdbserver.h"],
+    deps = [
+        "//mpact/sim/generic:core_debug_interface",
+        "//mpact/sim/generic:type_helpers",
+        "//mpact/sim/util/renode:socket_cli",
+        "@com_google_absl//absl/container:flat_hash_map",
+        "@com_google_absl//absl/log",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/strings:str_format",
+        "@com_google_absl//absl/strings:string_view",
+        "@com_google_absl//absl/types:span",
+    ],
+)
diff --git a/mpact/sim/util/gdbserver/gdbserver.cpp b/mpact/sim/util/gdbserver/gdbserver.cpp
new file mode 100644
index 0000000..c5b5d5a
--- /dev/null
+++ b/mpact/sim/util/gdbserver/gdbserver.cpp
@@ -0,0 +1,967 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "third_party/mpact_sim/util/gdbserver/gdbserver.h"
+
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include <cerrno>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <istream>
+#include <ostream>
+#include <string>
+#include <vector>
+
+#include "third_party/absl/container/flat_hash_map.h"
+#include "third_party/absl/log/log.h"
+#include "third_party/absl/strings/numbers.h"
+#include "third_party/absl/strings/str_cat.h"
+#include "third_party/absl/strings/str_format.h"
+#include "third_party/absl/strings/str_split.h"
+#include "third_party/absl/strings/string_view.h"
+#include "third_party/absl/types/span.h"
+#include "third_party/mpact_sim/generic/core_debug_interface.h"
+#include "third_party/mpact_sim/generic/type_helpers.h"
+
+namespace {
+
+// Some binary data may be escaped in the GDB command. This removes such
+// escapes after the command has been "unwrapped".
+std::string UnescapeCommand(std::string_view escaped_command) {
+  std::string command;
+  for (int i = 0; i < escaped_command.size(); ++i) {
+    char c = escaped_command[i];
+    if (c == 0x7d) {
+      c = escaped_command[++i] ^ 0x20;
+      command.push_back(c);
+    }
+    command.push_back(c);
+  }
+  return command;
+}
+
+}  // namespace
+
+namespace mpact::sim::util::gdbserver {
+
+using ::mpact::sim::generic::operator*;  // NOLINT
+
+GdbServer::GdbServer(
+    absl::Span<generic::CoreDebugInterface*> core_debug_interfaces,
+    const DebugInfo& debug_info)
+    : core_debug_interfaces_(core_debug_interfaces), debug_info_(debug_info) {}
+
+GdbServer::~GdbServer() {
+  Terminate();
+  delete os_;
+  delete is_;
+  delete out_buf_;
+  delete in_buf_;
+}
+
+bool GdbServer::Connect(int port) {
+  good_ = true;
+  if (port <= 0) {
+    return false;
+  }
+  // Create the socket on the given port.
+  server_socket_ =
+      socket(/*domain=*/AF_INET, /*type=*/SOCK_STREAM, /*protocol=*/0);
+  int one = 1;
+  int err =
+      setsockopt(server_socket_, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+  if (err != 0) {
+    LOG(ERROR) << absl::StrFormat("Failed to set socket options: %s",
+                                  strerror(errno));
+    good_ = false;
+    return false;
+  }
+  sockaddr_in server_address_int;
+  server_address_int.sin_family = AF_INET;
+  server_address_int.sin_addr.s_addr = INADDR_ANY;
+  server_address_int.sin_port = htons(port);
+  std::memset(&server_address_int.sin_zero, 0,
+              sizeof(server_address_int.sin_zero));
+  int res = bind(server_socket_,
+                 reinterpret_cast<const sockaddr*>(&server_address_int),
+                 sizeof(server_address_int));
+  if (res != 0) {
+    good_ = false;
+    LOG(ERROR) << absl::StrFormat("Failed to bind to port %d: %s", port,
+                                  strerror(errno));
+    return false;
+  }
+  res = listen(server_socket_, /*backlog=*/1);
+  if (res != 0) {
+    good_ = false;
+    LOG(ERROR) << absl::StrFormat("Failed to listen on port %d: %s", port,
+                                  strerror(errno));
+    return false;
+  }
+
+  // Accept the connection.
+  cli_fd_ = accept(server_socket_, /*addr=*/nullptr, /*addrlen=*/nullptr);
+  if (cli_fd_ == -1) {
+    good_ = false;
+    LOG(ERROR) << absl::StrFormat("Failed to accept connection on port %d: %s",
+                                  port, strerror(errno));
+    return false;
+  }
+  good_ = true;
+  // Create the input and output buffers and streams to handle reads and writes
+  // to the socket.
+  out_buf_ = new SocketStreambuf(cli_fd_);
+  in_buf_ = new SocketStreambuf(cli_fd_);
+  os_ = new std::ostream(out_buf_);
+  is_ = new std::istream(in_buf_);
+
+  // First expect a plus.
+  uint8_t val = is_->get();
+  if (val != '+') {
+    good_ = false;
+    LOG(ERROR) << absl::StrFormat("Failed handshake with GDB client on port %d",
+                                  port);
+    Terminate();
+    return false;
+  }
+  // Now read commands from the gdbserver client and process them.
+  while (good_) {
+    int buffer_pos = 0;
+    // First character is always '$'.
+    val = is_->get();
+    if (val != '$') {
+      if (val == 0xff) {
+        LOG(INFO) << "Client disconnected";
+        Terminate();
+        return false;
+      }
+      good_ = false;
+      LOG(ERROR) << absl::StrFormat(
+          "Failed to receive command on port %d (expected '$', but received "
+          "'%c' - 0x%x)",
+          port, val, static_cast<int>(val));
+      Terminate();
+      return false;
+    }
+    buffer_[buffer_pos++] = val;
+    // Next read the command until we see a '#'.
+    do {
+      val = is_->get();
+      if (!is_->good()) {
+        good_ = false;
+        LOG(ERROR) << absl::StrFormat(
+            "Failed to receive complete command on port %d received: '%s'",
+            port,
+            std::string_view(reinterpret_cast<char*>(buffer_), buffer_pos));
+        Terminate();
+        return false;
+      }
+      buffer_[buffer_pos++] = val;
+    } while (val != '#');
+    // Next read the checksum.
+    buffer_[buffer_pos++] = is_->get();
+    if (!is_->good()) break;
+    buffer_[buffer_pos++] = is_->get();
+    if (!is_->good()) break;
+    if ((buffer_pos < 3) || (buffer_[buffer_pos - 3] != '#')) continue;
+
+    AcceptGdbCommand(
+        std::string_view(reinterpret_cast<char*>(buffer_), buffer_pos));
+    buffer_pos = 0;
+  }
+  good_ = false;
+  return true;
+}
+
+void GdbServer::Terminate() {
+  // Shutdown the connection.
+  if (cli_fd_ != -1) {
+    int res = fcntl(cli_fd_, F_GETFD);
+    if (res >= 0) {
+      (void)shutdown(cli_fd_, SHUT_RDWR);
+      (void)close(cli_fd_);
+    }
+  }
+  if (server_socket_ != -1) {
+    int res = shutdown(server_socket_, SHUT_RDWR);
+    res = fcntl(server_socket_, F_GETFD);
+    if (res >= 0) {
+      (void)close(server_socket_);
+    }
+  }
+  good_ = false;
+}
+
+void GdbServer::Respond(std::string_view response) {
+  // Compute the checksum of the response.
+  uint8_t checksum = 0;
+  for (char c : response) {
+    checksum += c;
+  }
+  // Format the response string.
+  std::string response_str = absl::StrCat(
+      "$", response, "#", absl::StrFormat("%02x", static_cast<int>(checksum)));
+  LOG(INFO) << absl::StrFormat("Response: '%s'", response_str);
+  uint8_t ack = 0;
+  // Send the response to the GDB client.
+  for (int i = 0; i < 5; ++i) {
+    os_->write(response_str.data(), response_str.size());
+    os_->flush();
+    ack = is_->get();
+    if (ack == '+') break;
+    LOG(WARNING) << absl::StrFormat("Response not acknowledged ('%c' received)",
+                                    ack);
+  }
+  if (ack != '+') {
+    LOG(ERROR) << "Failed to send response after 5 attempts";
+    Terminate();
+  }
+}
+
+void GdbServer::SendError(std::string_view error) {
+  Respond(absl::StrCat("E.", error));
+}
+
+void GdbServer::AcceptGdbCommand(std::string_view command) {
+  LOG(INFO) << absl::StrFormat("Received: '%s'", command);
+  // The command is on the form "$<command>#<2 digit checksum>".
+  size_t pos = command.find_last_of('#');
+  if ((command.front() != '$') || (pos == std::string_view::npos) ||
+      (pos != command.size() - 3)) {
+    LOG(ERROR) << "Invalid GDB command syntax";
+    os_->put('-');
+    os_->flush();
+    return;
+  }
+  // Verify the checksum.
+  std::string_view checksum_str = command.substr(pos + 1, 2);
+  // Trim the command string.
+  command.remove_prefix(1);
+  command.remove_suffix(3);
+  uint8_t checksum = 0;
+  for (char c : command) {
+    checksum += c;
+  }
+  int orig_checksum;
+  bool success = absl::SimpleHexAtoi(checksum_str, &orig_checksum);
+  if (!success) {
+    LOG(ERROR) << absl::StrFormat("Invalid original checksum: '%s'",
+                                  checksum_str);
+    // Request a retransmission.
+    os_->put('-');
+    os_->flush();
+    return;
+  }
+  if (checksum != orig_checksum) {
+    LOG(ERROR) << absl::StrFormat("Invalid checksum: %x expected: %x",
+                                  static_cast<int>(checksum), orig_checksum);
+
+    // Request a retransmission.
+    os_->put('-');
+    os_->flush();
+    return;
+  }
+  // Acknowledge the command.
+  os_->put('+');
+  os_->flush();
+  std::string clean_command = UnescapeCommand(command);
+  ParseGdbCommand(command);
+}
+
+void GdbServer::ParseGdbCommand(std::string_view command) {
+  switch (command.front()) {
+    default:
+      // The command is not handled, so respond with an empty string which will
+      // indicate that the command is not supported.
+      Respond("");
+      break;
+    case '?':  // Inquire about the halt reason.
+      GdbHaltReason();
+      break;
+    case 'c':  // Continue packet.
+      command.remove_prefix(1);
+      GdbContinue(command);
+      break;
+    case 'D':  // Detach packet.
+      GdbDetach();
+      break;
+    case 'g':  // Read registers.
+      GdbReadGprRegisters();
+      break;
+    case 'H':  // Select thread
+      command.remove_prefix(1);
+      GdbSelectThread(command);
+      break;
+    case 'k':  // Kill packet.
+      Terminate();
+      break;
+    case 'm': {  // Read memory.
+      command.remove_prefix(1);
+      size_t pos = command.find(',');
+      if (pos == std::string_view::npos) {
+        if (error_message_supported_) {
+          SendError("invalid memory read format");
+        } else {
+          Respond("E01");
+        }
+        return;
+      }
+      std::string_view address = command.substr(0, pos);
+      std::string_view length = command.substr(pos + 1);
+      GdbReadMemory(address, length);
+      break;
+    }
+    case 'M': {  // Write memory.
+      command.remove_prefix(1);
+      size_t comma_pos = command.find(',');
+      if (comma_pos == std::string_view::npos) {
+        if (error_message_supported_) {
+          SendError("invalid memory write format");
+        } else {
+          Respond("E01");
+        }
+        return;
+      }
+      size_t colon_pos = command.find(':');
+      if (colon_pos == std::string_view::npos) {
+        SendError("invalid memory write format");
+        return;
+      }
+      std::string_view address = command.substr(0, comma_pos);
+      std::string_view length =
+          command.substr(comma_pos + 1, colon_pos - comma_pos - 1);
+      std::string_view data = command.substr(colon_pos + 1);
+      GdbWriteMemory(address, length, data);
+      break;
+    }
+    case 'p': {  // Read register.
+      command.remove_prefix(1);
+      GdbReadRegister(command);
+      break;
+    }
+    case 'P': {  // Write register.
+      command.remove_prefix(1);
+      size_t pos = command.find('=');
+      if (pos == std::string_view::npos) {
+        SendError("invalid register write format");
+        return;
+      }
+      std::string_view register_name = command.substr(0, pos);
+      std::string_view value = command.substr(pos + 1);
+      GdbWriteRegister(register_name, value);
+      break;
+    }
+    case 'q': {  // Query.
+      command.remove_prefix(1);
+      GdbQuery(command);
+      break;
+    }
+    case 'Q': {  // Set.
+      command.remove_prefix(1);
+      GdbSet(command);
+      break;
+    }
+    case 's':  // Step.
+      command.remove_prefix(1);
+      GdbStep(command);
+      break;
+    case 'z':
+    case 'Z': {  // Add or remove breakpoint or watchpoint.
+      // First make sure it's not a conditional breakpoint.
+      if (command.find(';')) {
+        Respond("");
+        return;
+      }
+      char type = command.front();
+      command.remove_prefix(1);
+      size_t address_pos = command.find_first_of(',');
+      if (address_pos == std::string_view::npos) {
+        SendError("invalid remove breakpoint format");
+        return;
+      }
+      size_t kind_pos = command.find_last_of(',');
+      if (kind_pos == std::string_view::npos) {
+        SendError("invalid remove breakpoint format");
+        return;
+      }
+      std::string_view address =
+          command.substr(address_pos + 1, kind_pos - address_pos - 1);
+      std::string_view kind = command.substr(kind_pos + 1);
+      if (type == 'z') {
+        GdbRemoveBreakpoint(type, address, kind);
+      } else {
+        GdbAddBreakpoint(type, address, kind);
+      }
+      break;
+    }
+  }
+}
+
+void GdbServer::GdbHalt() {
+  auto status = core_debug_interfaces_[0]->Halt(
+      generic::CoreDebugInterface::HaltReason::kUserRequest);
+  if (!status.ok()) {
+    SendError(status.message());
+    return;
+  }
+  status = core_debug_interfaces_[0]->Wait();
+  if (!status.ok()) {
+    SendError(status.message());
+    return;
+  }
+  Respond(absl::StrCat("T03"));
+}
+
+void GdbServer::GdbHaltReason() {
+  auto result = core_debug_interfaces_[0]->GetRunStatus();
+  if (!result.ok()) {
+    SendError(result.status().message());
+    return;
+  }
+  if (result.value() == generic::CoreDebugInterface::RunStatus::kHalted) {
+    auto result = core_debug_interfaces_[0]->GetLastHaltReason();
+    if (!result.ok()) {
+      SendError(result.status().message());
+      return;
+    }
+    switch (result.value()) {
+      default:
+        Respond("T05");
+        return;
+      case *generic::CoreDebugInterface::HaltReason::kSoftwareBreakpoint:
+      case *generic::CoreDebugInterface::HaltReason::kHardwareBreakpoint:
+      case *generic::CoreDebugInterface::HaltReason::kDataWatchPoint:
+      case *generic::CoreDebugInterface::HaltReason::kActionPoint:
+        Respond("T02");
+        return;
+      case *generic::CoreDebugInterface::HaltReason::kSimulatorError:
+        Respond("T06");
+        return;
+      case *generic::CoreDebugInterface::HaltReason::kUserRequest:
+        Respond("T03");
+        return;
+      case *generic::CoreDebugInterface::HaltReason::kProgramDone:
+        Respond("W00");
+        return;
+    }
+  }
+  Respond("E01");
+}
+
+void GdbServer::GdbContinue(std::string_view command) {
+  auto result = core_debug_interfaces_[0]->GetRunStatus();
+  if (!result.ok()) {
+    SendError(result.status().message());
+    return;
+  }
+  if (result.value() == generic::CoreDebugInterface::RunStatus::kHalted) {
+    if (!command.empty()) {
+      uint64_t address;
+      bool success = absl::SimpleHexAtoi(command, &address);
+      if (!success) {
+        SendError("invalid address");
+        return;
+      }
+      auto status = core_debug_interfaces_[0]->WriteRegister("pc", address);
+      if (!status.ok()) {
+        SendError(status.message());
+        return;
+      }
+    }
+    result = core_debug_interfaces_[0]->Run();
+    if (!result.ok()) {
+      SendError(result.status().message());
+      return;
+    }
+  }
+}
+
+void GdbServer::GdbDetach() {
+  Respond("OK");
+  Terminate();
+}
+
+void GdbServer::GdbSelectThread(std::string_view command) {
+  int thread_id;
+  char op = command.front();
+  command.remove_prefix(1);
+  if (command == "-1") {
+    thread_id = -1;
+  } else {
+    bool success = absl::SimpleHexAtoi(command, &thread_id);
+    if (!success) {
+      SendError("invalid thread id");
+      return;
+    }
+  }
+  thread_select_[op] = thread_id;
+  Respond("OK");
+}
+
+void GdbServer::GdbThreadInfo() {
+  std::string response = "m0";
+
+  for (int i = 1; i < core_debug_interfaces_.size(); ++i) {
+    absl::StrAppend(&response, ",", absl::Hex(i));
+  }
+  Respond(response);
+}
+
+void GdbServer::GdbReadMemory(std::string_view address_str,
+                              std::string_view length_str) {
+  uint64_t address;
+  bool success = absl::SimpleHexAtoi(address_str, &address);
+  if (!success) {
+    if (error_message_supported_) {
+      SendError("invalid memory read format");
+    } else {
+      Respond("E01");
+    }
+    return;
+  }
+  uint64_t length;
+  success = absl::SimpleHexAtoi(length_str, &length);
+  if (!success) {
+    if (error_message_supported_) {
+      SendError("invalid memory read format");
+    } else {
+      Respond("E01");
+    }
+    return;
+  }
+  if (length > sizeof(buffer_)) {
+    if (error_message_supported_) {
+      SendError("length exceeds buffer size of 4096");
+    } else {
+      Respond("E01");
+    }
+    return;
+  }
+  auto result = core_debug_interfaces_[0]->ReadMemory(address, buffer_, length);
+  if (!result.ok()) {
+    if (error_message_supported_) {
+      SendError(result.status().message());
+    } else {
+      Respond("E01");
+    }
+    return;
+  }
+  std::string response;
+  for (int i = 0; i < result.value(); ++i) {
+    absl::StrAppend(&response, absl::Hex(buffer_[i], absl::kZeroPad2));
+  }
+  Respond(response);
+}
+
+void GdbServer::GdbWriteMemory(std::string_view address_str,
+                               std::string_view length_str,
+                               std::string_view data) {
+  uint64_t address;
+  bool success = absl::SimpleHexAtoi(address_str, &address);
+  if (!success) {
+    if (error_message_supported_) {
+      SendError("invalid memory read format");
+    } else {
+      Respond("E01");
+    }
+    return;
+  }
+  uint64_t length;
+  success = absl::SimpleHexAtoi(length_str, &length);
+  if (!success) {
+    if (error_message_supported_) {
+      SendError("invalid memory read format");
+    } else {
+      Respond("E01");
+    }
+    return;
+  }
+  if (length > sizeof(buffer_)) {
+    if (error_message_supported_) {
+      SendError("length exceeds buffer size of 4096");
+    } else {
+      Respond("E01");
+    }
+    return;
+  }
+  int num_bytes = 0;
+  for (int i = 0; (i < length) && (data.size() >= 2); ++i) {
+    num_bytes++;
+    std::string_view byte = data.substr(0, 2);
+    data.remove_prefix(2);
+    uint8_t value;
+    (void)absl::SimpleHexAtoi(byte, &value);
+    buffer_[i] = value;
+  }
+  if (num_bytes != length) {
+    if (error_message_supported_) {
+      SendError("length does not match data size");
+    } else {
+      Respond("E01");
+    }
+    return;
+  }
+  auto result =
+      core_debug_interfaces_[0]->WriteMemory(address, buffer_, num_bytes);
+  if (!result.ok()) {
+    if (error_message_supported_) {
+      SendError(result.status().message());
+    } else {
+      Respond("E01");
+    }
+    return;
+  }
+  if (result.value() != length) {
+    if (error_message_supported_) {
+      SendError("length does not match number of bytes written");
+    } else {
+      Respond("E01");
+    }
+    return;
+  }
+  Respond("OK");
+}
+
+void GdbServer::GdbReadGprRegisters() {
+  std::string response;
+  for (int i = debug_info_.GetFirstGpr(); i <= debug_info_.GetLastGpr(); ++i) {
+    auto it = debug_info_.debug_register_map().find(i);
+    if (it == debug_info_.debug_register_map().end()) {
+      SendError("Internal error - failed to find register");
+      return;
+    }
+    const std::string& register_name = it->second;
+    auto result = core_debug_interfaces_[0]->ReadRegister(register_name);
+    if (!result.ok()) {
+      SendError(result.status().message());
+      return;
+    }
+    LOG(INFO) << absl::StrFormat("Register %s = %x", register_name,
+                                 result.value());
+    uint64_t value = result.value();
+    for (int j = 0; j < debug_info_.GetGprWidth(); j += 8) {
+      absl::StrAppend(&response, absl::Hex(value & 0xff, absl::kZeroPad2));
+      value >>= 8;
+    }
+  }
+  Respond(response);
+}
+
+void GdbServer::GdbWriteGprRegisters(std::string_view data) {
+  int num_bytes = 0;
+  for (int i = debug_info_.GetFirstGpr(); i <= debug_info_.GetLastGpr(); ++i) {
+    auto it = debug_info_.debug_register_map().find(i);
+    if (it == debug_info_.debug_register_map().end()) {
+      SendError("Internal error - failed to find register");
+      return;
+    }
+    const std::string& register_name = it->second;
+    uint64_t value = 0;
+    for (int j = 0; j < debug_info_.GetGprWidth(); j += 8) {
+      std::string_view byte = data.substr(0, 2);
+      data.remove_prefix(2);
+      uint8_t byte_value;
+      (void)absl::SimpleHexAtoi(byte, &byte_value);
+      value |= byte_value << (j * 8);
+      num_bytes++;
+    }
+    auto status =
+        core_debug_interfaces_[0]->WriteRegister(register_name, value);
+    if (!status.ok()) {
+      SendError(status.message());
+      return;
+    }
+  }
+  Respond("OK");
+}
+
+void GdbServer::GdbReadRegister(std::string_view register_number_str) {
+  uint64_t register_number;
+  bool success = absl::SimpleHexAtoi(register_number_str, &register_number);
+  if (!success) {
+    SendError("invalid register number");
+    return;
+  }
+  auto it = debug_info_.debug_register_map().find(register_number);
+  if (it == debug_info_.debug_register_map().end()) {
+    SendError("invalid register number");
+    return;
+  }
+  const std::string& register_name = it->second;
+  auto result = core_debug_interfaces_[0]->ReadRegister(register_name);
+  if (!result.ok()) {
+    SendError(result.status().message());
+    return;
+  }
+  std::string response;
+  uint64_t value = result.value();
+  while (value > 0) {
+    absl::StrAppend(&response, absl::Hex(value & 0xff, absl::kZeroPad2));
+    value >>= 8;
+  }
+  Respond(response);
+}
+
+void GdbServer::GdbWriteRegister(std::string_view register_number_str,
+                                 std::string_view register_value_str) {
+  uint64_t register_number;
+  bool success = absl::SimpleHexAtoi(register_number_str, &register_number);
+  if (!success) {
+    SendError("invalid register number");
+    return;
+  }
+  auto it = debug_info_.debug_register_map().find(register_number);
+  if (it == debug_info_.debug_register_map().end()) {
+    SendError("invalid register number");
+    return;
+  }
+  const std::string& register_name = it->second;
+  uint64_t value = 0;
+  int count = 0;
+  while (register_value_str.size() > 0) {
+    std::string_view byte = register_value_str.substr(0, 2);
+    register_value_str.remove_prefix(2);
+    uint8_t byte_value;
+    (void)absl::SimpleHexAtoi(byte, &byte_value);
+    value |= byte_value << (count * 8);
+    count++;
+  }
+  auto status = core_debug_interfaces_[0]->WriteRegister(register_name, value);
+  if (!status.ok()) {
+    SendError(status.message());
+    return;
+  }
+  Respond("OK");
+}
+
+void GdbServer::GdbQuery(std::string_view command) {
+  switch (command.front()) {
+    default:
+      break;
+    case 'A':  // Attached?
+      if (command.starts_with("Attached")) {
+        Respond("0");
+        return;
+      }
+      break;
+    case 'C':  // Current thread ID.
+      if (command == "C") {
+        Respond(absl::StrCat("QC", current_thread_id_));
+        return;
+      }
+      break;
+    case 'E':  // ExecAndArgs?
+      if (command.starts_with("ExecAndArgs")) {
+        command.remove_prefix(11);
+        GdbExecAndArgs(command);
+        return;
+      }
+      break;
+    case 'f':
+      if (command.starts_with("fThreadInfo")) {
+        GdbThreadInfo();
+        return;
+      }
+      break;
+    case 's':
+      if (command.starts_with("sThreadInfo")) {
+        Respond("l");
+        return;
+      }
+      break;
+    case 'S': {  // Search, Supported, or Symbol.
+      if (command.starts_with("Search")) break;
+      if (command.starts_with("Symbol")) break;
+      if (command.starts_with("Supported")) {
+        command.remove_prefix(9);
+        GdbSupported(command);
+        return;
+      }
+    }
+  }
+  Respond("");  // Not supported for now.
+}
+
+void GdbServer::GdbSet(std::string_view command) {
+  Respond("");
+  // TODO(torerik): Implement.
+}
+
+void GdbServer::GdbStep(std::string_view command) {
+  auto result = core_debug_interfaces_[0]->Step(1);
+  if (!result.ok()) {
+    SendError(result.status().message());
+    return;
+  }
+  Respond("T05");
+}
+
+void GdbServer::GdbAddBreakpoint(char type, std::string_view address_str,
+                                 std::string_view kind_str) {
+  uint64_t address;
+  bool success = absl::SimpleHexAtoi(address_str, &address);
+  if (!success) {
+    SendError("invalid breakpoint address");
+    return;
+  }
+  size_t kind;
+  switch (type) {
+    default:
+      SendError("invalid breakpoint type");
+      return;
+    case '0': {  // Software breakpoint.
+      auto status = core_debug_interfaces_[0]->SetSwBreakpoint(address);
+      if (!status.ok()) {
+        SendError(status.message());
+        return;
+      }
+      Respond("OK");
+      return;
+    }
+    case '2': {  // Data watchpoint write only.
+      bool success = absl::SimpleHexAtoi(kind_str, &kind);
+      if (!success) {
+        SendError("invalid watchpoint kind");
+        return;
+      }
+      auto status = core_debug_interfaces_[0]->SetDataWatchpoint(
+          address, kind, generic::AccessType::kStore);
+      if (!status.ok()) {
+        SendError(status.message());
+        return;
+      }
+      Respond("OK");
+      return;
+    }
+    case '3': {  // Data watchpoint read-only.
+      bool success = absl::SimpleHexAtoi(kind_str, &kind);
+      if (!success) {
+        SendError("invalid watchpoint kind");
+        return;
+      }
+      auto status = core_debug_interfaces_[0]->SetDataWatchpoint(
+          address, kind, generic::AccessType::kLoad);
+      if (!status.ok()) {
+        SendError(status.message());
+        return;
+      }
+      Respond("OK");
+      return;
+    }
+    case '4': {  // Data watchpoint read or write.
+      bool success = absl::SimpleHexAtoi(kind_str, &kind);
+      if (!success) {
+        SendError("invalid watchpoint kind");
+        return;
+      }
+      auto status = core_debug_interfaces_[0]->SetDataWatchpoint(
+          address, kind, generic::AccessType::kLoadStore);
+      if (!status.ok()) {
+        SendError(status.message());
+        return;
+      }
+      Respond("OK");
+      break;
+    }
+  }
+  // TODO(torerik): Implement.
+  Respond("");
+}
+
+void GdbServer::GdbRemoveBreakpoint(char type, std::string_view address_str,
+                                    std::string_view kind_str) {
+  uint64_t address;
+  bool success = absl::SimpleHexAtoi(address_str, &address);
+  if (!success) {
+    SendError("invalid breakpoint address");
+    return;
+  }
+  switch (type) {
+    default:
+      SendError("invalid breakpoint type");
+      return;
+    case '0': {  // Software breakpoint.
+      auto status = core_debug_interfaces_[0]->ClearSwBreakpoint(address);
+      if (!status.ok()) {
+        SendError(status.message());
+        return;
+      }
+      Respond("OK");
+      break;
+    }
+    case 2: {  // Data watchpoint write only.
+      auto status = core_debug_interfaces_[0]->ClearDataWatchpoint(
+          address, generic::AccessType::kStore);
+      if (!status.ok()) {
+        SendError(status.message());
+        return;
+      }
+      Respond("OK");
+      return;
+    }
+    case 3: {  // Data watchpoint read-only.
+      auto status = core_debug_interfaces_[0]->ClearDataWatchpoint(
+          address, generic::AccessType::kLoad);
+      if (!status.ok()) {
+        SendError(status.message());
+        return;
+      }
+      Respond("OK");
+      return;
+    }
+    case 4: {  // Data watchpoint read or write.
+      auto status = core_debug_interfaces_[0]->ClearDataWatchpoint(
+          address, generic::AccessType::kLoadStore);
+      if (!status.ok()) {
+        SendError(status.message());
+        return;
+      }
+      Respond("OK");
+      return;
+    }
+  }
+  Respond("");
+}
+
+void GdbServer::GdbSupported(std::string_view command) {
+  // Read supported GDB features.
+  if (command.front() == ':') {
+    command.remove_prefix(1);
+    std::vector<std::string> features = absl::StrSplit(command, ';');
+    for (const std::string& feature : features) {
+      if (feature == "error-message+") {
+        error_message_supported_ = true;
+      }
+    }
+  }
+  std::string response;
+  absl::StrAppend(
+      &response, "PacketSize=8192;multi-wp-addr-",
+      ";multiprocess-;hwbreak-;qRelocInsn-;fork-events-;exec-events-"
+      ";vContSupported-;QThreadEvents-;QThreadOptions-;no-resumed-"
+      ";memory-tagging-;vfork-events-");
+  Respond(response);
+}
+
+void GdbServer::GdbExecAndArgs(std::string_view command) {
+  std::string file_name = core_debug_interfaces_[0]->GetExecutableFileName();
+  Respond(file_name);
+}
+
+}  // namespace mpact::sim::util::gdbserver
diff --git a/mpact/sim/util/gdbserver/gdbserver.h b/mpact/sim/util/gdbserver/gdbserver.h
new file mode 100644
index 0000000..da3de18
--- /dev/null
+++ b/mpact/sim/util/gdbserver/gdbserver.h
@@ -0,0 +1,133 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file contains a GDB server that can be used to with MPACT-Sim based
+// simulators. It requires that the debugger (gdb, lldb, etc.) already
+// supports your architecture.
+
+#ifndef MPACT_SIM_UTIL_GDBSERVER_GDBSERVER_H_
+#define MPACT_SIM_UTIL_GDBSERVER_GDBSERVER_H_
+
+#include <cstdint>
+#include <istream>
+#include <ostream>
+#include <string_view>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/types/span.h"
+#include "mpact/sim/generic/core_debug_interface.h"
+#include "mpact/sim/generic/debug_info.h"
+#include "mpact/sim/util/renode/socket_streambuf.h"
+
+namespace mpact::sim::util::gdbserver {
+
+using ::mpact::sim::generic::DebugInfo;
+using ::mpact::sim::util::renode::SocketStreambuf;
+
+class GdbServer {
+ public:
+  // The constructor takes a span of core debug interfaces (usually the sim
+  // top objects), and a debug info object to provide information about the
+  // registers. Programs should already be loaded onto the core debug
+  // interfaces before the Gdb server is connected.
+
+  explicit GdbServer(
+      absl::Span<generic::CoreDebugInterface*> core_debug_interfaces,
+      const DebugInfo& debug_info);
+  ~GdbServer();
+
+  // Open the GDB server on the given port, wait for a connection, and process
+  // the GDB commands.
+  bool Connect(int port);
+
+ private:
+  // Terminate the connection to the GDB client.
+  void Terminate();
+  // Sends the given response to the GDB client. Wrapping it in the proper
+  // packet format and adding the checksum.
+  void Respond(std::string_view response);
+  // Sends the given error message to the GDB client in an error packet.
+  void SendError(std::string_view error);
+
+  // Takes a GDB command string, verifies the checksum, and if it is valid,
+  // then submits the command to be parsed.
+  void AcceptGdbCommand(std::string_view command);
+  // Parses the given GDB command and calls the appropriate command handler.
+  void ParseGdbCommand(std::string_view command);
+
+  // GDB command handlers.
+
+  // Halt the simulator.
+  void GdbHalt();
+  // Return the halt reason.
+  void GdbHaltReason();
+  // Continue the simulation.
+  void GdbContinue(std::string_view command);
+  // Detach from the simulator.
+  void GdbDetach();
+  // Select the thread to operate on.
+  void GdbSelectThread(std::string_view command);
+  // Return the thread info.
+  void GdbThreadInfo();
+  // Read memory from the simulator.
+  void GdbReadMemory(std::string_view address, std::string_view length);
+  // Write memory to the simulator.
+  void GdbWriteMemory(std::string_view address, std::string_view length,
+                      std::string_view data);
+  // Read GPR registers from the simulator.
+  void GdbReadGprRegisters();
+  // Write GPR registers to the simulator.
+  void GdbWriteGprRegisters(std::string_view data);
+  // Read a register from the simulator.
+  void GdbReadRegister(std::string_view register_number_str);
+  // Write a register to the simulator.
+  void GdbWriteRegister(std::string_view register_number_str,
+                        std::string_view register_value_str);
+  // Query gdbserver features.
+  void GdbQuery(std::string_view command);
+  // Set gdbserver features.
+  void GdbSet(std::string_view command);
+  // Step the simulator.
+  void GdbStep(std::string_view command);
+  // Add a breakpoint to the simulator.
+  void GdbAddBreakpoint(char type, std::string_view address_str,
+                        std::string_view kind_str);
+  // Remove a breakpoint from the simulator.
+  void GdbRemoveBreakpoint(char type, std::string_view address_str,
+                           std::string_view kind_str);
+  // Respond with the supported GDB features.
+  void GdbSupported(std::string_view command);
+  // Get the executable file name and program arguments.
+  void GdbExecAndArgs(std::string_view command);
+
+  SocketStreambuf* out_buf_ = nullptr;
+  SocketStreambuf* in_buf_ = nullptr;
+  std::ostream* os_ = nullptr;
+  std::istream* is_ = nullptr;
+
+  bool connection_active_ = false;
+  bool error_message_supported_ = false;
+  uint8_t buffer_[16 * 1024];
+  bool good_ = false;
+  int server_socket_ = -1;
+  int cli_fd_ = -1;
+  int current_thread_id_ = 0;
+  absl::flat_hash_map<char, int> thread_select_;
+  absl::Span<generic::CoreDebugInterface*> core_debug_interfaces_;
+  const DebugInfo& debug_info_;
+};
+
+}  // namespace mpact::sim::util::gdbserver
+
+#endif  // MPACT_SIM_UTIL_GDBSERVER_GDBSERVER_H_
diff --git a/mpact/sim/util/renode/socket_cli.cc b/mpact/sim/util/renode/socket_cli.cc
index 17a158b..4185f38 100644
--- a/mpact/sim/util/renode/socket_cli.cc
+++ b/mpact/sim/util/renode/socket_cli.cc
@@ -24,7 +24,7 @@
 #include <iostream>
 #include <istream>
 #include <ostream>
-#include <thread>
+#include <thread>  // NOLINT
 #include <utility>
 
 #include "absl/functional/any_invocable.h"
diff --git a/mpact/sim/util/renode/socket_cli.h b/mpact/sim/util/renode/socket_cli.h
index 56d4807..74124a8 100644
--- a/mpact/sim/util/renode/socket_cli.h
+++ b/mpact/sim/util/renode/socket_cli.h
@@ -5,7 +5,7 @@
 #include <sys/socket.h>
 #include <sys/types.h>
 
-#include <thread>
+#include <thread>  // NOLINT
 
 #include "absl/functional/any_invocable.h"
 #include "mpact/sim/generic/debug_command_shell_interface.h"