Fixed minor error in gdbserver. Added gdbserver tests

PiperOrigin-RevId: 890507166
Change-Id: Id5937d0529cda21d3f948e846d03ec5fb42c1407
diff --git a/mpact/sim/util/gdbserver/gdbserver.cc b/mpact/sim/util/gdbserver/gdbserver.cc
index adf340c..d39b2a8 100644
--- a/mpact/sim/util/gdbserver/gdbserver.cc
+++ b/mpact/sim/util/gdbserver/gdbserver.cc
@@ -70,6 +70,13 @@
 
 namespace mpact::sim::util::gdbserver {
 
+LazyRE2 GdbServer::gdb_command_re_(R"(\$([^#]*)#([0-9a-fA-F]{2}))");
+LazyRE2 GdbServer::thread_re_(R"(;?thread\:(\d+);?)");
+LazyRE2 GdbServer::xfer_read_target_re_(
+    R"(Xfer\:features\:read\:target\.xml\:([0-9a-fA-F]+),([0-9a-fA-F]+))");
+LazyRE2 GdbServer::swbreak_set_re_(R"(Z0,([0-9a-fA-F]+),(.+))");
+LazyRE2 GdbServer::swbreak_clear_re_(R"(z0,([0-9a-fA-F]+),(.+))");
+
 using ::mpact::sim::generic::operator*;  // NOLINT
 using HaltReason = ::mpact::sim::generic::CoreDebugInterface::HaltReason;
 
@@ -78,13 +85,7 @@
     const DebugInfo& debug_info)
     : log_packets_(absl::GetFlag(FLAGS_log_packets)),
       core_debug_interfaces_(core_debug_interfaces),
-      debug_info_(debug_info),
-      gdb_command_re_(R"(\$([^#]*)#([0-9a-fA-F]{2}))"),
-      thread_re_(R"(;?thread\:(\d+);?)"),
-      xfer_read_target_re_(
-          R"(Xfer\:features\:read\:target\.xml\:([0-9a-fA-F]+),([0-9a-fA-F]+))"),
-      swbreak_set_re_(R"(Z0,([0-9a-fA-F]+),(.+))"),
-      swbreak_clear_re_(R"(z0,([0-9a-fA-F]+),(.+))") {
+      debug_info_(debug_info) {
   if (core_debug_interfaces.size() > 1) {
     LOG(WARNING) << "MPACT GdbServer only supports one core for now - "
                     "ignoring extra cores.";
@@ -867,15 +868,13 @@
     // gap in the register numbers).
     if (it == debug_info_.debug_register_map().end()) continue;
     const std::string& register_name = it->second;
-    auto result =
-        core_debug_interfaces_[thread_index]->ReadRegister(register_name);
+    auto result = core_debug_interfaces_[thread_index]->GetRegisterDataBuffer(
+        register_name);
     if (!result.ok()) {
       return SendError(result.status().message());
     }
-    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;
+    for (const auto& byte : result.value()->Get<uint8_t>()) {
+      absl::StrAppend(&response, absl::Hex(byte, absl::kZeroPad2));
     }
   }
   Respond(response);
@@ -883,27 +882,23 @@
 
 void GdbServer::GdbWriteGprRegisters(int thread_id, std::string_view data) {
   int thread_index = thread_id - 1;
-  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()) {
       return SendError("Internal error - failed to find register");
     }
     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);
+    auto result = core_debug_interfaces_[thread_index]->GetRegisterDataBuffer(
+        register_name);
+    for (auto& byte : result.value()->Get<uint8_t>()) {
+      if (data.empty()) {
+        return SendError("Invalid data format");
+      }
+      std::string_view byte_str = data.substr(0, 2);
       data.remove_prefix(2);
       uint32_t byte_value_tmp;
-      (void)absl::SimpleHexAtoi(byte, &byte_value_tmp);
-      uint8_t byte_value = static_cast<uint8_t>(byte_value_tmp);
-      value |= byte_value << (j * 8);
-      num_bytes++;
-    }
-    auto status = core_debug_interfaces_[thread_index]->WriteRegister(
-        register_name, value);
-    if (!status.ok()) {
-      return SendError(status.message());
+      (void)absl::SimpleHexAtoi(byte_str, &byte_value_tmp);
+      byte = static_cast<uint8_t>(byte_value_tmp);
     }
   }
   Respond("OK");
