Import gdbstub 0.4.3 crate

Bug: 178531428
Bug: 182593751
Change-Id: I03b5373b926e88fd6f331d016c6b25c21bdcd6da
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..95b71cd
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,5 @@
+{
+  "git": {
+    "sha1": "dcbe203cd7d7fab60004aecefdbddd55fc2d115c"
+  }
+}
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..6d056ea
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+# These are supported funding model platforms
+ko_fi: prilik
diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml
new file mode 100644
index 0000000..39133dd
--- /dev/null
+++ b/.github/workflows/lints.yml
@@ -0,0 +1,33 @@
+on: [push, pull_request]
+
+name: Lints
+
+jobs:
+  clippy:
+    name: Clippy
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: nightly
+          override: true
+          components: rustfmt, clippy
+      - uses: actions-rs/cargo@v1
+        with:
+          command: fmt
+          args: --all -- --check
+      - uses: actions-rs/cargo@v1
+        with:
+          command: clippy
+          args: --all --examples --features=std -- -D warnings
+      # don't forget the no_std example!
+      - uses: actions-rs/cargo@v1
+        with:
+          command: fmt
+          args: --manifest-path example_no_std/Cargo.toml
+      - uses: actions-rs/cargo@v1
+        with:
+          command: clippy
+          args: --manifest-path example_no_std/Cargo.toml
diff --git a/.github/workflows/stable-ci.yml b/.github/workflows/stable-ci.yml
new file mode 100644
index 0000000..ea317c8
--- /dev/null
+++ b/.github/workflows/stable-ci.yml
@@ -0,0 +1,38 @@
+on: [push, pull_request]
+
+name: Continuous integration
+
+jobs:
+  check:
+    name: Check
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: stable
+          override: true
+      - uses: actions-rs/cargo@v1
+        with:
+          command: check
+          args: --all --examples --features=std
+      # don't forget the no_std example!
+      - uses: actions-rs/cargo@v1
+        with:
+          command: check
+          args: --manifest-path example_no_std/Cargo.toml
+  test:
+    name: Tests
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: stable
+          override: true
+      - uses: actions-rs/cargo@v1
+        with:
+          command: test
+          args: --all --features=std
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a97cf42
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+/target
+**/*.rs.bk
+Cargo.lock
+
+**/.gdb_history
+# GDB will core dump if the target is implemented incorrectly (which it often is during debugging)
+**/core
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..caa7e28
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,35 @@
+// This file is generated by cargo2android.py --run --device --dependencies --no-subdir.
+// Do not modify this file as changes will be overridden on upgrade.
+
+rust_library {
+    name: "libgdbstub",
+    host_supported: true,
+    crate_name: "gdbstub",
+    srcs: ["src/lib.rs"],
+    edition: "2018",
+    features: [
+        "alloc",
+        "default",
+        "std",
+    ],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.virt",
+    ],
+    rustlibs: [
+        "libcfg_if",
+        "liblog_rust",
+        "libmanaged",
+        "libnum_traits",
+    ],
+    proc_macros: ["libpaste"],
+}
+
+// dependent_library ["feature_list"]
+//   autocfg-1.0.1
+//   cfg-if-0.1.10
+//   cfg-if-1.0.0
+//   log-0.4.14 "std"
+//   managed-0.8.0 "alloc"
+//   num-traits-0.2.14
+//   paste-1.0.4
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d69f0e3
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,113 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+# 0.4.3
+
+#### New Arch Implementations
+
+-   Implement `RegId` for Mips/Mips64 [\#38](https://github.com/daniel5151/gdbstub/pull/38) ([starfleetcadet75](https://github.com/starfleetcadet75))
+-   Implement `RegId` for MSP430 [\#38](https://github.com/daniel5151/gdbstub/pull/38) ([starfleetcadet75](https://github.com/starfleetcadet75))
+
+# 0.4.2
+
+#### Packaging
+
+-   Exclude test object files from package [\#37](https://github.com/daniel5151/gdbstub/pull/37) ([keiichiw](https://github.com/keiichiw))
+
+# 0.4.1
+
+#### New Arch Implementations
+
+-   Implement `RegId` for x86/x86_64 [\#34](https://github.com/daniel5151/gdbstub/pull/34) ([keiichiw](https://github.com/keiichiw))
+
+#### Bugfixes
+
+-   Switch fatal error signal from `T06` to `S05`,
+-   specify cfg-if 0.1.10 or later [\#33](https://github.com/daniel5151/gdbstub/pull/33) ([keiichiw](https://github.com/keiichiw))
+    -   `cargo build` fails if cfg-if is 0.1.9 or older
+
+#### Misc
+
+-   Don't hard-code u64 when parsing packets (use big-endian byte arrays + late conversion to `Target::Arch::Usize`).
+
+# 0.4.0
+
+This version includes a _major_ API overhaul, alongside a slew of new features and general improvements. While updating to `0.4.0` will require some substantial code modifications, it's well worth the effort, as `0.4.0` is the safest, leanest, and most featureful release of `gdbstub` yet!
+
+Fun fact: Even after adding a _bunch_ of new features and bug-fixes, the in-tree `example_no_std` has remained just as small! The example on the `semver-fix-0.2.2` branch is `20251` bytes, while the example on `0.4.0` is `20246` bytes.
+
+#### API Changes
+
+-   Rewrite the `Target` API in terms of "Inlineable Dyn Extension Traits" (IDETs)
+    -   _By breaking up `Target` into smaller pieces which can be mixed-and-matched, it not only makes it easier to get up-and-running with `gdbstub`, but it also unlocks a lot of awesome internal optimizations:_
+        -   Substantially reduces binary-size footprint by guaranteeing dead-code-elimination of parsing/handling unimplemented GDB protocol features.
+        -   Compile-time enforcement that certain groups of methods are implemented in-tandem (e.g: `add_sw_breakpoint` and `remove_sw_breakpoint`).
+-   Update the `Target` API with support for non-fatal error handling.
+    -   _The old approach of only allowing \*fatal\* errors was woefully inadequate when dealing with potentially fallible operations such as reading from unauthorized memory (which GDB likes to do a bunch), or handling non-fatal `std::io::Error` that occur as a result of `ExtendedMode` operations. The new `TargetResult`/`TargetError` result is much more robust, and opens to door to supporting additional error handling extensions (such as LLDB's ASCII Errors)._
+-   Update the `Connection` trait with new methods (`flush` - required, `write_all`, `on_session_start`)
+-   Lift `Registers::RegId` to `Arch::RegId`, and introduce new temporary `RegIdImpl` solution for avoiding breaking API changes due to new `RegId` implementations (see [\#29](https://github.com/daniel5151/gdbstub/pull/29))
+-   Mark various `RegId` enums as `#[non_exhaustive]`, allowing more registers to be added if need be.
+-   Error types are now marked as `#[non_exhaustive]`.
+
+#### New Protocol Extensions
+
+-   `ExtendedMode` - Allow targets to run new processes / attach to existing processes / restart execution.
+    -   Includes support for `set disable-randomization`, `set environment`, `set startup-with-shell`, and `set cwd` and `cd`.
+-   `SectionOffsets` - Get section/segment relocation offsets from the target. [\#30](https://github.com/daniel5151/gdbstub/pull/30) ([mchesser](https://github.com/mchesser))
+    -   Uses the `qOffsets` packet under-the-hood.
+
+#### Bugfixes
+
+-   Fix issues related to selecting the incorrect thread after hitting a breakpoint in multi-threaded targets.
+-   Ensure that `set_nodelay` is set when using a `TcpStream` as a `Connection` (via the new `Connection::on_session_start` API)
+    -   _This should result in a noticeable performance improvement when debugging over TCP._
+
+#### Misc
+
+-   Removed `btou` dependency.
+-   Removed all `UTF-8` aware `str` handling code.
+    -   _GDB uses a pure ASCII protocol, so including code to deal with UTF-8 resulted in unnecessary binary bloat._
+
+# 0.3.0 (formerly 0.2.2)
+
+This version contains a few minor breaking changes from `0.2.1`. These are only surface-level changes, and can be fixed with minimal effort.
+
+Version `0.3.0` is identical to the yanked version `0.2.2`, except that it adheres to `cargo`'s [modified SemVer rule](https://doc.rust-lang.org/cargo/reference/manifest.html#the-version-field) which states that the pre-`0.x.y` breaking changes should still bump the minor version.
+
+Thanks to [h33p](https://github.com/h33p) for reporting this issue ([\#27](https://github.com/daniel5151/gdbstub/issues/27))
+
+#### API Changes
+
+-   Update `Target::resume` API to replace raw `&mut dyn Iterator` with a functionally identical concrete `Actions` iterator.
+-   Mark the `StopReason` enum as `#[non_exhaustive]`, allowing further types to be added without being considered as an API breaking change.
+
+#### New Protocol Extensions
+
+-   Add `Target::read/write_register` support (to support single register accesses) [\#22](https://github.com/daniel5151/gdbstub/pull/22) ([thomashk0](https://github.com/thomashk0))
+-   Add `StopReason::Signal(u8)` variant, to send arbitrary signal codes [\#19](https://github.com/daniel5151/gdbstub/pull/19) ([mchesser](https://github.com/mchesser))
+
+#### New Arch Implementations
+
+-   Add partial RISC-V support (only integer ISA at the moment) [\#21](https://github.com/daniel5151/gdbstub/pull/21) ([thomashk0](https://github.com/thomashk0))
+-   Add i386 (x86) support [\#23](https://github.com/daniel5151/gdbstub/pull/23) ([jamcleod](https://github.com/jamcleod))
+-   Add 32-bit PowerPC support [\#25](https://github.com/daniel5151/gdbstub/pull/25) ([jamcleod](https://github.com/jamcleod))
+
+# 0.2.1
+
+#### New Arch Implementations
+
+-   Add x86_86 support [\#11](https://github.com/daniel5151/gdbstub/pull/11) ([jamcleod](https://github.com/jamcleod))
+-   Add Mips and Mips64 support [\#13](https://github.com/daniel5151/gdbstub/pull/13) ([starfleetcadet75](https://github.com/starfleetcadet75))
+
+#### Misc
+
+-   Documentation improvements
+    -   Document PC adjustment requirements in `Target::resume`
+    -   Add docs on handling non-fatal invalid memory reads/writes in `Target::read/write_addrs`.
+
+# 0.2.0
+
+_start of changelog_
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..bffc8fa
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,64 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies
+#
+# If you believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+edition = "2018"
+name = "gdbstub"
+version = "0.4.3"
+authors = ["Daniel Prilik <[email protected]>"]
+exclude = ["examples/**/*.elf", "examples/**/*.o"]
+description = "An implementation of the GDB Remote Serial Protocol in Rust"
+homepage = "https://github.com/daniel5151/gdbstub"
+documentation = "https://docs.rs/gdbstub"
+readme = "README.md"
+keywords = ["gdb", "emulation", "no_std", "debugging"]
+categories = ["development-tools::debugging", "embedded", "emulators", "network-programming", "no-std"]
+license = "MIT"
+repository = "https://github.com/daniel5151/gdbstub"
+
+[[example]]
+name = "armv4t"
+required-features = ["std"]
+
+[[example]]
+name = "armv4t_multicore"
+required-features = ["std"]
+[dependencies.cfg-if]
+version = "0.1.10"
+
+[dependencies.log]
+version = "0.4"
+
+[dependencies.managed]
+version = "0.8"
+default-features = false
+
+[dependencies.num-traits]
+version = "0.2"
+default-features = false
+
+[dependencies.paste]
+version = "1.0"
+[dev-dependencies.armv4t_emu]
+version = "0.1"
+
+[dev-dependencies.goblin]
+version = "0.2"
+
+[dev-dependencies.pretty_env_logger]
+version = "0.4"
+
+[features]
+__dead_code_marker = []
+alloc = ["managed/alloc"]
+default = ["std"]
+std = ["alloc"]
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644
index 0000000..f01c556
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,43 @@
+[package]
+name = "gdbstub"
+description = "An implementation of the GDB Remote Serial Protocol in Rust"
+authors = ["Daniel Prilik <[email protected]>"]
+version = "0.4.3"
+license = "MIT"
+edition = "2018"
+readme = "README.md"
+documentation = "https://docs.rs/gdbstub"
+homepage = "https://github.com/daniel5151/gdbstub"
+repository  = "https://github.com/daniel5151/gdbstub"
+keywords = ["gdb", "emulation", "no_std", "debugging"]
+categories = ["development-tools::debugging", "embedded", "emulators", "network-programming", "no-std"]
+exclude = ["examples/**/*.elf", "examples/**/*.o"]
+
+[dependencies]
+cfg-if = "0.1.10"
+log = "0.4"
+managed = { version = "0.8", default-features = false }
+num-traits = { version = "0.2", default-features = false }
+paste = "1.0"
+
+[dev-dependencies]
+armv4t_emu = "0.1"
+pretty_env_logger = "0.4"
+goblin = "0.2"
+
+[features]
+default = ["std"]
+alloc = ["managed/alloc"]
+std = ["alloc"]
+
+# INTERNAL: enables the `__dead_code_marker!` macro.
+# used as part of the `scripts/test_dead_code_elim.sh`
+__dead_code_marker = []
+
+[[example]]
+name = "armv4t"
+required-features = ["std"]
+
+[[example]]
+name = "armv4t_multicore"
+required-features = ["std"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..148020e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Daniel Prilik
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..13894f8
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,19 @@
+name: "gdbstub"
+description: "An implementation of the GDB Remote Serial Protocol in Rust"
+third_party {
+  url {
+    type: HOMEPAGE
+    value: "https://crates.io/crates/gdbstub"
+  }
+  url {
+    type: ARCHIVE
+    value: "https://static.crates.io/crates/gdbstub/gdbstub-0.4.3.crate"
+  }
+  version: "0.4.3"
+  license_type: NOTICE
+  last_upgrade_date {
+    year: 2021
+    month: 2
+    day: 28
+  }
+}
diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_MIT
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..46fc303
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1 @@
+include platform/prebuilts/rust:/OWNERS
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..81cee45
--- /dev/null
+++ b/README.md
@@ -0,0 +1,174 @@
+# gdbstub
+
+[![](http://meritbadge.herokuapp.com/gdbstub)](https://crates.io/crates/gdbstub)
+[![](https://docs.rs/gdbstub/badge.svg)](https://docs.rs/gdbstub)
+
+An ergonomic and easy-to-integrate implementation of the [GDB Remote Serial Protocol](https://sourceware.org/gdb/onlinedocs/gdb/Remote-Protocol.html#Remote-Protocol) in Rust, with full `#![no_std]` support.
+
+Why `gdbstub`?
+
+-   **Excellent Ergonomics**
+    -   Unlike other GDB stub libraries, which simply expose the underlying GDB protocol "warts and all", `gdbstub` tries to abstract as much of the raw GDB protocol details from the user.
+        -   For example, instead of having to dig through some [obscure XML files deep the GDB codebase](https://github.com/bminor/binutils-gdb/tree/master/gdb/features) just to read/write from CPU registers, `gdbstub` comes with [built-in register definitions](https://docs.rs/gdbstub/*/gdbstub/arch/index.html) for most common architectures!
+    -   `gdbstub` makes _extensive_ use of Rust's powerful type system + generics to enforce protocol invariants at compile time, minimizing the number of tricky protocol details end users have to worry about.
+-   **Easy to Integrate**
+    -   `gdbstub`'s API is designed to be as unobtrusive as possible, and shouldn't require any large refactoring effort to integrate into an existing project. It doesn't require taking direct ownership of any key data structures, and aims to be a "drop in" solution when you need to add debugging to a project.
+-   **`#![no_std]` Ready & Size Optimized**
+    -   Can be configured to use fixed-size, pre-allocated buffers. **`gdbstub` does _not_ depend on `alloc`.**
+    -   `gdbstub` is transport-layer agnostic, and uses a basic [`Connection`](https://docs.rs/gdbstub/latest/gdbstub/trait.Connection.html) interface to communicate with the GDB server. As long as target has some method of performing in-order, serial, byte-wise I/O (e.g: putchar/getchar over UART), it's possible to run `gdbstub` on it.
+    -   "You don't pay for what you don't use": If you don't implement a particular protocol extension, the resulting binary won't include _any_ code related to parsing/handling that extension's packets! See the [Zero-overhead Protocol Extensions](#zero-overhead-protocol-extensions) section below for more details.
+    -   A lot of work has gone into reducing `gdbstub`'s binary and RAM footprints.
+        -   In release builds, using all the tricks outlined in [`min-sized-rust`](https://github.com/johnthagen/min-sized-rust), a baseline `gdbstub` implementation weighs in at roughly **_10kb of `.text` and negligible `.rodata`!_** \*
+        -   This is already pretty good, and I suspect that there are still lots of low-hanging optimizations which can reduce the size even further.
+
+\* Exact numbers vary by target platform, compiler version, and `gdbstub` revision. Data was collected using the included `example_no_std` project compiled on x86_64.
+
+`gdbstub` is particularly well suited for _emulation_, making it easy to add powerful, non-intrusive debugging support to an emulated system. Just provide an implementation of the [`Target`](https://docs.rs/gdbstub/latest/gdbstub/target/trait.Target.html) trait for your target platform, and you're ready to start debugging!
+
+-   [Documentation](https://docs.rs/gdbstub)
+
+### Can I Use `gdsbtub` in Production?
+
+**Yes, as long as you don't mind some API churn until `1.0.0` is released.**
+
+`gdbstub` has been integrated into [many projects](#real-world-examples) since its initial `0.1.0` release, and thusfar, no _major_ bugs have been reported. Reported issues have typically been the result of faulty `Target` implementations (e.g: forgetting to adjust the PC after a breakpoint is hit), or were related to certain unimplemented GDB protocol features.
+
+That being said, due to `gdbstub`'s heavy use of Rust's type system in enforcing GDB protocol invariants at compile time, it's often been the case that implementing new GDB protocol features has required making some breaking Trait/Type changes (e.g: adding the `RegId` associated type to `Arch` to support addressing individual registers). While these changes are typically quite minor, they are nonetheless breaking, and may require a code-change when moving between versions.
+
+See the [Future Plans + Roadmap to `1.0.0`](#future-plans--roadmap-to-100) for more information on what features `gdbstub` still needs to implement before committing to API stability with version `1.0.0`.
+
+## Debugging Features
+
+The GDB Remote Serial Protocol is surprisingly complex, supporting advanced features such as remote file I/O, spawning new processes, "rewinding" program execution, and much, _much_ more. Thankfully, most of these features are completely optional, and getting a basic debugging session up-and-running only requires implementing a few basic methods:
+
+-   Base GDB Protocol
+    -   Step + Continue
+    -   Read/Write memory
+    -   Read/Write registers
+    -   (optional) Multithreading support
+
+Of course, most use-cases will want to support additional debugging features as well. At the moment, `gdbstub` implements the following GDB protocol extensions:
+
+-   Automatic architecture + feature detection (automatically implemented)
+-   Breakpoints
+    -   Software Breakpoints
+    -   Hardware Breakpoints
+    -   Read/Write/Access Watchpoints (i.e: value breakpoints)
+-   Extended Mode
+    -   Run/Attach/Kill Processes
+    -   Pass environment variables / args to spawned processes
+    -   Change working directory
+-   Section offsets
+    -   Get section/segment relocation offsets from the target
+-   Custom `monitor` Commands
+    -   Extend the GDB protocol with custom debug commands using GDB's `monitor` command
+
+_Note:_ Which GDB features are implemented are decided on an as-needed basis by `gdbstub`'s contributors. If there's a missing GDB feature that you'd like `gdbstub` to implement, please file an issue / open a PR! Check out the [GDB Remote Configuration Docs](https://sourceware.org/gdb/onlinedocs/gdb/Remote-Configuration.html) for a table of GDB commands + their corresponding Remote Serial Protocol packets.
+
+### Zero-overhead Protocol Extensions
+
+Using a technique called **Inlineable Dyn Extension Traits** (IDETs), `gdbstub` is able to leverage the Rust compiler's powerful optimization passes to ensure any unused features are dead-code-eliminated in release builds _without_ having to rely on compile-time features flags!
+
+For example, if your target doesn't implement a custom GDB `monitor` command handler, the resulting binary won't include any code related to parsing / handling the underlying `qRcmd` packet!
+
+If you're interested in the low-level technical details of how IDETs work, I've included a brief writeup in the documentation [here](https://docs.rs/gdbstub/latest/gdbstub/target/ext/index.html#how-protocol-extensions-work---inlineable-dyn-extension-traits-idets).
+
+## Feature flags
+
+By default, the `std` and `alloc` features are enabled.
+
+When using `gdbstub` in `#![no_std]` contexts, make sure to set `default-features = false`.
+
+-   `alloc`
+    -   Implement `Connection` for `Box<dyn Connection>`.
+    -   Log outgoing packets via `log::trace!` (uses a heap-allocated output buffer).
+    -   Provide built-in implementations for certain protocol features:
+        -   Use a heap-allocated packet buffer in `GdbStub` (if none is provided via `GdbStubBuilder::with_packet_buffer`).
+        -   (Monitor Command) Use a heap-allocated output buffer in `ConsoleOutput`.
+        -   (Extended Mode) Automatically track Attached/Spawned PIDs without implementing `ExtendedMode::query_if_attached`.
+-   `std` (implies `alloc`)
+    -   Implement `Connection` for [`TcpStream`](https://doc.rust-lang.org/std/net/struct.TcpStream.html) and [`UnixStream`](https://doc.rust-lang.org/std/os/unix/net/struct.UnixStream.html).
+    -   Implement [`std::error::Error`](https://doc.rust-lang.org/std/error/trait.Error.html) for `gdbstub::Error`.
+    -   Add a `TargetError::Io` variant to simplify `std::io::Error` handling from Target methods.
+
+## Examples
+
+### Real-World Examples
+
+-   Virtual Machine Monitors (VMMs)
+    -   [crosvm](https://chromium.googlesource.com/chromiumos/platform/crosvm/+/refs/heads/main#gdb-support) - The Chrome OS Virtual Machine Monitor
+    -   [Firecracker](https://firecracker-microvm.github.io/) - A lightweight VMM developed by AWS - feature is in [PR](https://github.com/firecracker-microvm/firecracker/pull/2168)
+-   Emulators
+    -   [clicky](https://github.com/daniel5151/clicky/) - An emulator for classic clickwheel iPods (dual-core ARMv4T SoC)
+    -   [rustyboyadvance-ng](https://github.com/michelhe/rustboyadvance-ng/) - Nintendo GameBoy Advance emulator and debugger
+    -   [ts7200](https://github.com/daniel5151/ts7200/) - An emulator for the TS-7200, a somewhat bespoke embedded ARMv4t platform
+    -   [microcorruption-emu](https://github.com/sapir/microcorruption-emu) - msp430 emulator for the microcorruption.com ctf
+-   Other
+    -   [memflow](https://github.com/memflow/memflow) - A physical memory introspection framework (part of `memflow-cli`)
+
+While some of these projects may use older versions of `gdbstub`, they can nonetheless serve as useful examples of what a typical `gdbstub` integration might look like.
+
+If you end up using `gdbstub` in your project, consider opening a PR and add it to this list!
+
+### In-tree "Toy" Examples
+
+These examples are built as part of the CI, and are guaranteed to be kept up to date with the latest version of `gdbstub`'s API.
+
+- `armv4t` - `./examples/armv4t/`
+    - An incredibly simple ARMv4T-based system emulator with `gdbstub` support.
+    - Unlike all other examples, `armv4t` **implements (almost) all available `target::ext` features.**
+- `armv4t_multicore` - `./examples/armv4t_multicore/`
+    - A dual-core variation of the `armv4t` example.
+    - Implements the core of `gdbstub`'s multithread extensions API, but not much else.
+- `example_no_std` - `./example_no_std`
+    - An _extremely_ minimal example of how `gdbstub` can be used in a `#![no_std]` project.
+    - Unlike the `armv4t/armv4t_multicore` examples, this project does _not_ include a working emulator, and stubs-out all `gdbstub` functions.
+    - Tracks `gdbstub`'s approximate binary footprint (via the `check_size.sh` script)
+
+## Using `gdbstub` on bare-metal hardware
+
+Quite a bit of work has gone into making `gdbstub` optimized for `#![no_std]`, which means it should be entirely possible to implement a `Target` which uses low-level trap instructions + context switching to debug bare-metal code.
+
+If you happen to stumble across this crate and end up using it to debug some bare-metal code, please let me know! I'd love to link to your project, and/or create a simplified example based off your code!
+
+## `unsafe` in `gdbstub`
+
+`gdbstub` "core" only has 2 instances of unsafe code:
+
+-   A few trivially safe calls to `NonZeroUsize::new_unchecked()` when defining internal constants.
+-   A call to `str::from_utf8_unchecked()` when working with incoming GDB packets (the underlying `&[u8]` buffer is checked with `is_ascii()` prior to the call).
+
+With the `std` feature enabled, there is one additional instance of `unsafe` code:
+
+-   `gdbstub` includes an implementation of `UnixStream::peek` which uses `libc::recv`. This will be removed once [rust-lang/rust#73761](https://github.com/rust-lang/rust/pull/73761) is merged and stabilized.
+
+## Future Plans + Roadmap to `1.0.0`
+
+Before `gdbstub` can comfortably commit to a stable `1.0.0` API, there are several outstanding features that should be implemented and questions that need to be addressed. Due to `gdbstub`'s heavy reliance on the Rust type system to enforce GDB protocol invariants, it's likely that a certain subset of yet-unimplemented protocol features may require breaking API changes.
+
+Notably, the vast majority of GDB protocol features (e.g: remote filesystem support, tracepoint packets, most query packets, etc...) should _not_ require breaking API changes, and could most likely be implemented using the standard backwards-compatible protocol extension approach.
+
+The following features are most likely to require breaking API changes, and should therefore be implemented prior to `1.0.0`.
+
+-   [ ] Stabilize the `Arch` trait
+    -   [ ] Allow fine-grained control over target features ([\#12](https://github.com/daniel5151/gdbstub/issues/12))
+    -   [ ] Remove `RawRegId` ([\#29](https://github.com/daniel5151/gdbstub/issues/29))
+-   [ ] Implement GDB's various high-level operating modes:
+    -   [x] Single/Multi Thread debugging
+    -   [ ] Multiprocess Debugging
+        -   [ ] Add a third `base::multiprocess` API.
+        -   _Note:_ `gdbstub` already implements multiprocess extensions "under-the-hood", and just hard-codes a fake PID.
+    -   [x] [Extended Mode](https://sourceware.org/gdb/current/onlinedocs/gdb/Connecting.html) (`target extended-remote`)
+    -   [ ] [Non-Stop Mode](https://sourceware.org/gdb/onlinedocs/gdb/Remote-Non_002dStop.html#Remote-Non_002dStop)
+        -   This may require some breaking API changes and/or some internals rework -- more research is needed.
+-   [ ] Have a working example of `gdbstub` running in a "bare-metal" `#![no_std]` environment (e.g: debugging a hobby OS via serial).
+    -   While there's no reason it _wouldn't_ work, it would be good to validate that the API + implementation supports this use-case.
+
+Additionally, while not strict "blockers" to `1.0.0`, it would be good to explore these features as well:
+
+-   [ ] Commit to a MSRV
+-   [ ] Exposing an `async/await` interface
+    -   e.g: the current `check_gdb_interrupt` callback in `Target::resume()` could be modeled as a future.
+    -   Would require some tweaks to the Connection trait.
+-   [ ] Adding [LLDB extension](https://raw.githubusercontent.com/llvm-mirror/lldb/master/docs/lldb-gdb-remote.txt) support
+    -   Skimming through the list, it doesn't seem like these extensions would require breaking API changes -- more research is needed.
diff --git a/examples/armv4t/README.md b/examples/armv4t/README.md
new file mode 100644
index 0000000..dfa4269
--- /dev/null
+++ b/examples/armv4t/README.md
@@ -0,0 +1,25 @@
+# armv4t
+
+An incredibly simple emulator to run elf binaries compiled with `arm-none-eabi-cc -march=armv4t`.
+
+This emulator isn't based off any particular system -- it's moreso just a test-bed for showing off various bits of `gdbstub` functionality.
+
+## Usage
+
+Run `gdb-arm-none-eabi` (or alternatively, `gdb-multiarch`) from the `test_bin` directory to automatically connect to the emulator + load debug symbols for the emulated binary.
+
+This example can be run using:
+
+```bash
+cargo run --example armv4t --features=std
+```
+
+**NOTE:** If debug symbols couldn't be loaded, try rebuilding `test.elf` locally (requires the `arm-none-eabi` toolchain to be installed), and recompiling the example.
+
+### Unix Domain Sockets
+
+GDB versions since \~2018 support running a debugging session over Unix Domain Sockets (UDS). Debugging over UDS can feel much snappier than debugging over loopback TCP.
+
+Running the example with the `--uds` flag will bind the GdbStub to a socket at `/tmp/armv4t_gdb`.
+
+This feature is only supported on Unix-like systems.
diff --git a/examples/armv4t/emu.rs b/examples/armv4t/emu.rs
new file mode 100644
index 0000000..b7ef28f
--- /dev/null
+++ b/examples/armv4t/emu.rs
@@ -0,0 +1,108 @@
+use armv4t_emu::{reg, Cpu, ExampleMem, Memory, Mode};
+
+use crate::mem_sniffer::{AccessKind, MemSniffer};
+use crate::DynResult;
+
+const HLE_RETURN_ADDR: u32 = 0x12345678;
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum Event {
+    Halted,
+    Break,
+    WatchWrite(u32),
+    WatchRead(u32),
+}
+
+/// incredibly barebones armv4t-based emulator
+pub struct Emu {
+    start_addr: u32,
+
+    pub(crate) cpu: Cpu,
+    pub(crate) mem: ExampleMem,
+
+    pub(crate) watchpoints: Vec<u32>,
+    pub(crate) breakpoints: Vec<u32>,
+}
+
+impl Emu {
+    pub fn new(program_elf: &[u8]) -> DynResult<Emu> {
+        // set up emulated system
+        let mut cpu = Cpu::new();
+        let mut mem = ExampleMem::new();
+
+        // load ELF
+        let elf_header = goblin::elf::Elf::parse(program_elf)?;
+
+        // copy all in-memory sections from the ELF file into system RAM
+        let sections = elf_header
+            .section_headers
+            .iter()
+            .filter(|h| h.is_alloc() && h.sh_type != goblin::elf::section_header::SHT_NOBITS);
+
+        for h in sections {
+            eprintln!(
+                "loading section {:?} into memory from [{:#010x?}..{:#010x?}]",
+                elf_header.shdr_strtab.get(h.sh_name).unwrap().unwrap(),
+                h.sh_addr,
+                h.sh_addr + h.sh_size,
+            );
+
+            for (i, b) in program_elf[h.file_range()].iter().enumerate() {
+                mem.w8(h.sh_addr as u32 + i as u32, *b);
+            }
+        }
+
+        // setup execution state
+        eprintln!("Setting PC to {:#010x?}", elf_header.entry);
+        cpu.reg_set(Mode::User, reg::SP, 0x10000000);
+        cpu.reg_set(Mode::User, reg::LR, HLE_RETURN_ADDR);
+        cpu.reg_set(Mode::User, reg::PC, elf_header.entry as u32);
+        cpu.reg_set(Mode::User, reg::CPSR, 0x10); // user mode
+
+        Ok(Emu {
+            start_addr: elf_header.entry as u32,
+            cpu,
+            mem,
+            watchpoints: Vec::new(),
+            breakpoints: Vec::new(),
+        })
+    }
+
+    pub(crate) fn reset(&mut self) {
+        self.cpu.reg_set(Mode::User, reg::SP, 0x10000000);
+        self.cpu.reg_set(Mode::User, reg::LR, HLE_RETURN_ADDR);
+        self.cpu.reg_set(Mode::User, reg::PC, self.start_addr);
+        self.cpu.reg_set(Mode::User, reg::CPSR, 0x10);
+    }
+
+    pub fn step(&mut self) -> Option<Event> {
+        let mut hit_watchpoint = None;
+
+        let mut sniffer = MemSniffer::new(&mut self.mem, &self.watchpoints, |access| {
+            hit_watchpoint = Some(access)
+        });
+
+        self.cpu.step(&mut sniffer);
+        let pc = self.cpu.reg_get(Mode::User, reg::PC);
+
+        if let Some(access) = hit_watchpoint {
+            let fixup = if self.cpu.thumb_mode() { 2 } else { 4 };
+            self.cpu.reg_set(Mode::User, reg::PC, pc - fixup);
+
+            return Some(match access.kind {
+                AccessKind::Read => Event::WatchRead(access.addr),
+                AccessKind::Write => Event::WatchWrite(access.addr),
+            });
+        }
+
+        if self.breakpoints.contains(&pc) {
+            return Some(Event::Break);
+        }
+
+        if pc == HLE_RETURN_ADDR {
+            return Some(Event::Halted);
+        }
+
+        None
+    }
+}
diff --git a/examples/armv4t/gdb/extended_mode.rs b/examples/armv4t/gdb/extended_mode.rs
new file mode 100644
index 0000000..d2b851a
--- /dev/null
+++ b/examples/armv4t/gdb/extended_mode.rs
@@ -0,0 +1,144 @@
+use gdbstub::common::Pid;
+use gdbstub::target;
+use gdbstub::target::ext::extended_mode::{Args, ShouldTerminate};
+use gdbstub::target::TargetResult;
+
+use crate::emu::Emu;
+
+/*=====================================
+=            Extended Mode            =
+=====================================*/
+
+// This is a stub implementation of GDB's Extended Mode extensions.
+//
+// Truth be told, this particular emulator is _not_ very well suited to running
+// in extended mode, as it doesn't technically spawn/attach to any process.
+// Nonetheless, it's useful to have a stubbed implementation in-tree which can
+// be used for basic usability / regression testing.
+//
+// If you happen to implement a "proper" extended mode gdbstub, feel free to
+// file an issue / open a PR that links to your project!
+
+impl target::ext::extended_mode::ExtendedMode for Emu {
+    fn kill(&mut self, pid: Option<Pid>) -> TargetResult<ShouldTerminate, Self> {
+        eprintln!("GDB sent a kill request for pid {:?}", pid);
+        Ok(ShouldTerminate::No)
+    }
+
+    fn restart(&mut self) -> Result<(), Self::Error> {
+        eprintln!("GDB sent a restart request");
+        Ok(())
+    }
+
+    fn attach(&mut self, pid: Pid) -> TargetResult<(), Self> {
+        eprintln!("GDB tried to attach to a process with PID {}", pid);
+        Err(().into()) // non-specific failure
+    }
+
+    fn run(&mut self, filename: Option<&[u8]>, args: Args) -> TargetResult<Pid, Self> {
+        // simplified example: assume UTF-8 filenames / args
+        //
+        // To be 100% pedantically correct, consider converting to an `OsStr` in the
+        // least lossy way possible (e.g: using the `from_bytes` extension from
+        // `std::os::unix::ffi::OsStrExt`).
+
+        let filename = match filename {
+            None => None,
+            Some(raw) => Some(core::str::from_utf8(raw).map_err(drop)?),
+        };
+        let args = args
+            .map(|raw| core::str::from_utf8(raw).map_err(drop))
+            .collect::<Result<Vec<_>, _>>()?;
+
+        eprintln!(
+            "GDB tried to run a new process with filename {:?}, and args {:?}",
+            filename, args
+        );
+
+        self.reset();
+
+        // when running in single-threaded mode, this PID can be anything
+        Ok(Pid::new(1337).unwrap())
+    }
+
+    fn configure_aslr(&mut self) -> Option<target::ext::extended_mode::ConfigureASLROps<Self>> {
+        Some(self)
+    }
+
+    fn configure_env(&mut self) -> Option<target::ext::extended_mode::ConfigureEnvOps<Self>> {
+        Some(self)
+    }
+
+    fn configure_startup_shell(
+        &mut self,
+    ) -> Option<target::ext::extended_mode::ConfigureStartupShellOps<Self>> {
+        Some(self)
+    }
+
+    fn configure_working_dir(
+        &mut self,
+    ) -> Option<target::ext::extended_mode::ConfigureWorkingDirOps<Self>> {
+        Some(self)
+    }
+}
+
+impl target::ext::extended_mode::ConfigureASLR for Emu {
+    fn cfg_aslr(&mut self, enabled: bool) -> TargetResult<(), Self> {
+        eprintln!("GDB {} ASLR", if enabled { "enabled" } else { "disabled" });
+        Ok(())
+    }
+}
+
+impl target::ext::extended_mode::ConfigureEnv for Emu {
+    fn set_env(&mut self, key: &[u8], val: Option<&[u8]>) -> TargetResult<(), Self> {
+        // simplified example: assume UTF-8 key/val env vars
+        let key = core::str::from_utf8(key).map_err(drop)?;
+        let val = match val {
+            None => None,
+            Some(raw) => Some(core::str::from_utf8(raw).map_err(drop)?),
+        };
+
+        eprintln!("GDB tried to set a new env var: {:?}={:?}", key, val);
+
+        Ok(())
+    }
+
+    fn remove_env(&mut self, key: &[u8]) -> TargetResult<(), Self> {
+        let key = core::str::from_utf8(key).map_err(drop)?;
+        eprintln!("GDB tried to set remove a env var: {:?}", key);
+
+        Ok(())
+    }
+
+    fn reset_env(&mut self) -> TargetResult<(), Self> {
+        eprintln!("GDB tried to reset env vars");
+
+        Ok(())
+    }
+}
+
+impl target::ext::extended_mode::ConfigureStartupShell for Emu {
+    fn cfg_startup_with_shell(&mut self, enabled: bool) -> TargetResult<(), Self> {
+        eprintln!(
+            "GDB {} startup with shell",
+            if enabled { "enabled" } else { "disabled" }
+        );
+        Ok(())
+    }
+}
+
+impl target::ext::extended_mode::ConfigureWorkingDir for Emu {
+    fn cfg_working_dir(&mut self, dir: Option<&[u8]>) -> TargetResult<(), Self> {
+        let dir = match dir {
+            None => None,
+            Some(raw) => Some(core::str::from_utf8(raw).map_err(drop)?),
+        };
+
+        match dir {
+            None => eprintln!("GDB reset the working directory"),
+            Some(dir) => eprintln!("GDB set the working directory to {:?}", dir),
+        }
+
+        Ok(())
+    }
+}
diff --git a/examples/armv4t/gdb/mod.rs b/examples/armv4t/gdb/mod.rs
new file mode 100644
index 0000000..ae3cbd9
--- /dev/null
+++ b/examples/armv4t/gdb/mod.rs
@@ -0,0 +1,216 @@
+use core::convert::TryInto;
+
+use armv4t_emu::{reg, Memory};
+use gdbstub::arch;
+use gdbstub::arch::arm::reg::id::ArmCoreRegId;
+use gdbstub::target;
+use gdbstub::target::ext::base::singlethread::{ResumeAction, SingleThreadOps, StopReason};
+use gdbstub::target::ext::breakpoints::WatchKind;
+use gdbstub::target::{Target, TargetError, TargetResult};
+
+use crate::emu::{Emu, Event};
+
+// Additional GDB extensions
+
+mod extended_mode;
+mod monitor_cmd;
+mod section_offsets;
+
+/// Turn a `ArmCoreRegId` into an internal register number of `armv4t_emu`.
+fn cpu_reg_id(id: ArmCoreRegId) -> Option<u8> {
+    match id {
+        ArmCoreRegId::Gpr(i) => Some(i),
+        ArmCoreRegId::Sp => Some(reg::SP),
+        ArmCoreRegId::Lr => Some(reg::LR),
+        ArmCoreRegId::Pc => Some(reg::PC),
+        ArmCoreRegId::Cpsr => Some(reg::CPSR),
+        _ => None,
+    }
+}
+
+impl Target for Emu {
+    type Arch = arch::arm::Armv4t;
+    type Error = &'static str;
+
+    fn base_ops(&mut self) -> target::ext::base::BaseOps<Self::Arch, Self::Error> {
+        target::ext::base::BaseOps::SingleThread(self)
+    }
+
+    fn sw_breakpoint(&mut self) -> Option<target::ext::breakpoints::SwBreakpointOps<Self>> {
+        Some(self)
+    }
+
+    fn hw_watchpoint(&mut self) -> Option<target::ext::breakpoints::HwWatchpointOps<Self>> {
+        Some(self)
+    }
+
+    fn extended_mode(&mut self) -> Option<target::ext::extended_mode::ExtendedModeOps<Self>> {
+        Some(self)
+    }
+
+    fn monitor_cmd(&mut self) -> Option<target::ext::monitor_cmd::MonitorCmdOps<Self>> {
+        Some(self)
+    }
+
+    fn section_offsets(&mut self) -> Option<target::ext::section_offsets::SectionOffsetsOps<Self>> {
+        Some(self)
+    }
+}
+
+impl SingleThreadOps for Emu {
+    fn resume(
+        &mut self,
+        action: ResumeAction,
+        check_gdb_interrupt: &mut dyn FnMut() -> bool,
+    ) -> Result<StopReason<u32>, Self::Error> {
+        let event = match action {
+            ResumeAction::Step => match self.step() {
+                Some(e) => e,
+                None => return Ok(StopReason::DoneStep),
+            },
+            ResumeAction::Continue => {
+                let mut cycles = 0;
+                loop {
+                    if let Some(event) = self.step() {
+                        break event;
+                    };
+
+                    // check for GDB interrupt every 1024 instructions
+                    cycles += 1;
+                    if cycles % 1024 == 0 && check_gdb_interrupt() {
+                        return Ok(StopReason::GdbInterrupt);
+                    }
+                }
+            }
+        };
+
+        Ok(match event {
+            Event::Halted => StopReason::Halted,
+            Event::Break => StopReason::HwBreak,
+            Event::WatchWrite(addr) => StopReason::Watch {
+                kind: WatchKind::Write,
+                addr,
+            },
+            Event::WatchRead(addr) => StopReason::Watch {
+                kind: WatchKind::Read,
+                addr,
+            },
+        })
+    }
+
+    fn read_registers(&mut self, regs: &mut arch::arm::reg::ArmCoreRegs) -> TargetResult<(), Self> {
+        let mode = self.cpu.mode();
+
+        for i in 0..13 {
+            regs.r[i] = self.cpu.reg_get(mode, i as u8);
+        }
+        regs.sp = self.cpu.reg_get(mode, reg::SP);
+        regs.lr = self.cpu.reg_get(mode, reg::LR);
+        regs.pc = self.cpu.reg_get(mode, reg::PC);
+        regs.cpsr = self.cpu.reg_get(mode, reg::CPSR);
+
+        Ok(())
+    }
+
+    fn write_registers(&mut self, regs: &arch::arm::reg::ArmCoreRegs) -> TargetResult<(), Self> {
+        let mode = self.cpu.mode();
+
+        for i in 0..13 {
+            self.cpu.reg_set(mode, i, regs.r[i as usize]);
+        }
+        self.cpu.reg_set(mode, reg::SP, regs.sp);
+        self.cpu.reg_set(mode, reg::LR, regs.lr);
+        self.cpu.reg_set(mode, reg::PC, regs.pc);
+        self.cpu.reg_set(mode, reg::CPSR, regs.cpsr);
+
+        Ok(())
+    }
+
+    fn read_register(
+        &mut self,
+        reg_id: arch::arm::reg::id::ArmCoreRegId,
+        dst: &mut [u8],
+    ) -> TargetResult<(), Self> {
+        if let Some(i) = cpu_reg_id(reg_id) {
+            let w = self.cpu.reg_get(self.cpu.mode(), i);
+            dst.copy_from_slice(&w.to_le_bytes());
+            Ok(())
+        } else {
+            Err(().into())
+        }
+    }
+
+    fn write_register(
+        &mut self,
+        reg_id: arch::arm::reg::id::ArmCoreRegId,
+        val: &[u8],
+    ) -> TargetResult<(), Self> {
+        let w = u32::from_le_bytes(
+            val.try_into()
+                .map_err(|_| TargetError::Fatal("invalid data"))?,
+        );
+        if let Some(i) = cpu_reg_id(reg_id) {
+            self.cpu.reg_set(self.cpu.mode(), i, w);
+            Ok(())
+        } else {
+            Err(().into())
+        }
+    }
+
+    fn read_addrs(&mut self, start_addr: u32, data: &mut [u8]) -> TargetResult<(), Self> {
+        for (addr, val) in (start_addr..).zip(data.iter_mut()) {
+            *val = self.mem.r8(addr)
+        }
+        Ok(())
+    }
+
+    fn write_addrs(&mut self, start_addr: u32, data: &[u8]) -> TargetResult<(), Self> {
+        for (addr, val) in (start_addr..).zip(data.iter().copied()) {
+            self.mem.w8(addr, val)
+        }
+        Ok(())
+    }
+}
+
+impl target::ext::breakpoints::SwBreakpoint for Emu {
+    fn add_sw_breakpoint(&mut self, addr: u32) -> TargetResult<bool, Self> {
+        self.breakpoints.push(addr);
+        Ok(true)
+    }
+
+    fn remove_sw_breakpoint(&mut self, addr: u32) -> TargetResult<bool, Self> {
+        match self.breakpoints.iter().position(|x| *x == addr) {
+            None => return Ok(false),
+            Some(pos) => self.breakpoints.remove(pos),
+        };
+
+        Ok(true)
+    }
+}
+
+impl target::ext::breakpoints::HwWatchpoint for Emu {
+    fn add_hw_watchpoint(&mut self, addr: u32, kind: WatchKind) -> TargetResult<bool, Self> {
+        match kind {
+            WatchKind::Write => self.watchpoints.push(addr),
+            WatchKind::Read => self.watchpoints.push(addr),
+            WatchKind::ReadWrite => self.watchpoints.push(addr),
+        };
+
+        Ok(true)
+    }
+
+    fn remove_hw_watchpoint(&mut self, addr: u32, kind: WatchKind) -> TargetResult<bool, Self> {
+        let pos = match self.watchpoints.iter().position(|x| *x == addr) {
+            None => return Ok(false),
+            Some(pos) => pos,
+        };
+
+        match kind {
+            WatchKind::Write => self.watchpoints.remove(pos),
+            WatchKind::Read => self.watchpoints.remove(pos),
+            WatchKind::ReadWrite => self.watchpoints.remove(pos),
+        };
+
+        Ok(true)
+    }
+}
diff --git a/examples/armv4t/gdb/monitor_cmd.rs b/examples/armv4t/gdb/monitor_cmd.rs
new file mode 100644
index 0000000..4dbdffc
--- /dev/null
+++ b/examples/armv4t/gdb/monitor_cmd.rs
@@ -0,0 +1,28 @@
+use gdbstub::target;
+use gdbstub::target::ext::monitor_cmd::{outputln, ConsoleOutput};
+
+use crate::gdb::Emu;
+
+impl target::ext::monitor_cmd::MonitorCmd for Emu {
+    fn handle_monitor_cmd(
+        &mut self,
+        cmd: &[u8],
+        mut out: ConsoleOutput<'_>,
+    ) -> Result<(), Self::Error> {
+        let cmd = match core::str::from_utf8(cmd) {
+            Ok(cmd) => cmd,
+            Err(_) => {
+                outputln!(out, "command must be valid UTF-8");
+                return Ok(());
+            }
+        };
+
+        match cmd {
+            "" => outputln!(out, "Sorry, didn't catch that. Try `monitor ping`!"),
+            "ping" => outputln!(out, "pong!"),
+            _ => outputln!(out, "I don't know how to handle '{}'", cmd),
+        };
+
+        Ok(())
+    }
+}
diff --git a/examples/armv4t/gdb/section_offsets.rs b/examples/armv4t/gdb/section_offsets.rs
new file mode 100644
index 0000000..d04aab7
--- /dev/null
+++ b/examples/armv4t/gdb/section_offsets.rs
@@ -0,0 +1,17 @@
+use gdbstub::target;
+use gdbstub::target::ext::section_offsets::Offsets;
+
+use crate::gdb::Emu;
+
+// This implementation is for illustrative purposes only. If the offsets are
+// guaranteed to be zero, this extension does not need to be implemented.
+
+impl target::ext::section_offsets::SectionOffsets for Emu {
+    fn get_section_offsets(&mut self) -> Result<Offsets<u32>, Self::Error> {
+        Ok(Offsets::Sections {
+            text: 0,
+            data: 0,
+            bss: None,
+        })
+    }
+}
diff --git a/examples/armv4t/main.rs b/examples/armv4t/main.rs
new file mode 100644
index 0000000..58d6f7b
--- /dev/null
+++ b/examples/armv4t/main.rs
@@ -0,0 +1,85 @@
+use std::net::{TcpListener, TcpStream};
+
+#[cfg(unix)]
+use std::os::unix::net::{UnixListener, UnixStream};
+
+use gdbstub::{Connection, DisconnectReason, GdbStub};
+
+pub type DynResult<T> = Result<T, Box<dyn std::error::Error>>;
+
+static TEST_PROGRAM_ELF: &[u8] = include_bytes!("test_bin/test.elf");
+
+mod emu;
+mod gdb;
+mod mem_sniffer;
+
+fn wait_for_tcp(port: u16) -> DynResult<TcpStream> {
+    let sockaddr = format!("127.0.0.1:{}", port);
+    eprintln!("Waiting for a GDB connection on {:?}...", sockaddr);
+
+    let sock = TcpListener::bind(sockaddr)?;
+    let (stream, addr) = sock.accept()?;
+    eprintln!("Debugger connected from {}", addr);
+
+    Ok(stream)
+}
+
+#[cfg(unix)]
+fn wait_for_uds(path: &str) -> DynResult<UnixStream> {
+    match std::fs::remove_file(path) {
+        Ok(_) => {}
+        Err(e) => match e.kind() {
+            std::io::ErrorKind::NotFound => {}
+            _ => return Err(e.into()),
+        },
+    }
+
+    eprintln!("Waiting for a GDB connection on {}...", path);
+
+    let sock = UnixListener::bind(path)?;
+    let (stream, addr) = sock.accept()?;
+    eprintln!("Debugger connected from {:?}", addr);
+
+    Ok(stream)
+}
+
+fn main() -> DynResult<()> {
+    pretty_env_logger::init();
+
+    let mut emu = emu::Emu::new(TEST_PROGRAM_ELF)?;
+
+    let connection: Box<dyn Connection<Error = std::io::Error>> = {
+        if std::env::args().nth(1) == Some("--uds".to_string()) {
+            #[cfg(not(unix))]
+            {
+                return Err("Unix Domain Sockets can only be used on Unix".into());
+            }
+            #[cfg(unix)]
+            {
+                Box::new(wait_for_uds("/tmp/armv4t_gdb")?)
+            }
+        } else {
+            Box::new(wait_for_tcp(9001)?)
+        }
+    };
+
+    // hook-up debugger
+    let mut debugger = GdbStub::new(connection);
+
+    match debugger.run(&mut emu)? {
+        DisconnectReason::Disconnect => {
+            // run to completion
+            while emu.step() != Some(emu::Event::Halted) {}
+        }
+        DisconnectReason::TargetHalted => println!("Target halted!"),
+        DisconnectReason::Kill => {
+            println!("GDB sent a kill command!");
+            return Ok(());
+        }
+    }
+
+    let ret = emu.cpu.reg_get(armv4t_emu::Mode::User, 0);
+    println!("Program completed. Return value: {}", ret);
+
+    Ok(())
+}
diff --git a/examples/armv4t/mem_sniffer.rs b/examples/armv4t/mem_sniffer.rs
new file mode 100644
index 0000000..f9ac64b
--- /dev/null
+++ b/examples/armv4t/mem_sniffer.rs
@@ -0,0 +1,73 @@
+use armv4t_emu::Memory;
+
+pub enum AccessKind {
+    Read,
+    Write,
+}
+
+pub struct Access {
+    pub kind: AccessKind,
+    pub addr: u32,
+    pub val: u32,
+    pub len: usize,
+}
+
+/// Wraps a `Memory` object, logging any accesses with the provided callback.
+#[derive(Debug)]
+pub struct MemSniffer<'a, M, F: FnMut(Access)> {
+    mem: &'a mut M,
+    addrs: &'a [u32],
+    on_access: F,
+}
+
+impl<'a, M: Memory, F: FnMut(Access)> MemSniffer<'a, M, F> {
+    pub fn new(mem: &'a mut M, addrs: &'a [u32], on_access: F) -> MemSniffer<'a, M, F> {
+        MemSniffer {
+            mem,
+            addrs,
+            on_access,
+        }
+    }
+}
+
+macro_rules! impl_memsniff_r {
+    ($fn:ident, $ret:ty) => {
+        fn $fn(&mut self, addr: u32) -> $ret {
+            let ret = self.mem.$fn(addr);
+            if self.addrs.contains(&addr) {
+                (self.on_access)(Access {
+                    kind: AccessKind::Read,
+                    addr,
+                    val: ret as u32,
+                    len: ret.to_le_bytes().len(),
+                });
+            }
+            ret
+        }
+    };
+}
+
+macro_rules! impl_memsniff_w {
+    ($fn:ident, $val:ty) => {
+        fn $fn(&mut self, addr: u32, val: $val) {
+            self.mem.$fn(addr, val);
+            if self.addrs.contains(&addr) {
+                (self.on_access)(Access {
+                    kind: AccessKind::Write,
+                    addr,
+                    val: val as u32,
+                    len: val.to_le_bytes().len(),
+                });
+            }
+        }
+    };
+}
+
+impl<'a, M: Memory, F: FnMut(Access)> Memory for MemSniffer<'a, M, F> {
+    impl_memsniff_r!(r8, u8);
+    impl_memsniff_r!(r16, u16);
+    impl_memsniff_r!(r32, u32);
+    impl_memsniff_w!(w8, u8);
+    impl_memsniff_w!(w16, u16);
+    impl_memsniff_w!(w32, u32);
+}
diff --git a/examples/armv4t/test_bin/.gdbinit b/examples/armv4t/test_bin/.gdbinit
new file mode 100644
index 0000000..6f9d0b8
--- /dev/null
+++ b/examples/armv4t/test_bin/.gdbinit
@@ -0,0 +1,2 @@
+file test.elf
+target extended-remote :9001
diff --git a/examples/armv4t/test_bin/.gitignore b/examples/armv4t/test_bin/.gitignore
new file mode 100644
index 0000000..5bb25f0
--- /dev/null
+++ b/examples/armv4t/test_bin/.gitignore
@@ -0,0 +1,2 @@
+*.o
+.gdb_history
diff --git a/examples/armv4t/test_bin/compile_test.sh b/examples/armv4t/test_bin/compile_test.sh
new file mode 100755
index 0000000..5216cb5
--- /dev/null
+++ b/examples/armv4t/test_bin/compile_test.sh
@@ -0,0 +1,2 @@
+arm-none-eabi-gcc -c test.c -march=armv4t -O0 -g -std=c11 -fdebug-prefix-map=$(pwd)=.
+arm-none-eabi-ld -static -Ttest.ld test.o -o test.elf
diff --git a/examples/armv4t/test_bin/test.c b/examples/armv4t/test_bin/test.c
new file mode 100644
index 0000000..6fb75aa
--- /dev/null
+++ b/examples/armv4t/test_bin/test.c
@@ -0,0 +1,14 @@
+int main() {
+    int x = 4;
+    int y = 3;
+
+    x += 1;
+    y += 3;
+
+    // big, useless loop to test ctrl-c functionality
+    for (int i = 0; i < 1024 * 32; i++) {
+        x += 1;
+    }
+
+    return x;
+}
diff --git a/examples/armv4t/test_bin/test.ld b/examples/armv4t/test_bin/test.ld
new file mode 100644
index 0000000..b675d62
--- /dev/null
+++ b/examples/armv4t/test_bin/test.ld
@@ -0,0 +1,45 @@
+ENTRY(main)
+
+MEMORY {
+    ram : ORIGIN = 0x55550000, LENGTH = 0x10000000
+}
+
+SECTIONS {
+    . = 0x55550000;
+
+    .text : ALIGN(4)
+    {
+        __TEXT_START__ = .;
+        *(.text*);
+        . = ALIGN(4);
+        __TEXT_END__ = .;
+    } > ram
+
+    .got : ALIGN(4)
+    {
+        *(.got*);
+    } > ram
+
+    .data : ALIGN(4)
+    {
+        __DATA_START__ = .;
+        *(.data*);
+        *(.rodata*);
+        __DATA_END__ = .;
+    } > ram
+
+    .bss : ALIGN(4)
+    {
+        __BSS_START__ = .;
+        *(.bss*);
+        . = ALIGN(4);
+        __BSS_END__ = .;
+        end = __BSS_END__;
+    } > ram
+
+    /DISCARD/ :
+    {
+        *(.ARM.exidx*) /* index entries for section unwinding */
+        *(.ARM.extab*) /* exception unwinding information */
+    }
+}
diff --git a/examples/armv4t_multicore/README.md b/examples/armv4t_multicore/README.md
new file mode 100644
index 0000000..4eef502
--- /dev/null
+++ b/examples/armv4t_multicore/README.md
@@ -0,0 +1,29 @@
+# armv4t
+
+An incredibly simple emulator to run elf binaries compiled with `arm-none-eabi-cc -march=armv4t`. Uses a dual-core architecture to show off `gdbstub`'s multi-process support. It's not modeled after any real-world system.
+
+**Note:** The actual emulator's code is pretty sloppy, since it's just a contrived example to show off what `gdbstub` is capable of.
+
+Run `gdb-arm-none-eabi` (or alternatively, `gdb-multiarch`) from the `test_bin` directory to automatically connect to the emulator + load debug symbols for the emulated binary.
+
+This example can be run using:
+
+```bash
+cargo run --example armv4t --features=std
+```
+
+**NOTE:** If debug symbols couldn't be loaded, try rebuilding `test.elf` locally (requires the `arm-none-eabi` toolchain to be installed), and recompiling the example.
+
+## Memory Map
+
+The entire 32-bit address space is accessible as RAM.
+
+Reading from the magic memory location `0xffff_4200` returns `0xaa` if accessed by the CPU, and `0x55` if accessed by the COP.
+
+## Unix Domain Sockets
+
+GDB versions since \~2018 support running a debugging session over Unix Domain Sockets (UDS). Debugging over UDS can feel much snappier than debugging over loopback TCP.
+
+Running the example with the `--uds` flag will bind the GdbStub to a socket at `/tmp/armv4t_gdb`.
+
+This feature is only supported on Unix-like systems.
diff --git a/examples/armv4t_multicore/emu.rs b/examples/armv4t_multicore/emu.rs
new file mode 100644
index 0000000..95bd057
--- /dev/null
+++ b/examples/armv4t_multicore/emu.rs
@@ -0,0 +1,184 @@
+//! ------------------------------------------------------------------------ !//
+//! ------------------------------ DISCLAIMER ------------------------------ !//
+//! ------------------------------------------------------------------------ !//
+//!
+//! This code is absolutely awful, and completely slapped together for the sake
+//! of example. The watchpoint implementation is particularly awful.
+//!
+//! While it technically "gets the job done" and provides a simple multicore
+//! system that can be debugged, it would really merit a re-write, since it's
+//! not a good example of "proper Rust coding practices"
+
+use std::collections::HashMap;
+
+use armv4t_emu::{reg, Cpu, ExampleMem, Memory, Mode};
+
+use crate::mem_sniffer::{AccessKind, MemSniffer};
+use crate::DynResult;
+
+const HLE_RETURN_ADDR: u32 = 0x12345678;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum CpuId {
+    Cpu,
+    Cop,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum Event {
+    Halted,
+    Break,
+    WatchWrite(u32),
+    WatchRead(u32),
+}
+
+/// incredibly barebones armv4t-based emulator
+pub struct Emu {
+    pub(crate) cpu: Cpu,
+    pub(crate) cop: Cpu,
+    pub(crate) mem: ExampleMem,
+
+    pub(crate) watchpoints: Vec<u32>,
+    /// (read, write)
+    pub(crate) watchpoint_kind: HashMap<u32, (bool, bool)>,
+    pub(crate) breakpoints: Vec<u32>,
+
+    // GDB seems to get gets very confused if two threads are executing the exact same code at the
+    // exact same time. Maybe this is a bug with `gdbstub`?
+    stall_cop_cycles: usize,
+}
+
+impl Emu {
+    pub fn new(program_elf: &[u8]) -> DynResult<Emu> {
+        // set up emulated system
+        let mut cpu = Cpu::new();
+        let mut mem = ExampleMem::new();
+
+        // load ELF
+        let elf_header = goblin::elf::Elf::parse(program_elf)?;
+
+        // copy all in-memory sections from the ELF file into system RAM
+        let sections = elf_header
+            .section_headers
+            .iter()
+            .filter(|h| h.is_alloc() && h.sh_type != goblin::elf::section_header::SHT_NOBITS);
+
+        for h in sections {
+            eprintln!(
+                "loading section {:?} into memory from [{:#010x?}..{:#010x?}]",
+                elf_header.shdr_strtab.get(h.sh_name).unwrap().unwrap(),
+                h.sh_addr,
+                h.sh_addr + h.sh_size,
+            );
+
+            for (i, b) in program_elf[h.file_range()].iter().enumerate() {
+                mem.w8(h.sh_addr as u32 + i as u32, *b);
+            }
+        }
+
+        // setup execution state
+        eprintln!("Setting PC to {:#010x?}", elf_header.entry);
+        cpu.reg_set(Mode::User, reg::SP, 0x10000000);
+        cpu.reg_set(Mode::User, reg::LR, HLE_RETURN_ADDR);
+        cpu.reg_set(Mode::User, reg::PC, elf_header.entry as u32);
+        cpu.reg_set(Mode::User, reg::CPSR, 0x10); // user mode
+        let cop = cpu;
+
+        Ok(Emu {
+            cpu,
+            cop,
+            mem,
+            watchpoints: Vec::new(),
+            watchpoint_kind: HashMap::new(),
+            breakpoints: Vec::new(),
+            stall_cop_cycles: 24,
+        })
+    }
+
+    pub fn step_core(&mut self, id: CpuId) -> Option<Event> {
+        let cpu = match id {
+            CpuId::Cop if self.stall_cop_cycles != 0 => {
+                self.stall_cop_cycles -= 1;
+                return None;
+            }
+            CpuId::Cop => &mut self.cop,
+            CpuId::Cpu => &mut self.cpu,
+        };
+
+        // set up magic memory location
+        self.mem.w8(
+            0xffff_4200,
+            match id {
+                CpuId::Cpu => 0xaa,
+                CpuId::Cop => 0x55,
+            },
+        );
+
+        let mut hit_watchpoint = None;
+        let mut sniffer = MemSniffer::new(&mut self.mem, &self.watchpoints, |access| {
+            hit_watchpoint = Some(access)
+        });
+
+        cpu.step(&mut sniffer);
+        let pc = cpu.reg_get(Mode::User, reg::PC);
+
+        if pc == HLE_RETURN_ADDR {
+            match id {
+                CpuId::Cpu => return Some(Event::Halted),
+                CpuId::Cop => return Some(Event::Halted),
+            }
+        }
+
+        if let Some(access) = hit_watchpoint {
+            // NOTE: this isn't a particularly elegant way to do watchpoints! This works
+            // fine for some example code, but don't use this as inspiration in your own
+            // emulator!
+            match access.kind {
+                AccessKind::Read => {
+                    if *self
+                        .watchpoint_kind
+                        .get(&access.addr)
+                        .map(|(r, _w)| r)
+                        .unwrap_or(&false)
+                    {
+                        let fixup = if cpu.thumb_mode() { 2 } else { 4 };
+                        cpu.reg_set(Mode::User, reg::PC, pc - fixup);
+                        return Some(Event::WatchRead(access.addr));
+                    }
+                }
+                AccessKind::Write => {
+                    if *self
+                        .watchpoint_kind
+                        .get(&access.addr)
+                        .map(|(_r, w)| w)
+                        .unwrap_or(&false)
+                    {
+                        let fixup = if cpu.thumb_mode() { 2 } else { 4 };
+                        cpu.reg_set(Mode::User, reg::PC, pc - fixup);
+                        return Some(Event::WatchWrite(access.addr));
+                    }
+                }
+            }
+        }
+
+        if self.breakpoints.contains(&pc) {
+            return Some(Event::Break);
+        }
+
+        None
+    }
+
+    pub fn step(&mut self) -> Option<(Event, CpuId)> {
+        let mut evt = None;
+
+        for id in [CpuId::Cpu, CpuId::Cop].iter().copied() {
+            if let Some(event) = self.step_core(id) {
+                if evt.is_none() {
+                    evt = Some((event, id));
+                }
+            }
+        }
+
+        evt
+    }
+}
diff --git a/examples/armv4t_multicore/gdb.rs b/examples/armv4t_multicore/gdb.rs
new file mode 100644
index 0000000..47683e2
--- /dev/null
+++ b/examples/armv4t_multicore/gdb.rs
@@ -0,0 +1,233 @@
+use armv4t_emu::{reg, Memory};
+
+use gdbstub::arch;
+use gdbstub::common::Tid;
+use gdbstub::target;
+use gdbstub::target::ext::base::multithread::{
+    Actions, MultiThreadOps, ResumeAction, ThreadStopReason,
+};
+use gdbstub::target::ext::breakpoints::WatchKind;
+use gdbstub::target::{Target, TargetError, TargetResult};
+
+use crate::emu::{CpuId, Emu, Event};
+
+fn event_to_stopreason(e: Event, id: CpuId) -> ThreadStopReason<u32> {
+    let tid = cpuid_to_tid(id);
+    match e {
+        Event::Halted => ThreadStopReason::Halted,
+        Event::Break => ThreadStopReason::SwBreak(tid),
+        Event::WatchWrite(addr) => ThreadStopReason::Watch {
+            tid,
+            kind: WatchKind::Write,
+            addr,
+        },
+        Event::WatchRead(addr) => ThreadStopReason::Watch {
+            tid,
+            kind: WatchKind::Read,
+            addr,
+        },
+    }
+}
+
+fn cpuid_to_tid(id: CpuId) -> Tid {
+    match id {
+        CpuId::Cpu => Tid::new(1).unwrap(),
+        CpuId::Cop => Tid::new(2).unwrap(),
+    }
+}
+
+fn tid_to_cpuid(tid: Tid) -> Result<CpuId, &'static str> {
+    match tid.get() {
+        1 => Ok(CpuId::Cpu),
+        2 => Ok(CpuId::Cop),
+        _ => Err("specified invalid core"),
+    }
+}
+
+impl Target for Emu {
+    type Arch = arch::arm::Armv4t;
+    type Error = &'static str;
+
+    fn base_ops(&mut self) -> target::ext::base::BaseOps<Self::Arch, Self::Error> {
+        target::ext::base::BaseOps::MultiThread(self)
+    }
+
+    fn sw_breakpoint(&mut self) -> Option<target::ext::breakpoints::SwBreakpointOps<Self>> {
+        Some(self)
+    }
+
+    fn hw_watchpoint(&mut self) -> Option<target::ext::breakpoints::HwWatchpointOps<Self>> {
+        Some(self)
+    }
+}
+
+impl MultiThreadOps for Emu {
+    fn resume(
+        &mut self,
+        actions: Actions,
+        check_gdb_interrupt: &mut dyn FnMut() -> bool,
+    ) -> Result<ThreadStopReason<u32>, Self::Error> {
+        // in this emulator, each core runs in lock-step, so we can ignore the
+        // TidSelector associated with each action, and only care if GDB
+        // requests execution to start / stop.
+        //
+        // In general, the behavior of multi-threaded systems during debugging is
+        // determined by the system scheduler. On certain systems, this behavior can be
+        // configured using the GDB command `set scheduler-locking _mode_`, but at the
+        // moment, `gdbstub` doesn't plumb-through that configuration command.
+
+        // FIXME: properly handle multiple actions...
+        let actions = actions.collect::<Vec<_>>();
+        let (_, action) = actions[0];
+
+        match action {
+            ResumeAction::Step => match self.step() {
+                Some((event, id)) => Ok(event_to_stopreason(event, id)),
+                None => Ok(ThreadStopReason::DoneStep),
+            },
+            ResumeAction::Continue => {
+                let mut cycles: usize = 0;
+                loop {
+                    // check for GDB interrupt every 1024 instructions
+                    if cycles % 1024 == 0 && check_gdb_interrupt() {
+                        return Ok(ThreadStopReason::GdbInterrupt);
+                    }
+                    cycles += 1;
+
+                    if let Some((event, id)) = self.step() {
+                        return Ok(event_to_stopreason(event, id));
+                    };
+                }
+            }
+        }
+    }
+
+    fn read_registers(
+        &mut self,
+        regs: &mut arch::arm::reg::ArmCoreRegs,
+        tid: Tid,
+    ) -> TargetResult<(), Self> {
+        let cpu = match tid_to_cpuid(tid).map_err(TargetError::Fatal)? {
+            CpuId::Cpu => &mut self.cpu,
+            CpuId::Cop => &mut self.cop,
+        };
+
+        let mode = cpu.mode();
+
+        for i in 0..13 {
+            regs.r[i] = cpu.reg_get(mode, i as u8);
+        }
+        regs.sp = cpu.reg_get(mode, reg::SP);
+        regs.lr = cpu.reg_get(mode, reg::LR);
+        regs.pc = cpu.reg_get(mode, reg::PC);
+        regs.cpsr = cpu.reg_get(mode, reg::CPSR);
+
+        Ok(())
+    }
+
+    fn write_registers(
+        &mut self,
+        regs: &arch::arm::reg::ArmCoreRegs,
+        tid: Tid,
+    ) -> TargetResult<(), Self> {
+        let cpu = match tid_to_cpuid(tid).map_err(TargetError::Fatal)? {
+            CpuId::Cpu => &mut self.cpu,
+            CpuId::Cop => &mut self.cop,
+        };
+
+        let mode = cpu.mode();
+
+        for i in 0..13 {
+            cpu.reg_set(mode, i, regs.r[i as usize]);
+        }
+        cpu.reg_set(mode, reg::SP, regs.sp);
+        cpu.reg_set(mode, reg::LR, regs.lr);
+        cpu.reg_set(mode, reg::PC, regs.pc);
+        cpu.reg_set(mode, reg::CPSR, regs.cpsr);
+
+        Ok(())
+    }
+
+    fn read_addrs(
+        &mut self,
+        start_addr: u32,
+        data: &mut [u8],
+        _tid: Tid, // same address space for each core
+    ) -> TargetResult<(), Self> {
+        for (addr, val) in (start_addr..).zip(data.iter_mut()) {
+            *val = self.mem.r8(addr)
+        }
+        Ok(())
+    }
+
+    fn write_addrs(
+        &mut self,
+        start_addr: u32,
+        data: &[u8],
+        _tid: Tid, // same address space for each core
+    ) -> TargetResult<(), Self> {
+        for (addr, val) in (start_addr..).zip(data.iter().copied()) {
+            self.mem.w8(addr, val)
+        }
+        Ok(())
+    }
+
+    fn list_active_threads(
+        &mut self,
+        register_thread: &mut dyn FnMut(Tid),
+    ) -> Result<(), Self::Error> {
+        register_thread(cpuid_to_tid(CpuId::Cpu));
+        register_thread(cpuid_to_tid(CpuId::Cop));
+        Ok(())
+    }
+}
+
+impl target::ext::breakpoints::SwBreakpoint for Emu {
+    fn add_sw_breakpoint(&mut self, addr: u32) -> TargetResult<bool, Self> {
+        self.breakpoints.push(addr);
+        Ok(true)
+    }
+
+    fn remove_sw_breakpoint(&mut self, addr: u32) -> TargetResult<bool, Self> {
+        match self.breakpoints.iter().position(|x| *x == addr) {
+            None => return Ok(false),
+            Some(pos) => self.breakpoints.remove(pos),
+        };
+
+        Ok(true)
+    }
+}
+
+impl target::ext::breakpoints::HwWatchpoint for Emu {
+    fn add_hw_watchpoint(&mut self, addr: u32, kind: WatchKind) -> TargetResult<bool, Self> {
+        self.watchpoints.push(addr);
+
+        let entry = self.watchpoint_kind.entry(addr).or_insert((false, false));
+        match kind {
+            WatchKind::Write => entry.1 = true,
+            WatchKind::Read => entry.0 = true,
+            WatchKind::ReadWrite => entry.0 = true, // arbitrary
+        };
+
+        Ok(true)
+    }
+
+    fn remove_hw_watchpoint(&mut self, addr: u32, kind: WatchKind) -> TargetResult<bool, Self> {
+        let entry = self.watchpoint_kind.entry(addr).or_insert((false, false));
+        match kind {
+            WatchKind::Write => entry.1 = false,
+            WatchKind::Read => entry.0 = false,
+            WatchKind::ReadWrite => entry.0 = false, // arbitrary
+        };
+
+        if !self.watchpoint_kind.contains_key(&addr) {
+            let pos = match self.watchpoints.iter().position(|x| *x == addr) {
+                None => return Ok(false),
+                Some(pos) => pos,
+            };
+            self.watchpoints.remove(pos);
+        }
+
+        Ok(true)
+    }
+}
diff --git a/examples/armv4t_multicore/main.rs b/examples/armv4t_multicore/main.rs
new file mode 100644
index 0000000..3862513
--- /dev/null
+++ b/examples/armv4t_multicore/main.rs
@@ -0,0 +1,85 @@
+use std::net::{TcpListener, TcpStream};
+
+#[cfg(unix)]
+use std::os::unix::net::{UnixListener, UnixStream};
+
+use gdbstub::{Connection, DisconnectReason, GdbStub};
+
+pub type DynResult<T> = Result<T, Box<dyn std::error::Error>>;
+
+static TEST_PROGRAM_ELF: &[u8] = include_bytes!("test_bin/test.elf");
+
+mod emu;
+mod gdb;
+mod mem_sniffer;
+
+fn wait_for_tcp(port: u16) -> DynResult<TcpStream> {
+    let sockaddr = format!("127.0.0.1:{}", port);
+    eprintln!("Waiting for a GDB connection on {:?}...", sockaddr);
+
+    let sock = TcpListener::bind(sockaddr)?;
+    let (stream, addr) = sock.accept()?;
+    eprintln!("Debugger connected from {}", addr);
+
+    Ok(stream)
+}
+
+#[cfg(unix)]
+fn wait_for_uds(path: &str) -> DynResult<UnixStream> {
+    match std::fs::remove_file(path) {
+        Ok(_) => {}
+        Err(e) => match e.kind() {
+            std::io::ErrorKind::NotFound => {}
+            _ => return Err(e.into()),
+        },
+    }
+
+    eprintln!("Waiting for a GDB connection on {}...", path);
+
+    let sock = UnixListener::bind(path)?;
+    let (stream, addr) = sock.accept()?;
+    eprintln!("Debugger connected from {:?}", addr);
+
+    Ok(stream)
+}
+
+fn main() -> DynResult<()> {
+    pretty_env_logger::init();
+
+    let mut emu = emu::Emu::new(TEST_PROGRAM_ELF)?;
+
+    let connection: Box<dyn Connection<Error = std::io::Error>> = {
+        if std::env::args().nth(1) == Some("--uds".to_string()) {
+            #[cfg(not(unix))]
+            {
+                return Err("Unix Domain Sockets can only be used on Unix".into());
+            }
+            #[cfg(unix)]
+            {
+                Box::new(wait_for_uds("/tmp/armv4t_gdb")?)
+            }
+        } else {
+            Box::new(wait_for_tcp(9001)?)
+        }
+    };
+
+    // hook-up debugger
+    let mut debugger = GdbStub::new(connection);
+
+    match debugger.run(&mut emu)? {
+        DisconnectReason::Disconnect => {
+            // run to completion
+            while emu.step() != Some((emu::Event::Halted, emu::CpuId::Cpu)) {}
+        }
+        DisconnectReason::TargetHalted => println!("Target halted!"),
+        DisconnectReason::Kill => {
+            println!("GDB sent a kill command!");
+            return Ok(());
+        }
+    }
+
+    let ret = emu.cpu.reg_get(armv4t_emu::Mode::User, 0);
+    println!("Program completed. Return value: {}", ret);
+
+    Ok(())
+}
diff --git a/examples/armv4t_multicore/mem_sniffer.rs b/examples/armv4t_multicore/mem_sniffer.rs
new file mode 100644
index 0000000..878e118
--- /dev/null
+++ b/examples/armv4t_multicore/mem_sniffer.rs
@@ -0,0 +1,75 @@
+use armv4t_emu::Memory;
+
+#[derive(Debug)]
+pub enum AccessKind {
+    Read,
+    Write,
+}
+
+#[derive(Debug)]
+pub struct Access {
+    pub kind: AccessKind,
+    pub addr: u32,
+    pub val: u32,
+    pub len: usize,
+}
+
+/// Wraps a `Memory` object, logging any accesses with the provided callback.
+#[derive(Debug)]
+pub struct MemSniffer<'a, M, F: FnMut(Access)> {
+    mem: &'a mut M,
+    addrs: &'a [u32],
+    on_access: F,
+}
+
+impl<'a, M: Memory, F: FnMut(Access)> MemSniffer<'a, M, F> {
+    pub fn new(mem: &'a mut M, addrs: &'a [u32], on_access: F) -> MemSniffer<'a, M, F> {
+        MemSniffer {
+            mem,
+            addrs,
+            on_access,
+        }
+    }
+}
+
+macro_rules! impl_memsniff_r {
+    ($fn:ident, $ret:ty) => {
+        fn $fn(&mut self, addr: u32) -> $ret {
+            let ret = self.mem.$fn(addr);
+            if self.addrs.contains(&addr) {
+                (self.on_access)(Access {
+                    kind: AccessKind::Read,
+                    addr,
+                    val: ret as u32,
+                    len: ret.to_le_bytes().len(),
+                });
+            }
+            ret
+        }
+    };
+}
+
+macro_rules! impl_memsniff_w {
+    ($fn:ident, $val:ty) => {
+        fn $fn(&mut self, addr: u32, val: $val) {
+            self.mem.$fn(addr, val);
+            if self.addrs.contains(&addr) {
+                (self.on_access)(Access {
+                    kind: AccessKind::Write,
+                    addr,
+                    val: val as u32,
+                    len: val.to_le_bytes().len(),
+                });
+            }
+        }
+    };
+}
+
+impl<'a, M: Memory, F: FnMut(Access)> Memory for MemSniffer<'a, M, F> {
+    impl_memsniff_r!(r8, u8);
+    impl_memsniff_r!(r16, u16);
+    impl_memsniff_r!(r32, u32);
+    impl_memsniff_w!(w8, u8);
+    impl_memsniff_w!(w16, u16);
+    impl_memsniff_w!(w32, u32);
+}
diff --git a/examples/armv4t_multicore/test_bin/.gdbinit b/examples/armv4t_multicore/test_bin/.gdbinit
new file mode 100644
index 0000000..6270550
--- /dev/null
+++ b/examples/armv4t_multicore/test_bin/.gdbinit
@@ -0,0 +1,2 @@
+file test.elf
+target remote :9001
diff --git a/examples/armv4t_multicore/test_bin/.gitignore b/examples/armv4t_multicore/test_bin/.gitignore
new file mode 100644
index 0000000..5bb25f0
--- /dev/null
+++ b/examples/armv4t_multicore/test_bin/.gitignore
@@ -0,0 +1,2 @@
+*.o
+.gdb_history
diff --git a/examples/armv4t_multicore/test_bin/compile_test.sh b/examples/armv4t_multicore/test_bin/compile_test.sh
new file mode 100755
index 0000000..5216cb5
--- /dev/null
+++ b/examples/armv4t_multicore/test_bin/compile_test.sh
@@ -0,0 +1,2 @@
+arm-none-eabi-gcc -c test.c -march=armv4t -O0 -g -std=c11 -fdebug-prefix-map=$(pwd)=.
+arm-none-eabi-ld -static -Ttest.ld test.o -o test.elf
diff --git a/examples/armv4t_multicore/test_bin/test.c b/examples/armv4t_multicore/test_bin/test.c
new file mode 100644
index 0000000..5a64d04
--- /dev/null
+++ b/examples/armv4t_multicore/test_bin/test.c
@@ -0,0 +1,21 @@
+#define CPU_ID *((volatile unsigned char*)0xffff4200)
+
+int main() {
+    // try switching between threads using `thread 1` and `thread 2`!
+    int done = 0;
+    int x = 0;
+
+    // diverging paths on each CPU core
+    if (CPU_ID == 0xaa) {
+        while (!done) {}
+        return x;
+    } else {
+        // big, useless loop to test ctrl-c functionality
+        for (int i = 0; i < 1024 * 32; i++) {
+            x += 1;
+        }
+        done = 1;
+        // loop forever
+        for (;;) {}
+    }
+}
diff --git a/examples/armv4t_multicore/test_bin/test.ld b/examples/armv4t_multicore/test_bin/test.ld
new file mode 100644
index 0000000..b675d62
--- /dev/null
+++ b/examples/armv4t_multicore/test_bin/test.ld
@@ -0,0 +1,45 @@
+ENTRY(main)
+
+MEMORY {
+    ram : ORIGIN = 0x55550000, LENGTH = 0x10000000
+}
+
+SECTIONS {
+    . = 0x55550000;
+
+    .text : ALIGN(4)
+    {
+        __TEXT_START__ = .;
+        *(.text*);
+        . = ALIGN(4);
+        __TEXT_END__ = .;
+    } > ram
+
+    .got : ALIGN(4)
+    {
+        *(.got*);
+    } > ram
+
+    .data : ALIGN(4)
+    {
+        __DATA_START__ = .;
+        *(.data*);
+        *(.rodata*);
+        __DATA_END__ = .;
+    } > ram
+
+    .bss : ALIGN(4)
+    {
+        __BSS_START__ = .;
+        *(.bss*);
+        . = ALIGN(4);
+        __BSS_END__ = .;
+        end = __BSS_END__;
+    } > ram
+
+    /DISCARD/ :
+    {
+        *(.ARM.exidx*) /* index entries for section unwinding */
+        *(.ARM.extab*) /* exception unwinding information */
+    }
+}
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..606e292
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1 @@
+wrap_comments = true
\ No newline at end of file
diff --git a/scripts/test_dead_code_elim.sh b/scripts/test_dead_code_elim.sh
new file mode 100755
index 0000000..b689c5f
--- /dev/null
+++ b/scripts/test_dead_code_elim.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# Must be run from the project's root directory.
+
+# checks if a certain packet has been dead-code-eliminated from the resulting binary.
+# Arg 1: example to build
+# Arg 2: packet name
+
+if [ -z "$1" ]; then
+    echo "Must pass example name as first argument (e.g: armv4t)"
+    exit 1
+fi
+
+if [ -z "$2" ]; then
+    echo "Must pass packet name as second argument (e.g: qRcmd)"
+    exit 1
+fi
+
+cargo build --release --example $1 --features="std __dead_code_marker"
+strip ./target/release/examples/$1
+
+output=$(strings ./target/release/examples/$1 | sort | grep --color=always "<$2,")
+
+if [[ $output ]]; then
+    echo $output
+    echo "Dead code NOT eliminated!"
+    exit 1
+else
+    echo "Dead code eliminated."
+    exit 0
+fi
diff --git a/src/arch/arm/mod.rs b/src/arch/arm/mod.rs
new file mode 100644
index 0000000..eb26fbb
--- /dev/null
+++ b/src/arch/arm/mod.rs
@@ -0,0 +1,18 @@
+//! Implementations for various ARM architectures.
+
+use crate::arch::Arch;
+
+pub mod reg;
+
+/// Implements `Arch` for ARMv4T
+pub enum Armv4t {}
+
+impl Arch for Armv4t {
+    type Usize = u32;
+    type Registers = reg::ArmCoreRegs;
+    type RegId = reg::id::ArmCoreRegId;
+
+    fn target_description_xml() -> Option<&'static str> {
+        Some(r#"<target version="1.0"><architecture>armv4t</architecture></target>"#)
+    }
+}
diff --git a/src/arch/arm/reg/arm_core.rs b/src/arch/arm/reg/arm_core.rs
new file mode 100644
index 0000000..dba5f1d
--- /dev/null
+++ b/src/arch/arm/reg/arm_core.rs
@@ -0,0 +1,76 @@
+use crate::arch::Registers;
+
+/// 32-bit ARM core registers.
+///
+/// Source: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/arm/arm-core.xml
+#[derive(Debug, Default, Clone, Eq, PartialEq)]
+pub struct ArmCoreRegs {
+    /// General purpose registers (R0-R12)
+    pub r: [u32; 13],
+    /// Stack Pointer (R13)
+    pub sp: u32,
+    /// Link Register (R14)
+    pub lr: u32,
+    /// Program Counter (R15)
+    pub pc: u32,
+    /// Current Program Status Register (cpsr)
+    pub cpsr: u32,
+}
+
+impl Registers for ArmCoreRegs {
+    fn gdb_serialize(&self, mut write_byte: impl FnMut(Option<u8>)) {
+        macro_rules! write_bytes {
+            ($bytes:expr) => {
+                for b in $bytes {
+                    write_byte(Some(*b))
+                }
+            };
+        }
+
+        for reg in self.r.iter() {
+            write_bytes!(&reg.to_le_bytes());
+        }
+        write_bytes!(&self.sp.to_le_bytes());
+        write_bytes!(&self.lr.to_le_bytes());
+        write_bytes!(&self.pc.to_le_bytes());
+
+        // Floating point registers (unused)
+        for _ in 0..25 {
+            (0..4).for_each(|_| write_byte(None))
+        }
+
+        write_bytes!(&self.cpsr.to_le_bytes());
+    }
+
+    fn gdb_deserialize(&mut self, bytes: &[u8]) -> Result<(), ()> {
+        // ensure bytes.chunks_exact(4) won't panic
+        if bytes.len() % 4 != 0 {
+            return Err(());
+        }
+
+        use core::convert::TryInto;
+        let mut regs = bytes
+            .chunks_exact(4)
+            .map(|c| u32::from_le_bytes(c.try_into().unwrap()));
+
+        for reg in self.r.iter_mut() {
+            *reg = regs.next().ok_or(())?
+        }
+        self.sp = regs.next().ok_or(())?;
+        self.lr = regs.next().ok_or(())?;
+        self.pc = regs.next().ok_or(())?;
+
+        // Floating point registers (unused)
+        for _ in 0..25 {
+            regs.next().ok_or(())?;
+        }
+
+        self.cpsr = regs.next().ok_or(())?;
+
+        if regs.next().is_some() {
+            return Err(());
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/arch/arm/reg/id.rs b/src/arch/arm/reg/id.rs
new file mode 100644
index 0000000..f22de27
--- /dev/null
+++ b/src/arch/arm/reg/id.rs
@@ -0,0 +1,36 @@
+use crate::arch::RegId;
+
+/// 32-bit ARM core register identifier.
+#[derive(Debug, Clone, Copy)]
+#[non_exhaustive]
+pub enum ArmCoreRegId {
+    /// General purpose registers (R0-R12)
+    Gpr(u8),
+    /// Stack Pointer (R13)
+    Sp,
+    /// Link Register (R14)
+    Lr,
+    /// Program Counter (R15)
+    Pc,
+    /// Floating point registers (F0-F7)
+    Fpr(u8),
+    /// Floating point status
+    Fps,
+    /// Current Program Status Register (cpsr)
+    Cpsr,
+}
+
+impl RegId for ArmCoreRegId {
+    fn from_raw_id(id: usize) -> Option<(Self, usize)> {
+        let reg = match id {
+            0..=12 => Self::Gpr(id as u8),
+            13 => Self::Sp,
+            14 => Self::Lr,
+            15 => Self::Pc,
+            16..=23 => Self::Fpr((id as u8) - 16),
+            25 => Self::Cpsr,
+            _ => return None,
+        };
+        Some((reg, 4))
+    }
+}
diff --git a/src/arch/arm/reg/mod.rs b/src/arch/arm/reg/mod.rs
new file mode 100644
index 0000000..8f5a8be
--- /dev/null
+++ b/src/arch/arm/reg/mod.rs
@@ -0,0 +1,8 @@
+//! `Register` structs for various ARM architectures.
+
+/// `RegId` definitions for ARM architectures.
+pub mod id;
+
+mod arm_core;
+
+pub use arm_core::ArmCoreRegs;
diff --git a/src/arch/mips/mod.rs b/src/arch/mips/mod.rs
new file mode 100644
index 0000000..2bd8362
--- /dev/null
+++ b/src/arch/mips/mod.rs
@@ -0,0 +1,74 @@
+//! Implementations for the MIPS architecture.
+
+use crate::arch::Arch;
+use crate::arch::RegId;
+
+pub mod reg;
+
+/// Implements `Arch` for 32-bit MIPS.
+///
+/// Check out the [module level docs](../index.html#whats-with-regidimpl) for
+/// more info about the `RegIdImpl` type parameter.
+pub enum Mips<RegIdImpl: RegId = reg::id::MipsRegId<u32>> {
+    #[doc(hidden)]
+    _Marker(core::marker::PhantomData<RegIdImpl>),
+}
+
+/// Implements `Arch` for 64-bit MIPS.
+///
+/// Check out the [module level docs](../index.html#whats-with-regidimpl) for
+/// more info about the `RegIdImpl` type parameter.
+pub enum Mips64<RegIdImpl: RegId = reg::id::MipsRegId<u64>> {
+    #[doc(hidden)]
+    _Marker(core::marker::PhantomData<RegIdImpl>),
+}
+
+/// Implements `Arch` for 32-bit MIPS with the DSP feature enabled.
+pub enum MipsWithDsp {}
+
+/// Implements `Arch` for 64-bit MIPS with the DSP feature enabled.
+pub enum Mips64WithDsp {}
+
+impl<RegIdImpl: RegId> Arch for Mips<RegIdImpl> {
+    type Usize = u32;
+    type Registers = reg::MipsCoreRegs<u32>;
+    type RegId = RegIdImpl;
+
+    fn target_description_xml() -> Option<&'static str> {
+        Some(r#"<target version="1.0"><architecture>mips</architecture></target>"#)
+    }
+}
+
+impl<RegIdImpl: RegId> Arch for Mips64<RegIdImpl> {
+    type Usize = u64;
+    type Registers = reg::MipsCoreRegs<u64>;
+    type RegId = RegIdImpl;
+
+    fn target_description_xml() -> Option<&'static str> {
+        Some(r#"<target version="1.0"><architecture>mips64</architecture></target>"#)
+    }
+}
+
+impl Arch for MipsWithDsp {
+    type Usize = u32;
+    type Registers = reg::MipsCoreRegsWithDsp<u32>;
+    type RegId = reg::id::MipsRegId<u32>;
+
+    fn target_description_xml() -> Option<&'static str> {
+        Some(
+            r#"<target version="1.0"><architecture>mips</architecture><feature name="org.gnu.gdb.mips.dsp"></feature></target>"#,
+        )
+    }
+}
+
+impl Arch for Mips64WithDsp {
+    type Usize = u64;
+    type Registers = reg::MipsCoreRegsWithDsp<u64>;
+    type RegId = reg::id::MipsRegId<u64>;
+
+    fn target_description_xml() -> Option<&'static str> {
+        Some(
+            r#"<target version="1.0"><architecture>mips64</architecture><feature name="org.gnu.gdb.mips.dsp"></feature></target>"#,
+        )
+    }
+}
diff --git a/src/arch/mips/reg/id.rs b/src/arch/mips/reg/id.rs
new file mode 100644
index 0000000..424cb29
--- /dev/null
+++ b/src/arch/mips/reg/id.rs
@@ -0,0 +1,129 @@
+use crate::arch::RegId;
+
+/// MIPS register identifier.
+#[derive(Debug, Clone, Copy)]
+#[non_exhaustive]
+pub enum MipsRegId<U> {
+    /// General purpose registers (R0-R31)
+    Gpr(u8),
+    /// Status register
+    Status,
+    /// Low register
+    Lo,
+    /// High register
+    Hi,
+    /// Bad Virtual Address register
+    Badvaddr,
+    /// Exception Cause register
+    Cause,
+    /// Program Counter
+    Pc,
+    /// Floating point registers (F0-F31)
+    Fpr(u8),
+    /// Floating-point Control Status register
+    Fcsr,
+    /// Floating-point Implementation Register
+    Fir,
+    /// High 1 register
+    Hi1,
+    /// Low 1 register
+    Lo1,
+    /// High 2 register
+    Hi2,
+    /// Low 2 register
+    Lo2,
+    /// High 3 register
+    Hi3,
+    /// Low 3 register
+    Lo3,
+    /// DSP Control register
+    Dspctl,
+    /// Restart register
+    Restart,
+    #[doc(hidden)]
+    _Size(U),
+}
+
+fn from_raw_id<U>(id: usize) -> Option<(MipsRegId<U>, usize)> {
+    let reg = match id {
+        0..=31 => MipsRegId::Gpr(id as u8),
+        32 => MipsRegId::Status,
+        33 => MipsRegId::Lo,
+        34 => MipsRegId::Hi,
+        35 => MipsRegId::Badvaddr,
+        36 => MipsRegId::Cause,
+        37 => MipsRegId::Pc,
+        38..=69 => MipsRegId::Fpr((id as u8) - 38),
+        70 => MipsRegId::Fcsr,
+        71 => MipsRegId::Fir,
+        72 => MipsRegId::Hi1,
+        73 => MipsRegId::Lo1,
+        74 => MipsRegId::Hi2,
+        75 => MipsRegId::Lo2,
+        76 => MipsRegId::Hi3,
+        77 => MipsRegId::Lo3,
+        // `MipsRegId::Dspctl` is the only register that will always be 4 bytes wide
+        78 => return Some((MipsRegId::Dspctl, 4)),
+        79 => MipsRegId::Restart,
+        _ => return None,
+    };
+
+    let ptrsize = core::mem::size_of::<U>();
+    Some((reg, ptrsize))
+}
+
+impl RegId for MipsRegId<u32> {
+    fn from_raw_id(id: usize) -> Option<(Self, usize)> {
+        from_raw_id::<u32>(id)
+    }
+}
+
+impl RegId for MipsRegId<u64> {
+    fn from_raw_id(id: usize) -> Option<(Self, usize)> {
+        from_raw_id::<u64>(id)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::arch::traits::RegId;
+    use crate::arch::traits::Registers;
+
+    fn test<Rs: Registers, RId: RegId>() {
+        // Obtain the data length written by `gdb_serialize` by passing a custom
+        // closure.
+        let mut serialized_data_len = 0;
+        let counter = |b: Option<u8>| {
+            if b.is_some() {
+                serialized_data_len += 1;
+            }
+        };
+        Rs::default().gdb_serialize(counter);
+
+        // Accumulate register sizes returned by `from_raw_id`.
+        let mut i = 0;
+        let mut sum_reg_sizes = 0;
+        while let Some((_, size)) = RId::from_raw_id(i) {
+            sum_reg_sizes += size;
+            i += 1;
+        }
+
+        assert_eq!(serialized_data_len, sum_reg_sizes);
+    }
+
+    #[test]
+    fn test_mips32() {
+        test::<
+            crate::arch::mips::reg::MipsCoreRegsWithDsp<u32>,
+            crate::arch::mips::reg::id::MipsRegId<u32>,
+        >()
+    }
+
+    #[test]
+    fn test_mips64() {
+        test::<
+            crate::arch::mips::reg::MipsCoreRegsWithDsp<u64>,
+            crate::arch::mips::reg::id::MipsRegId<u64>,
+        >()
+    }
+}
diff --git a/src/arch/mips/reg/mips.rs b/src/arch/mips/reg/mips.rs
new file mode 100644
index 0000000..6d86b43
--- /dev/null
+++ b/src/arch/mips/reg/mips.rs
@@ -0,0 +1,259 @@
+use core::convert::TryInto;
+
+use num_traits::PrimInt;
+
+use crate::arch::Registers;
+use crate::internal::LeBytes;
+
+/// MIPS registers.
+///
+/// The register width is set to `u32` or `u64` based on the `<U>` type.
+///
+/// Source: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/mips-cpu.xml
+#[derive(Debug, Default, Clone, Eq, PartialEq)]
+pub struct MipsCoreRegs<U> {
+    /// General purpose registers (R0-R31)
+    pub r: [U; 32],
+    /// Low register (regnum 33)
+    pub lo: U,
+    /// High register (regnum 34)
+    pub hi: U,
+    /// Program Counter (regnum 37)
+    pub pc: U,
+    /// CP0 registers
+    pub cp0: MipsCp0Regs<U>,
+    /// FPU registers
+    pub fpu: MipsFpuRegs<U>,
+}
+
+/// MIPS CP0 (coprocessor 0) registers.
+///
+/// Source: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/mips-cp0.xml
+#[derive(Debug, Default, Clone, Eq, PartialEq)]
+pub struct MipsCp0Regs<U> {
+    /// Status register (regnum 32)
+    pub status: U,
+    /// Bad Virtual Address register (regnum 35)
+    pub badvaddr: U,
+    /// Exception Cause register (regnum 36)
+    pub cause: U,
+}
+
+/// MIPS FPU registers.
+///
+/// Source: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/mips-fpu.xml
+#[derive(Debug, Default, Clone, Eq, PartialEq)]
+pub struct MipsFpuRegs<U> {
+    /// FP registers (F0-F31) starting at regnum 38
+    pub r: [U; 32],
+    /// Floating-point Control Status register
+    pub fcsr: U,
+    /// Floating-point Implementation Register
+    pub fir: U,
+}
+
+/// MIPS DSP registers.
+///
+/// Source: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/mips-dsp.xml
+#[derive(Debug, Default, Clone, Eq, PartialEq)]
+pub struct MipsDspRegs<U> {
+    /// High 1 register (regnum 72)
+    pub hi1: U,
+    /// Low 1 register (regnum 73)
+    pub lo1: U,
+    /// High 2 register (regnum 74)
+    pub hi2: U,
+    /// Low 2 register (regnum 75)
+    pub lo2: U,
+    /// High 3 register (regnum 76)
+    pub hi3: U,
+    /// Low 3 register (regnum 77)
+    pub lo3: U,
+    /// DSP Control register (regnum 78)
+    /// Note: This register will always be 32-bit regardless of the target
+    /// https://sourceware.org/gdb/current/onlinedocs/gdb/MIPS-Features.html#MIPS-Features
+    pub dspctl: u32,
+    /// Restart register (regnum 79)
+    pub restart: U,
+}
+
+/// MIPS core and DSP registers.
+///
+/// Source: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/mips-dsp-linux.xml
+#[derive(Debug, Default, Clone, Eq, PartialEq)]
+pub struct MipsCoreRegsWithDsp<U> {
+    /// Core registers
+    pub core: MipsCoreRegs<U>,
+    /// DSP registers
+    pub dsp: MipsDspRegs<U>,
+}
+
+impl<U> Registers for MipsCoreRegs<U>
+where
+    U: PrimInt + LeBytes + Default + core::fmt::Debug,
+{
+    fn gdb_serialize(&self, mut write_byte: impl FnMut(Option<u8>)) {
+        macro_rules! write_le_bytes {
+            ($value:expr) => {
+                let mut buf = [0; 16];
+                // infallible (unless digit is a >128 bit number)
+                let len = $value.to_le_bytes(&mut buf).unwrap();
+                let buf = &buf[..len];
+                for b in buf {
+                    write_byte(Some(*b));
+                }
+            };
+        }
+
+        // Write GPRs
+        for reg in self.r.iter() {
+            write_le_bytes!(reg);
+        }
+
+        // Status register is regnum 32
+        write_le_bytes!(&self.cp0.status);
+
+        // Low and high registers are regnums 33 and 34
+        write_le_bytes!(&self.lo);
+        write_le_bytes!(&self.hi);
+
+        // Badvaddr and Cause registers are regnums 35 and 36
+        write_le_bytes!(&self.cp0.badvaddr);
+        write_le_bytes!(&self.cp0.cause);
+
+        // Program Counter is regnum 37
+        write_le_bytes!(&self.pc);
+
+        // Write FPRs
+        for reg in self.fpu.r.iter() {
+            write_le_bytes!(&reg);
+        }
+
+        // Write FCSR and FIR registers
+        write_le_bytes!(&self.fpu.fcsr);
+        write_le_bytes!(&self.fpu.fir);
+    }
+
+    fn gdb_deserialize(&mut self, bytes: &[u8]) -> Result<(), ()> {
+        let ptrsize = core::mem::size_of::<U>();
+
+        // Ensure bytes contains enough data for all 72 registers
+        if bytes.len() < ptrsize * 72 {
+            return Err(());
+        }
+
+        // All core registers are the same size
+        let mut regs = bytes
+            .chunks_exact(ptrsize)
+            .map(|c| U::from_le_bytes(c).unwrap());
+
+        // Read GPRs
+        for reg in self.r.iter_mut() {
+            *reg = regs.next().ok_or(())?
+        }
+
+        // Read Status register
+        self.cp0.status = regs.next().ok_or(())?;
+
+        // Read Low and High registers
+        self.lo = regs.next().ok_or(())?;
+        self.hi = regs.next().ok_or(())?;
+
+        // Read Badvaddr and Cause registers
+        self.cp0.badvaddr = regs.next().ok_or(())?;
+        self.cp0.cause = regs.next().ok_or(())?;
+
+        // Read the Program Counter
+        self.pc = regs.next().ok_or(())?;
+
+        // Read FPRs
+        for reg in self.fpu.r.iter_mut() {
+            *reg = regs.next().ok_or(())?
+        }
+
+        // Read FCSR and FIR registers
+        self.fpu.fcsr = regs.next().ok_or(())?;
+        self.fpu.fir = regs.next().ok_or(())?;
+
+        Ok(())
+    }
+}
+
+impl<U> Registers for MipsCoreRegsWithDsp<U>
+where
+    U: PrimInt + LeBytes + Default + core::fmt::Debug,
+{
+    fn gdb_serialize(&self, mut write_byte: impl FnMut(Option<u8>)) {
+        macro_rules! write_le_bytes {
+            ($value:expr) => {
+                let mut buf = [0; 16];
+                // infallible (unless digit is a >128 bit number)
+                let len = $value.to_le_bytes(&mut buf).unwrap();
+                let buf = &buf[..len];
+                for b in buf {
+                    write_byte(Some(*b));
+                }
+            };
+        }
+
+        // Serialize the core registers first
+        self.core.gdb_serialize(&mut write_byte);
+
+        // Write the DSP registers
+        write_le_bytes!(&self.dsp.hi1);
+        write_le_bytes!(&self.dsp.lo1);
+        write_le_bytes!(&self.dsp.hi2);
+        write_le_bytes!(&self.dsp.lo2);
+        write_le_bytes!(&self.dsp.hi3);
+        write_le_bytes!(&self.dsp.lo3);
+
+        for b in &self.dsp.dspctl.to_le_bytes() {
+            write_byte(Some(*b));
+        }
+
+        write_le_bytes!(&self.dsp.restart);
+    }
+
+    fn gdb_deserialize(&mut self, bytes: &[u8]) -> Result<(), ()> {
+        // Deserialize the core registers first
+        self.core.gdb_deserialize(bytes)?;
+
+        // Ensure bytes contains enough data for all 79 registers of target-width
+        // and the dspctl register which is always 4 bytes
+        let ptrsize = core::mem::size_of::<U>();
+        if bytes.len() < (ptrsize * 79) + 4 {
+            return Err(());
+        }
+
+        // Calculate the offsets to the DSP registers based on the ptrsize
+        let dspregs_start = ptrsize * 72;
+        let dspctl_start = ptrsize * 78;
+
+        // Read up until the dspctl register
+        let mut regs = bytes[dspregs_start..dspctl_start]
+            .chunks_exact(ptrsize)
+            .map(|c| U::from_le_bytes(c).unwrap());
+
+        self.dsp.hi1 = regs.next().ok_or(())?;
+        self.dsp.lo1 = regs.next().ok_or(())?;
+        self.dsp.hi2 = regs.next().ok_or(())?;
+        self.dsp.lo2 = regs.next().ok_or(())?;
+        self.dsp.hi3 = regs.next().ok_or(())?;
+        self.dsp.lo3 = regs.next().ok_or(())?;
+
+        // Dspctl will always be a u32
+        self.dsp.dspctl =
+            u32::from_le_bytes(bytes[dspctl_start..dspctl_start + 4].try_into().unwrap());
+
+        // Only 4 or 8 bytes should remain to be read
+        self.dsp.restart = U::from_le_bytes(
+            bytes[dspctl_start + 4..]
+                .chunks_exact(ptrsize)
+                .next()
+                .ok_or(())?,
+        )
+        .unwrap();
+
+        Ok(())
+    }
+}
diff --git a/src/arch/mips/reg/mod.rs b/src/arch/mips/reg/mod.rs
new file mode 100644
index 0000000..3cafbd1
--- /dev/null
+++ b/src/arch/mips/reg/mod.rs
@@ -0,0 +1,11 @@
+//! `Register` structs for MIPS architectures.
+
+/// `RegId` definitions for MIPS architectures.
+pub mod id;
+
+mod mips;
+
+pub use mips::MipsCoreRegs;
+pub use mips::MipsCoreRegsWithDsp;
+pub use mips::MipsCp0Regs;
+pub use mips::MipsFpuRegs;
diff --git a/src/arch/mod.rs b/src/arch/mod.rs
new file mode 100644
index 0000000..a37bcea
--- /dev/null
+++ b/src/arch/mod.rs
@@ -0,0 +1,60 @@
+//! Built-in implementations of [`Arch`] for various architectures.
+//!
+//! _Note:_ If an architecture is missing from this module, that does _not_ mean
+//! that it can't be used with `gdbstub`! So-long as there's support for the
+//! target architecture in GDB, it should be fairly straightforward to implement
+//! `Arch` manually.
+//!
+//! Please consider upstreaming any missing `Arch` implementations you happen to
+//! implement yourself! Aside from the altruistic motive of improving `gdbstub`,
+//! upstreaming your `Arch` implementation will ensure that it's kept up-to-date
+//! with any future breaking API changes.
+//!
+//! **Disclaimer:** These implementations are all community contributions, and
+//! while they are tested (by the PR's author) and code-reviewed, it's not
+//! particularly feasible to write detailed tests for each architecture! If you
+//! spot a bug in any of the implementations, please file an issue / open a PR!
+//!
+//! # What's with `RegIdImpl`?
+//!
+//! Supporting the `Target::read/write_register` API required introducing a new
+//! [`RegId`] trait + [`Arch::RegId`] associated type. `RegId` is used by
+//! `gdbstub` to translate raw GDB register ids (a protocol level arch-dependent
+//! `usize`) into human-readable enum variants.
+//!
+//! Unfortunately, this API was added after several contributors had already
+//! upstreamed their `Arch` implementations, and as a result, there are several
+//! built-in arch implementations which are missing proper `RegId` enums
+//! (tracked under [issue #29](https://github.com/daniel5151/gdbstub/issues/29)).
+//!
+//! As a stop-gap measure, affected `Arch` implementations have been modified to
+//! accept a `RegIdImpl` type parameter, which requires users to manually
+//! specify a `RegId` implementation.
+//!
+//! If you're not interested in implementing the `Target::read/write_register`
+//! methods and just want to get up-and-running with `gdbstub`, it's fine to
+//! set `RegIdImpl` to `()` and use the built-in stubbed `impl RegId for ()`.
+//!
+//! A better approach would be to implement (and hopefully upstream!) a proper
+//! `RegId` enum. While this will require doing a bit of digging through the GDB
+//! docs + [architecture XML definitions](https://github.com/bminor/binutils-gdb/tree/master/gdb/features/),
+//! it's not too tricky to get a working implementation up and running, and
+//! makes it possible to safely and efficiently implement the
+//! `Target::read/write_register` API. As an example, check out
+//! [`ArmCoreRegId`](arm/reg/id/enum.ArmCoreRegId.html#impl-RegId).
+//!
+//! Whenever a `RegId` enum is upstreamed, the associated `Arch`'s `RegIdImpl`
+//! parameter will be defaulted to the newly added enum. This will simplify the
+//! API without requiring an explicit breaking API change. Once all `RegIdImpl`
+//! have a default implementation, only a single breaking API change will be
+//! required to remove `RegIdImpl` entirely (along with this documentation).
+
+pub mod arm;
+pub mod mips;
+pub mod msp430;
+pub mod ppc;
+pub mod riscv;
+pub mod x86;
+
+mod traits;
+pub use traits::*;
diff --git a/src/arch/msp430/mod.rs b/src/arch/msp430/mod.rs
new file mode 100644
index 0000000..9baf44d
--- /dev/null
+++ b/src/arch/msp430/mod.rs
@@ -0,0 +1,25 @@
+//! Implementations for the TI-MSP430 family of MCUs.
+
+use crate::arch::Arch;
+use crate::arch::RegId;
+
+pub mod reg;
+
+/// Implements `Arch` for standard 16-bit TI-MSP430 MCUs.
+///
+/// Check out the [module level docs](../index.html#whats-with-regidimpl) for
+/// more info about the `RegIdImpl` type parameter.
+pub enum Msp430<RegIdImpl: RegId = reg::id::Msp430RegId> {
+    #[doc(hidden)]
+    _Marker(core::marker::PhantomData<RegIdImpl>),
+}
+
+impl<RegIdImpl: RegId> Arch for Msp430<RegIdImpl> {
+    type Usize = u32;
+    type Registers = reg::Msp430Regs;
+    type RegId = RegIdImpl;
+
+    fn target_description_xml() -> Option<&'static str> {
+        Some(r#"<target version="1.0"><architecture>msp430</architecture></target>"#)
+    }
+}
diff --git a/src/arch/msp430/reg/id.rs b/src/arch/msp430/reg/id.rs
new file mode 100644
index 0000000..eec7062
--- /dev/null
+++ b/src/arch/msp430/reg/id.rs
@@ -0,0 +1,71 @@
+use crate::arch::RegId;
+
+/// TI-MSP430 register identifier.
+///
+/// GDB does not provide a XML file for the MSP430.
+/// The best file to reference is [msp430-tdep.c](https://github.com/bminor/binutils-gdb/blob/master/gdb/msp430-tdep.c).
+#[derive(Debug, Clone, Copy)]
+#[non_exhaustive]
+pub enum Msp430RegId {
+    /// Program Counter (R0)
+    Pc,
+    /// Stack Pointer (R1)
+    Sp,
+    /// Status Register (R2)
+    Sr,
+    /// Constant Generator (R3)
+    Cg,
+    /// General Purpose Registers (R4-R15)
+    Gpr(u8),
+}
+
+impl RegId for Msp430RegId {
+    fn from_raw_id(id: usize) -> Option<(Self, usize)> {
+        let reg = match id {
+            0 => Self::Pc,
+            1 => Self::Sp,
+            2 => Self::Sr,
+            3 => Self::Cg,
+            4..=15 => Self::Gpr((id as u8) - 4),
+            _ => return None,
+        };
+        Some((reg, 2))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::arch::traits::RegId;
+    use crate::arch::traits::Registers;
+
+    fn test<Rs: Registers, RId: RegId>() {
+        // Obtain the data length written by `gdb_serialize` by passing a custom
+        // closure.
+        let mut serialized_data_len = 0;
+        let counter = |b: Option<u8>| {
+            if b.is_some() {
+                serialized_data_len += 1;
+            }
+        };
+        Rs::default().gdb_serialize(counter);
+
+        // The `Msp430Regs` implementation does not increment the size for
+        // the CG register since it will always be the constant zero.
+        serialized_data_len += 4;
+
+        // Accumulate register sizes returned by `from_raw_id`.
+        let mut i = 0;
+        let mut sum_reg_sizes = 0;
+        while let Some((_, size)) = RId::from_raw_id(i) {
+            sum_reg_sizes += size;
+            i += 1;
+        }
+
+        assert_eq!(serialized_data_len, sum_reg_sizes);
+    }
+
+    #[test]
+    fn test_msp430() {
+        test::<crate::arch::msp430::reg::Msp430Regs, crate::arch::msp430::reg::id::Msp430RegId>()
+    }
+}
diff --git a/src/arch/msp430/reg/mod.rs b/src/arch/msp430/reg/mod.rs
new file mode 100644
index 0000000..2b1285b
--- /dev/null
+++ b/src/arch/msp430/reg/mod.rs
@@ -0,0 +1,8 @@
+//! `Register` structs for various TI-MSP430 CPUs.
+
+/// `RegId` definitions for various TI-MSP430 CPUs.
+pub mod id;
+
+mod msp430;
+
+pub use msp430::Msp430Regs;
diff --git a/src/arch/msp430/reg/msp430.rs b/src/arch/msp430/reg/msp430.rs
new file mode 100644
index 0000000..456fb96
--- /dev/null
+++ b/src/arch/msp430/reg/msp430.rs
@@ -0,0 +1,65 @@
+use crate::arch::Registers;
+
+/// 16-bit TI-MSP430 registers.
+#[derive(Debug, Default, Clone, Eq, PartialEq)]
+pub struct Msp430Regs {
+    /// Program Counter (R0)
+    pub pc: u16,
+    /// Stack Pointer (R1)
+    pub sp: u16,
+    /// Status Register (R2)
+    pub sr: u16,
+    /// General Purpose Registers (R4-R15)
+    pub r: [u16; 11],
+}
+
+impl Registers for Msp430Regs {
+    fn gdb_serialize(&self, mut write_byte: impl FnMut(Option<u8>)) {
+        macro_rules! write_bytes {
+            ($bytes:expr) => {
+                for b in $bytes {
+                    write_byte(Some(*b))
+                }
+            };
+        }
+
+        write_bytes!(&self.pc.to_le_bytes());
+        write_bytes!(&self.sp.to_le_bytes());
+        write_bytes!(&self.sr.to_le_bytes());
+        (0..4).for_each(|_| write_byte(None)); // Constant Generator (CG/R3)
+        for reg in self.r.iter() {
+            write_bytes!(&reg.to_le_bytes());
+        }
+    }
+
+    fn gdb_deserialize(&mut self, bytes: &[u8]) -> Result<(), ()> {
+        // ensure bytes.chunks_exact(2) won't panic
+        if bytes.len() % 2 != 0 {
+            return Err(());
+        }
+
+        use core::convert::TryInto;
+        let mut regs = bytes
+            .chunks_exact(2)
+            .map(|c| u16::from_le_bytes(c.try_into().unwrap()));
+
+        self.pc = regs.next().ok_or(())?;
+        self.sp = regs.next().ok_or(())?;
+        self.sr = regs.next().ok_or(())?;
+
+        // Constant Generator (CG/R3) should always be 0
+        if regs.next().ok_or(())? != 0 {
+            return Err(());
+        }
+
+        for reg in self.r.iter_mut() {
+            *reg = regs.next().ok_or(())?
+        }
+
+        if regs.next().is_some() {
+            return Err(());
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/arch/ppc/mod.rs b/src/arch/ppc/mod.rs
new file mode 100644
index 0000000..bdb9465
--- /dev/null
+++ b/src/arch/ppc/mod.rs
@@ -0,0 +1,27 @@
+//! Implementations for various PowerPC architectures.
+
+use crate::arch::Arch;
+use crate::arch::RegId;
+
+pub mod reg;
+
+/// Implements `Arch` for 32-bit PowerPC + AltiVec SIMD.
+///
+/// Check out the [module level docs](../index.html#whats-with-regidimpl) for
+/// more info about the `RegIdImpl` type parameter.
+pub enum PowerPcAltivec32<RegIdImpl: RegId> {
+    #[doc(hidden)]
+    _Marker(core::marker::PhantomData<RegIdImpl>),
+}
+
+impl<RegIdImpl: RegId> Arch for PowerPcAltivec32<RegIdImpl> {
+    type Usize = u32;
+    type Registers = reg::PowerPcCommonRegs;
+    type RegId = RegIdImpl;
+
+    fn target_description_xml() -> Option<&'static str> {
+        Some(
+            r#"<target version="1.0"><architecture>powerpc:common</architecture><feature name="org.gnu.gdb.power.core"></feature><feature name="org.gnu.gdb.power.fpu"></feature><feature name="org.gnu.gdb.power.altivec"></feature></target>"#,
+        )
+    }
+}
diff --git a/src/arch/ppc/reg/common.rs b/src/arch/ppc/reg/common.rs
new file mode 100644
index 0000000..8936e3a
--- /dev/null
+++ b/src/arch/ppc/reg/common.rs
@@ -0,0 +1,159 @@
+use crate::arch::ppc::reg::PpcVector;
+use crate::arch::Registers;
+
+use core::convert::TryInto;
+
+/// 32-bit PowerPC core registers, FPU registers, and AltiVec SIMD registers.
+///
+/// Sources:
+/// * https://github.com/bminor/binutils-gdb/blob/master/gdb/features/rs6000/powerpc-altivec32.xml
+/// * https://github.com/bminor/binutils-gdb/blob/master/gdb/features/rs6000/power-core.xml
+/// * https://github.com/bminor/binutils-gdb/blob/master/gdb/features/rs6000/power-fpu.xml
+/// * https://github.com/bminor/binutils-gdb/blob/master/gdb/features/rs6000/power-altivec.xml
+#[derive(Debug, Default, Clone, PartialEq)]
+pub struct PowerPcCommonRegs {
+    /// General purpose registers
+    pub r: [u32; 32],
+    /// Floating Point registers
+    pub f: [f64; 32],
+    /// Program counter
+    pub pc: u32,
+    /// Machine state
+    pub msr: u32,
+    /// Condition register
+    pub cr: u32,
+    /// Link register
+    pub lr: u32,
+    /// Count register
+    pub ctr: u32,
+    /// Integer exception register
+    pub xer: u32,
+    /// Floating-point status and control register
+    pub fpscr: u32,
+    /// Vector registers
+    pub vr: [PpcVector; 32],
+    /// Vector status and control register
+    pub vscr: u32,
+    /// Vector context save register
+    pub vrsave: u32,
+}
+
+impl Registers for PowerPcCommonRegs {
+    fn gdb_serialize(&self, mut write_byte: impl FnMut(Option<u8>)) {
+        macro_rules! write_bytes {
+            ($bytes:expr) => {
+                for b in $bytes {
+                    write_byte(Some(*b))
+                }
+            };
+        }
+
+        macro_rules! write_regs {
+            ($($reg:ident),*) => {
+                $(
+                    write_bytes!(&self.$reg.to_be_bytes());
+                )*
+            }
+        }
+
+        for reg in &self.r {
+            write_bytes!(&reg.to_be_bytes());
+        }
+
+        for reg in &self.f {
+            write_bytes!(&reg.to_be_bytes());
+        }
+
+        write_regs!(pc, msr, cr, lr, ctr, xer, fpscr);
+
+        for &reg in &self.vr {
+            let reg: u128 = reg;
+            write_bytes!(&reg.to_be_bytes());
+        }
+
+        write_regs!(vscr, vrsave);
+    }
+
+    fn gdb_deserialize(&mut self, bytes: &[u8]) -> Result<(), ()> {
+        if bytes.len() < 0x3a4 {
+            return Err(());
+        }
+
+        let mut regs = bytes[0..0x80]
+            .chunks_exact(4)
+            .map(|x| u32::from_be_bytes(x.try_into().unwrap()));
+
+        for reg in &mut self.r {
+            *reg = regs.next().ok_or(())?;
+        }
+
+        let mut regs = bytes[0x80..0x180]
+            .chunks_exact(8)
+            .map(|x| f64::from_be_bytes(x.try_into().unwrap()));
+
+        for reg in &mut self.f {
+            *reg = regs.next().ok_or(())?;
+        }
+
+        macro_rules! parse_regs {
+            ($start:literal..$end:literal, $($reg:ident),*) => {
+                let mut regs = bytes[$start..$end]
+                    .chunks_exact(4)
+                    .map(|x| u32::from_be_bytes(x.try_into().unwrap()));
+                $(
+                    self.$reg = regs.next().ok_or(())?;
+                )*
+            }
+        }
+
+        parse_regs!(0x180..0x19c, pc, msr, cr, lr, ctr, xer, fpscr);
+
+        let mut regs = bytes[0x19c..0x39c]
+            .chunks_exact(0x10)
+            .map(|x| u128::from_be_bytes(x.try_into().unwrap()));
+
+        for reg in &mut self.vr {
+            *reg = regs.next().ok_or(())?;
+        }
+
+        parse_regs!(0x39c..0x3a4, vscr, vrsave);
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn ppc_core_round_trip() {
+        let regs_before = PowerPcCommonRegs {
+            r: [1; 32],
+            pc: 2,
+            msr: 3,
+            cr: 4,
+            lr: 5,
+            ctr: 6,
+            xer: 7,
+            fpscr: 8,
+            f: [9.0; 32],
+            vr: [52; 32],
+            vrsave: 10,
+            vscr: 11,
+        };
+
+        let mut data = vec![];
+
+        regs_before.gdb_serialize(|x| {
+            data.push(x.unwrap_or(b'x'));
+        });
+
+        assert_eq!(data.len(), 0x3a4);
+
+        let mut regs_after = PowerPcCommonRegs::default();
+        regs_after.gdb_deserialize(&data).unwrap();
+
+        assert_eq!(regs_before, regs_after);
+    }
+}
diff --git a/src/arch/ppc/reg/id.rs b/src/arch/ppc/reg/id.rs
new file mode 100644
index 0000000..97ccfea
--- /dev/null
+++ b/src/arch/ppc/reg/id.rs
@@ -0,0 +1,2 @@
+// TODO: Add proper `RegId` implementation. See [issue #29](https://github.com/daniel5151/gdbstub/issues/29)
+// pub enum PowerPc32RegId {}
diff --git a/src/arch/ppc/reg/mod.rs b/src/arch/ppc/reg/mod.rs
new file mode 100644
index 0000000..fde8e55
--- /dev/null
+++ b/src/arch/ppc/reg/mod.rs
@@ -0,0 +1,9 @@
+//! `Register` structs for PowerPC architectures
+
+/// `RegId` definitions for PowerPC architectures.
+pub mod id;
+
+mod common;
+
+pub use common::PowerPcCommonRegs;
+type PpcVector = u128;
diff --git a/src/arch/riscv/mod.rs b/src/arch/riscv/mod.rs
new file mode 100644
index 0000000..29dde28
--- /dev/null
+++ b/src/arch/riscv/mod.rs
@@ -0,0 +1,33 @@
+//! Implementations for the [RISC-V](https://riscv.org/) architecture.
+//!
+//! *Note*: currently only supports integer versions of the ISA.
+
+use crate::arch::Arch;
+
+pub mod reg;
+
+/// Implements `Arch` for 32-bit RISC-V.
+pub enum Riscv32 {}
+
+/// Implements `Arch` for 64-bit RISC-V.
+pub enum Riscv64 {}
+
+impl Arch for Riscv32 {
+    type Usize = u32;
+    type Registers = reg::RiscvCoreRegs<u32>;
+    type RegId = reg::id::RiscvRegId;
+
+    fn target_description_xml() -> Option<&'static str> {
+        Some(r#"<target version="1.0"><architecture>riscv</architecture></target>"#)
+    }
+}
+
+impl Arch for Riscv64 {
+    type Usize = u64;
+    type Registers = reg::RiscvCoreRegs<u64>;
+    type RegId = reg::id::RiscvRegId;
+
+    fn target_description_xml() -> Option<&'static str> {
+        Some(r#"<target version="1.0"><architecture>riscv64</architecture></target>"#)
+    }
+}
diff --git a/src/arch/riscv/reg/id.rs b/src/arch/riscv/reg/id.rs
new file mode 100644
index 0000000..9430d2e
--- /dev/null
+++ b/src/arch/riscv/reg/id.rs
@@ -0,0 +1,31 @@
+use crate::arch::RegId;
+
+/// RISC-V Register identifier.
+#[derive(Debug, Clone, Copy)]
+#[non_exhaustive]
+pub enum RiscvRegId {
+    /// General Purpose Register (x0-x31).
+    Gpr(u8),
+    /// Floating Point Register (f0-f31).
+    Fpr(u8),
+    /// Program Counter.
+    Pc,
+    /// Control and Status Register.
+    Csr(u16),
+    /// Privilege level.
+    Priv,
+}
+
+impl RegId for RiscvRegId {
+    fn from_raw_id(id: usize) -> Option<(Self, usize)> {
+        let reg_size = match id {
+            0..=31 => (Self::Gpr(id as u8), 4),
+            32 => (Self::Pc, 4),
+            33..=64 => (Self::Fpr((id - 33) as u8), 4),
+            65..=4160 => (Self::Csr((id - 65) as u16), 4),
+            4161 => (Self::Priv, 1),
+            _ => return None,
+        };
+        Some(reg_size)
+    }
+}
diff --git a/src/arch/riscv/reg/mod.rs b/src/arch/riscv/reg/mod.rs
new file mode 100644
index 0000000..e501c47
--- /dev/null
+++ b/src/arch/riscv/reg/mod.rs
@@ -0,0 +1,8 @@
+//! `Register` structs for RISC-V architectures.
+
+/// `RegId` definitions for RISC-V architectures.
+pub mod id;
+
+mod riscv;
+
+pub use riscv::RiscvCoreRegs;
diff --git a/src/arch/riscv/reg/riscv.rs b/src/arch/riscv/reg/riscv.rs
new file mode 100644
index 0000000..1dfb465
--- /dev/null
+++ b/src/arch/riscv/reg/riscv.rs
@@ -0,0 +1,71 @@
+use num_traits::PrimInt;
+
+use crate::arch::Registers;
+use crate::internal::LeBytes;
+
+/// RISC-V Integer registers.
+///
+/// The register width is set to `u32` or `u64` based on the `<U>` type.
+///
+/// Useful links:
+/// * [GNU binutils-gdb XML descriptions](https://github.com/bminor/binutils-gdb/blob/master/gdb/features/riscv)
+/// * [riscv-tdep.h](https://github.com/bminor/binutils-gdb/blob/master/gdb/riscv-tdep.h)
+#[derive(Debug, Default, Clone, PartialEq)]
+pub struct RiscvCoreRegs<U> {
+    /// General purpose registers (x0-x31)
+    pub x: [U; 32],
+    /// Program counter
+    pub pc: U,
+}
+
+impl<U> Registers for RiscvCoreRegs<U>
+where
+    U: PrimInt + LeBytes + Default + core::fmt::Debug,
+{
+    fn gdb_serialize(&self, mut write_byte: impl FnMut(Option<u8>)) {
+        macro_rules! write_le_bytes {
+            ($value:expr) => {
+                let mut buf = [0; 16];
+                // infallible (unless digit is a >128 bit number)
+                let len = $value.to_le_bytes(&mut buf).unwrap();
+                let buf = &buf[..len];
+                for b in buf {
+                    write_byte(Some(*b));
+                }
+            };
+        }
+
+        // Write GPRs
+        for reg in self.x.iter() {
+            write_le_bytes!(reg);
+        }
+
+        // Program Counter is regnum 33
+        write_le_bytes!(&self.pc);
+    }
+
+    fn gdb_deserialize(&mut self, bytes: &[u8]) -> Result<(), ()> {
+        let ptrsize = core::mem::size_of::<U>();
+
+        // ensure bytes.chunks_exact(ptrsize) won't panic
+        if bytes.len() % ptrsize != 0 {
+            return Err(());
+        }
+
+        let mut regs = bytes
+            .chunks_exact(ptrsize)
+            .map(|c| U::from_le_bytes(c).unwrap());
+
+        // Read GPRs
+        for reg in self.x.iter_mut() {
+            *reg = regs.next().ok_or(())?
+        }
+        self.pc = regs.next().ok_or(())?;
+
+        if regs.next().is_some() {
+            return Err(());
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/arch/traits.rs b/src/arch/traits.rs
new file mode 100644
index 0000000..1903739
--- /dev/null
+++ b/src/arch/traits.rs
@@ -0,0 +1,85 @@
+use core::fmt::Debug;
+
+use num_traits::{PrimInt, Unsigned};
+
+use crate::internal::{BeBytes, LeBytes};
+
+/// Register identifier for target registers.
+///
+/// These identifiers are used by GDB for single register operations.
+pub trait RegId: Sized + Debug {
+    /// Map raw GDB register number corresponding `RegId` and register size.
+    ///
+    /// Returns `None` if the register is not available.
+    fn from_raw_id(id: usize) -> Option<(Self, usize)>;
+}
+
+/// Stub implementation -- Returns `None` for all raw IDs.
+impl RegId for () {
+    fn from_raw_id(_id: usize) -> Option<(Self, usize)> {
+        None
+    }
+}
+
+/// Methods to read/write architecture-specific registers.
+///
+/// Registers must be de/serialized in the order specified by the architecture's
+/// `<target>.xml` in the GDB source tree.
+///
+/// e.g: for ARM:
+/// github.com/bminor/binutils-gdb/blob/master/gdb/features/arm/arm-core.xml
+// TODO: add way to de/serialize arbitrary "missing"/"uncollected" registers.
+pub trait Registers: Default + Debug + Clone + PartialEq {
+    /// Serialize `self` into a GDB register bytestream.
+    ///
+    /// Missing registers are serialized by passing `None` to write_byte.
+    fn gdb_serialize(&self, write_byte: impl FnMut(Option<u8>));
+
+    /// Deserialize a GDB register bytestream into `self`.
+    #[allow(clippy::clippy::result_unit_err)]
+    fn gdb_deserialize(&mut self, bytes: &[u8]) -> Result<(), ()>;
+}
+
+/// Encodes architecture-specific information, such as pointer size, register
+/// layout, etc...
+///
+/// Types implementing `Arch` should be
+/// [Zero-variant Enums](https://doc.rust-lang.org/reference/items/enumerations.html#zero-variant-enums),
+/// as they are only ever used at the type level, and should never be
+/// instantiated.
+pub trait Arch {
+    /// The architecture's pointer size (e.g: `u32` on a 32-bit system).
+    type Usize: PrimInt + Unsigned + BeBytes + LeBytes;
+
+    /// The architecture's register file.
+    type Registers: Registers;
+
+    /// Register identifier enum/struct.
+    ///
+    /// Used to access individual registers via `Target::read/write_register`.
+    ///
+    /// NOTE: The `RegId` type is not required to have a 1:1 correspondence with
+    /// the `Registers` type, and may include register identifiers which are
+    /// separate from the main `Registers` structure.
+    type RegId: RegId;
+
+    /// (optional) Return the platform's `features.xml` file.
+    ///
+    /// Implementing this method enables `gdb` to automatically detect the
+    /// target's architecture, saving the hassle of having to run `set
+    /// architecture <arch>` when starting a debugging session.
+    ///
+    /// These descriptions can be quite succinct. For example, the target
+    /// description for an `armv4t` platform can be as simple as:
+    ///
+    /// ```
+    /// r#"<target version="1.0"><architecture>armv4t</architecture></target>"#
+    /// # ;
+    /// ```
+    ///
+    /// See the [GDB docs](https://sourceware.org/gdb/current/onlinedocs/gdb/Target-Description-Format.html)
+    /// for details on the target description XML format.
+    fn target_description_xml() -> Option<&'static str> {
+        None
+    }
+}
diff --git a/src/arch/x86/mod.rs b/src/arch/x86/mod.rs
new file mode 100644
index 0000000..28fbe88
--- /dev/null
+++ b/src/arch/x86/mod.rs
@@ -0,0 +1,50 @@
+//! Implementations for various x86 architectures.
+
+use crate::arch::Arch;
+use crate::arch::RegId;
+
+pub mod reg;
+
+/// Implements `Arch` for 64-bit x86 + SSE Extensions.
+///
+/// Check out the [module level docs](../index.html#whats-with-regidimpl) for
+/// more info about the `RegIdImpl` type parameter.
+#[allow(non_camel_case_types)]
+pub enum X86_64_SSE<RegIdImpl: RegId = reg::id::X86_64CoreRegId> {
+    #[doc(hidden)]
+    _Marker(core::marker::PhantomData<RegIdImpl>),
+}
+
+impl<RegIdImpl: RegId> Arch for X86_64_SSE<RegIdImpl> {
+    type Usize = u64;
+    type Registers = reg::X86_64CoreRegs;
+    type RegId = RegIdImpl;
+
+    fn target_description_xml() -> Option<&'static str> {
+        Some(
+            r#"<target version="1.0"><architecture>i386:x86-64</architecture><feature name="org.gnu.gdb.i386.sse"></feature></target>"#,
+        )
+    }
+}
+
+/// Implements `Arch` for 32-bit x86 + SSE Extensions.
+///
+/// Check out the [module level docs](../index.html#whats-with-regidimpl) for
+/// more info about the `RegIdImpl` type parameter.
+#[allow(non_camel_case_types)]
+pub enum X86_SSE<RegIdImpl: RegId = reg::id::X86CoreRegId> {
+    #[doc(hidden)]
+    _Marker(core::marker::PhantomData<RegIdImpl>),
+}
+
+impl<RegIdImpl: RegId> Arch for X86_SSE<RegIdImpl> {
+    type Usize = u32;
+    type Registers = reg::X86CoreRegs;
+    type RegId = RegIdImpl;
+
+    fn target_description_xml() -> Option<&'static str> {
+        Some(
+            r#"<target version="1.0"><architecture>i386:intel</architecture><feature name="org.gnu.gdb.i386.sse"></feature></target>"#,
+        )
+    }
+}
diff --git a/src/arch/x86/reg/core32.rs b/src/arch/x86/reg/core32.rs
new file mode 100644
index 0000000..a49ee9a
--- /dev/null
+++ b/src/arch/x86/reg/core32.rs
@@ -0,0 +1,134 @@
+use core::convert::TryInto;
+
+use crate::arch::x86::reg::{X87FpuInternalRegs, F80};
+use crate::arch::Registers;
+
+/// 32-bit x86 core registers (+ SSE extensions).
+///
+/// Source: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/i386/32bit-core.xml
+/// Additionally: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/i386/32bit-sse.xml
+#[derive(Debug, Default, Clone, PartialEq)]
+pub struct X86CoreRegs {
+    /// Accumulator
+    pub eax: u32,
+    /// Count register
+    pub ecx: u32,
+    /// Data register
+    pub edx: u32,
+    /// Base register
+    pub ebx: u32,
+    /// Stack pointer
+    pub esp: u32,
+    /// Base pointer
+    pub ebp: u32,
+    /// Source index
+    pub esi: u32,
+    /// Destination index
+    pub edi: u32,
+    /// Instruction pointer
+    pub eip: u32,
+    /// Status register
+    pub eflags: u32,
+    /// Segment registers: CS, SS, DS, ES, FS, GS
+    pub segments: [u32; 6],
+    /// FPU registers: ST0 through ST7
+    pub st: [F80; 8],
+    /// FPU internal registers
+    pub fpu: X87FpuInternalRegs,
+    /// SIMD Registers: XMM0 through XMM7
+    pub xmm: [u128; 8],
+    /// SSE Status/Control Register
+    pub mxcsr: u32,
+}
+
+impl Registers for X86CoreRegs {
+    fn gdb_serialize(&self, mut write_byte: impl FnMut(Option<u8>)) {
+        macro_rules! write_bytes {
+            ($bytes:expr) => {
+                for b in $bytes {
+                    write_byte(Some(*b))
+                }
+            };
+        }
+
+        macro_rules! write_regs {
+            ($($reg:ident),*) => {
+                $(
+                    write_bytes!(&self.$reg.to_le_bytes());
+                )*
+            }
+        }
+
+        write_regs!(eax, ecx, edx, ebx, esp, ebp, esi, edi, eip, eflags);
+
+        // cs, ss, ds, es, fs, gs
+        for seg in &self.segments {
+            write_bytes!(&seg.to_le_bytes());
+        }
+
+        // st0 to st7
+        for st_reg in &self.st {
+            write_bytes!(st_reg);
+        }
+
+        self.fpu.gdb_serialize(&mut write_byte);
+
+        // xmm0 to xmm15
+        for xmm_reg in &self.xmm {
+            write_bytes!(&xmm_reg.to_le_bytes());
+        }
+
+        // mxcsr
+        write_bytes!(&self.mxcsr.to_le_bytes());
+
+        // padding
+        (0..4).for_each(|_| write_byte(None))
+    }
+
+    fn gdb_deserialize(&mut self, bytes: &[u8]) -> Result<(), ()> {
+        if bytes.len() < 0x138 {
+            return Err(());
+        }
+
+        macro_rules! parse_regs {
+            ($($reg:ident),*) => {
+                let mut regs = bytes[0..0x28]
+                    .chunks_exact(4)
+                    .map(|x| u32::from_le_bytes(x.try_into().unwrap()));
+                $(
+                    self.$reg = regs.next().ok_or(())?;
+                )*
+            }
+        }
+
+        parse_regs!(eax, ecx, edx, ebx, esp, ebp, esi, edi, eip, eflags);
+
+        let mut segments = bytes[0x28..0x40]
+            .chunks_exact(4)
+            .map(|x| u32::from_le_bytes(x.try_into().unwrap()));
+
+        for seg in self.segments.iter_mut() {
+            *seg = segments.next().ok_or(())?;
+        }
+
+        let mut regs = bytes[0x40..0x90].chunks_exact(10).map(TryInto::try_into);
+
+        for reg in self.st.iter_mut() {
+            *reg = regs.next().ok_or(())?.map_err(|_| ())?;
+        }
+
+        self.fpu.gdb_deserialize(&bytes[0x90..0xb0])?;
+
+        let mut regs = bytes[0xb0..0x130]
+            .chunks_exact(0x10)
+            .map(|x| u128::from_le_bytes(x.try_into().unwrap()));
+
+        for reg in self.xmm.iter_mut() {
+            *reg = regs.next().ok_or(())?;
+        }
+
+        self.mxcsr = u32::from_le_bytes(bytes[0x130..0x134].try_into().unwrap());
+
+        Ok(())
+    }
+}
diff --git a/src/arch/x86/reg/core64.rs b/src/arch/x86/reg/core64.rs
new file mode 100644
index 0000000..6f54581
--- /dev/null
+++ b/src/arch/x86/reg/core64.rs
@@ -0,0 +1,121 @@
+use core::convert::TryInto;
+
+use crate::arch::x86::reg::{X87FpuInternalRegs, F80};
+use crate::arch::Registers;
+
+/// 64-bit x86 core registers (+ SSE extensions).
+///
+/// Source: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/i386/64bit-core.xml
+/// Additionally: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/i386/64bit-sse.xml
+#[derive(Debug, Default, Clone, PartialEq)]
+pub struct X86_64CoreRegs {
+    /// RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, r8-r15
+    pub regs: [u64; 16],
+    /// Status register
+    pub eflags: u32,
+    /// Instruction pointer
+    pub rip: u64,
+    /// Segment registers: CS, SS, DS, ES, FS, GS
+    pub segments: [u32; 6],
+    /// FPU registers: ST0 through ST7
+    pub st: [F80; 8],
+    /// FPU internal registers
+    pub fpu: X87FpuInternalRegs,
+    /// SIMD Registers: XMM0 through XMM15
+    pub xmm: [u128; 0x10],
+    /// SSE Status/Control Register
+    pub mxcsr: u32,
+}
+
+impl Registers for X86_64CoreRegs {
+    fn gdb_serialize(&self, mut write_byte: impl FnMut(Option<u8>)) {
+        macro_rules! write_bytes {
+            ($bytes:expr) => {
+                for b in $bytes {
+                    write_byte(Some(*b))
+                }
+            };
+        }
+
+        // rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8-r15
+        for reg in &self.regs {
+            write_bytes!(&reg.to_le_bytes());
+        }
+
+        // rip
+        write_bytes!(&self.rip.to_le_bytes());
+
+        // eflags
+        write_bytes!(&self.eflags.to_le_bytes());
+
+        // cs, ss, ds, es, fs, gs
+        for seg in &self.segments {
+            write_bytes!(&seg.to_le_bytes());
+        }
+
+        // st0 to st7
+        for st_reg in &self.st {
+            write_bytes!(st_reg);
+        }
+
+        self.fpu.gdb_serialize(&mut write_byte);
+
+        // xmm0 to xmm15
+        for xmm_reg in &self.xmm {
+            write_bytes!(&xmm_reg.to_le_bytes());
+        }
+
+        // mxcsr
+        write_bytes!(&self.mxcsr.to_le_bytes());
+
+        // padding?
+        // XXX: Couldn't figure out what these do and GDB doesn't actually display any
+        // registers that use these values.
+        (0..0x18).for_each(|_| write_byte(None))
+    }
+
+    fn gdb_deserialize(&mut self, bytes: &[u8]) -> Result<(), ()> {
+        if bytes.len() < 0x218 {
+            return Err(());
+        }
+
+        let mut regs = bytes[0..0x80]
+            .chunks_exact(8)
+            .map(|x| u64::from_le_bytes(x.try_into().unwrap()));
+
+        for reg in self.regs.iter_mut() {
+            *reg = regs.next().ok_or(())?;
+        }
+
+        self.rip = u64::from_le_bytes(bytes[0x80..0x88].try_into().unwrap());
+        self.eflags = u32::from_le_bytes(bytes[0x88..0x8C].try_into().unwrap());
+
+        let mut segments = bytes[0x8C..0xA4]
+            .chunks_exact(4)
+            .map(|x| u32::from_le_bytes(x.try_into().unwrap()));
+
+        for seg in self.segments.iter_mut() {
+            *seg = segments.next().ok_or(())?;
+        }
+
+        let mut regs = bytes[0xA4..0xF4].chunks_exact(10).map(TryInto::try_into);
+
+        for reg in self.st.iter_mut() {
+            *reg = regs.next().ok_or(())?.map_err(|_| ())?;
+        }
+
+        self.fpu.gdb_deserialize(&bytes[0xF4..0x114])?;
+
+        let mut regs = bytes[0x114..0x214]
+            .chunks_exact(0x10)
+            .map(|x| u128::from_le_bytes(x.try_into().unwrap()));
+
+        for reg in self.xmm.iter_mut() {
+            *reg = regs.next().ok_or(())?;
+        }
+
+        self.mxcsr = u32::from_le_bytes(bytes[0x214..0x218].try_into().unwrap());
+
+        Ok(())
+    }
+}
diff --git a/src/arch/x86/reg/id.rs b/src/arch/x86/reg/id.rs
new file mode 100644
index 0000000..45d3de9
--- /dev/null
+++ b/src/arch/x86/reg/id.rs
@@ -0,0 +1,198 @@
+use crate::arch::RegId;
+
+/// FPU register identifier.
+#[derive(Debug, Clone, Copy)]
+pub enum X87FpuInternalRegId {
+    /// Floating-point control register
+    Fctrl,
+    /// Floating-point status register
+    Fstat,
+    /// Tag word
+    Ftag,
+    /// FPU instruction pointer segment
+    Fiseg,
+    /// FPU intstruction pointer offset
+    Fioff,
+    /// FPU operand segment
+    Foseg,
+    /// FPU operand offset
+    Fooff,
+    /// Floating-point opcode
+    Fop,
+}
+
+impl X87FpuInternalRegId {
+    fn from_u8(val: u8) -> Option<Self> {
+        use self::X87FpuInternalRegId::*;
+
+        let r = match val {
+            0 => Fctrl,
+            1 => Fstat,
+            2 => Ftag,
+            3 => Fiseg,
+            4 => Fioff,
+            5 => Foseg,
+            6 => Fooff,
+            7 => Fop,
+            _ => return None,
+        };
+        Some(r)
+    }
+}
+
+/// 32-bit x86 core + SSE register identifier.
+///
+/// Source: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/i386/32bit-core.xml
+/// Additionally: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/i386/32bit-sse.xml
+#[derive(Debug, Clone, Copy)]
+#[non_exhaustive]
+pub enum X86CoreRegId {
+    /// Accumulator
+    Eax,
+    /// Count register
+    Ecx,
+    /// Data register
+    Edx,
+    /// Base register
+    Ebx,
+    /// Stack pointer
+    Esp,
+    /// Base pointer
+    Ebp,
+    /// Source index
+    Esi,
+    /// Destination index
+    Edi,
+    /// Instruction pointer
+    Eip,
+    /// Status register
+    Eflags,
+    /// Segment registers: CS, SS, DS, ES, FS, GS
+    Segment(u8),
+    /// FPU registers: ST0 through ST7
+    St(u8),
+    /// FPU internal registers
+    Fpu(X87FpuInternalRegId),
+    /// SIMD Registers: XMM0 through XMM7
+    Xmm(u8),
+    /// SSE Status/Control Register
+    Mxcsr,
+}
+
+impl RegId for X86CoreRegId {
+    fn from_raw_id(id: usize) -> Option<(Self, usize)> {
+        use self::X86CoreRegId::*;
+
+        let r = match id {
+            0 => (Eax, 4),
+            1 => (Ecx, 4),
+            2 => (Edx, 4),
+            3 => (Ebx, 4),
+            4 => (Esp, 4),
+            5 => (Ebp, 4),
+            6 => (Esi, 4),
+            7 => (Edi, 4),
+            8 => (Eip, 4),
+            9 => (Eflags, 4),
+            10..=15 => (Segment(id as u8 - 10), 4),
+            16..=23 => (St(id as u8 - 16), 10),
+            24..=31 => match X87FpuInternalRegId::from_u8(id as u8 - 24) {
+                Some(r) => (Fpu(r), 4),
+                None => unreachable!(),
+            },
+            32..=39 => (Xmm(id as u8 - 32), 16),
+            40 => (Mxcsr, 4),
+            _ => return None,
+        };
+        Some(r)
+    }
+}
+
+/// 64-bit x86 core + SSE register identifier.
+///
+/// Source: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/i386/64bit-core.xml
+/// Additionally: https://github.com/bminor/binutils-gdb/blob/master/gdb/features/i386/64bit-sse.xml
+#[derive(Debug, Clone, Copy)]
+#[non_exhaustive]
+pub enum X86_64CoreRegId {
+    /// General purpose registers:
+    /// RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, r8-r15
+    Gpr(u8),
+    /// Instruction pointer
+    Rip,
+    /// Status register
+    Eflags,
+    /// Segment registers: CS, SS, DS, ES, FS, GS
+    Segment(u8),
+    /// FPU registers: ST0 through ST7
+    St(u8),
+    /// FPU internal registers
+    Fpu(X87FpuInternalRegId),
+    /// SIMD Registers: XMM0 through XMM15
+    Xmm(u8),
+    /// SSE Status/Control Register
+    Mxcsr,
+}
+
+impl RegId for X86_64CoreRegId {
+    fn from_raw_id(id: usize) -> Option<(Self, usize)> {
+        use self::X86_64CoreRegId::*;
+
+        let r = match id {
+            0..=15 => (Gpr(id as u8), 8),
+            16 => (Rip, 4),
+            17 => (Eflags, 8),
+            18..=23 => (Segment(id as u8 - 18), 4),
+            24..=31 => (St(id as u8 - 24), 10),
+            32..=39 => match X87FpuInternalRegId::from_u8(id as u8 - 32) {
+                Some(r) => (Fpu(r), 4),
+                None => unreachable!(),
+            },
+            40..=55 => (Xmm(id as u8 - 40), 16),
+            56 => (Mxcsr, 4),
+            _ => return None,
+        };
+        Some(r)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::arch::traits::RegId;
+    use crate::arch::traits::Registers;
+
+    /// Compare the following two values which are expected to be the same:
+    /// * length of data written by `Registers::gdb_serialize()` in byte
+    /// * sum of sizes of all registers obtained by `RegId::from_raw_id()`
+    fn test<Rs: Registers, RId: RegId>() {
+        // Obtain the data length written by `gdb_serialize` by passing a custom
+        // closure.
+        let mut serialized_data_len = 0;
+        let counter = |b: Option<u8>| {
+            if b.is_some() {
+                serialized_data_len += 1;
+            }
+        };
+        Rs::default().gdb_serialize(counter);
+
+        // Accumulate register sizes returned by `from_raw_id`.
+        let mut i = 0;
+        let mut sum_reg_sizes = 0;
+        while let Some((_, size)) = RId::from_raw_id(i) {
+            sum_reg_sizes += size;
+            i += 1;
+        }
+
+        assert_eq!(serialized_data_len, sum_reg_sizes);
+    }
+
+    #[test]
+    fn test_x86() {
+        test::<crate::arch::x86::reg::X86CoreRegs, crate::arch::x86::reg::id::X86CoreRegId>()
+    }
+
+    #[test]
+    fn test_x86_64() {
+        test::<crate::arch::x86::reg::X86_64CoreRegs, crate::arch::x86::reg::id::X86_64CoreRegId>()
+    }
+}
diff --git a/src/arch/x86/reg/mod.rs b/src/arch/x86/reg/mod.rs
new file mode 100644
index 0000000..46c8508
--- /dev/null
+++ b/src/arch/x86/reg/mod.rs
@@ -0,0 +1,82 @@
+//! `Register` structs for x86 architectures.
+
+use core::convert::TryInto;
+
+use crate::arch::Registers;
+
+/// `RegId` definitions for x86 architectures.
+pub mod id;
+
+mod core32;
+mod core64;
+
+pub use core32::X86CoreRegs;
+pub use core64::X86_64CoreRegs;
+
+/// 80-bit floating point value
+pub type F80 = [u8; 10];
+
+/// FPU registers
+#[derive(Debug, Default, Clone, PartialEq)]
+pub struct X87FpuInternalRegs {
+    /// Floating-point control register
+    pub fctrl: u32,
+    /// Floating-point status register
+    pub fstat: u32,
+    /// Tag word
+    pub ftag: u32,
+    /// FPU instruction pointer segment
+    pub fiseg: u32,
+    /// FPU intstruction pointer offset
+    pub fioff: u32,
+    /// FPU operand segment
+    pub foseg: u32,
+    /// FPU operand offset
+    pub fooff: u32,
+    /// Floating-point opcode
+    pub fop: u32,
+}
+
+impl Registers for X87FpuInternalRegs {
+    fn gdb_serialize(&self, mut write_byte: impl FnMut(Option<u8>)) {
+        macro_rules! write_bytes {
+            ($bytes:expr) => {
+                for b in $bytes {
+                    write_byte(Some(*b))
+                }
+            };
+        }
+
+        // Note: GDB section names don't make sense unless you read x87 FPU section 8.1:
+        // https://web.archive.org/web/20150123212110/http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-1-manual.pdf
+        write_bytes!(&self.fctrl.to_le_bytes());
+        write_bytes!(&self.fstat.to_le_bytes());
+        write_bytes!(&self.ftag.to_le_bytes());
+        write_bytes!(&self.fiseg.to_le_bytes());
+        write_bytes!(&self.fioff.to_le_bytes());
+        write_bytes!(&self.foseg.to_le_bytes());
+        write_bytes!(&self.fooff.to_le_bytes());
+        write_bytes!(&self.fop.to_le_bytes());
+    }
+
+    fn gdb_deserialize(&mut self, bytes: &[u8]) -> Result<(), ()> {
+        if bytes.len() != 0x20 {
+            return Err(());
+        }
+
+        let mut regs = bytes
+            .chunks_exact(4)
+            .map(|x| u32::from_le_bytes(x.try_into().unwrap()));
+
+        self.fctrl = regs.next().ok_or(())?;
+        self.fstat = regs.next().ok_or(())?;
+        self.ftag = regs.next().ok_or(())?;
+        self.fiseg = regs.next().ok_or(())?;
+        self.fioff = regs.next().ok_or(())?;
+        self.foseg = regs.next().ok_or(())?;
+        self.fooff = regs.next().ok_or(())?;
+        self.fop = regs.next().ok_or(())?;
+
+        Ok(())
+    }
+}
diff --git a/src/common.rs b/src/common.rs
new file mode 100644
index 0000000..d9ba55c
--- /dev/null
+++ b/src/common.rs
@@ -0,0 +1,7 @@
+//! Common types and definitions.
+
+/// Thread ID
+pub type Tid = core::num::NonZeroUsize;
+
+/// Process ID
+pub type Pid = core::num::NonZeroUsize;
diff --git a/src/connection/impls/boxed.rs b/src/connection/impls/boxed.rs
new file mode 100644
index 0000000..8acdb36
--- /dev/null
+++ b/src/connection/impls/boxed.rs
@@ -0,0 +1,35 @@
+use crate::Connection;
+
+use alloc::boxed::Box;
+
+impl<E> Connection for Box<dyn Connection<Error = E>> {
+    type Error = E;
+
+    fn read(&mut self) -> Result<u8, Self::Error> {
+        (**self).read()
+    }
+
+    fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
+        (**self).read_exact(buf)
+    }
+
+    fn write(&mut self, byte: u8) -> Result<(), Self::Error> {
+        (**self).write(byte)
+    }
+
+    fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
+        (**self).write_all(buf)
+    }
+
+    fn peek(&mut self) -> Result<Option<u8>, Self::Error> {
+        (**self).peek()
+    }
+
+    fn flush(&mut self) -> Result<(), Self::Error> {
+        (**self).flush()
+    }
+
+    fn on_session_start(&mut self) -> Result<(), Self::Error> {
+        (**self).on_session_start()
+    }
+}
diff --git a/src/connection/impls/mod.rs b/src/connection/impls/mod.rs
new file mode 100644
index 0000000..8d13f70
--- /dev/null
+++ b/src/connection/impls/mod.rs
@@ -0,0 +1,45 @@
+//! Implementations of the [`Connection`] trait for various built-in types
+// TODO: impl Connection for all `Read + Write` (blocked on specialization)
+
+#[cfg(feature = "alloc")]
+mod boxed;
+
+#[cfg(feature = "std")]
+mod tcpstream;
+
+#[cfg(all(feature = "std", unix))]
+mod unixstream;
+
+use super::Connection;
+
+impl<E> Connection for &mut dyn Connection<Error = E> {
+    type Error = E;
+
+    fn read(&mut self) -> Result<u8, Self::Error> {
+        (**self).read()
+    }
+
+    fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
+        (**self).read_exact(buf)
+    }
+
+    fn write(&mut self, byte: u8) -> Result<(), Self::Error> {
+        (**self).write(byte)
+    }
+
+    fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
+        (**self).write_all(buf)
+    }
+
+    fn peek(&mut self) -> Result<Option<u8>, Self::Error> {
+        (**self).peek()
+    }
+
+    fn flush(&mut self) -> Result<(), Self::Error> {
+        (**self).flush()
+    }
+
+    fn on_session_start(&mut self) -> Result<(), Self::Error> {
+        (**self).on_session_start()
+    }
+}
diff --git a/src/connection/impls/tcpstream.rs b/src/connection/impls/tcpstream.rs
new file mode 100644
index 0000000..52d3772
--- /dev/null
+++ b/src/connection/impls/tcpstream.rs
@@ -0,0 +1,61 @@
+use std::net::TcpStream;
+
+use crate::Connection;
+
+impl Connection for TcpStream {
+    type Error = std::io::Error;
+
+    fn read(&mut self) -> Result<u8, Self::Error> {
+        use std::io::Read;
+
+        self.set_nonblocking(false)?;
+
+        let mut buf = [0u8];
+        match Read::read_exact(self, &mut buf) {
+            Ok(_) => Ok(buf[0]),
+            Err(e) => Err(e),
+        }
+    }
+
+    fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
+        use std::io::Read;
+
+        self.set_nonblocking(false)?;
+
+        Read::read_exact(self, buf)
+    }
+
+    fn peek(&mut self) -> Result<Option<u8>, Self::Error> {
+        self.set_nonblocking(true)?;
+
+        let mut buf = [0u8];
+        match Self::peek(self, &mut buf) {
+            Ok(_) => Ok(Some(buf[0])),
+            Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
+            Err(e) => Err(e),
+        }
+    }
+
+    fn write(&mut self, byte: u8) -> Result<(), Self::Error> {
+        use std::io::Write;
+
+        Write::write_all(self, &[byte])
+    }
+
+    fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
+        use std::io::Write;
+
+        Write::write_all(self, buf)
+    }
+
+    fn flush(&mut self) -> Result<(), Self::Error> {
+        use std::io::Write;
+
+        Write::flush(self)
+    }
+
+    fn on_session_start(&mut self) -> Result<(), Self::Error> {
+        // see issue #28
+        self.set_nodelay(true)
+    }
+}
diff --git a/src/connection/impls/unixstream.rs b/src/connection/impls/unixstream.rs
new file mode 100644
index 0000000..81854ee
--- /dev/null
+++ b/src/connection/impls/unixstream.rs
@@ -0,0 +1,100 @@
+use core::ffi::c_void;
+use std::io;
+use std::os::unix::io::AsRawFd;
+use std::os::unix::net::UnixStream;
+
+use crate::Connection;
+
+// TODO: Remove PeekExt once `gdbstub`'s MSRV >1.48 (rust-lang/rust#73761)
+trait PeekExt {
+    fn peek(&self, buf: &mut [u8]) -> io::Result<usize>;
+}
+
+impl PeekExt for UnixStream {
+    #[allow(non_camel_case_types)]
+    fn peek(&self, buf: &mut [u8]) -> io::Result<usize> {
+        // Define some libc types inline (to avoid bringing in entire libc dep)
+
+        // every platform supported by the libc crate uses c_int = i32
+        type c_int = i32;
+        type size_t = usize;
+        type ssize_t = isize;
+        const MSG_PEEK: c_int = 2;
+        extern "C" {
+            fn recv(socket: c_int, buf: *mut c_void, len: size_t, flags: c_int) -> ssize_t;
+        }
+
+        // from std/sys/unix/mod.rs
+        pub fn cvt(t: isize) -> io::Result<isize> {
+            if t == -1 {
+                Err(io::Error::last_os_error())
+            } else {
+                Ok(t)
+            }
+        }
+
+        // from std/sys/unix/net.rs
+        let ret = cvt(unsafe {
+            recv(
+                self.as_raw_fd(),
+                buf.as_mut_ptr() as *mut c_void,
+                buf.len(),
+                MSG_PEEK,
+            )
+        })?;
+        Ok(ret as usize)
+    }
+}
+
+impl Connection for UnixStream {
+    type Error = std::io::Error;
+
+    fn read(&mut self) -> Result<u8, Self::Error> {
+        use std::io::Read;
+
+        self.set_nonblocking(false)?;
+
+        let mut buf = [0u8];
+        match Read::read_exact(self, &mut buf) {
+            Ok(_) => Ok(buf[0]),
+            Err(e) => Err(e),
+        }
+    }
+
+    fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
+        use std::io::Read;
+
+        self.set_nonblocking(false)?;
+
+        Read::read_exact(self, buf)
+    }
+
+    fn peek(&mut self) -> Result<Option<u8>, Self::Error> {
+        self.set_nonblocking(true)?;
+
+        let mut buf = [0u8];
+        match PeekExt::peek(self, &mut buf) {
+            Ok(_) => Ok(Some(buf[0])),
+            Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
+            Err(e) => Err(e),
+        }
+    }
+
+    fn write(&mut self, byte: u8) -> Result<(), Self::Error> {
+        use std::io::Write;
+
+        Write::write_all(self, &[byte])
+    }
+
+    fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
+        use std::io::Write;
+
+        Write::write_all(self, buf)
+    }
+
+    fn flush(&mut self) -> Result<(), Self::Error> {
+        use std::io::Write;
+
+        Write::flush(self)
+    }
+}
diff --git a/src/connection/mod.rs b/src/connection/mod.rs
new file mode 100644
index 0000000..7dbd4ea
--- /dev/null
+++ b/src/connection/mod.rs
@@ -0,0 +1,71 @@
+mod impls;
+
+/// A trait to perform in-order, serial, byte-wise I/O.
+///
+/// When the `std` feature is enabled, this trait is automatically implemented
+/// for [`TcpStream`](std::net::TcpStream) and
+/// [`UnixStream`](std::os::unix::net::UnixStream) (on unix systems).
+pub trait Connection {
+    /// Transport-specific error type.
+    type Error;
+
+    /// Read a single byte.
+    fn read(&mut self) -> Result<u8, Self::Error>;
+
+    /// Read the exact number of bytes required to fill the buffer.
+    ///
+    /// This method's default implementation calls `self.read()` for each byte
+    /// in the buffer. This can be quite inefficient, so if a more efficient
+    /// implementation exists (such as calling `read_exact()` on an underlying
+    /// `std::io::Read` object), this method should be overwritten.
+    fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
+        for b in buf {
+            *b = self.read()?;
+        }
+        Ok(())
+    }
+
+    /// Write a single byte.
+    fn write(&mut self, byte: u8) -> Result<(), Self::Error>;
+
+    /// Write the entire buffer, blocking until complete.
+    ///
+    /// This method's default implementation calls `self.write()` on each byte
+    /// in the buffer. This can be quite inefficient, so if a more efficient
+    /// implementation exists (such as calling `write_all()` on an underlying
+    /// `std::io::Write` object), this method should be overwritten.
+    fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
+        for b in buf {
+            self.write(*b)?;
+        }
+        Ok(())
+    }
+
+    /// Peek a single byte. This MUST be a **non-blocking** operation, returning
+    /// `None` if no byte is available.
+    fn peek(&mut self) -> Result<Option<u8>, Self::Error>;
+
+    /// Flush this Connection, ensuring that all intermediately buffered
+    /// contents reach their destination.
+    ///
+    /// _Note:_ Not all `Connection`s have internal buffering (e.g: writing data
+    /// to a UART TX register with FIFOs disabled). In these cases, it's fine to
+    /// simply return `Ok(())`.
+    fn flush(&mut self) -> Result<(), Self::Error>;
+
+    /// Called at the start of a debugging session _before_ any GDB packets have
+    /// been sent/received.
+    ///
+    /// This method's default implementation is a no-op.
+    ///
+    /// # Example
+    ///
+    /// The `on_session_start` implementation for `TcpStream` ensures that
+    /// [`set_nodelay(true)`](std::net::TcpStream::set_nodelay)
+    /// is called. The GDB remote serial protocol requires sending/receiving
+    /// many small packets, so forgetting to enable `TCP_NODELAY` can result in
+    /// a massively degraded debugging experience.
+    fn on_session_start(&mut self) -> Result<(), Self::Error> {
+        Ok(())
+    }
+}
diff --git a/src/gdbstub_impl/builder.rs b/src/gdbstub_impl/builder.rs
new file mode 100644
index 0000000..bf13e21
--- /dev/null
+++ b/src/gdbstub_impl/builder.rs
@@ -0,0 +1,111 @@
+use core::fmt::{self, Display};
+use core::marker::PhantomData;
+
+use managed::ManagedSlice;
+
+use super::{Connection, GdbStub, GdbStubImpl, Target};
+
+/// An error which may occur when building a [`GdbStub`].
+#[derive(Debug)]
+pub enum GdbStubBuilderError {
+    /// Must provide buffer using `with_packet_buffer` in `#![no_std]` mode.
+    MissingPacketBuffer,
+    /// Custom packet buffer size is larger than the provided buffer's length.
+    PacketBufSizeMismatch,
+}
+
+impl Display for GdbStubBuilderError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        use self::GdbStubBuilderError::*;
+        match self {
+            MissingPacketBuffer => write!(
+                f,
+                "Must provide buffer using `with_packet_buffer` in `#![no_std]` mode."
+            ),
+            PacketBufSizeMismatch => write!(
+                f,
+                "`packet_buffer_size` is larger than `with_packet_buffer`'s size."
+            ),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for GdbStubBuilderError {}
+
+/// Helper to construct and customize [`GdbStub`].
+pub struct GdbStubBuilder<'a, T: Target, C: Connection> {
+    conn: C,
+    packet_buffer: Option<&'a mut [u8]>,
+    packet_buffer_size: Option<usize>,
+
+    _target: PhantomData<T>,
+}
+
+impl<'a, T: Target, C: Connection> GdbStubBuilder<'a, T, C> {
+    /// Create a new `GdbStubBuilder` using the provided Connection.
+    pub fn new(conn: C) -> GdbStubBuilder<'static, T, C> {
+        GdbStubBuilder {
+            conn,
+            packet_buffer: None,
+            packet_buffer_size: None,
+
+            _target: PhantomData,
+        }
+    }
+
+    /// Use a pre-allocated packet buffer (instead of heap-allocating).
+    ///
+    /// _Note:_ This method is _required_ when the `alloc` feature is disabled!
+    pub fn with_packet_buffer(mut self, packet_buffer: &'a mut [u8]) -> Self {
+        self.packet_buffer = Some(packet_buffer);
+        self
+    }
+
+    /// Specify a custom size for the packet buffer. Defaults to 4096 bytes.
+    ///
+    /// When used alongside `with_packet_buffer`, the provided `size` must be
+    /// less than or equal to the length of the packet buffer.
+    pub fn packet_buffer_size(mut self, size: usize) -> Self {
+        self.packet_buffer_size = Some(size);
+        self
+    }
+
+    /// Build the GdbStub, returning an error if something went wrong.
+    pub fn build(self) -> Result<GdbStub<'a, T, C>, GdbStubBuilderError> {
+        let (packet_buffer, packet_buffer_len) = match self.packet_buffer {
+            Some(buf) => {
+                let len = match self.packet_buffer_size {
+                    Some(custom_len) => {
+                        if custom_len > buf.len() {
+                            return Err(GdbStubBuilderError::PacketBufSizeMismatch);
+                        } else {
+                            custom_len
+                        }
+                    }
+                    None => buf.len(),
+                };
+                (ManagedSlice::Borrowed(buf), len)
+            }
+            None => {
+                cfg_if::cfg_if! {
+                    if #[cfg(feature = "alloc")] {
+                        use alloc::vec::Vec;
+                        // need to pick some arbitrary value to report to GDB
+                        // 4096 seems reasonable?
+                        let len = self.packet_buffer_size.unwrap_or(4096);
+                        (ManagedSlice::Owned(Vec::with_capacity(len)), len)
+                    } else {
+                        return Err(GdbStubBuilderError::MissingPacketBuffer);
+                    }
+                }
+            }
+        };
+
+        Ok(GdbStub {
+            conn: self.conn,
+            packet_buffer,
+            state: GdbStubImpl::new(packet_buffer_len),
+        })
+    }
+}
diff --git a/src/gdbstub_impl/error.rs b/src/gdbstub_impl/error.rs
new file mode 100644
index 0000000..295c153
--- /dev/null
+++ b/src/gdbstub_impl/error.rs
@@ -0,0 +1,81 @@
+use core::fmt::{self, Debug, Display};
+
+use crate::protocol::{PacketParseError, ResponseWriterError};
+use crate::util::managed_vec::CapacityError;
+
+/// An error which may occur during a GDB debugging session.
+#[derive(Debug)]
+#[non_exhaustive]
+pub enum GdbStubError<T, C> {
+    /// Connection Error while reading request.
+    ConnectionRead(C),
+    /// Connection Error while writing response.
+    ConnectionWrite(ResponseWriterError<C>),
+    /// Client nack'd the last packet, but `gdbstub` doesn't implement
+    /// re-transmission.
+    ClientSentNack,
+    /// GdbStub was not provided with a packet buffer in `no_std` mode
+    /// (missing call to `with_packet_buffer`)
+    MissingPacketBuffer,
+    /// Packet cannot fit in the provided packet buffer.
+    PacketBufferOverlow,
+    /// Could not parse the packet into a valid command.
+    PacketParse(PacketParseError),
+    /// GDB client sent an unexpected packet.
+    PacketUnexpected,
+    /// GDB client sent a packet with too much data for the given target.
+    TargetMismatch,
+    /// Target threw a fatal error.
+    TargetError(T),
+    /// Target didn't report any active threads.
+    NoActiveThreads,
+    /// Resuming with a signal is not implemented yet. Consider opening a PR?
+    ResumeWithSignalUnimplemented,
+    /// Internal - A non-fatal error occurred (with errno-style error code)
+    #[doc(hidden)]
+    NonFatalError(u8),
+}
+
+impl<T, C> From<ResponseWriterError<C>> for GdbStubError<T, C> {
+    fn from(e: ResponseWriterError<C>) -> Self {
+        GdbStubError::ConnectionWrite(e)
+    }
+}
+
+impl<A, T, C> From<CapacityError<A>> for GdbStubError<T, C> {
+    fn from(_: CapacityError<A>) -> Self {
+        GdbStubError::PacketBufferOverlow
+    }
+}
+
+impl<T, C> Display for GdbStubError<T, C>
+where
+    C: Debug,
+    T: Debug,
+{
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        use self::GdbStubError::*;
+        match self {
+            ConnectionRead(e) => write!(f, "Connection Error while reading request: {:?}", e),
+            ConnectionWrite(e) => write!(f, "Connection Error while writing response: {:?}", e),
+            ClientSentNack => write!(f, "Client nack'd the last packet, but `gdbstub` doesn't implement re-transmission."),
+            MissingPacketBuffer => write!(f, "GdbStub was not provided with a packet buffer in `no_std` mode (missing call to `with_packet_buffer`)"),
+            PacketBufferOverlow => write!(f, "Packet too big for provided buffer!"),
+            PacketParse(e) => write!(f, "Could not parse the packet into a valid command: {:?}", e),
+            PacketUnexpected => write!(f, "Client sent an unexpected packet."),
+            TargetMismatch => write!(f, "GDB client sent a packet with too much data for the given target."),
+            TargetError(e) => write!(f, "Target threw a fatal error: {:?}", e),
+            NoActiveThreads => write!(f, "Target didn't report any active threads."),
+            ResumeWithSignalUnimplemented => write!(f, "Resuming with a signal is not implemented yet. Consider opening a PR?"),
+            NonFatalError(_) => write!(f, "Internal - A non-fatal error occurred (with errno-style error code)"),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl<T, C> std::error::Error for GdbStubError<T, C>
+where
+    C: Debug,
+    T: Debug,
+{
+}
diff --git a/src/gdbstub_impl/mod.rs b/src/gdbstub_impl/mod.rs
new file mode 100644
index 0000000..c0f714c
--- /dev/null
+++ b/src/gdbstub_impl/mod.rs
@@ -0,0 +1,1037 @@
+use core::marker::PhantomData;
+
+#[cfg(feature = "alloc")]
+use alloc::collections::BTreeMap;
+
+use managed::ManagedSlice;
+
+use crate::common::*;
+use crate::{
+    arch::{Arch, RegId, Registers},
+    connection::Connection,
+    internal::*,
+    protocol::{
+        commands::{ext, Command},
+        ConsoleOutput, IdKind, Packet, ResponseWriter, ThreadId,
+    },
+    target::ext::base::multithread::{Actions, ResumeAction, ThreadStopReason, TidSelector},
+    target::ext::base::BaseOps,
+    target::Target,
+    util::managed_vec::ManagedVec,
+    FAKE_PID, SINGLE_THREAD_TID,
+};
+
+mod builder;
+mod error;
+mod target_result_ext;
+
+pub use builder::{GdbStubBuilder, GdbStubBuilderError};
+pub use error::GdbStubError;
+
+use target_result_ext::TargetResultExt;
+
+use GdbStubError as Error;
+
+/// Describes why the GDB session ended.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum DisconnectReason {
+    /// Target Halted
+    TargetHalted,
+    /// GDB issued a disconnect command
+    Disconnect,
+    /// GDB issued a kill command
+    Kill,
+}
+
+/// Debug a [`Target`] using the GDB Remote Serial Protocol over a given
+/// [`Connection`].
+pub struct GdbStub<'a, T: Target, C: Connection> {
+    conn: C,
+    packet_buffer: ManagedSlice<'a, u8>,
+    state: GdbStubImpl<T, C>,
+}
+
+impl<'a, T: Target, C: Connection> GdbStub<'a, T, C> {
+    /// Create a [`GdbStubBuilder`] using the provided Connection.
+    pub fn builder(conn: C) -> GdbStubBuilder<'a, T, C> {
+        GdbStubBuilder::new(conn)
+    }
+
+    /// Create a new `GdbStub` using the provided connection.
+    ///
+    /// For fine-grained control over various `GdbStub` options, use the
+    /// [`builder()`](GdbStub::builder) method instead.
+    ///
+    /// _Note:_ `new` is only available when the `alloc` feature is enabled.
+    #[cfg(feature = "alloc")]
+    pub fn new(conn: C) -> GdbStub<'a, T, C> {
+        GdbStubBuilder::new(conn).build().unwrap()
+    }
+
+    /// Starts a GDB remote debugging session.
+    ///
+    /// Returns once the GDB client closes the debugging session, or if the
+    /// target halts.
+    pub fn run(&mut self, target: &mut T) -> Result<DisconnectReason, Error<T::Error, C::Error>> {
+        self.state
+            .run(target, &mut self.conn, &mut self.packet_buffer)
+    }
+}
+
+struct GdbStubImpl<T: Target, C: Connection> {
+    _target: PhantomData<T>,
+    _connection: PhantomData<C>,
+
+    packet_buffer_len: usize,
+    current_mem_tid: Tid,
+    current_resume_tid: TidSelector,
+    no_ack_mode: bool,
+
+    // Used to track which Pids were attached to / spawned when running in extended mode.
+    //
+    // An empty `BTreeMap<Pid, bool>` is only 24 bytes (on 64-bit systems), and doesn't allocate
+    // until the first element is inserted, so it should be fine to include it as part of the main
+    // state structure whether or not extended mode is actually being used.
+    #[cfg(feature = "alloc")]
+    attached_pids: BTreeMap<Pid, bool>,
+}
+
+enum HandlerStatus {
+    Handled,
+    NeedsOK,
+    Disconnect(DisconnectReason),
+}
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    fn new(packet_buffer_len: usize) -> GdbStubImpl<T, C> {
+        GdbStubImpl {
+            _target: PhantomData,
+            _connection: PhantomData,
+
+            packet_buffer_len,
+            // HACK: current_mem_tid is immediately updated with valid value once `run` is called.
+            // While the more idiomatic way to handle this would be to use an Option, given that
+            // it's only ever unset prior to the start of `run`, it's probably okay leaving it as-is
+            // for code-clarity purposes.
+            current_mem_tid: SINGLE_THREAD_TID,
+            current_resume_tid: TidSelector::All,
+            no_ack_mode: false,
+
+            #[cfg(feature = "alloc")]
+            attached_pids: BTreeMap::new(),
+        }
+    }
+
+    fn run(
+        &mut self,
+        target: &mut T,
+        conn: &mut C,
+        packet_buffer: &mut ManagedSlice<u8>,
+    ) -> Result<DisconnectReason, Error<T::Error, C::Error>> {
+        conn.on_session_start().map_err(Error::ConnectionRead)?;
+
+        // before even accepting packets, we query the target to get a sane value for
+        // `self.current_mem_tid`.
+        // NOTE: this will break if extended mode is ever implemented...
+
+        self.current_mem_tid = match target.base_ops() {
+            BaseOps::SingleThread(_) => SINGLE_THREAD_TID,
+            BaseOps::MultiThread(ops) => {
+                let mut first_tid = None;
+                ops.list_active_threads(&mut |tid| {
+                    if first_tid.is_none() {
+                        first_tid = Some(tid);
+                    }
+                })
+                .map_err(Error::TargetError)?;
+                first_tid.ok_or(Error::NoActiveThreads)?
+            }
+        };
+
+        loop {
+            match Self::recv_packet(conn, target, packet_buffer)? {
+                Packet::Ack => {}
+                Packet::Nack => return Err(Error::ClientSentNack),
+                Packet::Interrupt => {
+                    debug!("<-- interrupt packet");
+                    let mut res = ResponseWriter::new(conn);
+                    res.write_str("S05")?;
+                    res.flush()?;
+                }
+                Packet::Command(command) => {
+                    // Acknowledge the command
+                    if !self.no_ack_mode {
+                        conn.write(b'+').map_err(Error::ConnectionRead)?;
+                    }
+
+                    let mut res = ResponseWriter::new(conn);
+                    let disconnect = match self.handle_command(&mut res, target, command) {
+                        Ok(HandlerStatus::Handled) => None,
+                        Ok(HandlerStatus::NeedsOK) => {
+                            res.write_str("OK")?;
+                            None
+                        }
+                        Ok(HandlerStatus::Disconnect(reason)) => Some(reason),
+                        // HACK: handling this "dummy" error is required as part of the
+                        // `TargetResultExt::handle_error()` machinery.
+                        Err(Error::NonFatalError(code)) => {
+                            res.write_str("E")?;
+                            res.write_num(code)?;
+                            None
+                        }
+                        Err(Error::TargetError(e)) => {
+                            // unlike all other errors which are "unrecoverable" in the sense that
+                            // the GDB session cannot continue, there's still a chance that a target
+                            // might want to keep the debugging session alive to do a "post-mortem"
+                            // analysis. As such, we simply report a standard TRAP stop reason.
+                            let mut res = ResponseWriter::new(conn);
+                            res.write_str("S05")?;
+                            res.flush()?;
+                            return Err(Error::TargetError(e));
+                        }
+                        Err(e) => return Err(e),
+                    };
+
+                    // HACK: this could be more elegant...
+                    if disconnect != Some(DisconnectReason::Kill) {
+                        res.flush()?;
+                    }
+
+                    if let Some(disconnect_reason) = disconnect {
+                        return Ok(disconnect_reason);
+                    }
+                }
+            };
+        }
+    }
+
+    fn recv_packet<'a>(
+        conn: &mut C,
+        target: &mut T,
+        pkt_buf: &'a mut ManagedSlice<u8>,
+    ) -> Result<Packet<'a>, Error<T::Error, C::Error>> {
+        let header_byte = conn.read().map_err(Error::ConnectionRead)?;
+
+        // Wrap the buf in a `ManagedVec` to keep the code readable.
+        let mut buf = ManagedVec::new(pkt_buf);
+
+        buf.clear();
+        buf.push(header_byte)?;
+        if header_byte == b'$' {
+            // read the packet body
+            loop {
+                let c = conn.read().map_err(Error::ConnectionRead)?;
+                buf.push(c)?;
+                if c == b'#' {
+                    break;
+                }
+            }
+            // read the checksum as well
+            buf.push(conn.read().map_err(Error::ConnectionRead)?)?;
+            buf.push(conn.read().map_err(Error::ConnectionRead)?)?;
+        }
+
+        match Packet::from_buf(target, pkt_buf.as_mut()) {
+            Ok(packet) => Ok(packet),
+            Err(e) => Err(Error::PacketParse(e)),
+        }
+    }
+
+    fn handle_command(
+        &mut self,
+        res: &mut ResponseWriter<C>,
+        target: &mut T,
+        cmd: Command<'_>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        match cmd {
+            Command::Unknown(cmd) => {
+                info!("Unknown command: {}", cmd);
+                Ok(HandlerStatus::Handled)
+            }
+            Command::Base(cmd) => self.handle_base(res, target, cmd),
+            Command::ExtendedMode(cmd) => self.handle_extended_mode(res, target, cmd),
+            Command::MonitorCmd(cmd) => self.handle_monitor_cmd(res, target, cmd),
+            Command::SectionOffsets(cmd) => self.handle_section_offsets(res, target, cmd),
+        }
+    }
+
+    fn handle_base<'a>(
+        &mut self,
+        res: &mut ResponseWriter<C>,
+        target: &mut T,
+        command: ext::Base<'a>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let handler_status = match command {
+            // ------------------ Handshaking and Queries ------------------- //
+            ext::Base::qSupported(cmd) => {
+                // XXX: actually read what the client supports, and enable/disable features
+                // appropriately
+                let _features = cmd.features.into_iter();
+
+                res.write_str("PacketSize=")?;
+                res.write_num(self.packet_buffer_len)?;
+
+                res.write_str(";vContSupported+")?;
+                res.write_str(";multiprocess+")?;
+                res.write_str(";QStartNoAckMode+")?;
+
+                if let Some(ops) = target.extended_mode() {
+                    if ops.configure_aslr().is_some() {
+                        res.write_str(";QDisableRandomization+")?;
+                    }
+
+                    if ops.configure_env().is_some() {
+                        res.write_str(";QEnvironmentHexEncoded+")?;
+                        res.write_str(";QEnvironmentUnset+")?;
+                        res.write_str(";QEnvironmentReset+")?;
+                    }
+
+                    if ops.configure_startup_shell().is_some() {
+                        res.write_str(";QStartupWithShell+")?;
+                    }
+
+                    if ops.configure_working_dir().is_some() {
+                        res.write_str(";QSetWorkingDir+")?;
+                    }
+                }
+
+                res.write_str(";swbreak+")?;
+                if target.hw_breakpoint().is_some() || target.hw_watchpoint().is_some() {
+                    res.write_str(";hwbreak+")?;
+                }
+
+                // TODO: implement conditional breakpoint support (since that's kool).
+                // res.write_str("ConditionalBreakpoints+;")?;
+
+                if T::Arch::target_description_xml().is_some() {
+                    res.write_str(";qXfer:features:read+")?;
+                }
+
+                HandlerStatus::Handled
+            }
+            ext::Base::QStartNoAckMode(_) => {
+                self.no_ack_mode = true;
+                HandlerStatus::NeedsOK
+            }
+            ext::Base::qXferFeaturesRead(cmd) => {
+                match T::Arch::target_description_xml() {
+                    Some(xml) => {
+                        let xml = xml.trim();
+                        if cmd.offset >= xml.len() {
+                            // no more data
+                            res.write_str("l")?;
+                        } else if cmd.offset + cmd.len >= xml.len() {
+                            // last little bit of data
+                            res.write_str("l")?;
+                            res.write_binary(&xml.as_bytes()[cmd.offset..])?
+                        } else {
+                            // still more data
+                            res.write_str("m")?;
+                            res.write_binary(&xml.as_bytes()[cmd.offset..(cmd.offset + cmd.len)])?
+                        }
+                    }
+                    // If the target hasn't provided their own XML, then the initial response to
+                    // "qSupported" wouldn't have included  "qXfer:features:read", and gdb wouldn't
+                    // send this packet unless it was explicitly marked as supported.
+                    None => return Err(Error::PacketUnexpected),
+                }
+                HandlerStatus::Handled
+            }
+
+            // -------------------- "Core" Functionality -------------------- //
+            // TODO: Improve the '?' response based on last-sent stop reason.
+            ext::Base::QuestionMark(_) => {
+                res.write_str("S05")?;
+                HandlerStatus::Handled
+            }
+            ext::Base::qAttached(cmd) => {
+                let is_attached = match target.extended_mode() {
+                    // when _not_ running in extended mode, just report that we're attaching to an
+                    // existing process.
+                    None => true, // assume attached to an existing process
+                    // When running in extended mode, we must defer to the target
+                    Some(ops) => {
+                        let pid: Pid = cmd.pid.ok_or(Error::PacketUnexpected)?;
+
+                        #[cfg(feature = "alloc")]
+                        {
+                            let _ = ops; // doesn't actually query the target
+                            *self.attached_pids.get(&pid).unwrap_or(&true)
+                        }
+
+                        #[cfg(not(feature = "alloc"))]
+                        {
+                            ops.query_if_attached(pid).handle_error()?.was_attached()
+                        }
+                    }
+                };
+                res.write_str(if is_attached { "1" } else { "0" })?;
+                HandlerStatus::Handled
+            }
+            ext::Base::g(_) => {
+                let mut regs: <T::Arch as Arch>::Registers = Default::default();
+                match target.base_ops() {
+                    BaseOps::SingleThread(ops) => ops.read_registers(&mut regs),
+                    BaseOps::MultiThread(ops) => {
+                        ops.read_registers(&mut regs, self.current_mem_tid)
+                    }
+                }
+                .handle_error()?;
+
+                let mut err = Ok(());
+                regs.gdb_serialize(|val| {
+                    let res = match val {
+                        Some(b) => res.write_hex_buf(&[b]),
+                        None => res.write_str("xx"),
+                    };
+                    if let Err(e) = res {
+                        err = Err(e);
+                    }
+                });
+                err?;
+                HandlerStatus::Handled
+            }
+            ext::Base::G(cmd) => {
+                let mut regs: <T::Arch as Arch>::Registers = Default::default();
+                regs.gdb_deserialize(cmd.vals)
+                    .map_err(|_| Error::TargetMismatch)?;
+
+                match target.base_ops() {
+                    BaseOps::SingleThread(ops) => ops.write_registers(&regs),
+                    BaseOps::MultiThread(ops) => ops.write_registers(&regs, self.current_mem_tid),
+                }
+                .handle_error()?;
+
+                HandlerStatus::NeedsOK
+            }
+            ext::Base::m(cmd) => {
+                let buf = cmd.buf;
+                let addr = <T::Arch as Arch>::Usize::from_be_bytes(cmd.addr)
+                    .ok_or(Error::TargetMismatch)?;
+
+                let mut i = 0;
+                let mut n = cmd.len;
+                while n != 0 {
+                    let chunk_size = n.min(buf.len());
+
+                    use num_traits::NumCast;
+
+                    let addr = addr + NumCast::from(i).ok_or(Error::TargetMismatch)?;
+                    let data = &mut buf[..chunk_size];
+                    match target.base_ops() {
+                        BaseOps::SingleThread(ops) => ops.read_addrs(addr, data),
+                        BaseOps::MultiThread(ops) => {
+                            ops.read_addrs(addr, data, self.current_mem_tid)
+                        }
+                    }
+                    .handle_error()?;
+
+                    n -= chunk_size;
+                    i += chunk_size;
+
+                    res.write_hex_buf(data)?;
+                }
+                HandlerStatus::Handled
+            }
+            ext::Base::M(cmd) => {
+                let addr = <T::Arch as Arch>::Usize::from_be_bytes(cmd.addr)
+                    .ok_or(Error::TargetMismatch)?;
+
+                match target.base_ops() {
+                    BaseOps::SingleThread(ops) => ops.write_addrs(addr, cmd.val),
+                    BaseOps::MultiThread(ops) => {
+                        ops.write_addrs(addr, cmd.val, self.current_mem_tid)
+                    }
+                }
+                .handle_error()?;
+
+                HandlerStatus::NeedsOK
+            }
+            ext::Base::k(_) | ext::Base::vKill(_) => {
+                match target.extended_mode() {
+                    // When not running in extended mode, stop the `GdbStub` and disconnect.
+                    None => HandlerStatus::Disconnect(DisconnectReason::Kill),
+
+                    // When running in extended mode, a kill command does not necessarily result in
+                    // a disconnect...
+                    Some(ops) => {
+                        let pid = match command {
+                            ext::Base::vKill(cmd) => Some(cmd.pid),
+                            _ => None,
+                        };
+
+                        let should_terminate = ops.kill(pid).handle_error()?;
+                        if should_terminate.into() {
+                            // manually write OK, since we need to return a DisconnectReason
+                            res.write_str("OK")?;
+                            HandlerStatus::Disconnect(DisconnectReason::Kill)
+                        } else {
+                            HandlerStatus::NeedsOK
+                        }
+                    }
+                }
+            }
+            ext::Base::D(_) => {
+                // TODO: plumb-through Pid when exposing full multiprocess + extended mode
+                res.write_str("OK")?; // manually write OK, since we need to return a DisconnectReason
+                HandlerStatus::Disconnect(DisconnectReason::Disconnect)
+            }
+            ext::Base::Z(cmd) => {
+                let addr = <T::Arch as Arch>::Usize::from_be_bytes(cmd.addr)
+                    .ok_or(Error::TargetMismatch)?;
+
+                use crate::target::ext::breakpoints::WatchKind::*;
+                let supported = match cmd.type_ {
+                    0 => (target.sw_breakpoint()).map(|op| op.add_sw_breakpoint(addr)),
+                    1 => (target.hw_breakpoint()).map(|op| op.add_hw_breakpoint(addr)),
+                    2 => (target.hw_watchpoint()).map(|op| op.add_hw_watchpoint(addr, Write)),
+                    3 => (target.hw_watchpoint()).map(|op| op.add_hw_watchpoint(addr, Read)),
+                    4 => (target.hw_watchpoint()).map(|op| op.add_hw_watchpoint(addr, ReadWrite)),
+                    // only 5 types in the protocol
+                    _ => None,
+                };
+
+                match supported {
+                    None => HandlerStatus::Handled,
+                    Some(Err(e)) => {
+                        Err(e).handle_error()?;
+                        HandlerStatus::Handled
+                    }
+                    Some(Ok(true)) => HandlerStatus::NeedsOK,
+                    Some(Ok(false)) => return Err(Error::NonFatalError(22)),
+                }
+            }
+            ext::Base::z(cmd) => {
+                let addr = <T::Arch as Arch>::Usize::from_be_bytes(cmd.addr)
+                    .ok_or(Error::TargetMismatch)?;
+
+                use crate::target::ext::breakpoints::WatchKind::*;
+                let supported = match cmd.type_ {
+                    0 => (target.sw_breakpoint()).map(|op| op.remove_sw_breakpoint(addr)),
+                    1 => (target.hw_breakpoint()).map(|op| op.remove_hw_breakpoint(addr)),
+                    2 => (target.hw_watchpoint()).map(|op| op.remove_hw_watchpoint(addr, Write)),
+                    3 => (target.hw_watchpoint()).map(|op| op.remove_hw_watchpoint(addr, Read)),
+                    4 => {
+                        (target.hw_watchpoint()).map(|op| op.remove_hw_watchpoint(addr, ReadWrite))
+                    }
+                    // only 5 types in the protocol
+                    _ => None,
+                };
+
+                match supported {
+                    None => HandlerStatus::Handled,
+                    Some(Err(e)) => {
+                        Err(e).handle_error()?;
+                        HandlerStatus::Handled
+                    }
+                    Some(Ok(true)) => HandlerStatus::NeedsOK,
+                    Some(Ok(false)) => return Err(Error::NonFatalError(22)),
+                }
+            }
+            ext::Base::p(p) => {
+                let mut dst = [0u8; 32]; // enough for 256-bit registers
+                let reg = <T::Arch as Arch>::RegId::from_raw_id(p.reg_id);
+                let (reg_id, reg_size) = match reg {
+                    Some(v) => v,
+                    // empty packet indicates unrecognized query
+                    None => return Ok(HandlerStatus::Handled),
+                };
+                let dst = &mut dst[0..reg_size];
+                match target.base_ops() {
+                    BaseOps::SingleThread(ops) => ops.read_register(reg_id, dst),
+                    BaseOps::MultiThread(ops) => {
+                        ops.read_register(reg_id, dst, self.current_mem_tid)
+                    }
+                }
+                .handle_error()?;
+
+                res.write_hex_buf(dst)?;
+                HandlerStatus::Handled
+            }
+            ext::Base::P(p) => {
+                let reg = <T::Arch as Arch>::RegId::from_raw_id(p.reg_id);
+                match reg {
+                    None => return Err(Error::NonFatalError(22)),
+                    Some((reg_id, _)) => match target.base_ops() {
+                        BaseOps::SingleThread(ops) => ops.write_register(reg_id, p.val),
+                        BaseOps::MultiThread(ops) => {
+                            ops.write_register(reg_id, p.val, self.current_mem_tid)
+                        }
+                    }
+                    .handle_error()?,
+                }
+                HandlerStatus::NeedsOK
+            }
+            ext::Base::vCont(cmd) => {
+                use crate::protocol::commands::_vCont::{vCont, VContKind};
+
+                let actions = match cmd {
+                    vCont::Query => {
+                        res.write_str("vCont;c;C;s;S")?;
+                        return Ok(HandlerStatus::Handled);
+                    }
+                    vCont::Actions(actions) => actions,
+                };
+
+                // map raw vCont action iterator to a format the `Target` expects
+                let mut err = Ok(());
+                let mut actions = actions.into_iter().filter_map(|action| {
+                    let action = match action {
+                        Some(action) => action,
+                        None => {
+                            err = Err(Error::PacketParse(
+                                crate::protocol::PacketParseError::MalformedCommand,
+                            ));
+                            return None;
+                        }
+                    };
+
+                    let resume_action = match action.kind {
+                        VContKind::Step => ResumeAction::Step,
+                        VContKind::Continue => ResumeAction::Continue,
+                        _ => {
+                            // there seems to be a GDB bug where it doesn't use `vCont` unless
+                            // `vCont?` returns support for resuming with a signal.
+                            //
+                            // This error case can be removed once "Resume with Signal" is
+                            // implemented
+                            err = Err(Error::ResumeWithSignalUnimplemented);
+                            return None;
+                        }
+                    };
+
+                    let tid = match action.thread {
+                        Some(thread) => match thread.tid {
+                            IdKind::Any => {
+                                err = Err(Error::PacketUnexpected);
+                                return None;
+                            }
+                            IdKind::All => TidSelector::All,
+                            IdKind::WithID(tid) => TidSelector::WithID(tid),
+                        },
+                        // An action with no thread-id matches all threads
+                        None => TidSelector::All,
+                    };
+
+                    Some((tid, resume_action))
+                });
+
+                let ret = match self.do_vcont(res, target, &mut actions) {
+                    Ok(None) => HandlerStatus::Handled,
+                    Ok(Some(dc)) => HandlerStatus::Disconnect(dc),
+                    Err(e) => return Err(e),
+                };
+                err?;
+                ret
+            }
+            // TODO?: support custom resume addr in 'c' and 's'
+            ext::Base::c(_) => {
+                match self.do_vcont(
+                    res,
+                    target,
+                    &mut core::iter::once((self.current_resume_tid, ResumeAction::Continue)),
+                ) {
+                    Ok(None) => HandlerStatus::Handled,
+                    Ok(Some(dc)) => HandlerStatus::Disconnect(dc),
+                    Err(e) => return Err(e),
+                }
+            }
+            ext::Base::s(_) => {
+                match self.do_vcont(
+                    res,
+                    target,
+                    &mut core::iter::once((self.current_resume_tid, ResumeAction::Step)),
+                ) {
+                    Ok(None) => HandlerStatus::Handled,
+                    Ok(Some(dc)) => HandlerStatus::Disconnect(dc),
+                    Err(e) => return Err(e),
+                }
+            }
+
+            // ------------------- Multi-threading Support ------------------ //
+            ext::Base::H(cmd) => {
+                use crate::protocol::commands::_h_upcase::Op;
+                match cmd.kind {
+                    Op::Other => match cmd.thread.tid {
+                        IdKind::Any => {} // reuse old tid
+                        // "All" threads doesn't make sense for memory accesses
+                        IdKind::All => return Err(Error::PacketUnexpected),
+                        IdKind::WithID(tid) => self.current_mem_tid = tid,
+                    },
+                    // technically, this variant is deprecated in favor of vCont...
+                    Op::StepContinue => match cmd.thread.tid {
+                        IdKind::Any => {} // reuse old tid
+                        IdKind::All => self.current_resume_tid = TidSelector::All,
+                        IdKind::WithID(tid) => self.current_resume_tid = TidSelector::WithID(tid),
+                    },
+                }
+                HandlerStatus::NeedsOK
+            }
+            ext::Base::qfThreadInfo(_) => {
+                res.write_str("m")?;
+
+                match target.base_ops() {
+                    BaseOps::SingleThread(_) => res.write_thread_id(ThreadId {
+                        pid: Some(IdKind::WithID(FAKE_PID)),
+                        tid: IdKind::WithID(SINGLE_THREAD_TID),
+                    })?,
+                    BaseOps::MultiThread(ops) => {
+                        let mut err: Result<_, Error<T::Error, C::Error>> = Ok(());
+                        let mut first = true;
+                        ops.list_active_threads(&mut |tid| {
+                            // TODO: replace this with a try block (once stabilized)
+                            let e = (|| {
+                                if !first {
+                                    res.write_str(",")?
+                                }
+                                first = false;
+                                res.write_thread_id(ThreadId {
+                                    pid: Some(IdKind::WithID(FAKE_PID)),
+                                    tid: IdKind::WithID(tid),
+                                })?;
+                                Ok(())
+                            })();
+
+                            if let Err(e) = e {
+                                err = Err(e)
+                            }
+                        })
+                        .map_err(Error::TargetError)?;
+                        err?;
+                    }
+                }
+
+                HandlerStatus::Handled
+            }
+            ext::Base::qsThreadInfo(_) => {
+                res.write_str("l")?;
+                HandlerStatus::Handled
+            }
+            ext::Base::T(cmd) => {
+                let alive = match cmd.thread.tid {
+                    IdKind::WithID(tid) => match target.base_ops() {
+                        BaseOps::SingleThread(_) => tid == SINGLE_THREAD_TID,
+                        BaseOps::MultiThread(ops) => {
+                            ops.is_thread_alive(tid).map_err(Error::TargetError)?
+                        }
+                    },
+                    // TODO: double-check if GDB ever sends other variants
+                    // Even after ample testing, this arm has never been hit...
+                    _ => return Err(Error::PacketUnexpected),
+                };
+                if alive {
+                    HandlerStatus::NeedsOK
+                } else {
+                    // any error code will do
+                    return Err(Error::NonFatalError(1));
+                }
+            }
+        };
+        Ok(handler_status)
+    }
+
+    fn handle_monitor_cmd<'a>(
+        &mut self,
+        res: &mut ResponseWriter<C>,
+        target: &mut T,
+        command: ext::MonitorCmd<'a>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let ops = match target.monitor_cmd() {
+            Some(ops) => ops,
+            None => return Ok(HandlerStatus::Handled),
+        };
+
+        let handler_status = match command {
+            ext::MonitorCmd::qRcmd(cmd) => {
+                crate::__dead_code_marker!("qRcmd", "impl");
+
+                let mut err: Result<_, Error<T::Error, C::Error>> = Ok(());
+                let mut callback = |msg: &[u8]| {
+                    // TODO: replace this with a try block (once stabilized)
+                    let e = (|| {
+                        let mut res = ResponseWriter::new(res.as_conn());
+                        res.write_str("O")?;
+                        res.write_hex_buf(msg)?;
+                        res.flush()?;
+                        Ok(())
+                    })();
+
+                    if let Err(e) = e {
+                        err = Err(e)
+                    }
+                };
+
+                ops.handle_monitor_cmd(cmd.hex_cmd, ConsoleOutput::new(&mut callback))
+                    .map_err(Error::TargetError)?;
+                err?;
+
+                HandlerStatus::NeedsOK
+            }
+        };
+
+        Ok(handler_status)
+    }
+
+    fn handle_section_offsets(
+        &mut self,
+        res: &mut ResponseWriter<C>,
+        target: &mut T,
+        command: ext::SectionOffsets,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let ops = match target.section_offsets() {
+            Some(ops) => ops,
+            None => return Ok(HandlerStatus::Handled),
+        };
+
+        let handler_status = match command {
+            ext::SectionOffsets::qOffsets(_cmd) => {
+                use crate::target::ext::section_offsets::Offsets;
+
+                crate::__dead_code_marker!("qOffsets", "impl");
+
+                match ops.get_section_offsets().map_err(Error::TargetError)? {
+                    Offsets::Sections { text, data, bss } => {
+                        res.write_str("Text=")?;
+                        res.write_num(text)?;
+
+                        res.write_str(";Data=")?;
+                        res.write_num(data)?;
+
+                        // "Note: while a Bss offset may be included in the response,
+                        // GDB ignores this and instead applies the Data offset to the Bss section."
+                        //
+                        // While this would suggest that it's OK to omit `Bss=` entirely, recent
+                        // versions of GDB seem to require that `Bss=` is present.
+                        //
+                        // See https://github.com/bminor/binutils-gdb/blob/master/gdb/remote.c#L4149-L4159
+                        let bss = bss.unwrap_or(data);
+                        res.write_str(";Bss=")?;
+                        res.write_num(bss)?;
+                    }
+                    Offsets::Segments { text_seg, data_seg } => {
+                        res.write_str("TextSeg=")?;
+                        res.write_num(text_seg)?;
+
+                        if let Some(data) = data_seg {
+                            res.write_str(";DataSeg=")?;
+                            res.write_num(data)?;
+                        }
+                    }
+                }
+                HandlerStatus::Handled
+            }
+        };
+
+        Ok(handler_status)
+    }
+
+    fn handle_extended_mode<'a>(
+        &mut self,
+        res: &mut ResponseWriter<C>,
+        target: &mut T,
+        command: ext::ExtendedMode<'a>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let ops = match target.extended_mode() {
+            Some(ops) => ops,
+            None => return Ok(HandlerStatus::Handled),
+        };
+
+        let handler_status = match command {
+            ext::ExtendedMode::ExclamationMark(_cmd) => {
+                ops.on_start().map_err(Error::TargetError)?;
+                HandlerStatus::NeedsOK
+            }
+            ext::ExtendedMode::R(_cmd) => {
+                ops.restart().map_err(Error::TargetError)?;
+                HandlerStatus::Handled
+            }
+            ext::ExtendedMode::vAttach(cmd) => {
+                ops.attach(cmd.pid).handle_error()?;
+
+                #[cfg(feature = "alloc")]
+                self.attached_pids.insert(cmd.pid, true);
+
+                // TODO: sends OK when running in Non-Stop mode
+                HandlerStatus::Handled
+            }
+            ext::ExtendedMode::vRun(cmd) => {
+                use crate::target::ext::extended_mode::Args;
+
+                let mut pid = ops
+                    .run(cmd.filename, Args::new(&mut cmd.args.into_iter()))
+                    .handle_error()?;
+
+                // on single-threaded systems, we'll ignore the provided PID and keep
+                // using the FAKE_PID.
+                if let BaseOps::SingleThread(_) = target.base_ops() {
+                    pid = FAKE_PID;
+                }
+
+                let _ = pid; // squelch warning on no_std targets
+                #[cfg(feature = "alloc")]
+                self.attached_pids.insert(pid, false);
+
+                // TODO: send a more descriptive stop packet?
+                res.write_str("S05")?;
+                HandlerStatus::Handled
+            }
+            // --------- ASLR --------- //
+            ext::ExtendedMode::QDisableRandomization(cmd) if ops.configure_aslr().is_some() => {
+                let ops = ops.configure_aslr().unwrap();
+                ops.cfg_aslr(cmd.value).handle_error()?;
+                HandlerStatus::NeedsOK
+            }
+            // --------- Environment --------- //
+            ext::ExtendedMode::QEnvironmentHexEncoded(cmd) if ops.configure_env().is_some() => {
+                let ops = ops.configure_env().unwrap();
+                ops.set_env(cmd.key, cmd.value).handle_error()?;
+                HandlerStatus::NeedsOK
+            }
+            ext::ExtendedMode::QEnvironmentUnset(cmd) if ops.configure_env().is_some() => {
+                let ops = ops.configure_env().unwrap();
+                ops.remove_env(cmd.key).handle_error()?;
+                HandlerStatus::NeedsOK
+            }
+            ext::ExtendedMode::QEnvironmentReset(_cmd) if ops.configure_env().is_some() => {
+                let ops = ops.configure_env().unwrap();
+                ops.reset_env().handle_error()?;
+                HandlerStatus::NeedsOK
+            }
+            // --------- Working Dir --------- //
+            ext::ExtendedMode::QSetWorkingDir(cmd) if ops.configure_working_dir().is_some() => {
+                let ops = ops.configure_working_dir().unwrap();
+                ops.cfg_working_dir(cmd.dir).handle_error()?;
+                HandlerStatus::NeedsOK
+            }
+            // --------- Startup Shell --------- //
+            ext::ExtendedMode::QStartupWithShell(cmd)
+                if ops.configure_startup_shell().is_some() =>
+            {
+                let ops = ops.configure_startup_shell().unwrap();
+                ops.cfg_startup_with_shell(cmd.value).handle_error()?;
+                HandlerStatus::NeedsOK
+            }
+            _ => HandlerStatus::Handled,
+        };
+
+        Ok(handler_status)
+    }
+
+    fn do_vcont(
+        &mut self,
+        res: &mut ResponseWriter<C>,
+        target: &mut T,
+        actions: &mut dyn Iterator<Item = (TidSelector, ResumeAction)>,
+    ) -> Result<Option<DisconnectReason>, Error<T::Error, C::Error>> {
+        let mut err = Ok(());
+
+        let mut check_gdb_interrupt = || match res.as_conn().peek() {
+            Ok(Some(0x03)) => true, // 0x03 is the interrupt byte
+            Ok(Some(_)) => false,   // it's nothing that can't wait...
+            Ok(None) => false,
+            Err(e) => {
+                err = Err(Error::ConnectionRead(e));
+                true // break ASAP if a connection error occurred
+            }
+        };
+
+        let stop_reason = match target.base_ops() {
+            BaseOps::SingleThread(ops) => ops
+                .resume(
+                    // TODO?: add a more descriptive error if vcont has multiple threads in
+                    // single-threaded mode?
+                    actions.next().ok_or(Error::PacketUnexpected)?.1,
+                    &mut check_gdb_interrupt,
+                )
+                .map_err(Error::TargetError)?
+                .into(),
+            BaseOps::MultiThread(ops) => ops
+                .resume(Actions::new(actions), &mut check_gdb_interrupt)
+                .map_err(Error::TargetError)?,
+        };
+
+        err?;
+
+        self.finish_vcont(stop_reason, res)
+    }
+
+    // DEVNOTE: `do_vcont` and `finish_vcont` could be merged into a single
+    // function, at the expense of slightly larger code. In the future, if the
+    // `vCont` machinery is re-written, there's no reason why the two functions
+    // couldn't be re-merged.
+
+    fn finish_vcont(
+        &mut self,
+        stop_reason: ThreadStopReason<<T::Arch as Arch>::Usize>,
+        res: &mut ResponseWriter<C>,
+    ) -> Result<Option<DisconnectReason>, Error<T::Error, C::Error>> {
+        match stop_reason {
+            ThreadStopReason::DoneStep | ThreadStopReason::GdbInterrupt => {
+                res.write_str("S05")?;
+                Ok(None)
+            }
+            ThreadStopReason::Signal(code) => {
+                res.write_str("S")?;
+                res.write_num(code)?;
+                Ok(None)
+            }
+            ThreadStopReason::Halted => {
+                res.write_str("W19")?; // SIGSTOP
+                Ok(Some(DisconnectReason::TargetHalted))
+            }
+            ThreadStopReason::SwBreak(tid)
+            | ThreadStopReason::HwBreak(tid)
+            | ThreadStopReason::Watch { tid, .. } => {
+                self.current_mem_tid = tid;
+                self.current_resume_tid = TidSelector::WithID(tid);
+
+                res.write_str("T05")?;
+
+                res.write_str("thread:")?;
+                res.write_thread_id(ThreadId {
+                    pid: Some(IdKind::WithID(FAKE_PID)),
+                    tid: IdKind::WithID(tid),
+                })?;
+                res.write_str(";")?;
+
+                match stop_reason {
+                    // don't include addr on sw/hw break
+                    ThreadStopReason::SwBreak(_) => res.write_str("swbreak:")?,
+                    ThreadStopReason::HwBreak(_) => res.write_str("hwbreak:")?,
+                    ThreadStopReason::Watch { kind, addr, .. } => {
+                        use crate::target::ext::breakpoints::WatchKind;
+                        match kind {
+                            WatchKind::Write => res.write_str("watch:")?,
+                            WatchKind::Read => res.write_str("rwatch:")?,
+                            WatchKind::ReadWrite => res.write_str("awatch:")?,
+                        }
+                        res.write_num(addr)?;
+                    }
+                    _ => unreachable!(),
+                };
+
+                res.write_str(";")?;
+                Ok(None)
+            }
+        }
+    }
+}
+
+use crate::target::ext::base::singlethread::StopReason;
+impl<U> From<StopReason<U>> for ThreadStopReason<U> {
+    fn from(st_stop_reason: StopReason<U>) -> ThreadStopReason<U> {
+        match st_stop_reason {
+            StopReason::DoneStep => ThreadStopReason::DoneStep,
+            StopReason::GdbInterrupt => ThreadStopReason::GdbInterrupt,
+            StopReason::Halted => ThreadStopReason::Halted,
+            StopReason::SwBreak => ThreadStopReason::SwBreak(SINGLE_THREAD_TID),
+            StopReason::HwBreak => ThreadStopReason::HwBreak(SINGLE_THREAD_TID),
+            StopReason::Watch { kind, addr } => ThreadStopReason::Watch {
+                tid: SINGLE_THREAD_TID,
+                kind,
+                addr,
+            },
+            StopReason::Signal(sig) => ThreadStopReason::Signal(sig),
+        }
+    }
+}
diff --git a/src/gdbstub_impl/target_result_ext.rs b/src/gdbstub_impl/target_result_ext.rs
new file mode 100644
index 0000000..386205b
--- /dev/null
+++ b/src/gdbstub_impl/target_result_ext.rs
@@ -0,0 +1,27 @@
+use crate::target::TargetError;
+use crate::GdbStubError;
+
+/// Extension trait to ease working with `TargetResult` in the GdbStub
+/// implementation.
+pub(super) trait TargetResultExt<V, T, C> {
+    /// Encapsulates the boilerplate associated with handling `TargetError`s,
+    /// such as bailing-out on Fatal errors, or returning response codes.
+    fn handle_error(self) -> Result<V, GdbStubError<T, C>>;
+}
+
+impl<V, T, C> TargetResultExt<V, T, C> for Result<V, TargetError<T>> {
+    fn handle_error(self) -> Result<V, GdbStubError<T, C>> {
+        let code = match self {
+            Ok(v) => return Ok(v),
+            Err(TargetError::Fatal(e)) => return Err(GdbStubError::TargetError(e)),
+            // Recoverable errors:
+            // Error code 121 corresponds to `EREMOTEIO` lol
+            Err(TargetError::NonFatal) => 121,
+            Err(TargetError::Errno(code)) => code,
+            #[cfg(feature = "std")]
+            Err(TargetError::Io(e)) => e.raw_os_error().unwrap_or(121) as u8,
+        };
+
+        Err(GdbStubError::NonFatalError(code))
+    }
+}
diff --git a/src/internal/be_bytes.rs b/src/internal/be_bytes.rs
new file mode 100644
index 0000000..aebc1d2
--- /dev/null
+++ b/src/internal/be_bytes.rs
@@ -0,0 +1,85 @@
+/// A trait for working with structs as big-endian byte arrays. Automatically
+/// implemented for all built-in signed/unsigned integers.
+pub trait BeBytes: Sized {
+    /// Write the memory representation of `self` as a byte array in
+    /// big-endian (network) byte order into the provided buffer.
+    #[allow(clippy::wrong_self_convention)]
+    fn to_be_bytes(self, buf: &mut [u8]) -> Option<usize>;
+
+    /// Parse `self` from a byte array in big-endian (network) byte order.
+    /// Returns None upon overflow.
+    fn from_be_bytes(buf: &[u8]) -> Option<Self>;
+}
+
+macro_rules! impl_to_be_bytes {
+    ($($num:ty)*) => {
+        $(
+            impl BeBytes for $num {
+                fn to_be_bytes(self, buf: &mut [u8]) -> Option<usize> {
+                    let len = core::mem::size_of::<$num>();
+                    if buf.len() < len {
+                        return None
+                    }
+                    buf[..len].copy_from_slice(&<$num>::to_be_bytes(self));
+                    Some(len)
+                }
+
+                fn from_be_bytes(buf: &[u8]) -> Option<Self> {
+                    let len = core::mem::size_of::<$num>();
+
+                    let buf = if buf.len() > len {
+                        let (extra, buf) = buf.split_at(buf.len() - len);
+                        if extra.iter().any(|&b| b != 0) {
+                            return None
+                        }
+                        buf
+                    } else {
+                        buf
+                    };
+
+                    let mut res: Self = 0;
+                    for b in buf.iter().copied() {
+                        let b: Self = b as Self;
+                        // `res <<= 8` causes the compiler to complain in the `u8` case
+                        res <<= 4;
+                        res <<= 4;
+                        res |= b;
+                    }
+
+                    Some(res)
+                }
+            }
+        )*
+    };
+}
+
+impl_to_be_bytes!(u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize);
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn basic() {
+        assert_eq!(
+            0x12345678,
+            BeBytes::from_be_bytes(&[0x12, 0x34, 0x56, 0x78]).unwrap()
+        )
+    }
+
+    #[test]
+    fn small() {
+        assert_eq!(
+            0x123456,
+            BeBytes::from_be_bytes(&[0x12, 0x34, 0x56]).unwrap()
+        )
+    }
+
+    #[test]
+    fn too_big() {
+        assert_eq!(
+            0x1234_u16,
+            BeBytes::from_be_bytes(&[0xde, 0xad, 0xbe, 0xef]).unwrap_or(0x1234)
+        )
+    }
+}
diff --git a/src/internal/dead_code_marker.rs b/src/internal/dead_code_marker.rs
new file mode 100644
index 0000000..a91ed34
--- /dev/null
+++ b/src/internal/dead_code_marker.rs
@@ -0,0 +1,37 @@
+/// NOTE: We don't have a proper black box in stable Rust. This is
+/// a workaround implementation, that may have a too big performance overhead,
+/// depending on operation, or it may fail to properly avoid having code
+/// optimized out. It is good enough that it is used by default.
+///
+/// A function that is opaque to the optimizer, to allow benchmarks to
+/// pretend to use outputs to assist in avoiding dead-code
+/// elimination.
+// copied from https://docs.rs/bencher/0.1.5/src/bencher/lib.rs.html#590-596
+#[cfg(feature = "__dead_code_marker")]
+pub fn black_box<T>(dummy: T) -> T {
+    unsafe {
+        let ret = core::ptr::read_volatile(&dummy);
+        core::mem::forget(dummy);
+        ret
+    }
+}
+
+/// If the block of code which contains this macro doesn't get dead code
+/// eliminated, this macro ensures that the resulting binary contains a
+/// easy-to-find static string with the format `"<$feature,$ctx>"`.
+///
+/// In `gdbstub`, this macro makes it easy to see if the Rust compiler was able
+/// to dead-code-eliminate the packet parsing / handling code associated with
+/// unimplemented protocol extensions.
+///
+/// e.g: if the target didn't implement the `MonitorCmd` extension, then running
+/// the unix command `strings <finalbinary> | grep "<qRcmd,"` should return no
+/// results.
+#[doc(hidden)]
+#[macro_export]
+macro_rules! __dead_code_marker {
+    ($feature:literal, $ctx:literal) => {
+        #[cfg(feature = "__dead_code_marker")]
+        crate::internal::dead_code_marker::black_box(concat!("<", $feature, ",", $ctx, ">"));
+    };
+}
diff --git a/src/internal/le_bytes.rs b/src/internal/le_bytes.rs
new file mode 100644
index 0000000..fa12499
--- /dev/null
+++ b/src/internal/le_bytes.rs
@@ -0,0 +1,85 @@
+/// A trait for working with structs as little-endian byte arrays. Automatically
+/// implemented for all built-in signed/unsigned integers.
+pub trait LeBytes: Sized {
+    /// Write the memory representation of `self` as a byte array in
+    /// little-endian byte order into the provided buffer.
+    #[allow(clippy::wrong_self_convention)]
+    fn to_le_bytes(self, buf: &mut [u8]) -> Option<usize>;
+
+    /// Parse `self` from a byte array in little-endian byte order.
+    /// Returns None upon overflow.
+    fn from_le_bytes(buf: &[u8]) -> Option<Self>;
+}
+
+macro_rules! impl_to_le_bytes {
+    ($($num:ty)*) => {
+        $(
+            impl LeBytes for $num {
+                fn to_le_bytes(self, buf: &mut [u8]) -> Option<usize> {
+                    let len = core::mem::size_of::<$num>();
+                    if buf.len() < len {
+                        return None
+                    }
+                    buf[..len].copy_from_slice(&<$num>::to_le_bytes(self));
+                    Some(len)
+                }
+
+                fn from_le_bytes(buf: &[u8]) -> Option<Self> {
+                    let len = core::mem::size_of::<$num>();
+
+                    let buf = if buf.len() > len {
+                        let (extra, buf) = buf.split_at(buf.len() - len);
+                        if extra.iter().any(|&b| b != 0) {
+                            return None
+                        }
+                        buf
+                    } else {
+                        buf
+                    };
+
+                    let mut res: Self = 0;
+                    for b in buf.iter().copied().rev() {
+                        let b: Self = b as Self;
+                        // `res <<= 8` causes the compiler to complain in the `u8` case
+                        res <<= 4;
+                        res <<= 4;
+                        res |= b;
+                    }
+
+                    Some(res)
+                }
+            }
+        )*
+    };
+}
+
+impl_to_le_bytes!(u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize);
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn basic() {
+        assert_eq!(
+            0x12345678,
+            LeBytes::from_le_bytes(&[0x78, 0x56, 0x34, 0x12]).unwrap()
+        )
+    }
+
+    #[test]
+    fn small() {
+        assert_eq!(
+            0x123456,
+            LeBytes::from_le_bytes(&[0x56, 0x34, 0x12]).unwrap()
+        )
+    }
+
+    #[test]
+    fn too_big() {
+        assert_eq!(
+            0x1234_u16,
+            LeBytes::from_le_bytes(&[0xde, 0xad, 0xbe, 0xef]).unwrap_or(0x1234)
+        )
+    }
+}
diff --git a/src/internal/mod.rs b/src/internal/mod.rs
new file mode 100644
index 0000000..5a7580a
--- /dev/null
+++ b/src/internal/mod.rs
@@ -0,0 +1,10 @@
+//! Types / traits which are not expected to be directly implemented by
+//! `gdbstub` users.
+
+mod be_bytes;
+mod le_bytes;
+
+pub use be_bytes::*;
+pub use le_bytes::*;
+
+pub(crate) mod dead_code_marker;
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..451d380
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,155 @@
+//! An ergonomic and easy-to-integrate implementation of the
+//! [GDB Remote Serial Protocol](https://sourceware.org/gdb/onlinedocs/gdb/Remote-Protocol.html#Remote-Protocol)
+//! in Rust, with full `#![no_std]` support.
+//!
+//! ## Feature flags
+//!
+//! By default, the `std` and `alloc` features are enabled.
+//!
+//! When using `gdbstub` in `#![no_std]` contexts, make sure to set
+//! `default-features = false`.
+//!
+//! - `alloc`
+//!     - Implement `Connection` for `Box<dyn Connection>`.
+//!     - Log outgoing packets via `log::trace!` (uses a heap-allocated output
+//!       buffer).
+//!     - Provide built-in implementations for certain protocol features:
+//!         - Use a heap-allocated packet buffer in `GdbStub` (if none is
+//!           provided via `GdbStubBuilder::with_packet_buffer`).
+//!         - (Monitor Command) Use a heap-allocated output buffer in
+//!           `ConsoleOutput`.
+//!         - (Extended Mode) Automatically track Attached/Spawned PIDs without
+//!           implementing `ExtendedMode::query_if_attached`.
+//! - `std` (implies `alloc`)
+//!     - Implement `Connection` for [`TcpStream`](std::net::TcpStream) and
+//!       [`UnixStream`](std::os::unix::net::UnixStream).
+//!     - Implement [`std::error::Error`] for `gdbstub::Error`.
+//!     - Add a `TargetError::Io` error variant to simplify I/O Error handling
+//!       from `Target` methods.
+//!
+//! ## Getting Started
+//!
+//! This section provides a brief overview of the key traits and types used in
+//! `gdbstub`, and walks though the basic steps required to integrate `gdbstub`
+//! into a project.
+//!
+//! It is **highly recommended** to take a look at some of the
+//! [**examples**](https://github.com/daniel5151/gdbstub/blob/master/README.md#examples)
+//! listed in the project README. In particular, the included
+//! [**`armv4t`**](https://github.com/daniel5151/gdbstub/tree/master/examples/armv4t)
+//! example implements most of `gdbstub`'s protocol extensions, and can be a
+//! valuable resource when getting up-and-running with `gdbstub`.
+//!
+//! ### The `Connection` Trait
+//!
+//! The [`Connection`] trait describes how `gdbstub` should communicate with the
+//! main GDB process.
+//!
+//! `Connection` is automatically implemented for common `std` types such as
+//! [`TcpStream`](std::net::TcpStream) and
+//! [`UnixStream`](std::os::unix::net::UnixStream). In `#![no_std]`
+//! environments, `Connection` must be manually implemented using whatever
+//! in-order, serial, byte-wise I/O the hardware has available (e.g:
+//! putchar/getchar over UART, an embedded TCP stack, etc.).
+//!
+//! A common way to start a remote debugging session is to wait for a GDB client
+//! to connect via TCP:
+//!
+//! ```rust
+//! use std::net::{TcpListener, TcpStream};
+//!
+//! fn wait_for_gdb_connection(port: u16) -> std::io::Result<TcpStream> {
+//!     let sockaddr = format!("localhost:{}", port);
+//!     eprintln!("Waiting for a GDB connection on {:?}...", sockaddr);
+//!     let sock = TcpListener::bind(sockaddr)?;
+//!     let (stream, addr) = sock.accept()?;
+//!
+//!     // Blocks until a GDB client connects via TCP.
+//!     // i.e: Running `target remote localhost:<port>` from the GDB prompt.
+//!
+//!     eprintln!("Debugger connected from {}", addr);
+//!     Ok(stream)
+//! }
+//! ```
+//!
+//! ### The `Target` Trait
+//!
+//! The [`Target`](target::Target) trait describes how to control and modify
+//! a system's execution state during a GDB debugging session, and serves as the
+//! primary bridge between `gdbstub`'s generic protocol implementation and a
+//! target's project/platform-specific code.
+//!
+//! For example: the `Target` trait includes a method called `read_registers()`,
+//! which the `GdbStub` calls whenever the GDB client queries the state of the
+//! target's registers.
+//!
+//! **`Target` is the most important trait in `gdbstub`, and must be implemented
+//! by anyone who uses the library!**
+//!
+//! Please refer to the [`target` module documentation](target) for in-depth
+//! instructions on implementing `Target`.
+//!
+//! ### Starting the debugging session using `GdbStub`
+//!
+//! Once a `Connection` has been established and `Target` has been all wired up,
+//! all that's left is to hand things off to [`GdbStub`] and let it do the rest!
+//!
+//! ```rust,ignore
+//! // Set-up a valid `Target`
+//! let mut target = MyTarget::new()?; // implements `Target`
+//!
+//! // Establish a `Connection`
+//! let connection: TcpStream = wait_for_gdb_connection(9001);
+//!
+//! // Create a new `GdbStub` using the established `Connection`.
+//! let mut debugger = GdbStub::new(connection);
+//!
+//! // Instead of taking ownership of the system, `GdbStub` takes a &mut, yielding
+//! // ownership back to the caller once the debugging session is closed.
+//! match debugger.run(&mut target) {
+//!     Ok(disconnect_reason) => match disconnect_reason {
+//!         DisconnectReason::Disconnect => println!("GDB client disconnected."),
+//!         DisconnectReason::TargetHalted => println!("Target halted!"),
+//!         DisconnectReason::Kill => println!("GDB client sent a kill command!"),
+//!     }
+//!     // Handle any target-specific errors
+//!     Err(GdbStubError::TargetError(e)) => {
+//!         println!("Target raised a fatal error: {:?}", e);
+//!         // e.g: re-enter the debugging session after "freezing" a system to
+//!         // conduct some post-mortem debugging
+//!         debugger.run(&mut target)?;
+//!     }
+//!     Err(e) => return Err(e.into())
+//! }
+//! ```
+
+#![cfg_attr(not(feature = "std"), no_std)]
+#![deny(missing_docs)]
+
+#[cfg(feature = "alloc")]
+extern crate alloc;
+
+#[macro_use]
+extern crate log;
+
+mod connection;
+mod gdbstub_impl;
+mod protocol;
+mod util;
+
+#[doc(hidden)]
+pub mod internal;
+
+pub mod arch;
+pub mod common;
+pub mod target;
+
+pub use connection::Connection;
+pub use gdbstub_impl::*;
+
+/// (Internal) The fake Tid that's used when running in single-threaded mode.
+// SAFETY: 1 is clearly non-zero.
+const SINGLE_THREAD_TID: common::Tid = unsafe { common::Tid::new_unchecked(1) };
+/// (Internal) The fake Pid reported to GDB (since `gdbstub` only supports
+/// debugging a single process).
+const FAKE_PID: common::Pid = unsafe { common::Pid::new_unchecked(1) };
diff --git a/src/protocol/commands.rs b/src/protocol/commands.rs
new file mode 100644
index 0000000..aa39ca8
--- /dev/null
+++ b/src/protocol/commands.rs
@@ -0,0 +1,150 @@
+use paste::paste;
+
+use crate::protocol::packet::PacketBuf;
+use crate::target::Target;
+
+pub(self) mod prelude {
+    pub use super::ParseCommand;
+    pub use crate::common::*;
+    pub use crate::protocol::common::*;
+    pub use crate::protocol::packet::PacketBuf;
+    pub use core::convert::{TryFrom, TryInto};
+}
+
+pub trait ParseCommand<'a>: Sized {
+    /// Try to parse a packet from the packet buffer.
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self>;
+}
+
+macro_rules! commands {
+    (
+        $(
+            $ext:ident $(use $lt:lifetime)? {
+                $($name:literal => $mod:ident::$command:ident$(<$lifetime:lifetime>)?,)*
+            }
+        )*
+    ) => {paste! {
+        $($(
+            #[allow(non_snake_case, non_camel_case_types)]
+            pub mod $mod;
+        )*)*
+
+        pub mod ext {
+            $(
+                #[allow(non_camel_case_types)]
+                pub enum [<$ext:camel>] $(<$lt>)? {
+                    $($command(super::$mod::$command<$($lifetime)?>),)*
+                }
+            )*
+        }
+
+        /// GDB commands
+        pub enum Command<'a> {
+            $(
+                [<$ext:camel>](ext::[<$ext:camel>]$(<$lt>)?),
+            )*
+            Unknown(&'a str),
+        }
+
+        impl<'a> Command<'a> {
+            pub fn from_packet(
+                target: &mut impl Target,
+                buf: PacketBuf<'a>
+            ) -> Result<Command<'a>, CommandParseError<'a>> {
+                if buf.as_body().is_empty() {
+                    return Err(CommandParseError::Empty);
+                }
+
+                let body = buf.as_body();
+
+                // This scoped extension trait enables using `base` as an
+                // `$ext`, even through the `base` method on `Target` doesn't
+                // return an Option.
+                trait Hack { fn base(&mut self) -> Option<()> { Some(()) } }
+                impl<T: Target> Hack for T {}
+
+                $(
+                if target.$ext().is_some() {
+                    // TODO?: use tries for more efficient longest prefix matching
+                    #[allow(clippy::string_lit_as_bytes)]
+                    match body {
+                        $(_ if body.starts_with($name.as_bytes()) => {
+                            crate::__dead_code_marker!($name, "prefix_match");
+
+                            let buf = buf.trim_start_body_bytes($name.len());
+                            let cmd = $mod::$command::from_packet(buf)
+                                .ok_or(CommandParseError::MalformedCommand($name))?;
+
+                            return Ok(
+                                Command::[<$ext:camel>](
+                                    ext::[<$ext:camel>]::$command(cmd)
+                                )
+                            )
+                        })*
+                        _ => {},
+                    }
+                }
+                )*
+
+                Ok(Command::Unknown(buf.into_body_str()))
+            }
+        }
+    }};
+}
+
+/// Command parse error
+// TODO?: add more granular errors to command parsing code
+pub enum CommandParseError<'a> {
+    Empty,
+    /// catch-all
+    MalformedCommand(&'a str),
+}
+
+commands! {
+    base use 'a {
+        "?" => question_mark::QuestionMark,
+        "c" => _c::c<'a>,
+        "D" => _d_upcase::D,
+        "g" => _g::g,
+        "G" => _g_upcase::G<'a>,
+        "H" => _h_upcase::H,
+        "k" => _k::k,
+        "m" => _m::m<'a>,
+        "M" => _m_upcase::M<'a>,
+        "p" => _p::p,
+        "P" => _p_upcase::P<'a>,
+        "qAttached" => _qAttached::qAttached,
+        "qfThreadInfo" => _qfThreadInfo::qfThreadInfo,
+        "QStartNoAckMode" => _QStartNoAckMode::QStartNoAckMode,
+        "qsThreadInfo" => _qsThreadInfo::qsThreadInfo,
+        "qSupported" => _qSupported::qSupported<'a>,
+        "qXfer:features:read" => _qXfer_features_read::qXferFeaturesRead,
+        "s" => _s::s<'a>,
+        "T" => _t_upcase::T,
+        "vCont" => _vCont::vCont<'a>,
+        "vKill" => _vKill::vKill,
+        "z" => _z::z<'a>,
+        "Z" => _z_upcase::Z<'a>,
+    }
+
+    extended_mode use 'a {
+        "!" => exclamation_mark::ExclamationMark,
+        "QDisableRandomization" => _QDisableRandomization::QDisableRandomization,
+        "QEnvironmentHexEncoded" => _QEnvironmentHexEncoded::QEnvironmentHexEncoded<'a>,
+        "QEnvironmentReset" => _QEnvironmentReset::QEnvironmentReset,
+        "QEnvironmentUnset" => _QEnvironmentUnset::QEnvironmentUnset<'a>,
+        "QSetWorkingDir" => _QSetWorkingDir::QSetWorkingDir<'a>,
+        "QStartupWithShell" => _QStartupWithShell::QStartupWithShell,
+        "R" => _r_upcase::R,
+        "vAttach" => _vAttach::vAttach,
+        "vRun" => _vRun::vRun<'a>,
+    }
+
+    monitor_cmd use 'a {
+        "qRcmd" => _qRcmd::qRcmd<'a>,
+    }
+
+    section_offsets {
+        "qOffsets" => _qOffsets::qOffsets,
+    }
+}
diff --git a/src/protocol/commands/_QDisableRandomization.rs b/src/protocol/commands/_QDisableRandomization.rs
new file mode 100644
index 0000000..8ed6a8b
--- /dev/null
+++ b/src/protocol/commands/_QDisableRandomization.rs
@@ -0,0 +1,18 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct QDisableRandomization {
+    pub value: bool,
+}
+
+impl<'a> ParseCommand<'a> for QDisableRandomization {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        let value = match body as &[u8] {
+            b":0" => false,
+            b":1" => true,
+            _ => return None,
+        };
+        Some(QDisableRandomization { value })
+    }
+}
diff --git a/src/protocol/commands/_QEnvironmentHexEncoded.rs b/src/protocol/commands/_QEnvironmentHexEncoded.rs
new file mode 100644
index 0000000..af64e9c
--- /dev/null
+++ b/src/protocol/commands/_QEnvironmentHexEncoded.rs
@@ -0,0 +1,29 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct QEnvironmentHexEncoded<'a> {
+    pub key: &'a [u8],
+    pub value: Option<&'a [u8]>,
+}
+
+impl<'a> ParseCommand<'a> for QEnvironmentHexEncoded<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+
+        let (key, value) = match body {
+            [b':', keyval @ ..] => {
+                let keyval = decode_hex_buf(keyval).ok()?;
+                let mut keyval = keyval.splitn(2, |b| *b == b'=');
+                let key = keyval.next()?;
+                let value = match keyval.next()? {
+                    [] => None,
+                    s => Some(s),
+                };
+                (key, value)
+            }
+            _ => return None,
+        };
+
+        Some(QEnvironmentHexEncoded { key, value })
+    }
+}
diff --git a/src/protocol/commands/_QEnvironmentReset.rs b/src/protocol/commands/_QEnvironmentReset.rs
new file mode 100644
index 0000000..454fce0
--- /dev/null
+++ b/src/protocol/commands/_QEnvironmentReset.rs
@@ -0,0 +1,13 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct QEnvironmentReset;
+
+impl<'a> ParseCommand<'a> for QEnvironmentReset {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        if !buf.into_body().is_empty() {
+            return None;
+        }
+        Some(QEnvironmentReset)
+    }
+}
diff --git a/src/protocol/commands/_QEnvironmentUnset.rs b/src/protocol/commands/_QEnvironmentUnset.rs
new file mode 100644
index 0000000..d890bc4
--- /dev/null
+++ b/src/protocol/commands/_QEnvironmentUnset.rs
@@ -0,0 +1,18 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct QEnvironmentUnset<'a> {
+    pub key: &'a [u8],
+}
+
+impl<'a> ParseCommand<'a> for QEnvironmentUnset<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        let key = match body {
+            [b':', key @ ..] => decode_hex_buf(key).ok()?,
+            _ => return None,
+        };
+
+        Some(QEnvironmentUnset { key })
+    }
+}
diff --git a/src/protocol/commands/_QSetWorkingDir.rs b/src/protocol/commands/_QSetWorkingDir.rs
new file mode 100644
index 0000000..ac85c99
--- /dev/null
+++ b/src/protocol/commands/_QSetWorkingDir.rs
@@ -0,0 +1,21 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct QSetWorkingDir<'a> {
+    pub dir: Option<&'a [u8]>,
+}
+
+impl<'a> ParseCommand<'a> for QSetWorkingDir<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        let dir = match body {
+            [b':', dir @ ..] => match decode_hex_buf(dir).ok()? {
+                [] => None,
+                s => Some(s as &[u8]),
+            },
+            _ => return None,
+        };
+
+        Some(QSetWorkingDir { dir })
+    }
+}
diff --git a/src/protocol/commands/_QStartNoAckMode.rs b/src/protocol/commands/_QStartNoAckMode.rs
new file mode 100644
index 0000000..0ec92dc
--- /dev/null
+++ b/src/protocol/commands/_QStartNoAckMode.rs
@@ -0,0 +1,13 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct QStartNoAckMode;
+
+impl<'a> ParseCommand<'a> for QStartNoAckMode {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        if !buf.into_body().is_empty() {
+            return None;
+        }
+        Some(QStartNoAckMode)
+    }
+}
diff --git a/src/protocol/commands/_QStartupWithShell.rs b/src/protocol/commands/_QStartupWithShell.rs
new file mode 100644
index 0000000..59cac2a
--- /dev/null
+++ b/src/protocol/commands/_QStartupWithShell.rs
@@ -0,0 +1,18 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct QStartupWithShell {
+    pub value: bool,
+}
+
+impl<'a> ParseCommand<'a> for QStartupWithShell {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        let value = match body as &[u8] {
+            b":0" => false,
+            b":1" => true,
+            _ => return None,
+        };
+        Some(QStartupWithShell { value })
+    }
+}
diff --git a/src/protocol/commands/_c.rs b/src/protocol/commands/_c.rs
new file mode 100644
index 0000000..64b0786
--- /dev/null
+++ b/src/protocol/commands/_c.rs
@@ -0,0 +1,20 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct c<'a> {
+    pub addr: Option<&'a [u8]>,
+}
+
+impl<'a> ParseCommand<'a> for c<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        if body.is_empty() {
+            return Some(c { addr: None });
+        }
+        let addr = match body {
+            [] => None,
+            _ => Some(decode_hex_buf(body).ok()? as &[u8]),
+        };
+        Some(c { addr })
+    }
+}
diff --git a/src/protocol/commands/_d_upcase.rs b/src/protocol/commands/_d_upcase.rs
new file mode 100644
index 0000000..8f047b6
--- /dev/null
+++ b/src/protocol/commands/_d_upcase.rs
@@ -0,0 +1,17 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct D {
+    pub pid: Option<Pid>,
+}
+
+impl<'a> ParseCommand<'a> for D {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        let pid = match body {
+            [b';', pid @ ..] => Some(Pid::new(decode_hex(pid).ok()?)?),
+            _ => None,
+        };
+        Some(D { pid })
+    }
+}
diff --git a/src/protocol/commands/_g.rs b/src/protocol/commands/_g.rs
new file mode 100644
index 0000000..a795424
--- /dev/null
+++ b/src/protocol/commands/_g.rs
@@ -0,0 +1,13 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct g;
+
+impl<'a> ParseCommand<'a> for g {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        if !buf.into_body().is_empty() {
+            return None;
+        }
+        Some(g)
+    }
+}
diff --git a/src/protocol/commands/_g_upcase.rs b/src/protocol/commands/_g_upcase.rs
new file mode 100644
index 0000000..68298ca
--- /dev/null
+++ b/src/protocol/commands/_g_upcase.rs
@@ -0,0 +1,14 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct G<'a> {
+    pub vals: &'a [u8],
+}
+
+impl<'a> ParseCommand<'a> for G<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        Some(G {
+            vals: decode_hex_buf(buf.into_body()).ok()?,
+        })
+    }
+}
diff --git a/src/protocol/commands/_h_upcase.rs b/src/protocol/commands/_h_upcase.rs
new file mode 100644
index 0000000..101d0d0
--- /dev/null
+++ b/src/protocol/commands/_h_upcase.rs
@@ -0,0 +1,31 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub enum Op {
+    StepContinue,
+    Other,
+}
+
+#[derive(Debug)]
+pub struct H {
+    pub kind: Op,
+    pub thread: ThreadId,
+}
+
+impl<'a> ParseCommand<'a> for H {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        if body.is_empty() {
+            return None;
+        }
+
+        let kind = match body[0] {
+            b'g' => Op::Other,
+            b'c' => Op::StepContinue,
+            _ => return None,
+        };
+        let thread: ThreadId = body[1..].try_into().ok()?;
+
+        Some(H { kind, thread })
+    }
+}
diff --git a/src/protocol/commands/_k.rs b/src/protocol/commands/_k.rs
new file mode 100644
index 0000000..4fd1fc2
--- /dev/null
+++ b/src/protocol/commands/_k.rs
@@ -0,0 +1,13 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct k;
+
+impl<'a> ParseCommand<'a> for k {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        if !buf.into_body().is_empty() {
+            return None;
+        }
+        Some(k)
+    }
+}
diff --git a/src/protocol/commands/_m.rs b/src/protocol/commands/_m.rs
new file mode 100644
index 0000000..f7570ad
--- /dev/null
+++ b/src/protocol/commands/_m.rs
@@ -0,0 +1,49 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct m<'a> {
+    pub addr: &'a [u8],
+    pub len: usize,
+
+    pub buf: &'a mut [u8],
+}
+
+impl<'a> ParseCommand<'a> for m<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        // the total packet buffer currently looks like:
+        //
+        // +------+--------------------+-------------------+-------+-----------------+
+        // | "$m" | addr (hex-encoded) | len (hex-encoded) | "#XX" | empty space ... |
+        // +------+--------------------+-------------------+-------+-----------------+
+        //
+        // Unfortunately, while `len` can be hex-decoded right here and now into a
+        // `usize`, `addr` corresponds to a Target::Arch::Usize, which requires holding
+        // on to a valid &[u8] reference into the buffer.
+        //
+        // While it's not _perfectly_ efficient, simply leaving the decoded addr in
+        // place and wasting a couple bytes is probably the easiest way to tackle this
+        // problem:
+        //
+        // +------+------------------+------------------------------------------------+
+        // | "$m" | addr (raw bytes) | usable buffer ...                              |
+        // +------+------------------+------------------------------------------------+
+
+        let (buf, body_range) = buf.into_raw_buf();
+        let body = &mut buf[body_range.start..];
+
+        // should return 3 slices: the addr (hex-encoded), len (hex-encoded), and the
+        // "rest" of the buffer
+        let mut body = body.split_mut(|b| *b == b',' || *b == b'#');
+
+        let addr = decode_hex_buf(body.next()?).ok()?;
+        let addr_len = addr.len();
+        let len = decode_hex(body.next()?).ok()?;
+
+        drop(body);
+
+        let (addr, buf) = buf.split_at_mut(body_range.start + addr_len);
+        let addr = &addr[b"$m".len()..];
+
+        Some(m { addr, len, buf })
+    }
+}
diff --git a/src/protocol/commands/_m_upcase.rs b/src/protocol/commands/_m_upcase.rs
new file mode 100644
index 0000000..49773aa
--- /dev/null
+++ b/src/protocol/commands/_m_upcase.rs
@@ -0,0 +1,25 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct M<'a> {
+    pub addr: &'a [u8],
+    pub len: usize,
+    pub val: &'a [u8],
+}
+
+impl<'a> ParseCommand<'a> for M<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+
+        let mut body = body.split_mut(|&b| b == b',' || b == b':');
+        let addr = decode_hex_buf(body.next()?).ok()?;
+        let len = decode_hex(body.next()?).ok()?;
+        let val = body.next()?;
+
+        Some(M {
+            addr,
+            len,
+            val: decode_hex_buf(val).ok()?,
+        })
+    }
+}
diff --git a/src/protocol/commands/_p.rs b/src/protocol/commands/_p.rs
new file mode 100644
index 0000000..08e13b5
--- /dev/null
+++ b/src/protocol/commands/_p.rs
@@ -0,0 +1,13 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct p {
+    pub reg_id: usize,
+}
+
+impl<'a> ParseCommand<'a> for p {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let reg_id = decode_hex(buf.into_body()).ok()?;
+        Some(p { reg_id })
+    }
+}
diff --git a/src/protocol/commands/_p_upcase.rs b/src/protocol/commands/_p_upcase.rs
new file mode 100644
index 0000000..dd721d5
--- /dev/null
+++ b/src/protocol/commands/_p_upcase.rs
@@ -0,0 +1,17 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct P<'a> {
+    pub reg_id: usize,
+    pub val: &'a [u8],
+}
+
+impl<'a> ParseCommand<'a> for P<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        let mut body = body.split_mut(|&b| b == b'=');
+        let reg_id = decode_hex(body.next()?).ok()?;
+        let val = decode_hex_buf(body.next()?).ok()?;
+        Some(P { reg_id, val })
+    }
+}
diff --git a/src/protocol/commands/_qAttached.rs b/src/protocol/commands/_qAttached.rs
new file mode 100644
index 0000000..01edf37
--- /dev/null
+++ b/src/protocol/commands/_qAttached.rs
@@ -0,0 +1,17 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct qAttached {
+    pub pid: Option<Pid>,
+}
+
+impl<'a> ParseCommand<'a> for qAttached {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        let pid = match body {
+            [b':', pid @ ..] => Some(Pid::new(decode_hex(pid).ok()?)?),
+            _ => None,
+        };
+        Some(qAttached { pid })
+    }
+}
diff --git a/src/protocol/commands/_qOffsets.rs b/src/protocol/commands/_qOffsets.rs
new file mode 100644
index 0000000..a66e701
--- /dev/null
+++ b/src/protocol/commands/_qOffsets.rs
@@ -0,0 +1,15 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct qOffsets;
+
+impl<'a> ParseCommand<'a> for qOffsets {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        crate::__dead_code_marker!("qOffsets", "from_packet");
+
+        if !buf.into_body().is_empty() {
+            return None;
+        }
+        Some(qOffsets)
+    }
+}
diff --git a/src/protocol/commands/_qRcmd.rs b/src/protocol/commands/_qRcmd.rs
new file mode 100644
index 0000000..7d2ec11
--- /dev/null
+++ b/src/protocol/commands/_qRcmd.rs
@@ -0,0 +1,21 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct qRcmd<'a> {
+    pub hex_cmd: &'a [u8],
+}
+
+impl<'a> ParseCommand<'a> for qRcmd<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        crate::__dead_code_marker!("qRcmd", "from_packet");
+
+        let body = buf.into_body();
+        match body {
+            [] => Some(qRcmd { hex_cmd: &[] }),
+            [b',', hex_cmd @ ..] => Some(qRcmd {
+                hex_cmd: decode_hex_buf(hex_cmd).ok()?,
+            }),
+            _ => None,
+        }
+    }
+}
diff --git a/src/protocol/commands/_qSupported.rs b/src/protocol/commands/_qSupported.rs
new file mode 100644
index 0000000..1c644ff
--- /dev/null
+++ b/src/protocol/commands/_qSupported.rs
@@ -0,0 +1,62 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct qSupported<'a> {
+    pub features: Features<'a>,
+}
+
+impl<'a> ParseCommand<'a> for qSupported<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        if body.is_empty() {
+            return None;
+        }
+
+        Some(qSupported {
+            features: Features(body),
+        })
+    }
+}
+
+#[derive(Debug)]
+pub struct Features<'a>(&'a [u8]);
+
+impl<'a> Features<'a> {
+    pub fn into_iter(self) -> impl Iterator<Item = Option<Feature<'a>>> + 'a {
+        self.0.split(|b| *b == b';').map(|s| match s.last() {
+            None => None,
+            Some(&c) if c == b'+' || c == b'-' || c == b'?' => Some(Feature {
+                name: s[..s.len() - 1].into(),
+                val: None,
+                status: match c {
+                    b'+' => FeatureSupported::Yes,
+                    b'-' => FeatureSupported::No,
+                    b'?' => FeatureSupported::Maybe,
+                    _ => return None,
+                },
+            }),
+            Some(_) => {
+                let mut parts = s.split(|b| *b == b'=');
+                Some(Feature {
+                    name: parts.next()?.into(),
+                    val: Some(parts.next()?.into()),
+                    status: FeatureSupported::Yes,
+                })
+            }
+        })
+    }
+}
+
+#[derive(Debug)]
+pub enum FeatureSupported {
+    Yes,
+    No,
+    Maybe,
+}
+
+#[derive(Debug)]
+pub struct Feature<'a> {
+    name: Bstr<'a>,
+    val: Option<Bstr<'a>>,
+    status: FeatureSupported,
+}
diff --git a/src/protocol/commands/_qXfer_features_read.rs b/src/protocol/commands/_qXfer_features_read.rs
new file mode 100644
index 0000000..6e6d377
--- /dev/null
+++ b/src/protocol/commands/_qXfer_features_read.rs
@@ -0,0 +1,29 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct qXferFeaturesRead {
+    pub offset: usize,
+    pub len: usize,
+}
+
+impl<'a> ParseCommand<'a> for qXferFeaturesRead {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+
+        if body.is_empty() {
+            return None;
+        }
+
+        let mut body = body.split(|b| *b == b':').skip(1);
+        let annex = body.next()?;
+        if annex != b"target.xml" {
+            return None;
+        }
+
+        let mut body = body.next()?.split(|b| *b == b',');
+        let offset = decode_hex(body.next()?).ok()?;
+        let len = decode_hex(body.next()?).ok()?;
+
+        Some(qXferFeaturesRead { offset, len })
+    }
+}
diff --git a/src/protocol/commands/_qfThreadInfo.rs b/src/protocol/commands/_qfThreadInfo.rs
new file mode 100644
index 0000000..eff541f
--- /dev/null
+++ b/src/protocol/commands/_qfThreadInfo.rs
@@ -0,0 +1,13 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct qfThreadInfo;
+
+impl<'a> ParseCommand<'a> for qfThreadInfo {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        if !buf.into_body().is_empty() {
+            return None;
+        }
+        Some(qfThreadInfo)
+    }
+}
diff --git a/src/protocol/commands/_qsThreadInfo.rs b/src/protocol/commands/_qsThreadInfo.rs
new file mode 100644
index 0000000..d4347ff
--- /dev/null
+++ b/src/protocol/commands/_qsThreadInfo.rs
@@ -0,0 +1,13 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct qsThreadInfo;
+
+impl<'a> ParseCommand<'a> for qsThreadInfo {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        if !buf.into_body().is_empty() {
+            return None;
+        }
+        Some(qsThreadInfo)
+    }
+}
diff --git a/src/protocol/commands/_r_upcase.rs b/src/protocol/commands/_r_upcase.rs
new file mode 100644
index 0000000..ff4ec49
--- /dev/null
+++ b/src/protocol/commands/_r_upcase.rs
@@ -0,0 +1,23 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct R;
+
+impl<'a> ParseCommand<'a> for R {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        crate::__dead_code_marker!("R", "from_packet");
+
+        // Technically speaking, the `R` packet does include a hex-encoded byte as well,
+        // but even the GDB docs mention that it's unused (heck, the source-level
+        // comments in the GDB client suggest no-one really knows what it's used for).
+        //
+        // We'll pay some lip-service to this requirement by checking the body's length,
+        // but we won't actually parse the number.
+        let body = buf.into_body();
+        if body.len() != 2 {
+            None
+        } else {
+            Some(R)
+        }
+    }
+}
diff --git a/src/protocol/commands/_s.rs b/src/protocol/commands/_s.rs
new file mode 100644
index 0000000..83fe4ce
--- /dev/null
+++ b/src/protocol/commands/_s.rs
@@ -0,0 +1,20 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct s<'a> {
+    pub addr: Option<&'a [u8]>,
+}
+
+impl<'a> ParseCommand<'a> for s<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        if body.is_empty() {
+            return Some(s { addr: None });
+        }
+        let addr = match body {
+            [] => None,
+            _ => Some(decode_hex_buf(body).ok()? as &[u8]),
+        };
+        Some(s { addr })
+    }
+}
diff --git a/src/protocol/commands/_t_upcase.rs b/src/protocol/commands/_t_upcase.rs
new file mode 100644
index 0000000..c7b48ef
--- /dev/null
+++ b/src/protocol/commands/_t_upcase.rs
@@ -0,0 +1,15 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct T {
+    pub thread: ThreadId,
+}
+
+impl<'a> ParseCommand<'a> for T {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        Some(T {
+            thread: body.try_into().ok()?,
+        })
+    }
+}
diff --git a/src/protocol/commands/_vAttach.rs b/src/protocol/commands/_vAttach.rs
new file mode 100644
index 0000000..6329552
--- /dev/null
+++ b/src/protocol/commands/_vAttach.rs
@@ -0,0 +1,19 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct vAttach {
+    pub pid: Pid,
+}
+
+impl<'a> ParseCommand<'a> for vAttach {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        crate::__dead_code_marker!("vAttach", "from_packet");
+
+        let body = buf.into_body();
+        let pid = match body {
+            [b';', pid @ ..] => Pid::new(decode_hex(pid).ok()?)?,
+            _ => return None,
+        };
+        Some(vAttach { pid })
+    }
+}
diff --git a/src/protocol/commands/_vCont.rs b/src/protocol/commands/_vCont.rs
new file mode 100644
index 0000000..6a077d7
--- /dev/null
+++ b/src/protocol/commands/_vCont.rs
@@ -0,0 +1,82 @@
+use super::prelude::*;
+
+// TODO: instead of parsing lazily when invoked, parse the strings into a
+// compressed binary representations that can be stuffed back into the packet
+// buffer, and return an iterator over the binary data that's _guaranteed_ to be
+// valid. This would clean up some of the code in the vCont handler.
+#[derive(Debug)]
+pub enum vCont<'a> {
+    Query,
+    Actions(Actions<'a>),
+}
+
+impl<'a> ParseCommand<'a> for vCont<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        match body as &[u8] {
+            b"?" => Some(vCont::Query),
+            _ => Some(vCont::Actions(Actions(body))),
+        }
+    }
+}
+
+/// A lazily evaluated iterator over the actions specified in a vCont packet.
+#[derive(Debug)]
+pub struct Actions<'a>(&'a mut [u8]);
+
+impl<'a> Actions<'a> {
+    pub fn into_iter(self) -> impl Iterator<Item = Option<VContAction<'a>>> + 'a {
+        self.0.split_mut(|b| *b == b';').skip(1).map(|act| {
+            let mut s = act.split_mut(|b| *b == b':');
+            let kind = s.next()?;
+            let thread = match s.next() {
+                Some(s) => Some(s.try_into().ok()?),
+                None => None,
+            };
+
+            Some(VContAction {
+                kind: VContKind::from_bytes(kind)?,
+                thread,
+            })
+        })
+    }
+}
+
+#[derive(Debug)]
+pub struct VContAction<'a> {
+    pub kind: VContKind<'a>,
+    pub thread: Option<ThreadId>,
+}
+
+#[derive(Debug)]
+pub enum VContKind<'a> {
+    Continue,
+    ContinueWithSig(u8),
+    RangeStep(&'a [u8], &'a [u8]),
+    Step,
+    StepWithSig(u8),
+    Stop,
+}
+
+impl<'a> VContKind<'a> {
+    fn from_bytes(s: &mut [u8]) -> Option<VContKind> {
+        use self::VContKind::*;
+
+        let res = match s {
+            [b'c'] => Continue,
+            [b's'] => Step,
+            [b't'] => Stop,
+            [b'C', sig @ ..] => ContinueWithSig(decode_hex(sig).ok()?),
+            [b'S', sig @ ..] => StepWithSig(decode_hex(sig).ok()?),
+            [b'r', range @ ..] => {
+                let mut range = range.split_mut(|b| *b == b',');
+                let start = decode_hex_buf(range.next()?).ok()?;
+                let end = decode_hex_buf(range.next()?).ok()?;
+                RangeStep(start, end)
+            }
+            _ => return None,
+        };
+
+        Some(res)
+    }
+}
diff --git a/src/protocol/commands/_vKill.rs b/src/protocol/commands/_vKill.rs
new file mode 100644
index 0000000..9378aad
--- /dev/null
+++ b/src/protocol/commands/_vKill.rs
@@ -0,0 +1,17 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct vKill {
+    pub pid: Pid,
+}
+
+impl<'a> ParseCommand<'a> for vKill {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        let pid = match body {
+            [b';', pid @ ..] => Pid::new(decode_hex(pid).ok()?)?,
+            _ => return None,
+        };
+        Some(vKill { pid })
+    }
+}
diff --git a/src/protocol/commands/_vRun.rs b/src/protocol/commands/_vRun.rs
new file mode 100644
index 0000000..7d3e1eb
--- /dev/null
+++ b/src/protocol/commands/_vRun.rs
@@ -0,0 +1,119 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct vRun<'a> {
+    pub filename: Option<&'a [u8]>,
+    pub args: Args<'a>,
+}
+
+#[derive(Debug)]
+pub struct Args<'a>(&'a mut [u8]);
+
+impl<'a> Args<'a> {
+    pub fn into_iter(self) -> impl Iterator<Item = &'a [u8]> + 'a {
+        self.0
+            .split_mut(|b| *b == b';')
+            // the `from_packet` method guarantees that the args are valid hex ascii, so this should
+            // method should never fail.
+            .map(|raw| decode_hex_buf(raw).unwrap_or(&mut []))
+            .map(|s| s as &[u8])
+            .filter(|s| !s.is_empty())
+    }
+}
+
+impl<'a> ParseCommand<'a> for vRun<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+
+        let mut body = body.splitn_mut(3, |b| *b == b';');
+
+        let _first_semi = body.next()?;
+        let filename = match decode_hex_buf(body.next()?).ok()? {
+            [] => None,
+            s => Some(s as &[u8]),
+        };
+        let args = body.next().unwrap_or(&mut []); // args are optional
+
+        // validate that args have valid hex encoding (with ';' delimiters).
+        // this removes all the error handling from the lazy `Args` iterator.
+        if args.iter().any(|b| !(is_hex(*b) || *b == b';')) {
+            return None;
+        }
+
+        Some(vRun {
+            filename,
+            args: Args(args),
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    macro_rules! test_buf {
+        ($bufname:ident, $body:literal) => {
+            let mut test = $body.to_vec();
+            let buf = PacketBuf::new_with_raw_body(&mut test).unwrap();
+            let $bufname = buf.trim_start_body_bytes(b"vRun".len());
+        };
+    }
+
+    #[test]
+    fn valid_vRun_foobarbaz() {
+        test_buf!(buf, b"vRun;;666f6f;626172;62617a");
+
+        let pkt = vRun::from_packet(buf).unwrap();
+        let args = pkt.args.into_iter().collect::<Vec<_>>();
+
+        assert_eq!(pkt.filename, None);
+        assert_eq!(args, &[b"foo", b"bar", b"baz"]);
+    }
+
+    #[test]
+    fn valid_vRun_noname() {
+        test_buf!(buf, b"vRun;");
+
+        let pkt = vRun::from_packet(buf).unwrap();
+        let args = pkt.args.into_iter().collect::<Vec<_>>();
+
+        assert_eq!(pkt.filename, None);
+        assert_eq!(args, &[] as &[&[u8]]);
+    }
+
+    #[test]
+    fn valid_vRun_noargs() {
+        test_buf!(buf, b"vRun;74657374");
+
+        let pkt = vRun::from_packet(buf).unwrap();
+        let args = pkt.args.into_iter().collect::<Vec<_>>();
+
+        assert_eq!(pkt.filename, Some(&b"test"[..]));
+        assert_eq!(args, &[] as &[&[u8]]);
+    }
+
+    #[test]
+    fn valid_vRun_args() {
+        test_buf!(buf, b"vRun;74657374;74657374");
+
+        let pkt = vRun::from_packet(buf).unwrap();
+        let args = pkt.args.into_iter().collect::<Vec<_>>();
+
+        assert_eq!(pkt.filename, Some(&b"test"[..]));
+        assert_eq!(args, &[b"test"]);
+    }
+
+    #[test]
+    fn invalid_vRun_args() {
+        test_buf!(buf, b"vRun;74657374;nothex");
+
+        assert!(vRun::from_packet(buf).is_none());
+    }
+
+    #[test]
+    fn invalid_vRun() {
+        test_buf!(buf, b"vRun;nothex;nothex");
+
+        assert!(vRun::from_packet(buf).is_none());
+    }
+}
diff --git a/src/protocol/commands/_z.rs b/src/protocol/commands/_z.rs
new file mode 100644
index 0000000..7b97dd2
--- /dev/null
+++ b/src/protocol/commands/_z.rs
@@ -0,0 +1,20 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct z<'a> {
+    pub type_: u8,
+    pub addr: &'a [u8],
+    pub kind: u8,
+}
+
+impl<'a> ParseCommand<'a> for z<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        let mut body = body.split_mut(|&b| b == b',');
+        let type_ = decode_hex(body.next()?).ok()?;
+        let addr = decode_hex_buf(body.next()?).ok()?;
+        let kind = decode_hex(body.next()?).ok()?;
+
+        Some(z { type_, addr, kind })
+    }
+}
diff --git a/src/protocol/commands/_z_upcase.rs b/src/protocol/commands/_z_upcase.rs
new file mode 100644
index 0000000..d794be2
--- /dev/null
+++ b/src/protocol/commands/_z_upcase.rs
@@ -0,0 +1,24 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct Z<'a> {
+    pub type_: u8,
+    pub addr: &'a [u8],
+    /// architecture dependent
+    pub kind: u8,
+    // TODO: Add support for breakpoint 'conds', 'persist', and 'cmds' feature
+}
+
+impl<'a> ParseCommand<'a> for Z<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        let mut body = body.split_mut(|&b| b == b',');
+        let type_ = decode_hex(body.next()?).ok()?;
+        let addr = decode_hex_buf(body.next()?).ok()?;
+        let kind = decode_hex(body.next()?).ok()?;
+
+        // TODO: properly parse 'conds', 'persist', and 'cmds' fields in 'Z' packets
+
+        Some(Z { type_, addr, kind })
+    }
+}
diff --git a/src/protocol/commands/exclamation_mark.rs b/src/protocol/commands/exclamation_mark.rs
new file mode 100644
index 0000000..f99252d
--- /dev/null
+++ b/src/protocol/commands/exclamation_mark.rs
@@ -0,0 +1,13 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct ExclamationMark;
+
+impl<'a> ParseCommand<'a> for ExclamationMark {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        if !buf.into_body().is_empty() {
+            return None;
+        }
+        Some(ExclamationMark)
+    }
+}
diff --git a/src/protocol/commands/question_mark.rs b/src/protocol/commands/question_mark.rs
new file mode 100644
index 0000000..c0cf6ce
--- /dev/null
+++ b/src/protocol/commands/question_mark.rs
@@ -0,0 +1,13 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct QuestionMark;
+
+impl<'a> ParseCommand<'a> for QuestionMark {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        if !buf.into_body().is_empty() {
+            return None;
+        }
+        Some(QuestionMark)
+    }
+}
diff --git a/src/protocol/common/hex.rs b/src/protocol/common/hex.rs
new file mode 100644
index 0000000..7094826
--- /dev/null
+++ b/src/protocol/common/hex.rs
@@ -0,0 +1,190 @@
+use num_traits::{CheckedAdd, CheckedMul, FromPrimitive, Zero};
+
+#[derive(Debug)]
+pub enum DecodeHexError {
+    NotAscii,
+    Empty,
+    Overflow,
+    InvalidOutput,
+}
+
+/// Decode a GDB dex string into the specified integer.
+///
+/// GDB hex strings may include "xx", which represent "missing" data. This
+/// method simply treats "xx" as 0x00.
+pub fn decode_hex<I>(buf: &[u8]) -> Result<I, DecodeHexError>
+where
+    I: FromPrimitive + Zero + CheckedAdd + CheckedMul,
+{
+    use DecodeHexError::*;
+
+    let radix = I::from_u8(16).ok_or(InvalidOutput)?;
+
+    if buf.is_empty() {
+        return Err(Empty);
+    }
+
+    let mut result = I::zero();
+
+    for &digit in buf {
+        let x = I::from_u8(ascii2byte(digit).ok_or(NotAscii)?).ok_or(InvalidOutput)?;
+        result = result.checked_mul(&radix).ok_or(Overflow)?;
+        result = result.checked_add(&x).ok_or(Overflow)?
+    }
+
+    Ok(result)
+}
+
+#[derive(Debug)]
+pub enum DecodeHexBufError {
+    NotAscii,
+}
+
+fn ascii2byte(c: u8) -> Option<u8> {
+    match c {
+        b'0'..=b'9' => Some(c - b'0'),
+        b'a'..=b'f' => Some(c - b'a' + 10),
+        b'A'..=b'F' => Some(c - b'A' + 10),
+        b'x' | b'X' => Some(0),
+        _ => None,
+    }
+}
+
+/// Check if the byte `c` is a valid GDB hex digit `[0-9][a-f][A-F][xX]`
+#[allow(clippy::match_like_matches_macro)]
+pub fn is_hex(c: u8) -> bool {
+    match c {
+        b'0'..=b'9' => true,
+        b'a'..=b'f' => true,
+        b'A'..=b'F' => true,
+        b'x' | b'X' => true,
+        _ => false,
+    }
+}
+
+/// Decode a GDB hex string into a byte slice _in place_.
+///
+/// GDB hex strings may include "xx", which represent "missing" data. This
+/// method simply treats "xx" as 0x00.
+// TODO: maybe don't blindly translate "xx" as 0x00?
+// TODO: rewrite this method to elide bound checks
+pub fn decode_hex_buf(base_buf: &mut [u8]) -> Result<&mut [u8], DecodeHexBufError> {
+    use DecodeHexBufError::*;
+
+    let odd_adust = base_buf.len() % 2;
+    if odd_adust != 0 {
+        base_buf[0] = ascii2byte(base_buf[0]).ok_or(NotAscii)?;
+    }
+    let buf = &mut base_buf[odd_adust..];
+
+    let decoded_len = buf.len() / 2;
+    for i in 0..decoded_len {
+        let b = ascii2byte(buf[i * 2]).ok_or(NotAscii)? << 4
+            | ascii2byte(buf[i * 2 + 1]).ok_or(NotAscii)?;
+        buf[i] = b as u8;
+    }
+
+    Ok(&mut base_buf[..decoded_len + odd_adust])
+}
+
+#[allow(dead_code)]
+#[derive(Debug)]
+pub enum EncodeHexBufError {
+    SmallBuffer,
+}
+
+/// Encode a GDB hex string into a byte slice _in place_.
+///
+/// The data to be encoded should be copied into the buffer from
+/// `buf[start_idx..]`. The buffer itself must be at least `data.len() * 2`
+/// bytes in size, as each byte is expanded into a two byte hex string.
+#[allow(dead_code)]
+pub fn encode_hex_buf(buf: &mut [u8], start_idx: usize) -> Result<&mut [u8], EncodeHexBufError> {
+    use EncodeHexBufError::*;
+
+    let len = buf.len() - start_idx;
+    let encoded_len = len * 2;
+
+    if buf.len() < encoded_len {
+        return Err(SmallBuffer);
+    }
+
+    for i in 0..encoded_len {
+        let byte = buf[start_idx + i / 2];
+        let nybble = if i % 2 == 0 {
+            // high
+            (byte & 0xf0) >> 4
+        } else {
+            // low
+            byte & 0x0f
+        };
+
+        buf[i] = match nybble {
+            0x0..=0x9 => b'0' + nybble,
+            0xa..=0xf => b'A' + (nybble - 0xa),
+            _ => unreachable!(), // could be unreachable_unchecked...
+        };
+    }
+
+    Ok(&mut buf[..encoded_len])
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn encode_hex_simple() {
+        let payload = [0xde, 0xad, 0xbe, 0xef];
+        let mut buf = [0; 16];
+
+        let start_idx = buf.len() - payload.len();
+
+        // copy the payload into the buffer
+        buf[start_idx..].copy_from_slice(&payload);
+        let out = encode_hex_buf(&mut buf, start_idx).unwrap();
+
+        assert_eq!(out, b"DEADBEEF");
+    }
+
+    #[test]
+    fn encode_hex_in_chunks() {
+        let payload = (0..=255).collect::<Vec<u8>>();
+        let mut out = Vec::new();
+
+        let mut buf = [0; 30];
+
+        for c in payload.chunks(15) {
+            let start_idx = buf.len() - c.len();
+
+            let data_buf = &mut buf[start_idx..];
+            data_buf[..c.len()].copy_from_slice(c);
+            out.extend_from_slice(encode_hex_buf(&mut buf, start_idx).unwrap());
+        }
+
+        let expect = (0..=255).map(|b| format!("{:02X?}", b)).collect::<String>();
+
+        assert_eq!(out, expect.as_bytes())
+    }
+
+    #[test]
+    fn decode_hex_buf_odd() {
+        let mut payload = b"ffffff4".to_vec();
+        let res = decode_hex_buf(&mut payload).unwrap();
+        assert_eq!(res, [0xf, 0xff, 0xff, 0xf4]);
+    }
+
+    #[test]
+    fn decode_hex_buf_2() {
+        let mut payload = b"12345".to_vec();
+        let res = decode_hex_buf(&mut payload).unwrap();
+        assert_eq!(res, [0x1, 0x23, 0x45]);
+    }
+
+    #[test]
+    fn decode_hex_buf_short() {
+        let mut payload = b"1".to_vec();
+        let res = decode_hex_buf(&mut payload).unwrap();
+        assert_eq!(res, [0x1]);
+    }
+}
diff --git a/src/protocol/common/mod.rs b/src/protocol/common/mod.rs
new file mode 100644
index 0000000..b9acdf9
--- /dev/null
+++ b/src/protocol/common/mod.rs
@@ -0,0 +1,29 @@
+mod hex;
+mod thread_id;
+
+pub use hex::*;
+pub use thread_id::*;
+
+/// Lightweight wrapper around `&[u8]` which denotes that the contained data is
+/// a ASCII string.
+#[derive(Debug)]
+#[repr(transparent)]
+pub struct Bstr<'a>(&'a [u8]);
+
+impl<'a> From<&'a [u8]> for Bstr<'a> {
+    fn from(s: &'a [u8]) -> Bstr<'a> {
+        Bstr(s)
+    }
+}
+
+impl<'a> From<Bstr<'a>> for &'a [u8] {
+    fn from(s: Bstr<'a>) -> &'a [u8] {
+        s.0
+    }
+}
+
+impl AsRef<[u8]> for Bstr<'_> {
+    fn as_ref(&self) -> &[u8] {
+        self.0
+    }
+}
diff --git a/src/protocol/common/thread_id.rs b/src/protocol/common/thread_id.rs
new file mode 100644
index 0000000..e5e6eb4
--- /dev/null
+++ b/src/protocol/common/thread_id.rs
@@ -0,0 +1,81 @@
+use core::convert::{TryFrom, TryInto};
+use core::num::NonZeroUsize;
+
+use super::decode_hex;
+
+/// Tid/Pid Selector.
+#[derive(PartialEq, Eq, Debug, Clone, Copy)]
+pub enum IdKind {
+    /// All threads (-1)
+    All,
+    /// Any thread (0)
+    Any,
+    /// Thread with specific ID (id > 0)
+    WithID(NonZeroUsize),
+}
+
+/// Unique Thread ID.
+#[derive(PartialEq, Eq, Debug, Clone, Copy)]
+pub struct ThreadId {
+    /// Process ID (may or may not be present).
+    pub pid: Option<IdKind>,
+    /// Thread ID.
+    pub tid: IdKind,
+}
+
+impl TryFrom<&[u8]> for ThreadId {
+    type Error = ();
+
+    fn try_from(s: &[u8]) -> Result<Self, ()> {
+        match s {
+            [b'p', s @ ..] => {
+                // p<pid>.<tid>
+                let mut s = s.split(|b| *b == b'.');
+                let pid: IdKind = s.next().ok_or(())?.try_into()?;
+                let tid: IdKind = match s.next() {
+                    Some(s) => s.try_into()?,
+                    None => IdKind::All, // sending only p<pid> is valid
+                };
+
+                Ok(ThreadId {
+                    pid: Some(pid),
+                    tid,
+                })
+            }
+            _ => {
+                // <tid>
+                let tid: IdKind = s.try_into()?;
+
+                Ok(ThreadId { pid: None, tid })
+            }
+        }
+    }
+}
+
+impl TryFrom<&[u8]> for IdKind {
+    type Error = ();
+
+    fn try_from(s: &[u8]) -> Result<Self, ()> {
+        Ok(match s {
+            b"-1" => IdKind::All,
+            b"0" => IdKind::Any,
+            id => IdKind::WithID(NonZeroUsize::new(decode_hex(id).map_err(drop)?).ok_or(())?),
+        })
+    }
+}
+
+impl TryFrom<&mut [u8]> for ThreadId {
+    type Error = ();
+
+    fn try_from(s: &mut [u8]) -> Result<Self, ()> {
+        Self::try_from(s as &[u8])
+    }
+}
+
+impl TryFrom<&mut [u8]> for IdKind {
+    type Error = ();
+
+    fn try_from(s: &mut [u8]) -> Result<Self, ()> {
+        Self::try_from(s as &[u8])
+    }
+}
diff --git a/src/protocol/console_output.rs b/src/protocol/console_output.rs
new file mode 100644
index 0000000..73157bc
--- /dev/null
+++ b/src/protocol/console_output.rs
@@ -0,0 +1,98 @@
+use core::fmt;
+
+#[cfg(feature = "alloc")]
+use alloc::vec::Vec;
+
+/// Helper struct to send console output to GDB.
+///
+/// The recommended way to interact with `ConsoleOutput` is through the provided
+/// [`output!`](macro.output.html) / [`outputln!`](macro.outputln.html) macros.
+///
+/// On resource constrained systems which might want to avoid using Rust's
+/// [fairly "heavy" formatting machinery](https://jamesmunns.com/blog/fmt-unreasonably-expensive/),
+/// the `write_raw()` method can be used to write raw data directly to the GDB
+/// console.
+///
+/// When the `alloc` feature is disabled, all output buffering is disabled, and
+/// each call to `output!` will automatically flush data over the Connection.
+// TODO: support user-provided output buffers for no-`alloc` environments.
+pub struct ConsoleOutput<'a> {
+    #[cfg(feature = "alloc")]
+    buf: Vec<u8>,
+    callback: &'a mut dyn FnMut(&[u8]),
+}
+
+impl<'a> fmt::Write for ConsoleOutput<'a> {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        self.write_raw(s.as_bytes());
+        Ok(())
+    }
+}
+
+impl<'a> ConsoleOutput<'a> {
+    pub(crate) fn new(callback: &'a mut dyn FnMut(&[u8])) -> ConsoleOutput<'a> {
+        ConsoleOutput {
+            #[cfg(feature = "alloc")]
+            buf: Vec::new(),
+            callback,
+        }
+    }
+
+    /// Write raw (non UTF-8) data to the GDB console.
+    pub fn write_raw(&mut self, bytes: &[u8]) {
+        cfg_if::cfg_if! {
+            if #[cfg(feature = "alloc")] {
+                self.buf.extend_from_slice(bytes);
+            } else {
+                (self.callback)(bytes);
+            }
+        }
+    }
+
+    /// Flush the internal output buffer.
+    ///
+    /// Only available when `alloc` is enabled.
+    #[cfg(feature = "alloc")]
+    pub fn flush(&mut self) {
+        if !self.buf.is_empty() {
+            (self.callback)(&self.buf);
+            self.buf.clear()
+        }
+    }
+}
+
+impl Drop for ConsoleOutput<'_> {
+    fn drop(&mut self) {
+        #[cfg(feature = "alloc")]
+        self.flush()
+    }
+}
+
+/// Send formatted data to the GDB client console.
+///
+/// The first argument must be a [`ConsoleWriter`](struct.ConsoleWriter.html).
+#[macro_export]
+macro_rules! output {
+    ($console_output:expr, $($args:tt)*) => {{
+        use std::fmt::Write;
+        let _ = writeln!($console_output, $($args)*);
+    }};
+}
+
+/// Send formatted data to the GDB client console, with a newline appended.
+///
+/// The first argument must be a [`ConsoleWriter`](struct.ConsoleWriter.html).
+#[macro_export]
+macro_rules! outputln {
+    ($console_output:expr) => {{
+        use core::fmt::Write;
+        let _ = writeln!($console_output);
+    }};
+    ($console_output:expr,) => {
+        outputln!($console_output)
+    };
+    ($console_output:expr, $($args:tt)*) => {{
+        use core::fmt::Write;
+        let _ = writeln!($console_output, $($args)*);
+    }};
+}
diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs
new file mode 100644
index 0000000..fe09d57
--- /dev/null
+++ b/src/protocol/mod.rs
@@ -0,0 +1,14 @@
+mod common;
+mod console_output;
+mod packet;
+mod response_writer;
+
+pub(crate) mod commands;
+
+pub(crate) use common::{IdKind, ThreadId};
+pub(crate) use packet::Packet;
+pub(crate) use response_writer::{Error as ResponseWriterError, ResponseWriter};
+
+// These types end up a part of the public interface.
+pub use console_output::ConsoleOutput;
+pub use packet::PacketParseError;
diff --git a/src/protocol/packet.rs b/src/protocol/packet.rs
new file mode 100644
index 0000000..8b1d4e4
--- /dev/null
+++ b/src/protocol/packet.rs
@@ -0,0 +1,139 @@
+use crate::protocol::{commands::Command, common::decode_hex};
+use crate::target::Target;
+
+/// Packet parse error.
+#[derive(Debug)]
+pub enum PacketParseError {
+    ChecksumMismatched { checksum: u8, calculated: u8 },
+    EmptyBuf,
+    MissingChecksum,
+    MalformedChecksum,
+    MalformedCommand,
+    NotASCII,
+    UnexpectedHeader(u8),
+}
+
+/// Top-Level GDB packet
+pub enum Packet<'a> {
+    Ack,
+    Nack,
+    Interrupt,
+    Command(Command<'a>),
+}
+
+pub struct PacketBuf<'a> {
+    buf: &'a mut [u8],
+    body_range: core::ops::Range<usize>,
+}
+
+impl<'a> PacketBuf<'a> {
+    /// Validate the contents of the raw packet buffer, checking for checksum
+    /// consistency, structural correctness, and ASCII validation.
+    pub fn new(pkt_buf: &'a mut [u8]) -> Result<PacketBuf<'a>, PacketParseError> {
+        // validate the packet is valid ASCII
+        if !pkt_buf.is_ascii() {
+            return Err(PacketParseError::NotASCII);
+        }
+
+        let end_of_body = pkt_buf
+            .iter()
+            .position(|b| *b == b'#')
+            .ok_or(PacketParseError::MissingChecksum)?;
+
+        // split buffer into body and checksum components
+        let (body, checksum) = pkt_buf.split_at_mut(end_of_body);
+        let body = &mut body[1..]; // skip the '$'
+        let checksum = &mut checksum[1..][..2]; // skip the '#'
+
+        // validate the checksum
+        let checksum = decode_hex(checksum).map_err(|_| PacketParseError::MalformedChecksum)?;
+        let calculated = body.iter().fold(0u8, |a, x| a.wrapping_add(*x));
+        if calculated != checksum {
+            return Err(PacketParseError::ChecksumMismatched {
+                checksum,
+                calculated,
+            });
+        }
+
+        if log_enabled!(log::Level::Trace) {
+            // SAFETY: body confirmed to be `is_ascii()`
+            let body = unsafe { core::str::from_utf8_unchecked(body) };
+            trace!("<-- ${}#{:02x?}", body, checksum);
+        }
+
+        Ok(PacketBuf {
+            buf: pkt_buf,
+            body_range: 1..end_of_body,
+        })
+    }
+
+    /// (used for tests) Create a packet buffer from a raw body buffer, skipping
+    /// the header/checksum trimming stage. ASCII validation is still performed.
+    #[cfg(test)]
+    pub fn new_with_raw_body(body: &'a mut [u8]) -> Result<PacketBuf<'a>, PacketParseError> {
+        // validate the packet is valid ASCII
+        if !body.is_ascii() {
+            return Err(PacketParseError::NotASCII);
+        }
+
+        let len = body.len();
+        Ok(PacketBuf {
+            buf: body,
+            body_range: 0..len,
+        })
+    }
+
+    pub fn trim_start_body_bytes(self, n: usize) -> Self {
+        PacketBuf {
+            buf: self.buf,
+            body_range: (self.body_range.start + n)..self.body_range.end,
+        }
+    }
+
+    pub fn as_body(&'a self) -> &'a [u8] {
+        &self.buf[self.body_range.clone()]
+    }
+
+    /// Return a mut reference to slice of the packet buffer corresponding to
+    /// the current body.
+    pub fn into_body(self) -> &'a mut [u8] {
+        &mut self.buf[self.body_range]
+    }
+
+    pub fn into_body_str(self) -> &'a str {
+        // SAFETY: buffer confirmed to be `is_ascii()` in `new`, and no other PacketBuf
+        // member allow arbitrary modification of `self.buf`.
+        unsafe { core::str::from_utf8_unchecked(&self.buf[self.body_range]) }
+    }
+
+    /// Return a mut reference to the _entire_ underlying packet buffer, and the
+    /// current body's range.
+    #[allow(dead_code)]
+    pub fn into_raw_buf(self) -> (&'a mut [u8], core::ops::Range<usize>) {
+        (self.buf, self.body_range)
+    }
+}
+
+impl<'a> Packet<'a> {
+    pub fn from_buf(
+        target: &mut impl Target,
+        buf: &'a mut [u8],
+    ) -> Result<Packet<'a>, PacketParseError> {
+        // cannot have empty packet
+        if buf.is_empty() {
+            return Err(PacketParseError::EmptyBuf);
+        }
+
+        match buf[0] {
+            b'$' => Ok(Packet::Command(
+                Command::from_packet(target, PacketBuf::new(buf)?)
+                    // TODO?: preserve command parse error context
+                    .map_err(|_| PacketParseError::MalformedCommand)?,
+            )),
+            b'+' => Ok(Packet::Ack),
+            b'-' => Ok(Packet::Nack),
+            0x03 => Ok(Packet::Interrupt),
+            _ => Err(PacketParseError::UnexpectedHeader(buf[0])),
+        }
+    }
+}
diff --git a/src/protocol/response_writer.rs b/src/protocol/response_writer.rs
new file mode 100644
index 0000000..3dc5544
--- /dev/null
+++ b/src/protocol/response_writer.rs
@@ -0,0 +1,148 @@
+#[cfg(feature = "alloc")]
+use alloc::string::String;
+
+use num_traits::PrimInt;
+
+use crate::internal::BeBytes;
+use crate::protocol::{IdKind, ThreadId};
+use crate::Connection;
+
+/// Newtype around a Connection error. Having a newtype allows implementing a
+/// `From<ResponseWriterError<C>> for crate::Error<T, C>`, which greatly
+/// simplifies some of the error handling in the main gdbstub.
+#[derive(Debug, Clone)]
+pub struct Error<C>(C);
+
+/// A wrapper around [`Connection`] that computes the single-byte checksum of
+/// incoming / outgoing data.
+pub struct ResponseWriter<'a, C: Connection + 'a> {
+    inner: &'a mut C,
+    started: bool,
+    checksum: u8,
+    // buffer outgoing message
+    // TODO: add `write_all` method to Connection, and allow user to optionally pass outgoing
+    // packet buffer? This could improve performance (instead of writing a single byte at a time)
+    #[cfg(feature = "alloc")]
+    msg: String,
+}
+
+impl<'a, C: Connection + 'a> ResponseWriter<'a, C> {
+    /// Creates a new ResponseWriter
+    pub fn new(inner: &'a mut C) -> Self {
+        Self {
+            inner,
+            started: false,
+            checksum: 0,
+            #[cfg(feature = "alloc")]
+            msg: String::new(),
+        }
+    }
+
+    /// Consumes self, writing out the final '#' and checksum
+    pub fn flush(mut self) -> Result<(), Error<C::Error>> {
+        // don't include '#' in checksum calculation
+        let checksum = self.checksum;
+
+        #[cfg(feature = "alloc")]
+        trace!("--> ${}#{:02x?}", self.msg, checksum);
+
+        self.write(b'#')?;
+        self.write_hex(checksum)?;
+
+        Ok(())
+    }
+
+    /// Get a mutable reference to the underlying connection.
+    pub fn as_conn(&mut self) -> &mut C {
+        self.inner
+    }
+
+    /// Write a single byte.
+    pub fn write(&mut self, byte: u8) -> Result<(), Error<C::Error>> {
+        #[cfg(feature = "alloc")]
+        self.msg.push(byte as char);
+
+        if !self.started {
+            self.started = true;
+            self.inner.write(b'$').map_err(Error)?;
+        }
+
+        self.checksum = self.checksum.wrapping_add(byte);
+        self.inner.write(byte).map_err(Error)
+    }
+
+    /// Write an entire buffer over the connection.
+    pub fn write_all(&mut self, data: &[u8]) -> Result<(), Error<C::Error>> {
+        data.iter().try_for_each(|b| self.write(*b))
+    }
+
+    /// Write an entire string over the connection.
+    pub fn write_str(&mut self, s: &str) -> Result<(), Error<C::Error>> {
+        self.write_all(&s.as_bytes())
+    }
+
+    /// Write a single byte as a hex string (two ascii chars)
+    fn write_hex(&mut self, byte: u8) -> Result<(), Error<C::Error>> {
+        for digit in [(byte & 0xf0) >> 4, byte & 0x0f].iter() {
+            let c = match digit {
+                0..=9 => b'0' + digit,
+                10..=15 => b'a' + digit - 10,
+                _ => unreachable!(),
+            };
+            self.write(c)?;
+        }
+        Ok(())
+    }
+
+    /// Write a byte-buffer as a hex string (i.e: two ascii chars / byte).
+    pub fn write_hex_buf(&mut self, data: &[u8]) -> Result<(), Error<C::Error>> {
+        data.iter().try_for_each(|b| self.write_hex(*b))
+    }
+
+    /// Write data using the binary protocol.
+    pub fn write_binary(&mut self, data: &[u8]) -> Result<(), Error<C::Error>> {
+        data.iter().try_for_each(|b| match b {
+            b'#' | b'$' | b'}' | b'*' => {
+                self.write(b'}')?;
+                self.write(*b ^ 0x20)
+            }
+            _ => self.write(*b),
+        })
+    }
+
+    /// Write a number as a big-endian hex string using the most compact
+    /// representation possible (i.e: trimming leading zeros).
+    pub fn write_num<D: BeBytes + PrimInt>(&mut self, digit: D) -> Result<(), Error<C::Error>> {
+        if digit.is_zero() {
+            return self.write_hex(0);
+        }
+
+        let mut buf = [0; 16];
+        // infallible (unless digit is a >128 bit number)
+        let len = digit.to_be_bytes(&mut buf).unwrap();
+        let buf = &buf[..len];
+        buf.iter()
+            .copied()
+            .skip_while(|&b| b == 0)
+            .try_for_each(|b| self.write_hex(b))
+    }
+
+    pub fn write_id_kind(&mut self, tid: IdKind) -> Result<(), Error<C::Error>> {
+        match tid {
+            IdKind::All => self.write_str("-1")?,
+            IdKind::Any => self.write_str("0")?,
+            IdKind::WithID(id) => self.write_num(id.get())?,
+        };
+        Ok(())
+    }
+
+    pub fn write_thread_id(&mut self, tid: ThreadId) -> Result<(), Error<C::Error>> {
+        if let Some(pid) = tid.pid {
+            self.write_str("p")?;
+            self.write_id_kind(pid)?;
+            self.write_str(".")?;
+        }
+        self.write_id_kind(tid.tid)?;
+        Ok(())
+    }
+}
diff --git a/src/target/ext/base/mod.rs b/src/target/ext/base/mod.rs
new file mode 100644
index 0000000..3c3c293
--- /dev/null
+++ b/src/target/ext/base/mod.rs
@@ -0,0 +1,29 @@
+//! Base operations required to debug any target (read/write memory/registers,
+//! step/resume, etc...)
+//!
+//! While not strictly required, it's recommended that single threaded targets
+//! implement the simplified `singlethread` API.
+
+pub mod multithread;
+pub mod singlethread;
+
+/// Base operations for single/multi threaded targets.
+pub enum BaseOps<'a, A, E> {
+    /// Single-threaded target
+    SingleThread(&'a mut dyn singlethread::SingleThreadOps<Arch = A, Error = E>),
+    /// Multi-threaded target
+    MultiThread(&'a mut dyn multithread::MultiThreadOps<Arch = A, Error = E>),
+}
+
+/// Describes how the target should be resumed.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ResumeAction {
+    /// Continue execution (until the next event occurs).
+    Continue,
+    /// Step forward a single instruction.
+    Step,
+    /* ContinueWithSignal(u8),
+     * StepWithSignal(u8),
+     * Stop, // NOTE: won't be relevant until `gdbstub` supports non-stop mode
+     * StepInRange(core::ops::Range<U>), */
+}
diff --git a/src/target/ext/base/multithread.rs b/src/target/ext/base/multithread.rs
new file mode 100644
index 0000000..1da3c7c
--- /dev/null
+++ b/src/target/ext/base/multithread.rs
@@ -0,0 +1,256 @@
+//! Base debugging operations for multi threaded targets.
+
+use crate::arch::Arch;
+use crate::common::*;
+use crate::target::ext::breakpoints::WatchKind;
+use crate::target::{Target, TargetResult};
+
+// Convenient re-exports
+pub use super::ResumeAction;
+
+/// Selects a thread corresponding to a ResumeAction.
+// NOTE: this is a subset of the internal `IdKind` type, albeit without an `Any` variant. Selecting
+// `Any` thread is something that's handled by `gdbstub` internally, and shouldn't be exposed to the
+// end user.
+#[derive(PartialEq, Eq, Debug, Clone, Copy)]
+pub enum TidSelector {
+    /// Thread with a specific ID.
+    WithID(Tid),
+    /// All (other) threads.
+    All,
+}
+
+/// Base debugging operations for multi threaded targets.
+#[allow(clippy::type_complexity)]
+pub trait MultiThreadOps: Target {
+    /// Resume execution on the target.
+    ///
+    /// `actions` is an iterator over `(TidSelector, ResumeAction)` pairs which
+    /// specify how various threads should be resumed (i.e: single-step vs.
+    /// resume). It is _guaranteed_ to contain at least one action. It is not
+    /// guaranteed to be exhaustive over all live threads, and any threads
+    /// without a corresponding `TidSelector` should be left in the same state
+    /// (if possible).
+    ///
+    /// The `check_gdb_interrupt` callback can be invoked to check if GDB sent
+    /// an Interrupt packet (i.e: the user pressed Ctrl-C). It's recommended to
+    /// invoke this callback every-so-often while the system is running (e.g:
+    /// every X cycles/milliseconds). Periodically checking for incoming
+    /// interrupt packets is _not_ required, but it is _recommended_.
+    ///
+    /// # Implementation requirements
+    ///
+    /// These requirements cannot be satisfied by `gdbstub` internally, and must
+    /// be handled on a per-target basis.
+    ///
+    /// ### Adjusting PC after a breakpoint is hit
+    ///
+    /// The [GDB remote serial protocol documentation](https://sourceware.org/gdb/current/onlinedocs/gdb/Stop-Reply-Packets.html#swbreak-stop-reason)
+    /// notes the following:
+    ///
+    /// > On some architectures, such as x86, at the architecture level, when a
+    /// > breakpoint instruction executes the program counter points at the
+    /// > breakpoint address plus an offset. On such targets, the stub is
+    /// > responsible for adjusting the PC to point back at the breakpoint
+    /// > address.
+    ///
+    /// Omitting PC adjustment may result in unexpected execution flow and/or
+    /// breakpoints not appearing to work correctly.
+    ///
+    /// # Additional Considerations
+    ///
+    /// ### "Non-stop" mode
+    ///
+    /// At the moment, `gdbstub` only supports GDB's
+    /// ["All-Stop" mode](https://sourceware.org/gdb/current/onlinedocs/gdb/All_002dStop-Mode.html),
+    /// whereby _all_ threads should be stopped when returning from `resume`
+    /// (not just the thread associated with the `ThreadStopReason`).
+    ///
+    /// ### Bare-Metal Targets
+    ///
+    /// On bare-metal targets (such as microcontrollers or emulators), it's
+    /// common to treat individual _CPU cores_ as a separate "threads". e.g:
+    /// in a dual-core system, [CPU0, CPU1] might be mapped to [TID1, TID2]
+    /// (note that TIDs cannot be zero).
+    ///
+    /// In this case, the `Tid` argument of `read/write_addrs` becomes quite
+    /// relevant, as different cores may have different memory maps.
+    fn resume(
+        &mut self,
+        actions: Actions<'_>,
+        check_gdb_interrupt: &mut dyn FnMut() -> bool,
+    ) -> Result<ThreadStopReason<<Self::Arch as Arch>::Usize>, Self::Error>;
+
+    /// Read the target's registers.
+    ///
+    /// If the registers could not be accessed, an appropriate non-fatal error
+    /// should be returned.
+    fn read_registers(
+        &mut self,
+        regs: &mut <Self::Arch as Arch>::Registers,
+        tid: Tid,
+    ) -> TargetResult<(), Self>;
+
+    /// Write the target's registers.
+    ///
+    /// If the registers could not be accessed, an appropriate non-fatal error
+    /// should be returned.
+    fn write_registers(
+        &mut self,
+        regs: &<Self::Arch as Arch>::Registers,
+        tid: Tid,
+    ) -> TargetResult<(), Self>;
+
+    /// Read to a single register on the target.
+    ///
+    /// Implementations should write the value of the register using target's
+    /// native byte order in the buffer `dst`.
+    ///
+    /// If the requested register could not be accessed, an appropriate
+    /// non-fatal error should be returned.
+    ///
+    /// _Note:_ This method includes a stubbed default implementation which
+    /// simply returns `Ok(())`. This is due to the fact that several built-in
+    /// `arch` implementations haven't been updated with proper `RegId`
+    /// implementations.
+    fn read_register(
+        &mut self,
+        reg_id: <Self::Arch as Arch>::RegId,
+        dst: &mut [u8],
+        tid: Tid,
+    ) -> TargetResult<(), Self> {
+        let _ = (reg_id, dst, tid);
+        Ok(())
+    }
+
+    /// Write from a single register on the target.
+    ///
+    /// The `val` buffer contains the new value of the register in the target's
+    /// native byte order. It is guaranteed to be the exact length as the target
+    /// register.
+    ///
+    /// If the requested register could not be accessed, an appropriate
+    /// non-fatal error should be returned.
+    ///
+    /// _Note:_ This method includes a stubbed default implementation which
+    /// simply returns `Ok(())`. This is due to the fact that several built-in
+    /// `arch` implementations haven't been updated with proper `RegId`
+    /// implementations.
+    fn write_register(
+        &mut self,
+        reg_id: <Self::Arch as Arch>::RegId,
+        val: &[u8],
+        tid: Tid,
+    ) -> TargetResult<(), Self> {
+        let _ = (reg_id, val, tid);
+        Ok(())
+    }
+
+    /// Read bytes from the specified address range.
+    ///
+    /// If the requested address range could not be accessed (e.g: due to
+    /// MMU protection, unhanded page fault, etc...), an appropriate non-fatal
+    /// error should be returned.
+    fn read_addrs(
+        &mut self,
+        start_addr: <Self::Arch as Arch>::Usize,
+        data: &mut [u8],
+        tid: Tid,
+    ) -> TargetResult<(), Self>;
+
+    /// Write bytes to the specified address range.
+    ///
+    /// If the requested address range could not be accessed (e.g: due to
+    /// MMU protection, unhanded page fault, etc...), an appropriate non-fatal
+    /// error should be returned.
+    fn write_addrs(
+        &mut self,
+        start_addr: <Self::Arch as Arch>::Usize,
+        data: &[u8],
+        tid: Tid,
+    ) -> TargetResult<(), Self>;
+
+    /// List all currently active threads.
+    ///
+    /// See [the section above](#bare-metal-targets) on implementing
+    /// thread-related methods on bare-metal (threadless) targets.
+    fn list_active_threads(
+        &mut self,
+        thread_is_active: &mut dyn FnMut(Tid),
+    ) -> Result<(), Self::Error>;
+
+    /// Check if the specified thread is alive.
+    ///
+    /// As a convenience, this method provides a default implementation which
+    /// uses `list_active_threads` to do a linear-search through all active
+    /// threads. On thread-heavy systems, it may be more efficient
+    /// to override this method with a more direct query.
+    fn is_thread_alive(&mut self, tid: Tid) -> Result<bool, Self::Error> {
+        let mut found = false;
+        self.list_active_threads(&mut |active_tid| {
+            if tid == active_tid {
+                found = true;
+            }
+        })?;
+        Ok(found)
+    }
+}
+
+/// Describes why a thread stopped.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum ThreadStopReason<U> {
+    /// Completed the single-step request.
+    DoneStep,
+    /// `check_gdb_interrupt` returned `true`
+    GdbInterrupt,
+    /// Halted
+    Halted,
+    /// A thread hit a software breakpoint (e.g. due to a trap instruction).
+    ///
+    /// NOTE: This does not necessarily have to be a breakpoint configured by
+    /// the client/user of the current GDB session.
+    SwBreak(Tid),
+    /// A thread hit a hardware breakpoint.
+    HwBreak(Tid),
+    /// A thread hit a watchpoint.
+    Watch {
+        /// Which thread hit the watchpoint
+        tid: Tid,
+        /// Kind of watchpoint that was hit
+        kind: WatchKind,
+        /// Address of watched memory
+        addr: U,
+    },
+    /// The program received a signal
+    Signal(u8),
+}
+
+/// An iterator of `(TidSelector, ResumeAction)` used to specify how threads
+/// should be resumed when running in multi threaded mode. It is _guaranteed_ to
+/// contain at least one action.
+///
+/// See the documentation for
+/// [`Target::resume`](trait.Target.html#tymethod.resume) for more details.
+pub struct Actions<'a> {
+    inner: &'a mut dyn Iterator<Item = (TidSelector, ResumeAction)>,
+}
+
+impl core::fmt::Debug for Actions<'_> {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        write!(f, "Actions {{ .. }}")
+    }
+}
+
+impl Actions<'_> {
+    pub(crate) fn new(iter: &mut dyn Iterator<Item = (TidSelector, ResumeAction)>) -> Actions<'_> {
+        Actions { inner: iter }
+    }
+}
+
+impl Iterator for Actions<'_> {
+    type Item = (TidSelector, ResumeAction);
+    fn next(&mut self) -> Option<Self::Item> {
+        self.inner.next()
+    }
+}
diff --git a/src/target/ext/base/singlethread.rs b/src/target/ext/base/singlethread.rs
new file mode 100644
index 0000000..a92e1c4
--- /dev/null
+++ b/src/target/ext/base/singlethread.rs
@@ -0,0 +1,152 @@
+//! Base debugging operations for single threaded targets.
+
+use crate::arch::Arch;
+use crate::target::ext::breakpoints::WatchKind;
+use crate::target::{Target, TargetResult};
+
+// Convenient re-export
+pub use super::ResumeAction;
+
+/// Base debugging operations for single threaded targets.
+#[allow(clippy::type_complexity)]
+pub trait SingleThreadOps: Target {
+    /// Resume execution on the target.
+    ///
+    /// `action` specifies how the target should be resumed (i.e:
+    /// single-step vs. full continue).
+    ///
+    /// The `check_gdb_interrupt` callback can be invoked to check if GDB sent
+    /// an Interrupt packet (i.e: the user pressed Ctrl-C). It's recommended to
+    /// invoke this callback every-so-often while the system is running (e.g:
+    /// every X cycles/milliseconds). Periodically checking for incoming
+    /// interrupt packets is _not_ required, but it is _recommended_.
+    ///
+    /// # Implementation requirements
+    ///
+    /// These requirements cannot be satisfied by `gdbstub` internally, and must
+    /// be handled on a per-target basis.
+    ///
+    /// ### Adjusting PC after a breakpoint is hit
+    ///
+    /// The [GDB remote serial protocol documentation](https://sourceware.org/gdb/current/onlinedocs/gdb/Stop-Reply-Packets.html#swbreak-stop-reason)
+    /// notes the following:
+    ///
+    /// > On some architectures, such as x86, at the architecture level, when a
+    /// > breakpoint instruction executes the program counter points at the
+    /// > breakpoint address plus an offset. On such targets, the stub is
+    /// > responsible for adjusting the PC to point back at the breakpoint
+    /// > address.
+    ///
+    /// Omitting PC adjustment may result in unexpected execution flow and/or
+    /// breakpoints not appearing to work correctly.
+    fn resume(
+        &mut self,
+        action: ResumeAction,
+        check_gdb_interrupt: &mut dyn FnMut() -> bool,
+    ) -> Result<StopReason<<Self::Arch as Arch>::Usize>, Self::Error>;
+
+    /// Read the target's registers.
+    fn read_registers(
+        &mut self,
+        regs: &mut <Self::Arch as Arch>::Registers,
+    ) -> TargetResult<(), Self>;
+
+    /// Write the target's registers.
+    fn write_registers(&mut self, regs: &<Self::Arch as Arch>::Registers)
+        -> TargetResult<(), Self>;
+
+    /// Read to a single register on the target.
+    ///
+    /// Implementations should write the value of the register using target's
+    /// native byte order in the buffer `dst`.
+    ///
+    /// If the requested register could not be accessed, an appropriate
+    /// non-fatal error should be returned.
+    ///
+    /// _Note:_ This method includes a stubbed default implementation which
+    /// simply returns `Ok(())`. This is due to the fact that several built-in
+    /// `arch` implementations haven't been updated with proper `RegId`
+    /// implementations.
+    fn read_register(
+        &mut self,
+        reg_id: <Self::Arch as Arch>::RegId,
+        dst: &mut [u8],
+    ) -> TargetResult<(), Self> {
+        let _ = (reg_id, dst);
+        Ok(())
+    }
+
+    /// Write from a single register on the target.
+    ///
+    /// The `val` buffer contains the new value of the register in the target's
+    /// native byte order. It is guaranteed to be the exact length as the target
+    /// register.
+    ///
+    /// If the requested register could not be accessed, an appropriate
+    /// non-fatal error should be returned.
+    ///
+    /// _Note:_ This method includes a stubbed default implementation which
+    /// simply returns `Ok(())`. This is due to the fact that several built-in
+    /// `arch` implementations haven't been updated with proper `RegId`
+    /// implementations.
+    fn write_register(
+        &mut self,
+        reg_id: <Self::Arch as Arch>::RegId,
+        val: &[u8],
+    ) -> TargetResult<(), Self> {
+        let _ = (reg_id, val);
+        Ok(())
+    }
+
+    /// Read bytes from the specified address range.
+    ///
+    /// If the requested address range could not be accessed (e.g: due to
+    /// MMU protection, unhanded page fault, etc...), an appropriate
+    /// non-fatal error should be returned.
+    fn read_addrs(
+        &mut self,
+        start_addr: <Self::Arch as Arch>::Usize,
+        data: &mut [u8],
+    ) -> TargetResult<(), Self>;
+
+    /// Write bytes to the specified address range.
+    ///
+    /// If the requested address range could not be accessed (e.g: due to
+    /// MMU protection, unhanded page fault, etc...), an appropriate
+    /// non-fatal error should be returned.
+    fn write_addrs(
+        &mut self,
+        start_addr: <Self::Arch as Arch>::Usize,
+        data: &[u8],
+    ) -> TargetResult<(), Self>;
+}
+
+/// Describes why the target stopped.
+// NOTE: This is a simplified version of `multithread::ThreadStopReason` that omits any references
+// to Tid or threads. Internally, it is converted into multithread::ThreadStopReason.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum StopReason<U> {
+    /// Completed the single-step request.
+    DoneStep,
+    /// `check_gdb_interrupt` returned `true`
+    GdbInterrupt,
+    /// Halted
+    Halted,
+    /// Hit a software breakpoint (e.g. due to a trap instruction).
+    ///
+    /// NOTE: This does not necessarily have to be a breakpoint configured by
+    /// the client/user of the current GDB session.
+    SwBreak,
+    /// Hit a hardware breakpoint.
+    HwBreak,
+    /// Hit a watchpoint.
+    Watch {
+        /// Kind of watchpoint that was hit
+        kind: WatchKind,
+        /// Address of watched memory
+        addr: U,
+    },
+    /// The program received a signal
+    Signal(u8),
+}
diff --git a/src/target/ext/breakpoints.rs b/src/target/ext/breakpoints.rs
new file mode 100644
index 0000000..b39ed51
--- /dev/null
+++ b/src/target/ext/breakpoints.rs
@@ -0,0 +1,92 @@
+//! Add/Remove various kinds of breakpoints.
+
+use crate::arch::Arch;
+use crate::target::{Target, TargetResult};
+
+/// The kind of watchpoint that should be set/removed.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum WatchKind {
+    /// Fire when the memory location is written to.
+    Write,
+    /// Fire when the memory location is read from.
+    Read,
+    /// Fire when the memory location is written to and/or read from.
+    ReadWrite,
+}
+
+/// Target Extension - Set/remove Software Breakpoints.
+///
+/// See [this stackoverflow discussion](https://stackoverflow.com/questions/8878716/what-is-the-difference-between-hardware-and-software-breakpoints)
+/// about the differences between hardware and software breakpoints.
+///
+/// _Recommendation:_ If you're implementing `Target` for an emulator that's
+/// using an _interpreted_ CPU (as opposed to a JIT), the simplest way to
+/// implement "software" breakpoints would be to check the `PC` value after each
+/// CPU cycle.
+pub trait SwBreakpoint: Target {
+    /// Add a new software breakpoint.
+    /// Return `Ok(false)` if the operation could not be completed.
+    fn add_sw_breakpoint(&mut self, addr: <Self::Arch as Arch>::Usize) -> TargetResult<bool, Self>;
+
+    /// Remove an existing software breakpoint.
+    /// Return `Ok(false)` if the operation could not be completed.
+    fn remove_sw_breakpoint(
+        &mut self,
+        addr: <Self::Arch as Arch>::Usize,
+    ) -> TargetResult<bool, Self>;
+}
+
+define_ext!(SwBreakpointOps, SwBreakpoint);
+
+/// Target Extension - Set/remove Hardware Breakpoints.
+///
+/// See [this stackoverflow discussion](https://stackoverflow.com/questions/8878716/what-is-the-difference-between-hardware-and-software-breakpoints)
+/// about the differences between hardware and software breakpoints.
+///
+/// _Recommendation:_ If you're implementing `Target` for an emulator that's
+/// using an _interpreted_ CPU (as opposed to a JIT), there shouldn't be any
+/// reason to implement this extension (as software breakpoints are likely to be
+/// just-as-fast).
+pub trait HwBreakpoint: Target {
+    /// Add a new hardware breakpoint.
+    /// Return `Ok(false)` if the operation could not be completed.
+    fn add_hw_breakpoint(&mut self, addr: <Self::Arch as Arch>::Usize) -> TargetResult<bool, Self>;
+
+    /// Remove an existing hardware breakpoint.
+    /// Return `Ok(false)` if the operation could not be completed.
+    fn remove_hw_breakpoint(
+        &mut self,
+        addr: <Self::Arch as Arch>::Usize,
+    ) -> TargetResult<bool, Self>;
+}
+
+define_ext!(HwBreakpointOps, HwBreakpoint);
+
+/// Target Extension - Set/remove Hardware Watchpoints.
+///
+/// See the [GDB documentation](https://sourceware.org/gdb/current/onlinedocs/gdb/Set-Watchpoints.html)
+/// regarding watchpoints for how they're supposed to work.
+///
+/// _NOTE:_ If this extension isn't implemented, GDB will default to using
+/// _software watchpoints_, which tend to be excruciatingly slow (as
+/// they are implemented by single-stepping the system, and reading the
+/// watched memory location after each step).
+pub trait HwWatchpoint: Target {
+    /// Add a new hardware watchpoint.
+    /// Return `Ok(false)` if the operation could not be completed.
+    fn add_hw_watchpoint(
+        &mut self,
+        addr: <Self::Arch as Arch>::Usize,
+        kind: WatchKind,
+    ) -> TargetResult<bool, Self>;
+
+    /// Remove an existing hardware watchpoint.
+    /// Return `Ok(false)` if the operation could not be completed.
+    fn remove_hw_watchpoint(
+        &mut self,
+        addr: <Self::Arch as Arch>::Usize,
+        kind: WatchKind,
+    ) -> TargetResult<bool, Self>;
+}
+
+define_ext!(HwWatchpointOps, HwWatchpoint);
diff --git a/src/target/ext/extended_mode.rs b/src/target/ext/extended_mode.rs
new file mode 100644
index 0000000..843343f
--- /dev/null
+++ b/src/target/ext/extended_mode.rs
@@ -0,0 +1,257 @@
+//! Enables [Extended Mode](https://sourceware.org/gdb/current/onlinedocs/gdb/Connecting.html)
+//! functionality when connecting using `target extended-remote`, such as
+//! spawning new processes and/or attaching to existing processes.
+//!
+//! # Disclaimer
+//!
+//! While this API has been end-to-end tested and confirmed working with a "toy"
+//! target implementation (see the included `armv4t` example), it has _not_ been
+//! "battle-tested" with a fully-featured extended-mode capable target.
+//!
+//! If you end up using this API to implement an extended-mode capable target,
+//! _please_ file an issue on the repo detailing any bugs / usability issues you
+//! may encountered while implementing this API! If everything happens to Just
+//! Work as expected, nonetheless file an issue so that this disclaimer can be
+//! removed in future releases!
+
+use crate::common::*;
+use crate::target::{Target, TargetResult};
+
+/// Returned from `ExtendedMode::kill`
+///
+/// Retuning `ShouldTerminate::Yes` will cause the `GdbStub` to immediately
+/// shut down and return a `DisconnectReason::Kill`. Returning
+/// `ShouldTerminate::No` will keep the `GdbStub` running and listening for
+/// further run/attach requests.
+pub enum ShouldTerminate {
+    /// Terminate GdbStub
+    Yes,
+    /// Don't Terminate GdbStub
+    No,
+}
+
+impl From<ShouldTerminate> for bool {
+    fn from(st: ShouldTerminate) -> bool {
+        match st {
+            ShouldTerminate::Yes => true,
+            ShouldTerminate::No => false,
+        }
+    }
+}
+
+/// Describes how the target attached to a process.
+#[cfg(not(feature = "alloc"))]
+pub enum AttachKind {
+    /// It attached to an existing process.
+    Attach,
+    /// It spawned a new process.
+    Run,
+}
+
+#[cfg(not(feature = "alloc"))]
+impl AttachKind {
+    pub(crate) fn was_attached(self) -> bool {
+        match self {
+            AttachKind::Attach => true,
+            AttachKind::Run => false,
+        }
+    }
+}
+
+/// Target Extension - Support
+/// [Extended Mode](https://sourceware.org/gdb/current/onlinedocs/gdb/Connecting.html) functionality.
+///
+/// # Extended Mode for Single/Multi Threaded Targets
+///
+/// While extended-mode is primarily intended to be implemented by targets which
+/// support debugging multiple processes, there's no reason why a basic
+/// single/multi-threaded target can't implement these extensions as well.
+///
+/// For example, instead of "spawning" a process, the `run` command could be
+/// used to reset the execution state instead (e.g: resetting an emulator).
+pub trait ExtendedMode: Target {
+    /// Spawn and attach to the program `filename`, passing it the provided
+    /// `args` on its command line.
+    ///
+    /// The program is created in the stopped state.
+    ///
+    /// If no filename is provided, the stub may use a default program (e.g. the
+    /// last program run), or a non fatal error should be returned.
+    ///
+    /// `filename` and `args` are not guaranteed to be valid UTF-8, and are
+    /// passed as raw byte arrays. If the filenames/arguments could not be
+    /// converted into an appropriate representation, a non fatal error should
+    /// be returned.
+    ///
+    /// _Note:_ This method's implementation should handle any additional
+    /// configuration options set via the various `ConfigureXXX` extensions to
+    /// `ExtendedMode`. e.g: if the [`ConfigureEnv`](trait.ConfigureEnv.html)
+    /// extension is implemented and enabled, this method should set the spawned
+    /// processes' environment variables accordingly.
+    fn run(&mut self, filename: Option<&[u8]>, args: Args) -> TargetResult<Pid, Self>;
+
+    /// Attach to a new process with the specified PID.
+    ///
+    /// In all-stop mode, all threads in the attached process are stopped; in
+    /// non-stop mode, it may be attached without being stopped (if that is
+    /// supported by the target).
+    fn attach(&mut self, pid: Pid) -> TargetResult<(), Self>;
+
+    /// Query if specified PID was spawned by the target (via `run`), or if the
+    /// target attached to an existing process (via `attach`).
+    ///
+    /// This method is only required when the `alloc`/`std` features are
+    /// disabled. If `alloc` is available, `gdbstub` will automatically track
+    /// this property using a heap-allocated data structure.
+    #[cfg(not(feature = "alloc"))]
+    fn query_if_attached(&mut self, pid: Pid) -> TargetResult<AttachKind, Self>;
+
+    /// Called when the GDB client sends a Kill request.
+    ///
+    /// GDB may or may not specify a specific PID to kill. When no PID is
+    /// specified, the target is free to decide what to do (e.g: kill the
+    /// last-used pid, terminate the connection, etc...).
+    ///
+    /// If `ShouldTerminate::Yes` is returned, `GdbStub` will immediately stop
+    /// and return a `DisconnectReason::Kill`. Otherwise, the connection will
+    /// remain open, and `GdbStub` will continue listening for run/attach
+    /// requests.
+    fn kill(&mut self, pid: Option<Pid>) -> TargetResult<ShouldTerminate, Self>;
+
+    /// Restart the program being debugged.
+    ///
+    /// The GDB docs don't do a good job describing what a "restart" operation
+    /// entails. For reference, the official `gdbserver` seems to kill all
+    /// inferior processes, and then re-run whatever program was provided on the
+    /// command line (if one was provided).
+    ///
+    /// _Author's Note:_ Based on my current (as of Sept 2020) understanding of
+    /// the GDB client;s source code, it seems that the "R" packet is _never_
+    /// sent so-long as the target implements the "vRun" packet (which
+    /// corresponds to this trait's `run` method). As such, while `gdbstub`
+    /// exposes this functionality, and "requires" an implementation, unless
+    /// you're running a fairly old version of GDB, it should be fine to
+    /// simply stub it out -- e.g: using the `unimplemented!()` macro /
+    /// returning a fatal error.
+    fn restart(&mut self) -> Result<(), Self::Error>;
+
+    /// (optional) Invoked when GDB client switches to extended mode.
+    ///
+    /// The default implementation is a no-op.
+    ///
+    /// Target implementations can override this implementation if they need to
+    /// perform any operations once extended mode is activated.
+    fn on_start(&mut self) -> Result<(), Self::Error> {
+        Ok(())
+    }
+
+    /// Enable/Disable ASLR for spawned processes.
+    fn configure_aslr(&mut self) -> Option<ConfigureASLROps<Self>> {
+        None
+    }
+
+    /// Set/Remove/Reset Environment variables for spawned processes.
+    fn configure_env(&mut self) -> Option<ConfigureEnvOps<Self>> {
+        None
+    }
+
+    /// Configure if spawned processes should be spawned using a shell.
+    fn configure_startup_shell(&mut self) -> Option<ConfigureStartupShellOps<Self>> {
+        None
+    }
+
+    /// Configure the working directory for spawned processes.
+    fn configure_working_dir(&mut self) -> Option<ConfigureWorkingDirOps<Self>> {
+        None
+    }
+}
+
+define_ext!(ExtendedModeOps, ExtendedMode);
+
+/// Iterator of `args` passed to a spawned process (used in
+/// `ExtendedMode::run`)
+pub struct Args<'a, 'args> {
+    inner: &'a mut dyn Iterator<Item = &'args [u8]>,
+}
+
+impl core::fmt::Debug for Args<'_, '_> {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        write!(f, "Args {{ .. }}")
+    }
+}
+
+impl<'a, 'b> Args<'a, 'b> {
+    pub(crate) fn new(inner: &'a mut dyn Iterator<Item = &'b [u8]>) -> Args<'a, 'b> {
+        Args { inner }
+    }
+}
+
+impl<'args> Iterator for Args<'_, 'args> {
+    type Item = &'args [u8];
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.inner.next()
+    }
+}
+
+/// Enable/Disable ASLR for spawned processes (for a more consistent debugging
+/// experience).
+///
+/// Corresponds to GDB's [`set disable-randomization`](https://sourceware.org/gdb/onlinedocs/gdb/Starting.html) command.
+pub trait ConfigureASLR: ExtendedMode {
+    /// Enable/Disable ASLR for spawned processes.
+    fn cfg_aslr(&mut self, enabled: bool) -> TargetResult<(), Self>;
+}
+
+define_ext!(ConfigureASLROps, ConfigureASLR);
+
+/// Set/Remove/Reset the Environment variables for spawned processes.
+///
+/// Corresponds to GDB's [`set environment`](https://sourceware.org/gdb/onlinedocs/gdb/Environment.html#set-environment) cmd.
+///
+/// _Note:_ Environment variables are not guaranteed to be UTF-8, and are passed
+/// as raw byte arrays. If the provided keys/values could not be converted into
+/// an appropriate representation, a non fatal error should be returned.
+pub trait ConfigureEnv: ExtendedMode {
+    /// Set an environment variable.
+    fn set_env(&mut self, key: &[u8], val: Option<&[u8]>) -> TargetResult<(), Self>;
+
+    /// Remove an environment variable.
+    fn remove_env(&mut self, key: &[u8]) -> TargetResult<(), Self>;
+
+    /// Reset all environment variables to their initial state (i.e: undo all
+    /// previous `set/remove_env` calls).
+    fn reset_env(&mut self) -> TargetResult<(), Self>;
+}
+
+define_ext!(ConfigureEnvOps, ConfigureEnv);
+
+/// Configure if spawned processes should be spawned using a shell.
+///
+/// Corresponds to GDB's [`set startup-with-shell`](https://sourceware.org/gdb/onlinedocs/gdb/Starting.html) command.
+pub trait ConfigureStartupShell: ExtendedMode {
+    /// Configure if spawned processes should be spawned using a shell.
+    ///
+    /// On UNIX-like targets, it is possible to start the inferior using a shell
+    /// program. This is the default behavior on both `GDB` and `gdbserver`.
+    fn cfg_startup_with_shell(&mut self, enabled: bool) -> TargetResult<(), Self>;
+}
+
+define_ext!(ConfigureStartupShellOps, ConfigureStartupShell);
+
+/// Configure the working directory for spawned processes.
+///
+/// Corresponds to GDB's [`set cwd` and `cd`](https://sourceware.org/gdb/onlinedocs/gdb/Working-Directory.html) commands.
+pub trait ConfigureWorkingDir: ExtendedMode {
+    /// Set the working directory for spawned processes.
+    ///
+    /// If no directory is provided, the stub should reset the value to it's
+    /// original value.
+    ///
+    /// The path is not guaranteed to be valid UTF-8, and is passed as a raw
+    /// byte array. If the path could not be converted into an appropriate
+    /// representation, a non fatal error should be returned.
+    fn cfg_working_dir(&mut self, dir: Option<&[u8]>) -> TargetResult<(), Self>;
+}
+
+define_ext!(ConfigureWorkingDirOps, ConfigureWorkingDir);
diff --git a/src/target/ext/mod.rs b/src/target/ext/mod.rs
new file mode 100644
index 0000000..f3a0b54
--- /dev/null
+++ b/src/target/ext/mod.rs
@@ -0,0 +1,252 @@
+//! Extensions to [`Target`](super::Target) which add support for various
+//! subsets of the GDB Remote Serial Protocol.
+//!
+//! On it's own, the [`Target`](super::Target) trait doesn't actually include
+//! any methods to debug the target. Instead, `Target` uses a collection of
+//! "Inlineable Dyn Extension Traits" (IDETs) to optionally implement various
+//! subsets of the GDB protocol. For more details on IDETs, scroll down to the
+//! [How Protocol Extensions Work - Inlineable Dyn Extension Traits
+//! (IDETs)](#how-protocol-extensions-work---inlineable-dyn-extension-traits-idets)
+//! section below.
+//!
+//! As a starting point, consider implementing some of the extensions under
+//! [`breakpoints`]. For example, adding support for Software Breakpoints would
+//! require implementing the
+//! [`breakpoints::SwBreakpoint`](breakpoints::SwBreakpoint) extension, and
+//! overriding the `Target::sw_breakpoint` method to return `Some(self)`.
+//!
+//! ### Note: Missing Protocol Extensions
+//!
+//! `gdbstub`'s development is guided by the needs of it's contributors, with
+//! new features being added on an "as-needed" basis.
+//!
+//! If there's a GDB feature you need that hasn't been implemented yet, (e.g:
+//! remote filesystem access, tracepoint support, etc...), consider opening an
+//! issue / filing a PR on Github!
+//!
+//! Check out the [GDB Remote Configuration Docs](https://sourceware.org/gdb/onlinedocs/gdb/Remote-Configuration.html)
+//! for a table of GDB commands + their corresponding Remote Serial Protocol
+//! packets.
+//!
+//! ### Note: What's with all the `<Self::Arch as Arch>::` syntax?
+//!
+//! Many of the method signatures across the `Target` extension traits include
+//! some pretty gnarly type syntax.
+//!
+//! If [rust-lang/rust#38078](https://github.com/rust-lang/rust/issues/38078)
+//! gets fixed, then types like `<Self::Arch as Arch>::Foo` could be simplified
+//! to just `Self::Arch::Foo`. Until then, the much more explicit
+//! [fully qualified syntax](https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#fully-qualified-syntax-for-disambiguation-calling-methods-with-the-same-name)
+//! must be used instead.
+//!
+//! When you come across this syntax, it's highly recommended to use the
+//! concrete type instead. e.g: on a 32-bit target, instead of cluttering up
+//! the implementation with `<Self::Arch as Arch>::Usize`, just use `u32`
+//! directly.
+//!
+//! ## How Protocol Extensions Work - Inlineable Dyn Extension Traits (IDETs)
+//!
+//! The GDB protocol is massive, and contains all sorts of optional
+//! functionality. In previous versions of `gdbstub`, the `Target` trait would
+//! directly have a method for _every single protocol extension_, resulting in
+//! literally _hundreds_ of associated methods!
+//!
+//! This approach had numerous drawbacks:
+//!
+//!  - Implementations that did not implement all available protocol extensions
+//!    still had to "pay" for the unused packet parsing/handler code, resulting
+//!    in substantial code bloat, even on `no_std` platforms.
+//!  - Required the `GdbStub` implementation to include runtime checks to deal
+//!    with incorrectly implemented `Target`s.
+//!      - No way to enforce "mutually-dependent" trait methods at compile-time.
+//!          - e.g: When implementing hardware breakpoint extensions, targets
+//!            _must_ implement both the `add_breakpoint` and
+//!            `remove_breakpoints` methods.
+//!      - No way to enforce "mutually-exclusive" trait methods at compile-time.
+//!          - e.g: The `resume` method for single-threaded targets has a much
+//!            simpler API than for multi-threaded targets, but it would be
+//!            incorrect for a target to implement both.
+//!
+//! Starting from version `0.4.0`, `gdbstub` is taking a new approach to
+//! implementing and enumerating available Target features, using a technique
+//! called **Inlineable Dyn Extension Traits**.
+//!
+//! _Author's note:_ As far as I can tell, this isn't a very well-known trick,
+//! or at the very least, I've personally never encountered any library that
+//! uses this sort of API. As such, I've decided to be a bit cheeky and give it
+//! a name! At some point, I'm hoping to write a standalone blog post which
+//! further explores this technique, comparing it to other/existing approaches,
+//! and diving into details of the how the compiler optimizes this sort of code.
+//!
+//! So, what are "Inlineable Dyn Extension Traits"? Well, let's break it down:
+//!
+//! - **Extension Traits** - A common [Rust convention](https://rust-lang.github.io/rfcs/0445-extension-trait-conventions.html#what-is-an-extension-trait)
+//!   to extend the functionality of a Trait, _without_ modifying the original
+//!   trait.
+//! - **Dyn** - Alludes to the use of Dynamic Dispatch via [Trait Objects](https://doc.rust-lang.org/book/ch17-02-trait-objects.html).
+//! - **Inlineable** - Alludes to the fact that this approach can be easily
+//!   inlined, making it a truly zero-cost abstraction.
+//!
+//! In a nutshell, Inlineable Dyn Extension Traits (or IDETs) are an abuse of
+//! the Rust trait system + modern compiler optimizations to emulate zero-cost,
+//! runtime-query-able optional trait methods!
+//!
+//! #### Technical overview
+//!
+//! The basic principles behind Inlineable Dyn Extension Traits are best
+//! explained though example:
+//!
+//! Lets say we want to add an optional protocol extension described by an
+//! `OptExt` trait to the `Target` trait. How would we do that using IDETs?
+//!
+//! - (library) Define a `trait OptExt: Target { ... }` with all the optional
+//!   methods:
+//!    - Making `OptExt` a supertrait of `Target` enables using `Target`'s
+//!      associated types.
+//!
+//! ```rust,ignore
+//! /// `foo` and `bar` are mutually-dependent methods.
+//! trait OptExt: Target {
+//!     fn foo(&self);
+//!     // can use associated types in method signature!
+//!     fn bar(&mut self) -> Result<(), Self::Error>;
+//! }
+//! ```
+//!
+//! - (library) "Tie" the `OptExt` extension trait to the original `Target`
+//!   trait by adding a new `Target` method that simply returns `self` cast to a
+//!   `&mut dyn OptExt`:
+//!
+//! ```rust,ignore
+//! trait Target {
+//!     // Optional extension
+//!     fn ext_optfeat(&mut self) -> Option<OptExtOps<Self>> {
+//!         // disabled by default
+//!         None
+//!     }
+//!     // Mutually-exclusive extensions
+//!     fn ext_a_or_b(&mut self) -> EitherOrExt<Self::Arch, Self::Error>;
+//! }
+//!
+//! // Using a typedef for readability
+//! type OptExtOps<T> =
+//!     &'a mut dyn OptExt<Arch = <T as Target>::Arch, Error = <T as Target>::Error>;
+//!
+//! enum EitherOrExt<A, E> {
+//!     OptExtA(&'a mut dyn OptExtA<Arch = A, Error = E>),
+//!     OptExtB(&'a mut dyn OptExtB<Arch = A, Error = E>),
+//! }
+//! ```
+//!
+//! - (user) Implements the `OptExt` extension for their target (just like a
+//!   normal trait).
+//!
+//! ```rust,ignore
+//! impl OptExt for Target {
+//!     fn foo(&self) { ... }
+//!     fn bar(&mut self) -> Result<(), Self::Error> { ... }
+//! }
+//! ```
+//!
+//! - (user) Implements the base `Target` trait, returning `Some(self)` to
+//!   "enable" an extension, or `None` to leave it disabled.
+//!
+//! ```rust,ignore
+//! impl Target for MyTarget {
+//!     // Optional extension - Always enabled
+//!     fn ext_optfeat(&mut self) -> Option<OptExtOps<Self>> {
+//!         Some(self) // will not compile unless `MyTarget` also implements `OptExt`
+//!     }
+//!     // Mutually-exclusive extensions
+//!     fn ext_a_or_b(&mut self) -> EitherOrExt<Self::Arch, Self::Error> {
+//!         EitherOrExt::OptExtA(self)
+//!     }
+//! }
+//! ```
+//!
+//! If the user didn't implement `OptExt`, but tried to return `Some(self)`,
+//! they'll get an error similar to:
+//!
+//! ```text
+//! error[E0277]: the trait bound `MyTarget: OptExt` is not satisfied
+//!   --> path/to/implementation.rs:44:14
+//!    |
+//! 44 |         Some(self)
+//!    |              ^^^^ the trait `OptExt` is not implemented for `MyTarget`
+//!    |
+//!    = note: required for the cast to the object type `dyn OptExt<Arch = ..., Error = ...>`
+//! ```
+//!
+//! - (library) Can now _query_ whether or not the extension is available,
+//!   _without_ having to actually invoke any method on the target!
+//! ```rust,ignore
+//! // in a method that accepts `target: impl Target`
+//! match target.ext_optfeat() {
+//!     Some(ops) => ops.cool_feature(),
+//!     None => { /* do nothing */ }
+//! }
+//! ```
+//!
+//! Moreover, if you take a look at the generated assembly (e.g: using
+//! godbolt.org), you'll find that the compiler is able to efficiently inline
+//! and devirtualize all the single-line `ext_` methods, which in-turn allows
+//! the dead-code-eliminator to work it's magic, and remove the unused branches
+//! from the generated code! i.e: If a target didn't implement the `OptExt`
+//! extension, then that `match` statement would be converted into a noop!
+//!
+//! Check out [daniel5151/optional-trait-methods](https://github.com/daniel5151/optional-trait-methods)
+//! for some sample code that shows off the power of IDETs. It includes code
+//! snippets which can be pasted into godbolt.org directly to confirm the
+//! optimizations described above.
+//!
+//! Optimizing compilers really are magic!
+//!
+//! #### Summary: The Benefits of IDETs
+//!
+//! IDETs solve the numerous issues and shortcomings that arise from the
+//! traditional single trait + "optional" methods approach:
+//!
+//! - **Compile-time enforcement of mutually-dependent methods**
+//!    - By grouping mutually-dependent methods behind a single extension trait
+//!      and marking them all as required methods, the Rust compiler is able to
+//!      catch missing mutually-dependent methods at compile time, with no need
+//!      for any runtime checks!
+//! - **Compile-time enforcement of mutually-exclusive methods**
+//!    - By grouping mutually-exclusive methods behind two extension traits, and
+//!      wrapping those in an `enum`, the API is able to document
+//!      mutually-exclusive functions _at the type-level_, in-turn enabling the
+//!      library to omit any runtime checks!
+//!    - _Note:_ Strictly speaking, this isn't really compile time
+//!      "enforcement", as there's nothing stopping an "adversarial"
+//!      implementation from implementing both sets of methods, and then
+//!      "flipping" between the two at runtime. Nonetheless, it serves as a good
+//!      guardrail.
+//! - **Enforce dead-code-elimination _without_ `cargo` feature flags**
+//!     - This is a really awesome trick: by wrapping code in a `if
+//!       target.ext_optfeat().is_some()` block, it's possible to specify
+//!       _arbitrary_ blocks of code to be feature-dependent!
+//!     - This is used to great effect in `gdbstub` to optimize-out any packet
+//!       parsing / handler code for unimplemented protocol extensions.
+
+macro_rules! doc_comment {
+    ($x:expr, $($tt:tt)*) => {
+        #[doc = $x]
+        $($tt)*
+    };
+}
+
+macro_rules! define_ext {
+    ($extname:ident, $exttrait:ident) => {
+        doc_comment! {
+            concat!("See [`", stringify!($exttrait), "`](trait.", stringify!($exttrait), ".html)."),
+            pub type $extname<'a, T> =
+                &'a mut dyn $exttrait<Arch = <T as Target>::Arch, Error = <T as Target>::Error>;
+        }
+    };
+}
+
+pub mod base;
+pub mod breakpoints;
+pub mod extended_mode;
+pub mod monitor_cmd;
+pub mod section_offsets;
diff --git a/src/target/ext/monitor_cmd.rs b/src/target/ext/monitor_cmd.rs
new file mode 100644
index 0000000..5317e67
--- /dev/null
+++ b/src/target/ext/monitor_cmd.rs
@@ -0,0 +1,32 @@
+//! Create custom target-specific debugging commands accessible via GDB's
+//! `monitor` command!
+
+use crate::target::Target;
+
+pub use crate::protocol::ConsoleOutput;
+pub use crate::{output, outputln};
+
+/// Target Extension - Handle custom GDB `monitor` commands.
+pub trait MonitorCmd: Target {
+    /// Handle custom commands sent using the `monitor` command.
+    ///
+    /// The GDB remote serial protocol includes a built-in mechanism to send
+    /// arbitrary commands to the remote stub: the `monitor` command. For
+    /// example, running `monitor dbg` from the GDB client will invoke
+    /// `handle_monitor_cmd` with `cmd = b"dbg"`.
+    ///
+    /// Commands are _not_ guaranteed to be valid UTF-8, hence the use of
+    /// `&[u8]` as opposed to `&str`.
+    ///
+    /// Intermediate console output can be written back to the GDB client using
+    /// the provided `ConsoleOutput` object + the
+    /// [`gdbstub::output!`](macro.output.html) macro.
+    ///
+    /// _Note:_ The maximum length of incoming commands is limited by the size
+    /// of the packet buffer provided to the [`GdbStub`](struct.GdbStub.html).
+    /// Specifically, commands can only be up to `(buf.len() - 10) / 2` bytes.
+    fn handle_monitor_cmd(&mut self, cmd: &[u8], out: ConsoleOutput<'_>)
+        -> Result<(), Self::Error>;
+}
+
+define_ext!(MonitorCmdOps, MonitorCmd);
diff --git a/src/target/ext/section_offsets.rs b/src/target/ext/section_offsets.rs
new file mode 100644
index 0000000..eb80578
--- /dev/null
+++ b/src/target/ext/section_offsets.rs
@@ -0,0 +1,66 @@
+//! Get section/segment relocation offsets from the target.
+//!
+//! For some targets, sections may be relocated from their base address. As
+//! a result, the stub may need to tell GDB the final section addresses
+//! to ensure that debug symbols are resolved correctly after relocation.
+//!
+//! _Note:_ This extension corresponds to the `qOffsets` command, which is
+//! limited to reporting the offsets for code, data and bss, and is
+//! generally considered a legacy feature.
+//!
+//! For targets where library offsets are maintained externally (e.g. Windows)
+//! you should consider implementing the more flexible `qXfer:library:read`.
+//! See issue [#20](https://github.com/daniel5151/gdbstub/issues/20) for more
+//! info.
+//!
+//! For System-V architectures GDB is capable of extracting library offsets
+//! from memory if it knows the base address of the dynamic linker. The base
+//! address can be specified by either implementing this command or by including
+//! a `AT_BASE` entry in the response to the more modern `qXfer:auxv:read`
+//! command. See issue [#20](https://github.com/daniel5151/gdbstub/issues/20)
+//! for more info.
+
+use crate::arch::Arch;
+use crate::target::Target;
+
+/// Describes the offset the target loaded the image sections at, so the target
+/// can notify GDB that it needs to adjust the addresses of symbols.
+///
+/// GDB supports either section offsets, or segment addresses.
+pub enum Offsets<U> {
+    /// Section offsets relative to their base addresses.
+    Sections {
+        /// The offset of the `.text` section.
+        text: U,
+        /// The offset of the `.data` section.
+        data: U,
+        /// The offset of the `.bss` section.
+        ///
+        /// _Note:_ GDB expects that `bss` is either `None` or equal to `data`.
+        bss: Option<U>,
+    },
+
+    /// Absolute addresses of the first two segments.
+    ///
+    /// _Note:_ any extra segments will kept at fixed offsets relative to the
+    /// last relocated segment.
+    Segments {
+        /// The absolute address of the first segment which conventionally
+        /// contains program code.
+        text_seg: U,
+        /// The absolute address of the second segment which conventionally
+        /// contains modifiable data.
+        data_seg: Option<U>,
+    },
+}
+
+/// Target Extension - Get section/segment relocation offsets from the target.
+///
+/// Corresponds to the `qOffset` command. See the [section_offset module
+/// documentation](index.html).
+pub trait SectionOffsets: Target {
+    /// Return the target's current section (or segment) offsets.
+    fn get_section_offsets(&mut self) -> Result<Offsets<<Self::Arch as Arch>::Usize>, Self::Error>;
+}
+
+define_ext!(SectionOffsetsOps, SectionOffsets);
diff --git a/src/target/mod.rs b/src/target/mod.rs
new file mode 100644
index 0000000..c027824
--- /dev/null
+++ b/src/target/mod.rs
@@ -0,0 +1,287 @@
+//! Everything related to the [`Target`] trait + associated extension traits.
+//!
+//! The [`Target`] trait describes how to control and modify a system's
+//! execution state during a GDB debugging session, and serves as the
+//! primary bridge between `gdbstub`'s generic protocol implementation and a
+//! target's project/platform-specific code.
+//!
+//! **`Target` is the most important trait in `gdbstub`, and must be implemented
+//! by all consumers of the library!**
+//!
+//! # Implementing `Target`
+//!
+//! `gdbstub` uses a technique called "Inlineable Dyn Extension Traits" (IDETs)
+//! to expose an ergonomic and extensible interface to the GDB protocol. It's
+//! not a very common pattern, and can seem a little "weird" at first glance,
+//! but it's actually very straightforward to use!
+//!
+//! Please refer to the [documentation in the `ext` module](ext) for more
+//! information on IDETs, and how they're used to implement `Target` and it's
+//! various extension traits.
+//!
+//! **TL;DR:** Whenever you see a method that has `Option<FooOps>` in the return
+//! type, that method should return `Some(self)` if the extension is
+//! implemented, or `None` if it's unimplemented / disabled.
+//!
+//! ## Associated Types
+//!
+//! - The [`Target::Arch`](trait.Target.html#associatedtype.Arch) associated
+//!   type encodes information about the target's architecture, such as it's
+//!   pointer size, register layout, etc... `gdbstub` comes with several
+//!   built-in architecture definitions, which can be found under the
+//!   [`arch`](../arch/index.html) module.
+//!
+//! - The [`Target::Error`](trait.Target.html#associatedtype.Error) associated
+//!   type allows implementors to plumb-through their own project-specific fatal
+//!   error type into the `Target` trait. This is a big-boost to library
+//!   ergonomics, as it enables consumers of `gdbstub` to preserve
+//!   target-specific context while using `gdbstub`, without having to do any
+//!   "error-stashing".
+//!
+//! For example: consider an emulated target where certain devices might return
+//! a `MyEmuError::ContractViolation` error whenever they're accessed
+//! "improperly" (e.g: setting registers in the wrong order). By setting `type
+//! Error = MyEmuError`, the method signature of the `Target`'s `resume` method
+//! becomes `fn resume(&mut self, ...) -> Result<_, MyEmuError>`, which makes it
+//! possible to preserve the target-specific error while using `gdbstub`!
+//!
+//! ## Required Methods
+//!
+//! The [`Target::base_ops`](trait.Target.html#tymethod.base_ops) method
+//! describes the base debugging operations that must be implemented by any
+//! target. These are things such as starting/stopping execution,
+//! reading/writing memory, etc..
+//!
+//! All other methods are entirely optional! Check out the
+//! [`target_ext`](../target_ext/index.html) module for a full list of currently
+//! supported protocol extensions.
+//!
+//! ## Example: A Bare-Minimum Single Threaded `Target`
+//!
+//! ```rust,ignore
+//! use gdbstub::target::Target;
+//! use gdbstub::target::ext::base::singlethread::SingleThreadOps;
+//!
+//! impl SingleThreadOps for MyTarget {
+//!     // ... omitted for brevity
+//! }
+//!
+//! impl Target for MyTarget {
+//!     fn base_ops(&mut self) -> base::BaseOps<Self::Arch, Self::Error> {
+//!         base::BaseOps::SingleThread(self)
+//!     }
+//! }
+//! ```
+
+use crate::arch::Arch;
+
+pub mod ext;
+
+/// The error type for various methods on `Target` and it's assorted associated
+/// extension traits.
+///
+/// # Error Handling over the GDB Remote Serial Protocol
+///
+/// The GDB Remote Serial Protocol has less-than-stellar support for error
+/// handling, typically taking the form of a single-byte
+/// [`errno`-style error codes](https://www-numi.fnal.gov/offline_software/srt_public_context/WebDocs/Errors/unix_system_errors.html).
+/// Moreover, often times the GDB client will simply _ignore_ the specific error
+/// code returned by the stub, and print a generic failure message instead.
+///
+/// As such, while it's certainly better to use appropriate error codes when
+/// possible (e.g: returning a `EFAULT` (14) when reading from invalid memory),
+/// it's often fine to simply return the more general `TargetError::NonFatal`
+/// instead, and avoid the headache of picking a "descriptive" error code. Under
+/// the good, `TargetError::NonFatal` is sent to the GDB client as a generic
+/// `EREMOTEIO` (121) error.
+///
+/// # `From` and `Into` implementations
+///
+/// - `From<()>` -> `TargetError::NonFatal`
+/// - `From<io::Error>` -> `TargetError::Io(io::Error)` (requires `std` feature)
+///
+/// When using a custom target-specific fatal error type, users are encouraged
+/// to write the following impl to simplify error handling in `Target` methods:
+///
+/// ```rust,ignore
+/// type MyTargetFatalError = ...; // Target-specific Fatal Error
+/// impl From<MyTargetFatalError> for TargetError<MyTargetFatalError> {
+///     fn from(e: MyTargetFatalError) -> Self {
+///         TargetError::Fatal(e)
+///     }
+/// }
+/// ```
+///
+/// Unfortunately, a blanket impl such as `impl<T: Target> From<T::Error> for
+/// TargetError<T::Error>` isn't possible, as it could result in impl conflicts.
+/// For example, if a Target decided to use `()` as it's fatal error type, then
+/// there would be conflict with the existing `From<()>` impl.
+#[non_exhaustive]
+pub enum TargetError<E> {
+    /// A non-specific, non-fatal error has occurred.
+    NonFatal,
+    /// I/O Error.
+    ///
+    /// At the moment, this is just shorthand for
+    /// `TargetError::NonFatal(e.raw_os_err().unwrap_or(121))`. Error code `121`
+    /// corresponds to `EREMOTEIO`.
+    ///
+    /// In the future, `gdbstub` may add support for the "QEnableErrorStrings"
+    /// LLDB protocol extension, which would allow sending additional error
+    /// context (in the form of an ASCII string) when an I/O error occurs. If
+    /// this is something you're interested in, consider opening a PR!
+    ///
+    /// Only available when the `std` feature is enabled.
+    #[cfg(feature = "std")]
+    Io(std::io::Error),
+    /// An operation-specific non-fatal error code.
+    Errno(u8),
+    /// A target-specific fatal error.
+    ///
+    /// **WARNING:** Returning this error will immediately halt the target's
+    /// execution and return a `GdbStubError::TargetError` from `GdbStub::run`!
+    /// Note that the debugging session will will _not_ be terminated, and can
+    /// be resumed by calling `GdbStub::run` after resolving the error and/or
+    /// setting up a post-mortem debugging environment.
+    Fatal(E),
+}
+
+/// Converts a `()` into a `TargetError::NonFatal`.
+impl<E> From<()> for TargetError<E> {
+    fn from(_: ()) -> TargetError<E> {
+        TargetError::NonFatal
+    }
+}
+
+/// Converts a `std::io::Error` into a `TargetError::Io`.
+#[cfg(feature = "std")]
+impl<E> From<std::io::Error> for TargetError<E> {
+    fn from(e: std::io::Error) -> TargetError<E> {
+        TargetError::Io(e)
+    }
+}
+
+/// A specialized `Result` type for `Target` operations.
+///
+/// _Note:_ While it's typically parameterized as `TargetResult<T, Self>`, the
+/// error value is in-fact `TargetError<Self::Error>` (not `Self`).
+pub type TargetResult<T, Tgt> = Result<T, TargetError<<Tgt as Target>::Error>>;
+
+/// Describes the architecture and capabilities of a target which can be
+/// debugged by [`GdbStub`](../struct.GdbStub.html).
+///
+/// The [`Target`](trait.Target.html) trait describes how to control and modify
+/// a system's execution state during a GDB debugging session, and serves as the
+/// primary bridge between `gdbstub`'s generic protocol implementation and a
+/// target's project/platform-specific code.
+///
+/// **`Target` is the most important trait in `gdbstub`, and must be implemented
+/// by anyone who uses the library!**
+///
+/// Please refer to the the documentation in the [`target` module](index.html)
+/// for more information on how to implement and work with `Target` and it's
+/// various extension traits.
+pub trait Target {
+    /// The target's architecture.
+    type Arch: Arch;
+
+    /// A target-specific **fatal** error.
+    type Error;
+
+    /// Base operations such as reading/writing from memory/registers,
+    /// stopping/resuming the target, etc....
+    ///
+    /// For example, on a single-threaded target:
+    ///
+    /// ```rust,ignore
+    /// use gdbstub::target::Target;
+    /// use gdbstub::target::base::singlethread::SingleThreadOps;
+    ///
+    /// impl SingleThreadOps for MyTarget {
+    ///     // ...
+    /// }
+    ///
+    /// impl Target for MyTarget {
+    ///     fn base_ops(&mut self) -> base::BaseOps<Self::Arch, Self::Error> {
+    ///         base::BaseOps::SingleThread(self)
+    ///     }
+    /// }
+    /// ```
+    fn base_ops(&mut self) -> ext::base::BaseOps<Self::Arch, Self::Error>;
+
+    /// Set/Remote software breakpoints.
+    fn sw_breakpoint(&mut self) -> Option<ext::breakpoints::SwBreakpointOps<Self>> {
+        None
+    }
+
+    /// Set/Remote hardware breakpoints.
+    fn hw_breakpoint(&mut self) -> Option<ext::breakpoints::HwBreakpointOps<Self>> {
+        None
+    }
+
+    /// Set/Remote hardware watchpoints.
+    fn hw_watchpoint(&mut self) -> Option<ext::breakpoints::HwWatchpointOps<Self>> {
+        None
+    }
+
+    /// Handle custom GDB `monitor` commands.
+    fn monitor_cmd(&mut self) -> Option<ext::monitor_cmd::MonitorCmdOps<Self>> {
+        None
+    }
+
+    /// Support for Extended Mode operations.
+    fn extended_mode(&mut self) -> Option<ext::extended_mode::ExtendedModeOps<Self>> {
+        None
+    }
+
+    /// Handle requests to get the target's current section (or segment)
+    /// offsets.
+    fn section_offsets(&mut self) -> Option<ext::section_offsets::SectionOffsetsOps<Self>> {
+        None
+    }
+}
+
+macro_rules! impl_dyn_target {
+    ($type:ty) => {
+        #[allow(clippy::type_complexity)]
+        impl<A, E> Target for $type
+        where
+            A: Arch,
+        {
+            type Arch = A;
+            type Error = E;
+
+            fn base_ops(&mut self) -> ext::base::BaseOps<Self::Arch, Self::Error> {
+                (**self).base_ops()
+            }
+
+            fn sw_breakpoint(&mut self) -> Option<ext::breakpoints::SwBreakpointOps<Self>> {
+                (**self).sw_breakpoint()
+            }
+
+            fn hw_breakpoint(&mut self) -> Option<ext::breakpoints::HwBreakpointOps<Self>> {
+                (**self).hw_breakpoint()
+            }
+
+            fn hw_watchpoint(&mut self) -> Option<ext::breakpoints::HwWatchpointOps<Self>> {
+                (**self).hw_watchpoint()
+            }
+
+            fn monitor_cmd(&mut self) -> Option<ext::monitor_cmd::MonitorCmdOps<Self>> {
+                (**self).monitor_cmd()
+            }
+
+            fn extended_mode(&mut self) -> Option<ext::extended_mode::ExtendedModeOps<Self>> {
+                (**self).extended_mode()
+            }
+
+            fn section_offsets(&mut self) -> Option<ext::section_offsets::SectionOffsetsOps<Self>> {
+                (**self).section_offsets()
+            }
+        }
+    };
+}
+
+impl_dyn_target!(&mut dyn Target<Arch = A, Error = E>);
+#[cfg(feature = "alloc")]
+impl_dyn_target!(alloc::boxed::Box<dyn Target<Arch = A, Error = E>>);
diff --git a/src/util/managed_vec.rs b/src/util/managed_vec.rs
new file mode 100644
index 0000000..f43fc66
--- /dev/null
+++ b/src/util/managed_vec.rs
@@ -0,0 +1,46 @@
+use managed::ManagedSlice;
+
+/// Error value indicating insufficient capacity.
+#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
+pub struct CapacityError<Element>(pub Element);
+
+/// Wraps a ManagedSlice in a vec-like interface.
+///
+/// TODO?: Upstream ManagedVec into the main `managed` crate?
+pub struct ManagedVec<'a, 'b, T: 'a> {
+    buf: &'b mut ManagedSlice<'a, T>,
+    len: usize,
+}
+
+impl<'a, 'b, T> ManagedVec<'a, 'b, T> {
+    pub fn new(buf: &'b mut ManagedSlice<'a, T>) -> Self {
+        ManagedVec { buf, len: 0 }
+    }
+
+    pub fn clear(&mut self) {
+        match &mut self.buf {
+            ManagedSlice::Borrowed(_) => self.len = 0,
+            #[cfg(feature = "alloc")]
+            ManagedSlice::Owned(buf) => buf.clear(),
+        }
+    }
+
+    pub fn push(&mut self, value: T) -> Result<(), CapacityError<T>> {
+        match &mut self.buf {
+            ManagedSlice::Borrowed(buf) => {
+                if self.len < buf.len() {
+                    buf[self.len] = value;
+                    self.len += 1;
+                    Ok(())
+                } else {
+                    Err(CapacityError(value))
+                }
+            }
+            #[cfg(feature = "alloc")]
+            ManagedSlice::Owned(buf) => {
+                buf.push(value);
+                Ok(())
+            }
+        }
+    }
+}
diff --git a/src/util/mod.rs b/src/util/mod.rs
new file mode 100644
index 0000000..a0b1e45
--- /dev/null
+++ b/src/util/mod.rs
@@ -0,0 +1 @@
+pub mod managed_vec;