This guide focuses on how to use the C++ API to build a Remote Serial Protocol (RSP) server around your own target. It is intentionally practical and end-user oriented: what to implement, how the pieces fit together, and how to extend support for more features as your target grows.
The library does not provide an emulator, register database, or operating system abstractions. You bring those. gdbstub_cpp supplies the RSP server, a small capability model, and a transport abstraction.
At a high level, you wire three things together:
RSP client <-> transport <-> server <-> target capabilities
- transport: how bytes move between the server and the remote client.
- server: parses/constructs RSP packets and dispatches them to your code.
- target capabilities: tiny objects that let the server read/write registers, memory, and control execution.
The server is non-templated and type-erased. Your target is built from capability
objects and stored behind a target_view that consists of function pointers plus
a context pointer. This makes the server stable and easy to embed.
Include one header to get the public API:
#include "gdbstub/gdbstub.hpp"Key types:
gdbstub::targetandgdbstub::make_target(...)(seesrc/gdbstub/target/target.hpp)gdbstub::arch_spec(seesrc/gdbstub/server/server.hpp)gdbstub::servergdbstub::transportandgdbstub::transport_tcp- RSP data types (stop/restart enums,
resume_request,stop_reason, etc.)
This is the minimal shape of a working integration: implement three required
capabilities (registers, memory, run control), build a target, provide an
arch_spec, and run a server over TCP.
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <memory>
#include <span>
#include <string>
#include <vector>
#include "gdbstub/gdbstub.hpp"
struct DemoRegs {
static constexpr int kRegCount = 16;
std::array<uint64_t, kRegCount> regs{};
size_t reg_size(int regno) const {
if (regno < 0 || regno >= kRegCount) {
return 0;
}
return sizeof(uint64_t);
}
gdbstub::target_status read_reg(int regno, std::span<std::byte> out) {
if (regno < 0 || regno >= kRegCount || out.size() != sizeof(uint64_t)) {
return gdbstub::target_status::invalid;
}
std::memcpy(out.data(), ®s[regno], sizeof(uint64_t));
return gdbstub::target_status::ok;
}
gdbstub::target_status write_reg(int regno, std::span<const std::byte> data) {
if (regno < 0 || regno >= kRegCount || data.size() != sizeof(uint64_t)) {
return gdbstub::target_status::invalid;
}
std::memcpy(®s[regno], data.data(), sizeof(uint64_t));
return gdbstub::target_status::ok;
}
};
struct DemoMem {
std::vector<std::byte> mem;
explicit DemoMem(size_t size) : mem(size) {}
gdbstub::target_status read_mem(uint64_t addr, std::span<std::byte> out) {
if (addr + out.size() > mem.size()) {
return gdbstub::target_status::fault;
}
std::memcpy(out.data(), mem.data() + addr, out.size());
return gdbstub::target_status::ok;
}
gdbstub::target_status write_mem(uint64_t addr, std::span<const std::byte> data) {
if (addr + data.size() > mem.size()) {
return gdbstub::target_status::fault;
}
std::memcpy(mem.data() + addr, data.data(), data.size());
return gdbstub::target_status::ok;
}
};
struct DemoRun {
gdbstub::resume_result resume(const gdbstub::resume_request& request) {
// Minimal behavior: always stop immediately.
gdbstub::stop_reason stop;
stop.kind = gdbstub::stop_kind::signal;
stop.signal = 5; // generic stop
gdbstub::resume_result result;
result.state = gdbstub::resume_result::state::stopped;
result.stop = stop;
result.status = gdbstub::target_status::ok;
return result;
}
};
int main() {
DemoRegs regs;
DemoMem mem(64 * 1024);
DemoRun run;
auto target = gdbstub::make_target(regs, mem, run);
gdbstub::arch_spec arch;
arch.reg_count = DemoRegs::kRegCount;
arch.pc_reg_num = 15; // pick the register you use as PC
arch.target_xml = "<target>...</target>"; // provide real XML
arch.xml_arch_name = "demo";
auto transport = std::make_unique<gdbstub::transport_tcp>();
gdbstub::server server(target, arch, std::move(transport));
server.listen("127.0.0.1:5555");
server.serve_forever();
return 0;
}This example is intentionally minimal: the run capability always reports a stop immediately. For real targets, you will implement execution, stop reporting, and optional features.
The library uses capability objects rather than inheritance. Each capability is identified by the presence of a small set of methods.
Required capabilities (must be provided):
- regs: register access
- mem: memory access
- run: execution control
Optional capabilities (provide any you can):
- breakpoints
- threads
- memory_layout
- host
- process
- shlib
- register_info
gdbstub::make_target(...) is a variadic template that accepts the required
capability objects followed by any number of optional capability objects.
auto target = gdbstub::make_target(regs, mem, run, breakpoints, threads, layout);Important details:
- The required objects are checked at compile time by C++20 concepts.
- Optional capability objects are also detected by concepts.
- Each optional capability can be provided at most once.
Design implication: optional capability objects must not also implement the
required capability method signatures. If you have a single class that implements
everything, split it into small adapters (even if they just forward to the same
underlying object). The toy target in src/gdbstub_tool/toy is a good example
of this composition style.
Signature requirements:
size_t reg_size(int regno);
gdbstub::target_status read_reg(int regno, std::span<std::byte> out);
gdbstub::target_status write_reg(int regno, std::span<const std::byte> data);Semantics:
regnois the register number used by the server (0..reg_count-1).reg_sizereturns the size in bytes for that register.read_regmust fill the whole output buffer.write_regmust consume the whole input buffer.
Guidance:
- If a register is unavailable, return size 0. The server treats size 0 as invalid for single-register access and skips it in bulk reads.
- Ensure
reg_countand your target XML or register_info use the same ordering and sizes. - Use
swap_register_endiannessinarch_specif your internal register storage uses opposite endianness from the RSP representation. The server will reverse bytes per register on both read and write.
Signature requirements:
gdbstub::target_status read_mem(uint64_t addr, std::span<std::byte> out);
gdbstub::target_status write_mem(uint64_t addr, std::span<const std::byte> data);Semantics:
- Reads and writes are all-or-nothing. If you cannot satisfy the full request, return an error status.
- The server clamps read requests to 2048 bytes internally.
- Write size is determined by the incoming packet; the server expects exact size matches and will reject malformed packets before calling you.
Recommended status usage:
ok: successfault: address not mapped or access faultinvalid: malformed request or unsupported sizeunsupported: explicit refusal of optional feature (rare for mem)
Signature requirements:
gdbstub::resume_result resume(const gdbstub::resume_request& request);resume_request fields:
action:cont,step, orrange_stepdirection:forwardorreverseaddr: optional resume address (set PC to this before resuming)signal: optional signal valuerange: optional address range forrange_step
resume_result fields:
state:running,stopped, orexitedstop: astop_reasonthat explains the stopexit_code: optional exit codestatus:ok,fault,invalid,unsupported
Typical patterns:
- Blocking execution: do the run inside
resume()and return astoppedresult when you stop. - Non-blocking execution: return
running, then report stops later viapoll_stop()orset_stop_notifier()(see below).
Optional run-related methods (strongly recommended):
void interrupt();
std::optional<gdbstub::stop_reason> poll_stop();
void set_stop_notifier(gdbstub::stop_notifier notifier);
std::optional<gdbstub::run_capabilities> capabilities();How the server uses them:
interrupt()is called when the client sends an interrupt or when a non-stop stop request arrives.poll_stop()is called duringserver::poll()when the target is running.set_stop_notifier()is invoked when a connection is established and cleared on shutdown. Call the notifier when your run loop detects an async stop.capabilities()advertises optional features (reverse, range_step, non-stop).
If you do not implement poll_stop() or set_stop_notifier(), then resume()
should block until a stop and return stopped. Otherwise the server will never
receive a stop notification for a running target.
arch_spec describes register layout and target characteristics. It is required
for a correct and useful RSP session.
struct arch_spec {
std::string target_xml;
std::string xml_arch_name;
std::string osabi;
int reg_count = 0;
int pc_reg_num = -1;
std::optional<int> address_bits;
bool swap_register_endianness = false;
};Field details:
- target_xml: target description XML. This is how a client knows your register set and layout. If empty, the server will not advertise the XML capability.
- xml_arch_name: an identifier used when advertising the XML set. Must be non-empty to enable XML support.
- reg_count: total number of registers (0..reg_count-1). The server uses this to iterate registers and validate indices.
- pc_reg_num: register number containing the program counter. Used in stop replies and for certain metadata requests. Set to -1 if not available.
- address_bits: address space width in bits. Used for memory region info calculations when the target provides a memory map.
- swap_register_endianness: reverse byte order for each register on read/write.
Notes:
osabiis currently not consumed by the server core. You can embed it in target XML or keep it for your own logic.- If
address_bitsis not set, the server may infer it from optional host/process info (if provided). If none are available, it assumes a full 64-bit space.
Construct and run a server:
auto target = gdbstub::make_target(...);
// Fill arch_spec correctly.
gdbstub::arch_spec arch{...};
auto transport = std::make_unique<gdbstub::transport_tcp>();
gdbstub::server server(target, arch, std::move(transport));
server.listen("127.0.0.1:5555");
server.serve_forever();Key methods:
listen(address): bind and listen (transport-specific).wait_for_connection(): accept a connection and install the stop notifier.has_connection(): returns connection state.serve_forever(): blocking loop built onpoll().poll(timeout): non-blocking (or timed) processing. Returns true if any events were processed.notify_stop(stop_reason): enqueue a stop from outside the server thread.stop(): close transport and clear notifier.
If you need to integrate with your own event loop, call poll() regularly:
while (server.has_connection()) {
server.poll(std::chrono::milliseconds(10));
// Your other work...
}poll() does two main things:
- Processes incoming packets and dispatches to your target.
- Checks for asynchronous stops (via
poll_stop()and queued notifications).
Required methods if you implement breakpoints:
gdbstub::target_status set_breakpoint(const gdbstub::breakpoint_request& request);
gdbstub::target_status remove_breakpoint(const gdbstub::breakpoint_request& request);Optional capability query:
std::optional<gdbstub::breakpoint_capabilities> capabilities();breakpoint_request fields:
spec.type: software/hardware/watchpointspec.addr: addressspec.length: size (interpretation depends on breakpoint type)thread_id: optional thread selectorconditions: optional bytecode expressions for conditional breakpointscommands: optional command list with persistence flag
Behavior notes:
- If you return
unsupported, the server replies with an empty response for optional features. - If you do not provide
capabilities(), the server assumes software breakpoints are supported and advertises only that.
Required methods if you implement thread support:
std::vector<uint64_t> thread_ids();
uint64_t current_thread();
gdbstub::target_status set_current_thread(uint64_t tid);
std::optional<uint64_t> thread_pc(uint64_t tid);
std::optional<std::string> thread_name(uint64_t tid);
std::optional<gdbstub::stop_reason> thread_stop_reason(uint64_t tid);Guidance:
- Thread IDs are 64-bit opaque values. Keep them stable for the duration of a session.
thread_ids()should include the current thread.- Return
std::nulloptforthread_pc,thread_name, orthread_stop_reasonwhen unknown. - If you do not provide threads, the server will report a default thread ID of 1.
You may implement either or both of these:
std::optional<gdbstub::memory_region_info> region_info(uint64_t addr);
std::vector<gdbstub::memory_region> memory_map();Details:
region_inforeturns a mapping ornulloptif the address is unknown.memory_mapreturns all known regions. The server can synthesizeregion_infofrom the map and will also describe unmapped gaps.
memory_region fields:
start,sizeperms(bitmask of read/write/exec)- optional
name, optionaltypes
If you provide a map but not region_info, the server will:
- Find the region containing the address, or
- Infer the size of the unmapped gap up to the next region or the end of address
space (based on
arch_spec.address_bits).
These are purely informational and are only used if you provide them:
std::optional<gdbstub::host_info> get_host_info();
std::optional<gdbstub::process_info> get_process_info();
std::optional<gdbstub::shlib_info> get_shlib_info();
std::optional<gdbstub::offsets_info> get_offsets_info();You can omit them entirely if you do not have meaningful data. The server will silently omit the corresponding features.
std::optional<gdbstub::register_info> get_register_info(int regno);register_info fields include:
- name, alternate name
- bitsize
- offset (byte offset within the register file)
- encoding and format
- set, generic name
- GCC/DWARF numbers
- container/invalidate register lists
Server behavior:
- If
bitsizeis 0, the server substitutesreg_size(regno) * 8. - If
offsetis not provided, the server computes it by summing register sizes from 0..regno-1. - If
encodingorformatare empty, the server defaults touintandhex.
stop_reason identifies why execution stopped:
struct stop_reason {
stop_kind kind = stop_kind::signal;
int signal = 0;
uint64_t addr = 0;
int exit_code = 0;
std::optional<uint64_t> thread_id;
std::optional<replay_log_boundary> replay_log;
};Guidelines:
- Use
stop_kind::exitedwhen the target is done; setexit_code. - Use
sw_break/hw_break/watch_*for breakpoint-related stops. - Provide
thread_idif you have threads; otherwise the server will choose one.
How the server reacts to resume_result:
state::running: server transitions to running and expects a later stop.state::stopped: server sends a stop reply immediately.state::exited: server sends an exit reply.
If you return running, you must later deliver a stop via one of:
poll_stop()returning a stop reasonset_stop_notifier()calling the notifier- Explicit call to
server.notify_stop()from your own code
transport is a simple interface. Implement it if you want a non-TCP transport.
class transport {
public:
virtual ~transport() = default;
virtual bool listen(std::string_view address) = 0;
virtual bool accept() = 0;
virtual bool connected() const = 0;
virtual bool readable(std::chrono::milliseconds timeout) = 0;
virtual std::ptrdiff_t read(std::span<std::byte> out) = 0;
virtual std::ptrdiff_t write(std::span<const std::byte> data) = 0;
virtual void disconnect() = 0;
virtual void close() = 0;
};Expected behavior:
readable(timeout)should block up totimeoutand return true when data is available. Return false if not readable or not connected.read()returns bytes read. Return <= 0 to indicate disconnection.write()returns bytes written. Return <= 0 to indicate disconnection.disconnect()closes the current connection,close()also tears down any listening sockets or resources.
transport_tcp implements this interface and supports:
host:portand[ipv6]:portaddresses*as the host to bind to all interfaces
This is the easiest way to avoid capability conflicts and keep code clean:
struct Regs { /* reg_size/read_reg/write_reg */ };
struct Mem { /* read_mem/write_mem */ };
struct Run { /* resume + optional interrupt/poll/notify */ };
struct Breakpoints { /* set/remove + optional caps */ };
Regs regs;
Mem mem;
Run run;
Breakpoints bps;
auto target = gdbstub::make_target(regs, mem, run, bps);If you already have a monolithic target type, create thin adapters so optional capabilities do not also implement required methods (a compile-time rule):
struct CoreTarget { /* everything */ };
struct RegsAdapter { CoreTarget& t; /* reg methods */ };
struct MemAdapter { CoreTarget& t; /* mem methods */ };
struct RunAdapter { CoreTarget& t; /* run methods */ };
struct ThreadsAdapter { CoreTarget& t; /* thread methods */ };
CoreTarget core;
RegsAdapter regs{core};
MemAdapter mem{core};
RunAdapter run{core};
ThreadsAdapter threads{core};
auto target = gdbstub::make_target(regs, mem, run, threads);When your run loop is in another thread, capture the notifier and call it when you detect a stop:
struct Run {
std::atomic<bool> running{false};
gdbstub::stop_notifier notifier{};
void set_stop_notifier(gdbstub::stop_notifier n) { notifier = n; }
gdbstub::resume_result resume(const gdbstub::resume_request&) {
running.store(true);
return gdbstub::resume_result{.state = gdbstub::resume_result::state::running};
}
void on_stop_detected(uint64_t tid) {
if (notifier.notify) {
gdbstub::stop_reason reason;
reason.kind = gdbstub::stop_kind::signal;
reason.signal = 5;
reason.thread_id = tid;
notifier(reason);
}
}
};If your target advances in small increments, implement poll_stop() and keep
resume() non-blocking:
struct Run {
std::atomic<bool> running{false};
gdbstub::resume_result resume(const gdbstub::resume_request&) {
running.store(true);
return gdbstub::resume_result{.state = gdbstub::resume_result::state::running};
}
std::optional<gdbstub::stop_reason> poll_stop() {
if (!running.load()) {
return std::nullopt;
}
if (/* detected stop */) {
running.store(false);
gdbstub::stop_reason reason;
reason.kind = gdbstub::stop_kind::signal;
reason.signal = 5;
return reason;
}
return std::nullopt;
}
};- Packet buffer size is fixed at 4096 bytes for incoming packets.
- Memory reads are clamped to 2048 bytes per request.
- Bulk register writes (
G) require the exact concatenated size of all registers.
If you have very large register files or need large memory transfers, consider:
- minimizing register count and size where possible
- optimizing your read/write routines for contiguous requests
Register access issues
- If
reg_countdoes not match your register definitions, bulk reads/writes will fail or misalign. - If
reg_size(regno)returns 0 for a register that you advertise in target XML or register_info, the client may see inconsistent data. - If PC register number (
pc_reg_num) is wrong, stop replies will report an incorrect PC.
No stops while running
- If
resume()returnsrunningbut you do not implementpoll_stop()orset_stop_notifier(), the server will never learn that execution stopped. - If
interrupt()is missing, interrupt requests will have no effect.
Memory map confusion
- If you provide a memory map but no
address_bits, the server may assume a huge unmapped region size when queried at unmapped addresses.
Unexpected empty replies
- The server responds with empty replies for optional features that are not
supported or not advertised. If you expect a feature to work, make sure
you provided the matching capability object and that its methods return
okinstead ofunsupported.
Required capabilities:
- regs:
reg_size,read_reg,write_reg - mem:
read_mem,write_mem - run:
resume
Optional capabilities:
- breakpoints:
set_breakpoint,remove_breakpoint, optionalcapabilities - threads:
thread_ids,current_thread,set_current_thread,thread_pc,thread_name,thread_stop_reason - memory_layout:
region_infoand/ormemory_map - host:
get_host_info - process:
get_process_info - shlib:
get_shlib_info - register_info:
get_register_info
Optional run helpers (strongly recommended):
interruptpoll_stopset_stop_notifiercapabilities
- Implement
regs,mem, andruncapability objects. - Build a
targetwithmake_target(regs, mem, run). - Create an
arch_specwith correctreg_count,pc_reg_num, andtarget_xml. - Create a
transport(transport_tcpor your own). - Construct
server, calllisten, thenserve_foreverorpoll. - Add optional capabilities as you need richer features.
src/gdbstub/target/target.hpp: capability definitions andmake_target.src/gdbstub/server/server.hppandsrc/gdbstub/server/: full server behavior.src/gdbstub_tool/toy: a complete C++ example with many optional capabilities.
This guide should be sufficient to build a clean, fully functional integration using the C++ API alone. If you want a deeper dive on any specific capability or pattern, expand that section with target-specific details and examples.