@@ -923,16 +918,14 @@
     return SendError(absl::StrCat("Register not found: ", register_number));
   }
   const std::string& register_name = it->second;
-  auto result =
-      core_debug_interfaces_[thread_index]->ReadRegister(register_name);
+  auto result = core_debug_interfaces_[thread_index]->GetRegisterDataBuffer(
+      register_name);
   if (!result.ok()) {
     return SendError(result.status().message());
   }
   std::string response;
-  uint64_t value = result.value();
-  for (int i = 0; i < debug_info_.GetRegisterByteWidth(register_number); i++) {
-    absl::StrAppend(&response, absl::Hex(value & 0xff, absl::kZeroPad2));
-    value >>= 8;
+  for (const auto& byte : result.value()->Get<uint8_t>()) {
+    absl::StrAppend(&response, absl::Hex(byte, absl::kZeroPad2));
   }
   Respond(response);
 }
@@ -953,21 +946,18 @@
     return SendError(absl::StrCat("Register not found: ", register_number));
   }
   const std::string& register_name = it->second;
-  uint64_t value = 0;
-  int count = 0;
-  while (!register_value_str.empty()) {
-    std::string_view byte = register_value_str.substr(0, 2);
+  auto result = core_debug_interfaces_[thread_index]->GetRegisterDataBuffer(
+      register_name);
+  if (!result.ok()) {
+    return SendError(result.status().message());
+  }
+  for (auto& byte : result.value()->Get<uint8_t>()) {
+    if (register_value_str.empty()) break;
+    std::string_view byte_str = register_value_str.substr(0, 2);
     register_value_str.remove_prefix(2);
     uint32_t byte_value_tmp;
-    (void)absl::SimpleHexAtoi(byte, &byte_value_tmp);
-    uint8_t byte_value = static_cast<uint8_t>(byte_value_tmp);
-    value |= byte_value << (count * 8);
-    count++;
-  }
-  auto status =
-      core_debug_interfaces_[thread_index]->WriteRegister(register_name, value);
-  if (!status.ok()) {
-    return SendError(status.message());
+    (void)absl::SimpleHexAtoi(byte_str, &byte_value_tmp);
+    byte = static_cast<uint8_t>(byte_value_tmp);
   }
   Respond("OK");
 }
diff --git a/mpact/sim/util/gdbserver/gdbserver.h b/mpact/sim/util/gdbserver/gdbserver.h
index 0ee9dca..791eccd 100644
--- a/mpact/sim/util/gdbserver/gdbserver.h
+++ b/mpact/sim/util/gdbserver/gdbserver.h
@@ -182,11 +182,13 @@
   absl::Span<generic::CoreDebugInterface*> core_debug_interfaces_;
   std::vector<int> halt_reasons_;
   const DebugInfo& debug_info_;
-  LazyRE2 gdb_command_re_;
-  LazyRE2 thread_re_;
-  LazyRE2 xfer_read_target_re_;
-  LazyRE2 swbreak_set_re_;
-  LazyRE2 swbreak_clear_re_;
+  // Regular expressions used to parse GDB commands.
+  // These are static since they are immutable once initialized.
+  static LazyRE2 gdb_command_re_;
+  static LazyRE2 thread_re_;
+  static LazyRE2 xfer_read_target_re_;
+  static LazyRE2 swbreak_set_re_;
+  static LazyRE2 swbreak_clear_re_;
 };
 
 }  // namespace mpact::sim::util::gdbserver
diff --git a/mpact/sim/util/gdbserver/test/BUILD b/mpact/sim/util/gdbserver/test/BUILD
new file mode 100644
index 0000000..519999e
--- /dev/null
+++ b/mpact/sim/util/gdbserver/test/BUILD
@@ -0,0 +1,41 @@
+# 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.
+
+load("@rules_cc//cc:cc_test.bzl", "cc_test")
+
+package(
+    default_applicable_licenses = ["//:license"],
+    default_visibility = ["//visibility:public"],
+)
+
+cc_test(
+    name = "gdbserver_test",
+    srcs = ["gdbserver_test.cc"],
+    deps = [
+        "//mpact/sim/generic:core",
+        "//mpact/sim/generic:core_debug_interface",
+        "//mpact/sim/generic:instruction",
+        "//mpact/sim/util/gdbserver",
+        "//net/util:ports",
+        "//thread/fiber",
+        "@abseil-cpp//absl/container:flat_hash_map",
+        "@abseil-cpp//absl/log",
+        "@abseil-cpp//absl/status",
+        "@abseil-cpp//absl/strings",
+        "@abseil-cpp//absl/strings:str_format",
+        "@abseil-cpp//absl/time",
+        "@abseil-cpp//absl/types:span",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/mpact/sim/util/gdbserver/test/gdbserver_test.cc b/mpact/sim/util/gdbserver/test/gdbserver_test.cc
new file mode 100644
index 0000000..aa9dc35
--- /dev/null
+++ b/mpact/sim/util/gdbserver/test/gdbserver_test.cc
@@ -0,0 +1,646 @@
+// 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 "mpact/sim/util/gdbserver/gdbserver.h"
+
+#include <arpa/inet.h>
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <string>
+#include <vector>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/log/log.h"
+#include "absl/status/status.h"
+#include "absl/strings/numbers.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_format.h"
+#include "absl/strings/string_view.h"
+#include "absl/time/clock.h"
+#include "absl/time/time.h"
+#include "absl/types/span.h"
+#include "googlemock/include/gmock/gmock.h"
+#include "googletest/include/gtest/gtest.h"
+#include "mpact/sim/generic/core_debug_interface.h"
+#include "mpact/sim/generic/data_buffer.h"
+#include "mpact/sim/generic/debug_info.h"
+#include "mpact/sim/generic/instruction.h"
+#include "net/util/ports.h"
+#include "thread/fiber/fiber.h"
+
+namespace mpact::sim::util::gdbserver {
+namespace {
+
+using ::mpact::sim::generic::AccessType;
+using ::mpact::sim::generic::CoreDebugInterface;
+using ::mpact::sim::generic::DataBufferFactory;
+using ::mpact::sim::generic::DebugInfo;
+using RunStatus = ::mpact::sim::generic::CoreDebugInterface::RunStatus;
+using HaltReason = ::mpact::sim::generic::CoreDebugInterface::HaltReason;
+using HaltReasonValueType =
+    ::mpact::sim::generic::CoreDebugInterface::HaltReasonValueType;
+using ::mpact::sim::generic::Instruction;
+using ::testing::_;
+using ::testing::Return;
+
+// Mock debug interface for testing.
+class MockCoreDebugInterface : public CoreDebugInterface {
+ public:
+  MOCK_METHOD(absl::StatusOr<uint64_t>, ReadRegister, (const std::string& name),
+              (override));
+  MOCK_METHOD(absl::Status, WriteRegister,
+              (const std::string& name, uint64_t value), (override));
+  MOCK_METHOD(absl::StatusOr<size_t>, ReadMemory,
+              (uint64_t address, void* buf, size_t length), (override));
+  MOCK_METHOD(absl::StatusOr<size_t>, WriteMemory,
+              (uint64_t address, const void* buf, size_t length), (override));
+  MOCK_METHOD(absl::StatusOr<generic::DataBuffer*>, GetRegisterDataBuffer,
+              (const std::string& name), (override));
+  MOCK_METHOD(absl::Status, Run, (), (override));
+  MOCK_METHOD(absl::Status, Halt, (), (override));
+  MOCK_METHOD(absl::Status, Halt, (HaltReason reason), (override));
+  MOCK_METHOD(absl::Status, Halt, (HaltReasonValueType halt_reason),
+              (override));
+  MOCK_METHOD(absl::StatusOr<int>, Step, (int num), (override));
+  MOCK_METHOD(absl::StatusOr<RunStatus>, GetRunStatus, (), (override));
+  MOCK_METHOD(absl::Status, Wait, (), (override));
+  MOCK_METHOD(absl::StatusOr<HaltReasonValueType>, GetLastHaltReason, (),
+              (override));
+  MOCK_METHOD(absl::Status, SetSwBreakpoint, (uint64_t address), (override));
+  MOCK_METHOD(absl::Status, ClearSwBreakpoint, (uint64_t address), (override));
+  MOCK_METHOD(absl::Status, ClearAllSwBreakpoints, (), (override));
+  MOCK_METHOD(bool, HasBreakpoint, (uint64_t address), (override));
+  MOCK_METHOD(uint64_t, GetSwBreakpointInfo, (), (const, override));
+  MOCK_METHOD(absl::Status, SetDataWatchpoint,
+              (uint64_t address, size_t length, AccessType access_type),
+              (override));
+  MOCK_METHOD(absl::Status, ClearDataWatchpoint,
+              (uint64_t address, AccessType access_type), (override));
+  MOCK_METHOD(void, GetWatchpointInfo,
+              (uint64_t& address, AccessType& access_type), (override));
+  MOCK_METHOD(std::string, GetExecutableFileName, (), (override));
+  MOCK_METHOD(absl::StatusOr<Instruction*>, GetInstruction, (uint64_t address),
+              (override));
+  MOCK_METHOD(absl::StatusOr<std::string>, GetDisassembly, (uint64_t address),
+              (override));
+};
+
+// Test debug info class.
+class TestDebugInfo : public DebugInfo {
+ public:
+  TestDebugInfo() {
+    for (int i = 0; i < 32; ++i) {
+      debug_register_map_[i] = absl::StrCat("x", i);
+    }
+    debug_register_map_[32] = "pc";
+  }
+  const DebugRegisterMap& debug_register_map() const override {
+    return debug_register_map_;
+  }
+  int GetFirstGpr() const override { return 0; }
+  int GetLastGpr() const override { return 31; }
+  int GetGprWidth() const override { return 64; }
+  int GetRegisterByteWidth(int register_number) const override {
+    return 64 / 8;
+  }
+  std::string_view GetLLDBHostInfo() const override { return ""; }
+  std::string_view GetGdbTargetXml() const override { return ""; }
+
+ private:
+  DebugRegisterMap debug_register_map_;
+};
+
+// Minimal GDB client for testing.
+class GdbTestClient {
+ public:
+  GdbTestClient() = default;
+  ~GdbTestClient() {
+    if (sock_fd_ != -1) {
+      close(sock_fd_);
+    }
+  }
+
+  // Connect to the server.
+  bool Connect(int port) {
+    sock_fd_ = socket(AF_INET, SOCK_STREAM, 0);
+    if (sock_fd_ < 0) {
+      LOG(ERROR) << "Socket creation error";
+      return false;
+    }
+
+    struct sockaddr_in serv_addr;
+    serv_addr.sin_family = AF_INET;
+    serv_addr.sin_port = htons(port);
+
+    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
+      LOG(ERROR) << "Invalid address/ Address not supported";
+      return false;
+    }
+
+    if (connect(sock_fd_, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) <
+        0) {
+      LOG(ERROR) << "Connection Failed";
+      return false;
+    }
+    return true;
+  }
+
+  // GDB handshake: send '+'.
+  void SendAck() {
+    char ack = '+';
+    send(sock_fd_, &ack, 1, 0);
+  }
+
+  // Read ack from server, expecting '+'.
+  bool ExpectAck() {
+    char ack = 0;
+    read(sock_fd_, &ack, 1);
+    return ack == '+';
+  }
+
+  // Send a command string, wrapping it in GDB packet format.
+  void SendCommand(const std::string& command) {
+    uint8_t checksum = 0;
+    for (char c : command) {
+      checksum += c;
+    }
+    std::string packet =
+        absl::StrFormat("$%s#%02x", command, static_cast<int>(checksum));
+    send(sock_fd_, packet.c_str(), packet.length(), 0);
+  }
+
+  // Receive a response packet from server and extract content.
+  std::string ReceiveResponse() {
+    char c;
+    // Look for '$'.
+    do {
+      read(sock_fd_, &c, 1);
+    } while (c != '$');
+
+    std::string response;
+    uint8_t checksum = 0;
+    while (true) {
+      read(sock_fd_, &c, 1);
+      if (c == '#') break;
+      response += c;
+      checksum += c;
+    }
+    char checksum_chars[3];
+    int read_count = 0;
+    while (read_count < 2) {
+      read_count += read(sock_fd_, &checksum_chars[read_count], 2 - read_count);
+    }
+    checksum_chars[2] = '\0';
+    int received_checksum;
+    if (!absl::SimpleHexAtoi(checksum_chars, &received_checksum)) {
+      LOG(ERROR) << "Invalid checksum format in response";
+      return "ERROR";
+    }
+    if (checksum != received_checksum) {
+      LOG(ERROR) << "Checksum mismatch: expected "
+                 << absl::StrFormat("%02x", checksum) << ", got "
+                 << checksum_chars;
+      return "ERROR";
+    }
+    return response;
+  }
+
+ private:
+  int sock_fd_ = -1;
+};
+
+class GdbServerTest : public ::testing::Test {
+ protected:
+  GdbServerTest() {
+    core_debug_interfaces_.push_back(&mock_core_);
+    debug_info_ = new TestDebugInfo();
+    gdb_server_ =
+        new GdbServer(absl::MakeSpan(core_debug_interfaces_), *debug_info_);
+  }
+
+  ~GdbServerTest() override {
+    delete gdb_server_;
+    delete debug_info_;
+  }
+
+  MockCoreDebugInterface mock_core_;
+  std::vector<CoreDebugInterface*> core_debug_interfaces_;
+  DebugInfo* debug_info_;
+  GdbServer* gdb_server_;
+  DataBufferFactory db_factory_;
+};
+
+TEST_F(GdbServerTest, Detach) {
+  int port = net_util::PickUnusedPortOrDie();
+
+  thread::Fiber server_fiber([&]() { gdb_server_->Connect(port); });
+
+  // Give server time to start up and listen.
+  absl::SleepFor(absl::Milliseconds(100));
+
+  GdbTestClient client;
+  ASSERT_TRUE(client.Connect(port));
+
+  // Initial handshake.
+  client.SendAck();
+  // Send 'D' - detach command.
+  client.SendCommand("D");
+  // Server should ack command.
+  EXPECT_TRUE(client.ExpectAck());
+  // Server should respond with OK.
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  // Client should ack response.
+  client.SendAck();
+
+  server_fiber.Join();
+}
+
+TEST_F(GdbServerTest, HaltReason) {
+  int port = net_util::PickUnusedPortOrDie();
+  EXPECT_CALL(mock_core_, GetLastHaltReason())
+      .WillOnce(Return(
+          static_cast<HaltReasonValueType>(HaltReason::kSoftwareBreakpoint)));
+
+  thread::Fiber server_fiber([&]() { gdb_server_->Connect(port); });
+
+  // Give server time to start up and listen.
+  absl::SleepFor(absl::Milliseconds(100));
+
+  GdbTestClient client;
+  ASSERT_TRUE(client.Connect(port));
+
+  // Initial handshake.
+  client.SendAck();
+  // Send '?' - halt reason command.
+  client.SendCommand("?");
+  // Server should ack command.
+  EXPECT_TRUE(client.ExpectAck());
+  // Server should respond with T02thread:1.
+  EXPECT_EQ("T02thread:1", client.ReceiveResponse());
+  // Client should ack response.
+  client.SendAck();
+
+  // Send 'D' - detach command to terminate connection.
+  client.SendCommand("D");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  server_fiber.Join();
+}
+
+TEST_F(GdbServerTest, ReadGpr) {
+  int port = net_util::PickUnusedPortOrDie();
+
+  std::vector<std::array<uint8_t, 8>> reg_storage(32);
+  std::vector<generic::DataBuffer*> dbs;
+  for (int i = 0; i < 32; ++i) {
+    generic::DataBuffer* db = db_factory_.Allocate<uint8_t>(8);
+    dbs.push_back(db);
+    uint64_t reg_val = 0x0102030405060700 | i;
+    memcpy(db->raw_ptr(), &reg_val, 8);
+  }
+
+  for (int i = 0; i < 32; ++i) {
+    EXPECT_CALL(mock_core_, GetRegisterDataBuffer(absl::StrCat("x", i)))
+        .WillOnce(Return(dbs[i]));
+  }
+
+  thread::Fiber server_fiber([&]() { gdb_server_->Connect(port); });
+
+  absl::SleepFor(absl::Milliseconds(100));
+
+  GdbTestClient client;
+  ASSERT_TRUE(client.Connect(port));
+
+  client.SendAck();
+  client.SendCommand("g");
+  EXPECT_TRUE(client.ExpectAck());
+  std::string response = client.ReceiveResponse();
+  client.SendAck();
+  // 32 registers * 8 bytes/reg * 2 hex chars/byte = 512 hex chars.
+  EXPECT_EQ(response.length(), 32 * 8 * 2);
+  // Reg x0: 0007060504030201
+  EXPECT_EQ(response.substr(0, 16), "0007060504030201");
+  // Reg x31: 1f07060504030201
+  EXPECT_EQ(response.substr(31 * 16, 16), "1f07060504030201");
+
+  for (auto* db : dbs) db->DecRef();
+
+  client.SendCommand("D");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+  server_fiber.Join();
+}
+
+TEST_F(GdbServerTest, ReadRegister) {
+  int port = net_util::PickUnusedPortOrDie();
+
+  uint64_t reg_val = 0x1234;
+  generic::DataBuffer* db = db_factory_.Allocate<uint8_t>(8);
+  memcpy(db->raw_ptr(), &reg_val, 8);
+
+  // Reading register 32 ('pc', hex '20').
+  EXPECT_CALL(mock_core_, GetRegisterDataBuffer("pc")).WillOnce(Return(db));
+
+  thread::Fiber server_fiber([&]() { gdb_server_->Connect(port); });
+
+  absl::SleepFor(absl::Milliseconds(100));
+
+  GdbTestClient client;
+  ASSERT_TRUE(client.Connect(port));
+
+  client.SendAck();
+  // Read register 0x20 (pc).
+  client.SendCommand("p20");
+  EXPECT_TRUE(client.ExpectAck());
+  // 0x1234 in little endian hex is 3412000000000000 for 64 bits.
+  EXPECT_EQ("3412000000000000", client.ReceiveResponse());
+  client.SendAck();
+
+  db->DecRef();
+
+  client.SendCommand("D");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+  server_fiber.Join();
+}
+
+TEST_F(GdbServerTest, ReadMemory) {
+  int port = net_util::PickUnusedPortOrDie();
+
+  // Read 4 bytes from 0x1000.
+  EXPECT_CALL(mock_core_, ReadMemory(0x1000, _, 4))
+      .WillOnce([](uint64_t address, void* buf, size_t length) {
+        uint8_t data[] = {0x01, 0x02, 0x03, 0x04};
+        memcpy(buf, data, 4);
+        return 4;
+      });
+
+  thread::Fiber server_fiber([&]() { gdb_server_->Connect(port); });
+
+  absl::SleepFor(absl::Milliseconds(100));
+
+  GdbTestClient client;
+  ASSERT_TRUE(client.Connect(port));
+
+  client.SendAck();
+  // Read 4 bytes from 0x1000: m1000,4
+  client.SendCommand("m1000,4");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("01020304", client.ReceiveResponse());
+  client.SendAck();
+
+  client.SendCommand("D");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  server_fiber.Join();
+}
+
+TEST_F(GdbServerTest, Continue) {
+  int port = net_util::PickUnusedPortOrDie();
+
+  EXPECT_CALL(mock_core_, GetRunStatus()).WillOnce(Return(RunStatus::kHalted));
+  EXPECT_CALL(mock_core_, Run()).WillOnce(Return(absl::OkStatus()));
+  EXPECT_CALL(mock_core_, Wait()).WillOnce(Return(absl::OkStatus()));
+  EXPECT_CALL(mock_core_, GetLastHaltReason())
+      .WillOnce(Return(
+          static_cast<HaltReasonValueType>(HaltReason::kHardwareBreakpoint)));
+
+  thread::Fiber server_fiber([&]() { gdb_server_->Connect(port); });
+
+  absl::SleepFor(absl::Milliseconds(100));
+
+  GdbTestClient client;
+  ASSERT_TRUE(client.Connect(port));
+
+  client.SendAck();
+  client.SendCommand("c");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("T02thread:1", client.ReceiveResponse());
+  client.SendAck();
+
+  client.SendCommand("D");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  server_fiber.Join();
+}
+
+TEST_F(GdbServerTest, Step) {
+  int port = net_util::PickUnusedPortOrDie();
+
+  EXPECT_CALL(mock_core_, Step(1)).WillOnce(Return(1));
+
+  thread::Fiber server_fiber([&]() { gdb_server_->Connect(port); });
+
+  absl::SleepFor(absl::Milliseconds(100));
+
+  GdbTestClient client;
+  ASSERT_TRUE(client.Connect(port));
+
+  client.SendAck();
+  client.SendCommand("s");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("T05", client.ReceiveResponse());
+  client.SendAck();
+
+  client.SendCommand("D");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  server_fiber.Join();
+}
+
+TEST_F(GdbServerTest, WriteGpr) {
+  int port = net_util::PickUnusedPortOrDie();
+
+  std::vector<generic::DataBuffer*> dbs;
+  for (int i = 0; i < 32; ++i) {
+    generic::DataBuffer* db = db_factory_.Allocate<uint8_t>(8);
+    dbs.push_back(db);
+  }
+
+  for (int i = 0; i < 32; ++i) {
+    EXPECT_CALL(mock_core_, GetRegisterDataBuffer(absl::StrCat("x", i)))
+        .WillOnce(Return(dbs[i]));
+  }
+
+  thread::Fiber server_fiber([&]() { gdb_server_->Connect(port); });
+
+  absl::SleepFor(absl::Milliseconds(100));
+
+  GdbTestClient client;
+  ASSERT_TRUE(client.Connect(port));
+
+  client.SendAck();
+  std::string write_cmd = "G";
+  for (int i = 0; i < 32; ++i) {
+    absl::StrAppend(&write_cmd, absl::StrFormat("%02x07060504030201", i));
+  }
+  client.SendCommand(write_cmd);
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  for (int i = 0; i < 32; ++i) {
+    EXPECT_EQ(dbs[i]->Get<uint64_t>(0), 0x0102030405060700 | i);
+    dbs[i]->DecRef();
+  }
+
+  client.SendCommand("D");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  server_fiber.Join();
+}
+
+TEST_F(GdbServerTest, WriteRegister) {
+  int port = net_util::PickUnusedPortOrDie();
+
+  generic::DataBuffer* db = db_factory_.Allocate<uint8_t>(8);
+
+  // Writing register 32 ('pc', hex '20').
+  EXPECT_CALL(mock_core_, GetRegisterDataBuffer("pc")).WillOnce(Return(db));
+
+  thread::Fiber server_fiber([&]() { gdb_server_->Connect(port); });
+
+  absl::SleepFor(absl::Milliseconds(100));
+
+  GdbTestClient client;
+  ASSERT_TRUE(client.Connect(port));
+
+  client.SendAck();
+  // Write register 0x20 (pc) with 0x1234.
+  client.SendCommand("P20=3412000000000000");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  EXPECT_EQ(db->Get<uint64_t>(0), 0x1234);
+
+  db->DecRef();
+
+  client.SendCommand("D");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  server_fiber.Join();
+}
+
+TEST_F(GdbServerTest, WriteMemory) {
+  int port = net_util::PickUnusedPortOrDie();
+
+  // Write 4 bytes to 0x1000 with value 01020304.
+  EXPECT_CALL(mock_core_, WriteMemory(0x1000, _, 4))
+      .WillOnce([](uint64_t address, const void* buf,
+                   size_t length) -> absl::StatusOr<size_t> {
+        uint8_t expected[] = {0x01, 0x02, 0x03, 0x04};
+        if (memcmp(buf, expected, 4) == 0) return 4;
+        return absl::InternalError("Memory content mismatch");
+      });
+
+  thread::Fiber server_fiber([&]() { gdb_server_->Connect(port); });
+
+  absl::SleepFor(absl::Milliseconds(100));
+
+  GdbTestClient client;
+  ASSERT_TRUE(client.Connect(port));
+
+  client.SendAck();
+  // Write 4 bytes to 0x1000: M1000,4:01020304
+  client.SendCommand("M1000,4:01020304");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  client.SendCommand("D");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  server_fiber.Join();
+}
+
+TEST_F(GdbServerTest, SetSwBreakpoint) {
+  int port = net_util::PickUnusedPortOrDie();
+
+  EXPECT_CALL(mock_core_, SetSwBreakpoint(0x1000))
+      .WillOnce(Return(absl::OkStatus()));
+
+  thread::Fiber server_fiber([&]() { gdb_server_->Connect(port); });
+
+  absl::SleepFor(absl::Milliseconds(100));
+  GdbTestClient client;
+  ASSERT_TRUE(client.Connect(port));
+
+  client.SendAck();
+  // Set sw breakpoint at 0x1000
+  client.SendCommand("Z0,1000,0");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  client.SendCommand("D");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  server_fiber.Join();
+}
+
+TEST_F(GdbServerTest, ClearSwBreakpoint) {
+  int port = net_util::PickUnusedPortOrDie();
+
+  EXPECT_CALL(mock_core_, ClearSwBreakpoint(0x1000))
+      .WillOnce(Return(absl::OkStatus()));
+
+  thread::Fiber server_fiber([&]() { gdb_server_->Connect(port); });
+
+  absl::SleepFor(absl::Milliseconds(100));
+  GdbTestClient client;
+  ASSERT_TRUE(client.Connect(port));
+
+  client.SendAck();
+  // Clear sw breakpoint at 0x1000
+  client.SendCommand("z0,1000,0");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  client.SendCommand("D");
+  EXPECT_TRUE(client.ExpectAck());
+  EXPECT_EQ("OK", client.ReceiveResponse());
+  client.SendAck();
+
+  server_fiber.Join();
+}
+
+}  // namespace
+}  // namespace mpact::sim::util::gdbserver