Update gdbstub crate to version 0.6.1

Generated with the command:
tools/external_updater/updater.sh update
/usr/local/google/home/devinmoore/workspace/aosp/master/external/rust/crates/gdbstub/

Bug: 229270573
Test: m crosvm
Test: atest gdbstub_arch_test_src_lib

Change-Id: I450d12b8775c379ed1e1ac95959c04acd77761d8
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
index 5653d83..a740986 100644
--- a/.cargo_vcs_info.json
+++ b/.cargo_vcs_info.json
@@ -1,5 +1,6 @@
 {
   "git": {
-    "sha1": "90aad2e136d15d486324f1985a398c02da982cdb"
-  }
-}
+    "sha1": "1e4bd678a71de84981c01cf5100eaf8f754e24b6"
+  },
+  "path_in_vcs": ""
+}
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..478b222
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,56 @@
+on: [push, pull_request]
+
+name: ci
+
+jobs:
+  test:
+    name: clippy + tests + docs
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: stable
+      - name: cargo clippy
+        uses: actions-rs/cargo@v1
+        with:
+          command: clippy
+          args: --workspace --tests --examples --features=std -- -D warnings
+      # don't forget the no_std example!
+      - name: cargo clippy (example_no_std)
+        uses: actions-rs/cargo@v1
+        with:
+          command: clippy
+          args: --manifest-path example_no_std/Cargo.toml
+      - name: cargo test
+        uses: actions-rs/cargo@v1
+        with:
+          command: test
+          args: --workspace --features=std
+      - name: cargo doc
+        uses: actions-rs/cargo@v1
+        with:
+          command: doc
+          args: --workspace --features=std
+  rustfmt:
+    name: rustfmt (nightly)
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: nightly
+          components: rustfmt
+      - name: cargo +nightly fmt
+        uses: actions-rs/cargo@v1
+        with:
+          command: fmt
+          args: --all -- --check
+      # don't forget the no_std example!
+      - name: cargo +nightly fmt (example_no_std)
+        uses: actions-rs/cargo@v1
+        with:
+          command: fmt
+          args: --manifest-path example_no_std/Cargo.toml
diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml
deleted file mode 100644
index 39133dd..0000000
--- a/.github/workflows/lints.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-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
deleted file mode 100644
index ea317c8..0000000
--- a/.github/workflows/stable-ci.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-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/Android.bp b/Android.bp
index 6155ca7..8c60f47 100644
--- a/Android.bp
+++ b/Android.bp
@@ -23,15 +23,17 @@
     host_supported: true,
     crate_name: "gdbstub",
     cargo_env_compat: true,
-    cargo_pkg_version: "0.5.0",
+    cargo_pkg_version: "0.6.1",
     srcs: ["src/lib.rs"],
     edition: "2018",
     features: [
         "alloc",
         "default",
         "std",
+        "trace-pkt",
     ],
     rustlibs: [
+        "libbitflags",
         "libcfg_if",
         "liblog_rust",
         "libmanaged",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5c2ed0..ab1e028 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,101 @@
 
 This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+# 0.6.1
+
+- add LLDB-specific HostIoOpenFlags [\#100](https://github.com/daniel5151/gdbstub/pull/100) ([mrk](https://github.com/mrk-its))
+
+# 0.6.0
+
+After over a half-year of development, `gdbstub` 0.6 has finally been released!
+
+This massive release delivers a slew of new protocol extensions, internal improvements, and key API improvements. Some highlights include:
+
+- A new _non-blocking_ `GdbStubStateMachine` API, enabling `gdbstub` to integrate nicely with async event loops!
+  - Moreover, on `no_std` platforms, this new API enables `gdbstub` to be driven directly via breakpoint/serial interrupt handlers!
+  - This API is already being used in several Rust kernel projects, such as [`vmware-labs/node-replicated-kernel`](https://github.com/vmware-labs/node-replicated-kernel/tree/4326704/kernel/src/arch/x86_64/gdb) and [`betrusted-io/xous-core`](https://github.com/betrusted-io/xous-core/blob/7d3d710/kernel/src/debug/gdb_server.rs) to enable bare-metal, in-kernel debugging.
+- `gdbstub` is now entirely **panic free** in release builds!
+  - \* subject to `rustc`'s compiler optimizations
+  - This was a pretty painstaking effort, but the end result is a substantial reduction in binary size on `no_std` platforms.
+- Tons of new and exciting protocol extensions, including but not limited to:
+  - Support for remote file I/O (reading/writing files to the debug target)
+  - Fetching remote memory maps
+  - Catching + reporting syscall entry/exit conditions
+  - ...and many more!
+- A new license: `gdbstub` is licensed under MIT OR Apache-2.0
+
+See the [changelog](https://github.com/daniel5151/gdbstub/blob/dev/0.6/CHANGELOG.md) for a comprehensive rundown of all the new features.
+
+While this release does come with quite a few breaking changes, the core IDET-based `Target` API has remained much the same, which should make porting code over from 0.5.x to 0.6 pretty mechanical. See the [`transition_guide.md`](./docs/transition_guide.md) for guidance on upgrading from `0.5.x` to `0.6`.
+
+And as always, a huge shoutout to the folks who contributed PRs, Issues, and ideas to `gdbstub` - this release wouldn't have been possible without you! Special shoutouts to [gz](https://github.com/gz) and [xobs](https://github.com/xobs) for helping me test and iterate on the new bare-metal state machine API, and [bet4it](https://github.com/bet4it) for pointing out and implementing many useful API improvements and internal refactors.
+
+Cheers!
+
+#### New Features
+
+- The new `GdbStubStateMachine` API gives users the power and flexibility to integrate `gdbstub` into their project-specific event loop infrastructure.
+  - e.g: A global instance of `GdbStubStateMachine` can be driven directly from bare-metal interrupt handlers in `no_std` environments
+  - e.g: A project using `async`/`await` can wrap `GdbStubStateMachine` in a task, yielding execution while waiting for the target to resume / new data to arrive down the `Connection`
+- Removed all panicking code from `gdbstub`
+  - See the [commit message](https://github.com/daniel5151/gdbstub/commit/ecbbaf72e01293b410ef3bc5970d18aa81e45599) for more details on how this was achieved.
+- Introduced strongly-typed enum for protocol defined signal numbers (instead of using bare `u8`s)
+- Added basic feature negotiation to support clients that don't support `multiprocess+` extensions.
+- Relicensed `gdbstub` under MIT OR Apache-2.0 [\#68](https://github.com/daniel5151/gdbstub/pull/68)
+- Added several new "guard rails" to avoid common integration footguns:
+  - `Target::guard_rail_implicit_sw_breakpoints` - guards against the GDB client silently overriding target instructions with breakpoints if `SwBreakpoints` hasn't been implemented.
+  - `Target::guard_rail_single_step_gdb_behavior` - guards against a GDB client bug where support for single step may be required / ignored on certain platforms (e.g: required on x86, ignored on MIPS)
+- Added several new "toggle switches" to enable/disable parts of the protocol (all default to `true`)
+  - `Target::use_x_upcase_packet` - toggle support for the more efficient `X` memory write packet
+  - `Target::use_resume_stub` - toggle `gdbstub`'s built-in "stub" resume handler that returns `SIGRAP` if a target doesn't implement support for resumption
+  - `Target::use_rle` - toggle whether outgoing packets are Run Length Encoded (RLE)
+
+#### New Protocol Extensions
+
+- `MemoryMap` - Get memory map XML file from the target. [\#54](https://github.com/daniel5151/gdbstub/pull/54) ([Tiwalun](https://github.com/Tiwalun))
+- `CatchSyscalls` - Enable and disable catching syscalls from the inferior process. [\#57](https://github.com/daniel5151/gdbstub/pull/57) ([mchesser](https://github.com/mchesser))
+- `HostIo` - Perform I/O operations on host. [\#66](https://github.com/daniel5151/gdbstub/pull/66) ([bet4it](https://github.com/bet4it))
+  - Support for all Host I/O operations: `open`, `close`, `pread`, `pwrite`, `fstat`, `unlink`, `readlink`, `setfs`
+- `ExecFile` - Get full absolute path of the file that was executed to create a process running on the remote system. [\#69](https://github.com/daniel5151/gdbstub/pull/69) ([bet4it](https://github.com/bet4it))
+- `Auxv` - Access the target’s auxiliary vector. [\#86](https://github.com/daniel5151/gdbstub/pull/86) ([bet4it](https://github.com/bet4it))
+- Implement `X` packet - More efficient bulk-write to memory (superceding the `M` packet). [\#82](https://github.com/daniel5151/gdbstub/pull/82) ([gz](https://github.com/gz))
+
+#### Breaking API Changes
+
+- `Connection` API:
+  - Removed the `read` and `peek` methods from `Connection`
+    - These have been moved to the new `ConnectionExt` trait, which is used in the new `GdbStub::run_blocking` API
+- `Arch` API:
+  - Dynamic read_register + RegId support. [\#85](https://github.com/daniel5151/gdbstub/pull/85) ([bet4it](https://github.com/bet4it))
+- `Target` APIs:
+  - prefix all IDET methods with `support_`
+    - _makes it far easier to tell at-a-glance whether a method is an IDET, or an actual handler method.
+  - Introduce strongly-typed enum for protocol defined signal numbers (instead of using bare `u8`s)
+  - `Base` API:
+    - Make single-stepping optional [\#92](https://github.com/daniel5151/gdbstub/pull/92)
+    - Remove `GdbInterrupt` type (interrupt handling lifted to higher-level APIs)
+    - Remove `ResumeAction` type (in favor of separate methods for various resume types)
+  - `Breakpoints` API:
+    - `HwWatchpoint`: Plumb watchpoint `length` parameter to public API
+  - `TargetXml` API:
+    - Support for `<xi:include>` in target.xml, which required including the `annex` parameter in the handler method.
+    - `annex` is set to `b"target.xml"` on the fist call, though it may be set to other values in subsequent calls if `<xi:include>` is being used.
+  - Pass `PacketBuf`-backed `&mut [u8]` as a response buffer to various APIs [\#72](https://github.com/daniel5151/gdbstub/pull/72) ([bet4it](https://github.com/bet4it))
+    - Improvement over the callback-based approach.
+    - This change is possible thanks to a clause in the GDB spec that specifies that responses will never exceed the size of the `PacketBuf`.
+    - Also see [\#70](https://github.com/daniel5151/gdbstub/pull/70), which tracks some other methods that might be refactored to use this approach in the future.
+
+#### Internal Improvements
+
+- Documentation
+  - Fix crates.io badges [\#71](https://github.com/daniel5151/gdbstub/pull/71) ([atouchet](https://github.com/atouchet))
+  - Add `uhyve` to real-world examples [\#73](https://github.com/daniel5151/gdbstub/pull/73) ([mkroening](https://github.com/mkroening))
+- Use stable `clippy` in CI
+- Enable logging for responses with only alloc [\#78](https://github.com/daniel5151/gdbstub/pull/78) ([gz](https://github.com/gz))
+- Lots of internal refactoring and cleanup
+
+#### Bugfixes
+
 # 0.5.0
 
 While the overall structure of the API has remained the same, `0.5.0` does introduce a few breaking API changes that require some attention. That being said, it should not be a difficult migration, and updating to `0.5.0` from `0.4` shouldn't take more than 10 mins of refactoring.
@@ -18,7 +113,7 @@
 - Added the `Exited(u8)`, `Terminated(u8)`, and `ReplayLog("begin"|"end")` stop reasons.
 - Added `DisconnectReason::Exited(u8)` and `DisconnectReason::Terminated(u8)`.
 - Reworked the `MultiThreadOps::resume` API to be significantly more ergonomic and efficient
-  - See the [transition guide](https://github.com/daniel5151/gdbstub/blob/dev/0.5/docs/transition_guide.md#new-multithreadopsresume-api) for more details.
+  - See the [transition guide](https://github.com/daniel5151/gdbstub/blob/master/docs/transition_guide.md#new-multithreadopsresume-api) for more details.
 
 #### New Protocol Extensions
 
diff --git a/Cargo.toml b/Cargo.toml
index 0706d77..94363bf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,17 +3,16 @@
 # 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
+# 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)
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
 
 [package]
 edition = "2018"
 name = "gdbstub"
-version = "0.5.0"
+version = "0.6.1"
 authors = ["Daniel Prilik <danielprilik@gmail.com>"]
 exclude = ["examples/**/*.elf", "examples/**/*.o"]
 description = "An implementation of the GDB Remote Serial Protocol in Rust"
@@ -22,7 +21,7 @@
 readme = "README.md"
 keywords = ["gdb", "emulation", "no_std", "debugging"]
 categories = ["development-tools::debugging", "embedded", "emulators", "network-programming", "no-std"]
-license = "MIT"
+license = "MIT OR Apache-2.0"
 repository = "https://github.com/daniel5151/gdbstub"
 
 [[example]]
@@ -32,8 +31,11 @@
 [[example]]
 name = "armv4t_multicore"
 required-features = ["std"]
+[dependencies.bitflags]
+version = "1.3"
+
 [dependencies.cfg-if]
-version = "0.1.10"
+version = "1.0"
 
 [dependencies.log]
 version = "0.4"
@@ -52,7 +54,7 @@
 version = "0.1"
 
 [dev-dependencies.goblin]
-version = "0.2"
+version = "0.4"
 
 [dev-dependencies.pretty_env_logger]
 version = "0.4"
@@ -60,5 +62,7 @@
 [features]
 __dead_code_marker = []
 alloc = ["managed/alloc"]
-default = ["std"]
+default = ["std", "trace-pkt"]
+paranoid_unsafe = []
 std = ["alloc"]
+trace-pkt = ["alloc"]
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
index 0c176ea..5c83903 100644
--- a/Cargo.toml.orig
+++ b/Cargo.toml.orig
@@ -2,8 +2,8 @@
 name = "gdbstub"
 description = "An implementation of the GDB Remote Serial Protocol in Rust"
 authors = ["Daniel Prilik <danielprilik@gmail.com>"]
-version = "0.5.0"
-license = "MIT"
+version = "0.6.1"
+license = "MIT OR Apache-2.0"
 edition = "2018"
 readme = "README.md"
 documentation = "https://docs.rs/gdbstub"
@@ -14,7 +14,8 @@
 exclude = ["examples/**/*.elf", "examples/**/*.o"]
 
 [dependencies]
-cfg-if = "0.1.10"
+bitflags = "1.3"
+cfg-if = "1.0"
 log = "0.4"
 managed = { version = "0.8", default-features = false }
 num-traits = { version = "0.2", default-features = false }
@@ -25,12 +26,14 @@
 
 armv4t_emu = "0.1"
 pretty_env_logger = "0.4"
-goblin = "0.2"
+goblin = "0.4"
 
 [features]
-default = ["std"]
+default = ["std", "trace-pkt"]
 alloc = ["managed/alloc"]
 std = ["alloc"]
+trace-pkt = ["alloc"]
+paranoid_unsafe = []
 
 # INTERNAL: enables the `__dead_code_marker!` macro.
 # used as part of the `scripts/test_dead_code_elim.sh`
diff --git a/LICENSE b/LICENSE
index 148020e..1a3e9de 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,6 @@
-MIT License
+gdbstub is dual-licensed under either
 
-Copyright (c) 2020 Daniel Prilik
+* MIT License (docs/LICENSE-MIT or http://opensource.org/licenses/MIT)
+* Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
 
-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.
+at your option.
diff --git a/METADATA b/METADATA
index 0d4065e..535454a 100644
--- a/METADATA
+++ b/METADATA
@@ -7,13 +7,13 @@
   }
   url {
     type: ARCHIVE
-    value: "https://static.crates.io/crates/gdbstub/gdbstub-0.5.0.crate"
+    value: "https://static.crates.io/crates/gdbstub/gdbstub-0.6.1.crate"
   }
-  version: "0.5.0"
+  version: "0.6.1"
   license_type: NOTICE
   last_upgrade_date {
-    year: 2021
-    month: 6
-    day: 21
+    year: 2022
+    month: 4
+    day: 19
   }
 }
diff --git a/README.md b/README.md
index d073267..feb54f4 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,52 @@
 # gdbstub
 
-[![](http://meritbadge.herokuapp.com/gdbstub)](https://crates.io/crates/gdbstub)
+[![](https://img.shields.io/crates/v/gdbstub.svg)](https://crates.io/crates/gdbstub)
 [![](https://docs.rs/gdbstub/badge.svg)](https://docs.rs/gdbstub)
+[![](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](./LICENSE)
 
 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.
 
- `gdbstub`  makes it easy to integrate powerful guest debugging support to your emulator/hypervisor/debugger/embedded project. By implementing just a few basic methods of the [`gdbstub::Target`](https://docs.rs/gdbstub/latest/gdbstub/target/ext/base/singlethread/trait.SingleThreadOps.html) trait, you can have a rich GDB debugging session up and running in no time!
+`gdbstub`  makes it easy to integrate powerful guest debugging support to your emulator / hypervisor / debugger / embedded project. By implementing just a few basic methods of the [`gdbstub::Target`](https://docs.rs/gdbstub/latest/gdbstub/target/ext/base/singlethread/trait.SingleThreadBase.html) trait, you can have a rich GDB debugging session up and running in no time!
 
-**If you're looking for a quick snippet of example code to see what a typical `gdbstub` integration might look like, check out [examples/armv4t/gdb/mod.rs](https://github.com/daniel5151/gdbstub/blob/dev/0.5/examples/armv4t/gdb/mod.rs)**
+`gdbstub`'s API makes extensive use of a technique called [**Inlineable Dyn Extension Traits**](#zero-overhead-protocol-extensions) (IDETs) to expose fine-grained, zero-cost control over enabled GDB protocol features _without_ relying on compile-time features flags. Aside from making it effortless to toggle enabled protocol features, IDETs also ensure that any unimplemented features are guaranteed to be dead-code-eliminated in release builds!
+
+**If you're looking for a quick snippet of example code to see what a typical `gdbstub` integration might look like, check out [examples/armv4t/gdb/mod.rs](https://github.com/daniel5151/gdbstub/blob/master/examples/armv4t/gdb/mod.rs)**
 
 -   [Documentation (gdbstub)](https://docs.rs/gdbstub)
 -   [Documentation (gdbstub_arch)](https://docs.rs/gdbstub_arch)
 -   [Changelog](CHANGELOG.md)
--   [0.4 to 0.5 Transition Guide](docs/transition_guide.md)
+-   [0.5 to 0.6 Transition Guide](docs/transition_guide.md)
 
 Why use `gdbstub`?
 
 -   **Excellent Ergonomics**
-    -   Instead of simply exposing 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 simply exposing the underlying GDB protocol "warts and all", `gdbstub` tries to abstract as much of the raw GDB protocol details from the user.
         -   Instead of having to dig through [obscure XML files deep the GDB codebase](https://github.com/bminor/binutils-gdb/tree/master/gdb/features) just to read/write from CPU/architecture registers, `gdbstub` comes with a community-curated collection of [built-in architecture definitions](https://docs.rs/gdbstub_arch) for most popular platforms!
         -   Organizes GDB's countless optional protocol extensions into a coherent, understandable, and type-safe hierarchy of traits.
         -   Automatically handles client/server protocol feature negotiation, without needing to micro-manage the specific [`qSupported` packet](https://sourceware.org/gdb/onlinedocs/gdb/General-Query-Packets.html#qSupported) response.
     -   `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.
-    -   Using a novel technique called [**Inlineable Dyn Extension Traits**](#zero-overhead-protocol-extensions) (IDETs), `gdbstub` enables fine-grained control over active protocol extensions _without_ relying on clunky `cargo` features.
+    -   Using a novel technique called [**Inlineable Dyn Extension Traits**](#zero-overhead-protocol-extensions) (IDETs), `gdbstub` enables fine-grained control over active protocol extensions _without_ relying on clunky `cargo` features or the use of `unsafe` code!
 -   **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.
+    -   `gdbstub`'s API is designed to be a "drop in" solution when you want to add debugging support into a project, and shouldn't require any large refactoring effort to integrate into an existing project.
 -   **`#![no_std]` Ready & Size Optimized**
     -   `gdbstub` is a **`no_std` first** library, whereby all protocol features are required to be `no_std` compatible.
     -   `gdbstub` does not require _any_ dynamic memory allocation, and can be configured to use fixed-size, pre-allocated buffers. This enables `gdbstub` to be used on even the most resource constrained, no-[`alloc`](https://doc.rust-lang.org/alloc/) platforms.
-    -   `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": All code related to parsing/handling protocol extensions is guaranteed to be dead-code-eliminated from an optimized binary if left unimplmeneted! See the [Zero-overhead Protocol Extensions](#zero-overhead-protocol-extensions) section below for more details.
-    -   `gdbstub` tries to keep the binary and RAM footprint of its minimal configuration to a bare minimum, enabling it to be used on even the most resource-constrained microcontrollers.
-        -   When compiled in release mode, 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`!_** \*
-        - \* 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 entirely **panic free** in most minimal configurations\*
+        -   \*when compiled in release mode, without the `paranoid_unsafe` cargo feature, on certain platforms.
+        -   Validated by inspecting the asm output of the in-tree `example_no_std`.
+    -   `gdbstub` is transport-layer agnostic, and uses a basic [`Connection`](https://docs.rs/gdbstub/latest/gdbstub/conn/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": All code related to parsing/handling protocol extensions is guaranteed to be dead-code-eliminated from an optimized binary if left unimplemented. See the [Zero-overhead Protocol Extensions](#zero-overhead-protocol-extensions) section below for more details.
+    -   `gdbstub`'s minimal configuration has an incredibly low binary size + RAM overhead, enabling it to be used on even the most resource-constrained microcontrollers.
+        -   When compiled in release mode, using all the tricks outlined in [`min-sized-rust`](https://github.com/johnthagen/min-sized-rust), a baseline `gdbstub` implementation can weigh in at **_less than 10kb of `.text` + `.rodata`!_** \*
+        - \*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.
 
 ### Can I Use `gdbstub` in Production?
 
 **Yes, as long as you don't mind some API churn until `1.0.0` is released.**
 
-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. While these changes are typically quite minor, they are nonetheless semver-breaking, and may require a code-change when moving between versions. Any particularly involved changes will typically be documented in a dedicated [transition guide](docs/transition_guide.md) document.
+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 API changes. While these changes are typically quite minor, they are nonetheless semver-breaking, and may require a code-change when moving between versions. Any particularly involved changes will typically be documented in a dedicated [transition guide](docs/transition_guide.md) document.
 
-That being said, `gdbstub` has already been integrated into [many real-world projects](#real-world-examples) since its initial `0.1` release, and empirical evidence suggests that it seems to be doing its job quite well! Thusfar, there haven't been any reported issues related to core GDB debugging functionality, with most issues being caused by faulty `Target` and/or `Arch` implementations.
+That being said, `gdbstub` has already been integrated into [many real-world projects](#real-world-examples) since its initial `0.1` release, and empirical evidence suggests that it seems to be doing its job quite well! Thusfar, most reported issues have been caused by improperly implemented `Target` and/or `Arch` implementations, while the core `gdbstub` library itself has proven to be reasonably bug-free.
 
 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`.
 
@@ -49,30 +55,39 @@
 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
     -   Enumerating threads
-        -   Only required in multithreaded targets
+
+Yep, that's right! That's all it takes to get `gdb` connected!
 
 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 target architecture + feature reporting
+-   Automatic target architecture + feature configuration
+-   Resume
+    -   Continue
+    -   Single Step
+    -   Range Step
+    -   _Reverse_ Step/Continue
 -   Breakpoints
     -   Software Breakpoints
     -   Hardware Breakpoints
     -   Read/Write/Access Watchpoints (i.e: value breakpoints)
--   Advanced step/continue
-    -   Reverse execution (reverse-step, reverse-continue)
-    -   Range-stepping
 -   Extended Mode
-    -   Run/Attach/Kill Processes
-    -   Pass environment variables / args to spawned processes
+    -   Launch new processes
+    -   Attach to an existing process
+    -   Kill an existing process
+    -   Pass env vars + 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
+    -   Enable/disable ASLR
+-   Read Memory Map (`info mem`)
+-   Read Section/Segment relocation offsets
+-   Handle custom `monitor` Commands
+    -   Extend the GDB protocol with custom debug commands using GDB's `monitor` command!
+-   Host I/O
+    -   Access the remote target's filesystem to read/write file
+    -   Can be used to automatically read the remote executable on attach (using `ExecFile`)
+-   Read auxiliary vector (`info auxv`)
 
 _Note:_ GDB features are implemented 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 and/or open a PR!
 
@@ -102,14 +117,20 @@
     -   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.
+-   `paranoid_unsafe`
+    -   Please refer to the [`unsafe` in `gdbstub`](#unsafe-in-gdbstub) section below for more details.
 
 ## 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 (x64)
-    -   [Firecracker](https://firecracker-microvm.github.io/) - A lightweight VMM developed by AWS - feature is in [PR](https://github.com/firecracker-microvm/firecracker/pull/2168)
+    -   [crosvm](https://google.github.io/crosvm/running_crosvm/usage.html#gdb-support) - The Chrome OS Virtual Machine Monitor (x64)
+    -   [Firecracker](https://firecracker-microvm.github.io/) - A lightweight VMM developed by AWS - feature is in [PR](https://github.com/firecracker-microvm/firecracker/pull/2333)
+    -   [uhyve](https://github.com/hermitcore/uhyve) - A minimal hypervisor for [RustyHermit](https://github.com/hermitcore/rusty-hermit)
+-   OS Kernels (using `gdbstub` on `no_std`)
+    -   [`vmware-labs/node-replicated-kernel`](https://github.com/vmware-labs/node-replicated-kernel/tree/4326704/kernel/src/arch/x86_64/gdb) - An (experimental) research OS kernel for x86-64 (amd64) machines
+    -   [`betrusted-io/xous-core`](https://github.com/betrusted-io/xous-core/blob/7d3d710/kernel/src/debug/gdb_server.rs) - The Xous microkernel operating system
 -   Emulators (x64)
     -   [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 (ARMv4T)
@@ -117,7 +138,7 @@
     -   [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-cli) - A physical memory introspection framework (part of `memflow-cli`) (64)
+    -   [memflow](https://github.com/memflow/memflow-cli) - 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.
 
@@ -136,32 +157,34 @@
 - `example_no_std` - `./example_no_std`
     - An _extremely_ minimal example which shows off 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 simply stubs all `gdbstub` functions.
-    - Doubles as a test-bed for tracking `gdbstub`'s approximate binary footprint (via the `check_size.sh` script), and validating certain dead-code-elimination optimizations.
-
-## 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!
+    - Doubles as a test-bed for tracking `gdbstub`'s approximate binary footprint (via the `check_size.sh` script), as well as validating certain dead-code-elimination optimizations.
 
 ## `unsafe` in `gdbstub`
 
 `gdbstub` limits its use of `unsafe` to a bare minimum, with all uses of `unsafe` required to have a corresponding `// SAFETY` comment as justification. The following list exhaustively documents all uses of `unsafe` in `gdbstub`.
 
+`rustc` + LLVM do a pretty incredible job at eliding bounds checks... most of the time. Unfortunately, there are a few places in the code where the compiler is not smart enough to "prove" that a bounds check isn't needed, and a bit of unsafe code is required to remove those bounds checks.
+
+Enabling the `paranoid_unsafe` feature will swap out a handful of unsafe `get_unchecked_mut` operations with their safe equivalents, at the expense of introducing panicking code into `gdbstub`. This feature is **disabled** by default, as the unsafe code has been aggressively audited and tested for correctness. That said, if you're particularly paranoid about the use of unsafe code, enabling this feature may offer some piece of mind.
+
 -   When no cargo features are enabled:
     -   A few trivially safe calls to `NonZeroUsize::new_unchecked()` when defining internal constants.
+
+-   When the `paranoid_unsafe` feature is enabled, the following `unsafe` code is _removed_:
+    -   `src/protocol/packet.rs`: Swaps a couple slice-index methods in `PacketBuf` to use `get_unchecked_mut`. The public API of struct ensures that the bounds used to index into the array remain in-bounds.
+    -   `src/protocol/common/hex.rs`: Use an alternate implementation of `decode_hex_buf`/`decode_bin_buf` which uses unsafe slice indexing.
+    -   `src/common.rs`: Use a checked transmute to convert a `u8` to a `Signal`
+
 -   When the `std` feature is enabled:
-    -   An implementation of `UnixStream::peek` which uses `libc::recv`. This manual implementation will be removed once [rust-lang/rust#76923](https://github.com/rust-lang/rust/issues/76923) is stabilized.
+    -   `src/connection/impls/unixstream.rs`: An implementation of `UnixStream::peek` which uses `libc::recv`. This manual implementation will be removed once [rust-lang/rust#76923](https://github.com/rust-lang/rust/issues/76923) is stabilized.
 
 ## Future Plans + Roadmap to `1.0.0`
 
-While the vast majority of GDB protocol features (e.g: remote filesystem support, tracepoint packets, most query packets, etc...) should _not_ require breaking API changes, there are still several key protocol features that'll need breaking API changes to be implemented.
+While the vast majority of GDB protocol features (e.g: remote filesystem support, tracepoint packets, most query packets, etc...) should _not_ require breaking API changes, the following features will most likely require at least some breaking API changes, and should therefore be implemented prior to `1.0.0`.
 
-The following features are most likely to require breaking API changes, and should therefore be implemented prior to `1.0.0`. Not that this is _not_ an exhaustive list, and is subject to change.
+Not that this is _not_ an exhaustive list, and is subject to change.
 
--   [ ] 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))
+-   [ ] Allow fine-grained control over target features via the `Arch` trait ([\#12](https://github.com/daniel5151/gdbstub/issues/12))
 -   [ ] Implement GDB's various high-level operating modes:
     -   [x] Single/Multi Thread debugging
     -   [ ] Multiprocess Debugging
@@ -170,15 +193,28 @@
     -   [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 _shouldn't_ work, it would be good to validate that the API + implementation supports this use-case.
+-   [x] Have a working example of `gdbstub` running in a "bare-metal" `#![no_std]` environment.
 
-Additionally, while not strict "blockers" to `1.0.0`, it would be good to explore these features as well:
+Additionally, while not _strict_ blockers to `1.0.0`, it would be good to explore these features as well:
 
 -   [ ] Should `gdbstub` commit to a MSRV?
--   [ ] Exposing `async/await` interfaces (particularly wrt. handling GDB client interrupts) ([\#36](https://github.com/daniel5151/gdbstub/issues/36))
+-   [ ] Remove lingering instances of `RawRegId` from `gdbstub_arch` ([\#29](https://github.com/daniel5151/gdbstub/issues/29))
+-   [x] Exposing `async/await` interfaces (particularly wrt. handling GDB client interrupts) ([\#36](https://github.com/daniel5151/gdbstub/issues/36))
 -   [ ] Supporting various [LLDB extensions](https://raw.githubusercontent.com/llvm-mirror/lldb/master/docs/lldb-gdb-remote.txt) to the GDB RSP
     -   Skimming through the list, it doesn't seem like these extensions would require breaking API changes -- more research is needed.
 -   [ ] Supporting multi-arch debugging via a single target
     -   e.g: debugging both x86 and x64 processes when running in extended mode
--   Proper handling of client "nack" packets for spotty connections.
+-   [ ] Proper handling of "nack" packets (for spotty connections)
+    - Responding with "nack" is easy - the client has to re-transmit the command
+    - Re-transmitting after receiving a "nack" might be a bit harder...
+
+## License
+
+gdbstub is free and open source! All code in this repository is dual-licensed under either:
+
+* MIT License ([LICENSE-MIT](docs/LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
+* Apache License, Version 2.0 ([LICENSE-APACHE](docs/LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
+
+at your option. This means you can select the license you prefer! This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are [very good reasons](https://github.com/daniel5151/gdbstub/issues/68) to include both.
+
+Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
diff --git a/docs/LICENSE-APACHE b/docs/LICENSE-APACHE
new file mode 100644
index 0000000..f433b1a
--- /dev/null
+++ b/docs/LICENSE-APACHE
@@ -0,0 +1,177 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
diff --git a/docs/LICENSE-MIT b/docs/LICENSE-MIT
new file mode 100644
index 0000000..62c6168
--- /dev/null
+++ b/docs/LICENSE-MIT
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 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/docs/transition_guide.md b/docs/transition_guide.md
index 32eb98a..6197171 100644
--- a/docs/transition_guide.md
+++ b/docs/transition_guide.md
@@ -1,11 +1,260 @@
-You may also find it useful to refer to the in-tree `armv4t` and `armv4t_multicore` examples when transitioning between versions.
+# Transition Guide
 
-# `0.4` -> `0.5`
+This document provides a brief overview of breaking changes between major `gdbstub` releases, along with tips/tricks/suggestions on how to migrate between `gdbstub` releases.
+
+This document does _not_ discuss any new features that might have been added between releases. For a comprehensive overview of what's been _added_ to `gdbstub` (as opposed to what's _changed_), check out the [`CHANGELOG.md`](../CHANGELOG.md).
+
+> _Note:_ after reading through this doc, you may also find it helpful to refer to the in-tree `armv4t` and `armv4t_multicore` examples when transitioning between versions.
+
+## `0.5` -> `0.6`
+
+`0.6` introduces a large number of breaking changes to the public APIs, and will require quite a bit more more "hands on" porting than previous `gdbstub` upgrades.
+
+The following guide is a **best-effort** attempt to document all the changes, but there are some parts that may be missing / incomplete.
+
+##### General API change - _lots_ of renaming + exported type reorganization
+
+Many types have been renamed, and many import paths have changed in `0.6`.
+
+Exhaustively listing them would be nearly impossible, but suffice it to say, you will need to tweak your imports.
+
+##### `Connection` API changes
+
+> _Note:_ If you haven't implemented `Connection` yourself (i.e: you are using one of the built-in `Connection` impls on `TcpStream`/`UnixStream`), you can skip this section.
+
+The blocking `read` method and non-blocking `peek` methods have been removed from the base `Connection` API, and have been moved to a new `ConnectionExt` type.
+
+For more context around this change, please refer to [Moving from `GdbStub::run` to `GdbStub::run_blocking`](#moving-from-gdbstubrun-to-gdbstubrun_blocking).
+
+Porting a `0.5` `Connection` to `0.6` is incredibly straightforward - you simply split your existing implementation in two:
+
+```rust
+// ==== 0.5.x ==== //
+
+impl Connection for MyConnection {
+    type Error = MyError;
+
+    fn write(&mut self, byte: u8) -> Result<(), Self::Error> { .. }
+    fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { .. }
+    fn read(&mut self) -> Result<u8, Self::Error> { .. }
+    fn peek(&mut self) -> Result<Option<u8>, Self::Error> { .. }
+    fn flush(&mut self) -> Result<(), Self::Error> { .. }
+    fn on_session_start(&mut self) -> Result<(), Self::Error> { .. }
+}
+
+// ==== 0.6.0 ==== //
+
+impl Connection for MyConnection {
+    type Error = MyError;
+
+    fn write(&mut self, byte: u8) -> Result<(), Self::Error> { .. }
+    fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { .. }
+    fn flush(&mut self) -> Result<(), Self::Error> { .. }
+    fn on_session_start(&mut self) -> Result<(), Self::Error> { .. }
+}
+
+impl ConnectionExt for MyConnection {
+    type Error = MyError;
+
+    fn read(&mut self) -> Result<u8, Self::Error> { .. }
+    fn peek(&mut self) -> Result<Option<u8>, Self::Error> { .. }
+}
+
+```
+
+##### `Arch` API - `RegId::from_raw_id`
+
+> _Note:_ If you haven't implemented `Arch` yourself (i.e: you are any of the `Arch` impls from `gdbstub_arch`), you can skip this section.
+
+The `Arch` API has had one breaking changes: The `RegId::from_raw_id` method's "register size" return value has been changed from `usize` to `Option<NonZeroUsize>`.
+
+If the register size is `Some`, `gdbstub` will include a runtime check to ensures that the target implementation does not send back more bytes than the register allows when responding to single-register read requests.
+
+If the register size is `None`, `gdbstub` will _omit_ this runtime check, and trust that the target's implementation of `read_register` is correct.
+
+_Porting advice:_ If your `Arch` implementation targets a specific architecture, it is _highly recommended_ that you simply wrap your existing size value with `Some`. This API change was made to support dynamic `Arch` implementations, whereby the behavior of the `Arch` varies on the runtime state of the program (e.g: in multi-system emulators), and there is not "fixed" register size per id.
+
+##### `Target` API - IDET methods are now prefixed with `supports_`
+
+All IDET methods have been prefixed with `supports_`, to make it easier to tell at-a-glance which methods are actual handler methods, and which are simply IDET plumbing.
+
+As such, when porting target code from `0.5` to `0.6`, before you dive into any functional changes, you should take a moment to find and rename any methods that have had their name changed.
+
+##### `Target` API - Introducing `enum Signal`
+
+In prior versions of `gdbstub`, signals were encoded as raw `u8` values. This wasn't very user-friendly, as it meant users had to manually locate the signal-to-integer mapping table themselves when working with signals in code.
+
+`0.6` introduces a new `enum Signal` which encodes this information within `gdbstub` itself.
+
+This new `Signal` type has replaced `u8` in any places that a `u8` was used to represent a signal, such as in `StopReason::Signal`, or as part of the various `resume` APIs.
+
+_Porting advice:_ The Rust compiler should catch any type errors due to this change, making it easy to swap out any instances of `u8` with the new `Signal` type.
+
+##### `HwWatchpoint` API - Plumb watchpoint `length` parameter to public API
+
+The watchpoint API has been updated to include a new `length` parameter, specifying what range of memory addresses the watchpoint should encompass.
+
+##### `TargetXmlOverride` API - Return data via `&mut [u8]` buffer
+
+In an effort to unify the implementations of various new `qXfer`-backed protocol extensions, the existing `TargetXmlOverride` has been changed from returning a `&str` value to using a `std::io::Read`-style "write the data into a `&mut [u8]` buffer" API.
+
+Porting a `0.5` `TargetDescriptionXmlOverride` to `0.6` is straightforward, though a bit boilerplate-y.
+
+```rust
+// ==== 0.5.x ==== //
+
+impl target::ext::target_description_xml_override::TargetDescriptionXmlOverride for Emu {
+    fn target_description_xml(&self) -> &str {
+        r#"<target version="1.0"><!-- custom override string --><architecture>armv4t</architecture></target>"#
+    }
+}
+
+// ==== 0.6.0 ==== //
+
+pub fn copy_to_buf(data: &[u8], buf: &mut [u8]) -> usize {
+    let len = data.len();
+    let buf = &mut buf[..len];
+    buf.copy_from_slice(data);
+    len
+}
+
+pub fn copy_range_to_buf(data: &[u8], offset: u64, length: usize, buf: &mut [u8]) -> usize {
+    let offset = match usize::try_from(offset) {
+        Ok(v) => v,
+        Err(_) => return 0,
+    };
+    let len = data.len();
+    let data = &data[len.min(offset)..len.min(offset + length)];
+    copy_to_buf(data, buf)
+}
+
+impl target::ext::target_description_xml_override::TargetDescriptionXmlOverride for Emu {
+    fn target_description_xml(
+        &self,
+        offset: u64,
+        length: usize,
+        buf: &mut [u8],
+    ) -> TargetResult<usize, Self> {
+        let xml = r#"<target version="1.0"><!-- custom override string --><architecture>armv4t</architecture></target>"#
+            .trim()
+            .as_bytes();
+        Ok(copy_range_to_buf(xml, offset, length, buf))
+    }
+}
+```
+
+##### Updates to `{Single,Multi}ThreadOps::resume` API
+
+`0.6` includes three fairly major behavioral changes to the `resume` method:
+
+###### Support for `resume` is now entirely optional
+
+There are quite a few use cases where it might make sense to debug a target that does _not_ support resumption, e.g: a post-mortem debugging session, or when debugging crash dumps. In these cases, past version of `gdbstub` would force the user to nonetheless implement "stub" methods for resuming these targets, along with forcing users to pay the "cost" of including all the handler code related to resumption (of which there is quite a bit.)
+
+In `0.6`, all resume-related functionality has been extracted out of `{Single,Multi}ThreadBase`, and split into new `{Singe,Multi}ThreadResume` IDETs.
+
+###### Removing `ResumeAction`, and making single-step support optional
+
+The GDB protocol only requires that targets implement support for _continuing_ execution - support for instruction-level single-step execution is totally optional.
+
+> Note: this isn't actually true in practice, thanks to a bug in the mainline GDB client... See the docs for `Target::use_optional_single_step` for details...
+
+To model this behavior, `0.6` has split single-step support into its own IDET, in a manner similar to how optimized range step support was handled in `0.5`.
+
+In doing so, the `enum ResumeAction` type could be removed entirely, as single-step resume was to be handled in its own method.
+
+###### Removing `gdb_interrupt: GdbInterrupt`, and making `resume` non-blocking
+
+In past versions of `gdbstub`, the `resume` API would _block_ the thread waiting for the target to hit some kind of stop condition. In this model, checking for pending GDB interrupts was quite unergonomic, requiring that the thread periodically wake up and check whether an interrupt has arrived via the `GdbInterrupt` type.
+
+`gdbstub` `0.6` introduces a new paradigm of driving target execution, predicated on the idea that the target's `resume` method _does not block_, instead yielding execution immediately, and deferring the responsibility of "selecting" between incoming stop events and GDB interrupts to higher levels of the `gdbstub` "stack".
+
+In practice, this means that much of the logic that used to live in the `resume` implementation will now move into upper-levels of the `gdbstub` API, with the `resume` API serving more of a "bookkeeping" purpose, recording what kind of resumption mode the GDB client has requested from the target, while not actually resuming the target itself.
+
+For more context around this change, please refer to [Moving from `GdbStub::run` to `GdbStub::run_blocking`](#moving-from-gdbstubrun-to-gdbstubrun_blocking).
+
+###### Example: migrating `resume` from `0.5` to `0.6`
+
+Much of the code contained within methods such as `block_until_stop_reason_or_interrupt` will be lifted into upper layers of the `gdbstub` API, leaving behind just a small bit of code in the target's `resume` method to perform "bookkeeping" regarding how the GDB client requested the target to be resumed.
+
+```rust
+// ==== 0.5.x ==== //
+
+impl SingleThreadOps for Emu {
+    fn resume(
+        &mut self,
+        action: ResumeAction,
+        gdb_interrupt: GdbInterrupt<'_>,
+    ) -> Result<StopReason<u32>, Self::Error> {
+        match action {
+            ResumeAction::Step => self.do_single_step(),
+            ResumeAction::Continue => self.block_until_stop_reason_or_interrupt(action, || gdb_interrupt.pending()),
+            _ => self.handle_resume_with_signal(action),
+        }
+    }
+}
+
+// ==== 0.6.0 ==== //
+
+impl SingleThreadBase for Emu {
+    // resume has been split into a separate IDET
+    #[inline(always)]
+    fn support_resume(
+        &mut self
+    ) -> Option<SingleThreadResumeOps<Self>> {
+        Some(self)
+    }
+}
+
+
+impl SingleThreadResume for Emu {
+    fn resume(
+        &mut self,
+        signal: Option<Signal>,
+    ) -> Result<(), Self::Error> { // <-- no longer returns a stop reason!
+        if let Some(signal) = signal {
+            self.handle_signal(signal)?;
+        }
+
+        // upper layers of the `gdbstub` API will be responsible for "driving"
+        // target execution - `resume` simply performs book keeping on _how_ the
+        // target should be resumed.
+        self.set_execution_mode(ExecMode::Continue)?;
+
+        Ok(())
+    }
+
+    // single-step support has been split into a separate IDET
+    #[inline(always)]
+    fn support_single_step(
+        &mut self
+    ) -> Option<SingleThreadSingleStepOps<'_, Self>> {
+        Some(self)
+    }
+}
+
+impl SingleThreadSingleStep for Emu {
+    fn step(&mut self, signal: Option<Signal>) -> Result<(), Self::Error> {
+        if let Some(signal) = signal {
+            self.handle_signal(signal)?;
+        }
+
+        self.set_execution_mode(ExecMode::Step)?;
+        Ok(())
+    }
+}
+```
+
+##### Moving from `GdbStub::run` to `GdbStub::run_blocking`
+
+With the introduction of the new state-machine API, the responsibility of reading incoming has been lifted out of `gdbstub` itself, and is now something implementations are responsible for . The alternative approach would've been to have `Connection` include multiple different `read`-like methods for various kinds of paradigms - such as `async`/`await`, `epoll`, etc...
+
+> TODO. In the meantime, I would suggest looking at rustdoc for details on how to use `GdbStub::run_blocking`...
+
+## `0.4` -> `0.5`
 
 While the overall structure of the API has remained the same, `0.5.0` does introduce a few breaking API changes that require some attention. That being said, it should not be a difficult migration, and updating to `0.5.0` from `0.4` shouldn't take more than 10 mins of refactoring.
 
-Check out [`CHANGELOG.md`](../CHANGELOG.md) for a full list of changes.
-
 ##### Consolidating the `{Hw,Sw}Breakpoint/Watchpoint` IDETs under the newly added `Breakpoints` IDETs.
 
 The various breakpoint IDETs that were previously directly implemented on the top-level `Target` trait have now been consolidated under a single `Breakpoints` IDET. This is purely an organizational change, and will not require rewriting any existing `{add, remove}_{sw_break,hw_break,watch}point` implementations.
@@ -54,7 +303,7 @@
     }
 }
 
-// (Almost Unchanged) // 
+// (Almost Unchanged) //
 impl target::ext::breakpoints::SwBreakpoint for Emu {
     //                                            /-- New `kind` parameter
     //                                           \/
@@ -62,7 +311,7 @@
     fn remove_sw_breakpoint(&mut self, addr: u32, _kind: arch::arm::ArmBreakpointKind) -> TargetResult<bool, Self> { ... }
 }
 
-// (Unchanged) // 
+// (Unchanged) //
 impl target::ext::breakpoints::HwWatchpoint for Emu {
     fn add_hw_watchpoint(&mut self, addr: u32, kind: WatchKind) -> TargetResult<bool, Self> { ... }
     fn remove_hw_watchpoint(&mut self, addr: u32, kind: WatchKind) -> TargetResult<bool, Self> { ... }
diff --git a/examples/armv4t/emu.rs b/examples/armv4t/emu.rs
index 3a1a4d3..e8f3305 100644
--- a/examples/armv4t/emu.rs
+++ b/examples/armv4t/emu.rs
@@ -7,21 +7,34 @@
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 pub enum Event {
+    DoneStep,
     Halted,
     Break,
     WatchWrite(u32),
     WatchRead(u32),
 }
 
+pub enum ExecMode {
+    Step,
+    Continue,
+    RangeStep(u32, u32),
+}
+
 /// incredibly barebones armv4t-based emulator
 pub struct Emu {
     start_addr: u32,
 
+    // example custom register. only read/written to from the GDB client
+    pub(crate) custom_reg: u32,
+
+    pub(crate) exec_mode: ExecMode,
+
     pub(crate) cpu: Cpu,
     pub(crate) mem: ExampleMem,
 
     pub(crate) watchpoints: Vec<u32>,
     pub(crate) breakpoints: Vec<u32>,
+    pub(crate) files: Vec<Option<std::fs::File>>,
 }
 
 impl Emu {
@@ -42,12 +55,12 @@
         for h in sections {
             eprintln!(
                 "loading section {:?} into memory from [{:#010x?}..{:#010x?}]",
-                elf_header.shdr_strtab.get(h.sh_name).unwrap().unwrap(),
+                elf_header.shdr_strtab.get_at(h.sh_name).unwrap(),
                 h.sh_addr,
                 h.sh_addr + h.sh_size,
             );
 
-            for (i, b) in program_elf[h.file_range()].iter().enumerate() {
+            for (i, b) in program_elf[h.file_range().unwrap()].iter().enumerate() {
                 mem.w8(h.sh_addr as u32 + i as u32, *b);
             }
         }
@@ -61,11 +74,17 @@
 
         Ok(Emu {
             start_addr: elf_header.entry as u32,
+
+            custom_reg: 0x12345678,
+
+            exec_mode: ExecMode::Continue,
+
             cpu,
             mem,
 
             watchpoints: Vec::new(),
             breakpoints: Vec::new(),
+            files: Vec::new(),
         })
     }
 
@@ -76,6 +95,7 @@
         self.cpu.reg_set(Mode::User, reg::CPSR, 0x10);
     }
 
+    /// single-step the interpreter
     pub fn step(&mut self) -> Option<Event> {
         let mut hit_watchpoint = None;
 
@@ -106,4 +126,57 @@
 
         None
     }
+
+    /// run the emulator in accordance with the currently set `ExecutionMode`.
+    ///
+    /// since the emulator runs in the same thread as the GDB loop, the emulator
+    /// will use the provided callback to poll the connection for incoming data
+    /// every 1024 steps.
+    pub fn run(&mut self, mut poll_incoming_data: impl FnMut() -> bool) -> RunEvent {
+        match self.exec_mode {
+            ExecMode::Step => RunEvent::Event(self.step().unwrap_or(Event::DoneStep)),
+            ExecMode::Continue => {
+                let mut cycles = 0;
+                loop {
+                    if cycles % 1024 == 0 {
+                        // poll for incoming data
+                        if poll_incoming_data() {
+                            break RunEvent::IncomingData;
+                        }
+                    }
+                    cycles += 1;
+
+                    if let Some(event) = self.step() {
+                        break RunEvent::Event(event);
+                    };
+                }
+            }
+            // just continue, but with an extra PC check
+            ExecMode::RangeStep(start, end) => {
+                let mut cycles = 0;
+                loop {
+                    if cycles % 1024 == 0 {
+                        // poll for incoming data
+                        if poll_incoming_data() {
+                            break RunEvent::IncomingData;
+                        }
+                    }
+                    cycles += 1;
+
+                    if let Some(event) = self.step() {
+                        break RunEvent::Event(event);
+                    };
+
+                    if !(start..end).contains(&self.cpu.reg_get(self.cpu.mode(), reg::PC)) {
+                        break RunEvent::Event(Event::DoneStep);
+                    }
+                }
+            }
+        }
+    }
+}
+
+pub enum RunEvent {
+    IncomingData,
+    Event(Event),
 }
diff --git a/examples/armv4t/gdb/auxv.rs b/examples/armv4t/gdb/auxv.rs
new file mode 100644
index 0000000..a0f163b
--- /dev/null
+++ b/examples/armv4t/gdb/auxv.rs
@@ -0,0 +1,12 @@
+use gdbstub::target;
+use gdbstub::target::TargetResult;
+
+use super::copy_range_to_buf;
+use crate::emu::Emu;
+
+impl target::ext::auxv::Auxv for Emu {
+    fn get_auxv(&self, offset: u64, length: usize, buf: &mut [u8]) -> TargetResult<usize, Self> {
+        let auxv = b"\x00\x00\x00\x00\x00\x00\x00\x00";
+        Ok(copy_range_to_buf(auxv, offset, length, buf))
+    }
+}
diff --git a/examples/armv4t/gdb/breakpoints.rs b/examples/armv4t/gdb/breakpoints.rs
index 1512943..6fa3038 100644
--- a/examples/armv4t/gdb/breakpoints.rs
+++ b/examples/armv4t/gdb/breakpoints.rs
@@ -6,12 +6,16 @@
 
 impl target::ext::breakpoints::Breakpoints for Emu {
     #[inline(always)]
-    fn sw_breakpoint(&mut self) -> Option<target::ext::breakpoints::SwBreakpointOps<Self>> {
+    fn support_sw_breakpoint(
+        &mut self,
+    ) -> Option<target::ext::breakpoints::SwBreakpointOps<'_, Self>> {
         Some(self)
     }
 
     #[inline(always)]
-    fn hw_watchpoint(&mut self) -> Option<target::ext::breakpoints::HwWatchpointOps<Self>> {
+    fn support_hw_watchpoint(
+        &mut self,
+    ) -> Option<target::ext::breakpoints::HwWatchpointOps<'_, Self>> {
         Some(self)
     }
 }
@@ -41,27 +45,41 @@
 }
 
 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),
-        };
+    fn add_hw_watchpoint(
+        &mut self,
+        addr: u32,
+        len: u32,
+        kind: WatchKind,
+    ) -> TargetResult<bool, Self> {
+        for addr in addr..(addr + len) {
+            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,
-        };
+    fn remove_hw_watchpoint(
+        &mut self,
+        addr: u32,
+        len: u32,
+        kind: WatchKind,
+    ) -> TargetResult<bool, Self> {
+        for addr in addr..(addr + len) {
+            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),
-        };
+            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/catch_syscalls.rs b/examples/armv4t/gdb/catch_syscalls.rs
new file mode 100644
index 0000000..63686f1
--- /dev/null
+++ b/examples/armv4t/gdb/catch_syscalls.rs
@@ -0,0 +1,28 @@
+use gdbstub::target;
+use gdbstub::target::ext::catch_syscalls::SyscallNumbers;
+
+use crate::gdb::Emu;
+
+// This implementation is for illustrative purposes only. If the target doesn't
+// support syscalls then there is no need to implement this extension
+
+impl target::ext::catch_syscalls::CatchSyscalls for Emu {
+    fn enable_catch_syscalls(
+        &mut self,
+        filter: Option<SyscallNumbers<'_, u32>>,
+    ) -> target::TargetResult<(), Self> {
+        match filter {
+            Some(numbers) => eprintln!(
+                "Enabled catching syscalls: {:?}",
+                numbers.collect::<Vec<u32>>()
+            ),
+            None => eprintln!("Enabled catching all syscalls"),
+        }
+        Ok(())
+    }
+
+    fn disable_catch_syscalls(&mut self) -> target::TargetResult<(), Self> {
+        eprintln!("Disabled catching syscalls");
+        Ok(())
+    }
+}
diff --git a/examples/armv4t/gdb/exec_file.rs b/examples/armv4t/gdb/exec_file.rs
new file mode 100644
index 0000000..e70a022
--- /dev/null
+++ b/examples/armv4t/gdb/exec_file.rs
@@ -0,0 +1,19 @@
+use gdbstub::common::Pid;
+use gdbstub::target;
+use gdbstub::target::TargetResult;
+
+use super::copy_range_to_buf;
+use crate::emu::Emu;
+
+impl target::ext::exec_file::ExecFile for Emu {
+    fn get_exec_file(
+        &self,
+        _pid: Option<Pid>,
+        offset: u64,
+        length: usize,
+        buf: &mut [u8],
+    ) -> TargetResult<usize, Self> {
+        let filename = b"/test.elf";
+        Ok(copy_range_to_buf(filename, offset, length, buf))
+    }
+}
diff --git a/examples/armv4t/gdb/extended_mode.rs b/examples/armv4t/gdb/extended_mode.rs
index 6532507..4b30657 100644
--- a/examples/armv4t/gdb/extended_mode.rs
+++ b/examples/armv4t/gdb/extended_mode.rs
@@ -35,7 +35,7 @@
         Err(().into()) // non-specific failure
     }
 
-    fn run(&mut self, filename: Option<&[u8]>, args: Args) -> TargetResult<Pid, Self> {
+    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
@@ -70,26 +70,30 @@
     }
 
     #[inline(always)]
-    fn configure_aslr(&mut self) -> Option<target::ext::extended_mode::ConfigureAslrOps<Self>> {
-        Some(self)
-    }
-
-    #[inline(always)]
-    fn configure_env(&mut self) -> Option<target::ext::extended_mode::ConfigureEnvOps<Self>> {
-        Some(self)
-    }
-
-    #[inline(always)]
-    fn configure_startup_shell(
+    fn support_configure_aslr(
         &mut self,
-    ) -> Option<target::ext::extended_mode::ConfigureStartupShellOps<Self>> {
+    ) -> Option<target::ext::extended_mode::ConfigureAslrOps<'_, Self>> {
         Some(self)
     }
 
     #[inline(always)]
-    fn configure_working_dir(
+    fn support_configure_env(
         &mut self,
-    ) -> Option<target::ext::extended_mode::ConfigureWorkingDirOps<Self>> {
+    ) -> Option<target::ext::extended_mode::ConfigureEnvOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_configure_startup_shell(
+        &mut self,
+    ) -> Option<target::ext::extended_mode::ConfigureStartupShellOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_configure_working_dir(
+        &mut self,
+    ) -> Option<target::ext::extended_mode::ConfigureWorkingDirOps<'_, Self>> {
         Some(self)
     }
 }
diff --git a/examples/armv4t/gdb/host_io.rs b/examples/armv4t/gdb/host_io.rs
new file mode 100644
index 0000000..40d6304
--- /dev/null
+++ b/examples/armv4t/gdb/host_io.rs
@@ -0,0 +1,277 @@
+use std::io::{Read, Seek, Write};
+
+use gdbstub::target;
+use gdbstub::target::ext::host_io::{
+    FsKind, HostIoErrno, HostIoError, HostIoOpenFlags, HostIoOpenMode, HostIoResult, HostIoStat,
+};
+
+use super::{copy_range_to_buf, copy_to_buf};
+use crate::emu::Emu;
+use crate::TEST_PROGRAM_ELF;
+
+const FD_RESERVED: u32 = 1;
+
+impl target::ext::host_io::HostIo for Emu {
+    #[inline(always)]
+    fn support_open(&mut self) -> Option<target::ext::host_io::HostIoOpenOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_close(&mut self) -> Option<target::ext::host_io::HostIoCloseOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_pread(&mut self) -> Option<target::ext::host_io::HostIoPreadOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_pwrite(&mut self) -> Option<target::ext::host_io::HostIoPwriteOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_fstat(&mut self) -> Option<target::ext::host_io::HostIoFstatOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_unlink(&mut self) -> Option<target::ext::host_io::HostIoUnlinkOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_readlink(&mut self) -> Option<target::ext::host_io::HostIoReadlinkOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_setfs(&mut self) -> Option<target::ext::host_io::HostIoSetfsOps<'_, Self>> {
+        Some(self)
+    }
+}
+
+impl target::ext::host_io::HostIoOpen for Emu {
+    fn open(
+        &mut self,
+        filename: &[u8],
+        flags: HostIoOpenFlags,
+        _mode: HostIoOpenMode,
+    ) -> HostIoResult<u32, Self> {
+        if filename.starts_with(b"/proc") {
+            return Err(HostIoError::Errno(HostIoErrno::ENOENT));
+        }
+
+        // In this example, the test binary is compiled into the binary itself as the
+        // `TEST_PROGRAM_ELF` array using `include_bytes!`. As such, we must "spoof" the
+        // existence of a real file, which will actually be backed by the in-binary
+        // `TEST_PROGRAM_ELF` array.
+        if filename == b"/test.elf" {
+            return Ok(0);
+        }
+
+        let path =
+            std::str::from_utf8(filename).map_err(|_| HostIoError::Errno(HostIoErrno::ENOENT))?;
+
+        let mut read = false;
+        let mut write = false;
+        if flags.contains(HostIoOpenFlags::O_RDWR) {
+            read = true;
+            write = true;
+        } else if flags.contains(HostIoOpenFlags::O_WRONLY) {
+            write = true;
+        } else {
+            read = true;
+        }
+
+        let file = std::fs::OpenOptions::new()
+            .read(read)
+            .write(write)
+            .append(flags.contains(HostIoOpenFlags::O_APPEND))
+            .create(flags.contains(HostIoOpenFlags::O_CREAT))
+            .truncate(flags.contains(HostIoOpenFlags::O_TRUNC))
+            .create_new(flags.contains(HostIoOpenFlags::O_EXCL))
+            .open(path)?;
+
+        let n = match self.files.iter_mut().enumerate().find(|(_, f)| f.is_none()) {
+            Some((n, free_file)) => {
+                *free_file = Some(file);
+                n
+            }
+            None => {
+                self.files.push(Some(file));
+                self.files.len() - 1
+            }
+        };
+
+        Ok(n as u32 + FD_RESERVED)
+    }
+}
+
+impl target::ext::host_io::HostIoClose for Emu {
+    fn close(&mut self, fd: u32) -> HostIoResult<(), Self> {
+        if fd < FD_RESERVED {
+            return Ok(());
+        }
+
+        let file = match self.files.get_mut((fd - FD_RESERVED) as usize) {
+            Some(file) => file,
+            _ => return Err(HostIoError::Errno(HostIoErrno::EBADF)),
+        };
+
+        file.take().ok_or(HostIoError::Errno(HostIoErrno::EBADF))?;
+        while let Some(None) = self.files.last() {
+            self.files.pop();
+        }
+        Ok(())
+    }
+}
+
+impl target::ext::host_io::HostIoPread for Emu {
+    fn pread<'a>(
+        &mut self,
+        fd: u32,
+        count: usize,
+        offset: u64,
+        buf: &mut [u8],
+    ) -> HostIoResult<usize, Self> {
+        if fd < FD_RESERVED {
+            if fd == 0 {
+                return Ok(copy_range_to_buf(TEST_PROGRAM_ELF, offset, count, buf));
+            } else {
+                return Err(HostIoError::Errno(HostIoErrno::EBADF));
+            }
+        }
+
+        let file = match self.files.get_mut((fd - FD_RESERVED) as usize) {
+            Some(Some(file)) => file,
+            _ => return Err(HostIoError::Errno(HostIoErrno::EBADF)),
+        };
+
+        file.seek(std::io::SeekFrom::Start(offset))?;
+        let n = file.read(buf)?;
+        Ok(n)
+    }
+}
+
+impl target::ext::host_io::HostIoPwrite for Emu {
+    fn pwrite(&mut self, fd: u32, offset: u32, data: &[u8]) -> HostIoResult<u32, Self> {
+        if fd < FD_RESERVED {
+            return Err(HostIoError::Errno(HostIoErrno::EACCES));
+        }
+
+        let file = match self.files.get_mut((fd - FD_RESERVED) as usize) {
+            Some(Some(file)) => file,
+            _ => return Err(HostIoError::Errno(HostIoErrno::EBADF)),
+        };
+
+        file.seek(std::io::SeekFrom::Start(offset as u64))?;
+        let n = file.write(data)?;
+        Ok(n as u32)
+    }
+}
+
+impl target::ext::host_io::HostIoFstat for Emu {
+    fn fstat(&mut self, fd: u32) -> HostIoResult<HostIoStat, Self> {
+        if fd < FD_RESERVED {
+            if fd == 0 {
+                return Ok(HostIoStat {
+                    st_dev: 0,
+                    st_ino: 0,
+                    st_mode: HostIoOpenMode::empty(),
+                    st_nlink: 0,
+                    st_uid: 0,
+                    st_gid: 0,
+                    st_rdev: 0,
+                    st_size: TEST_PROGRAM_ELF.len() as u64,
+                    st_blksize: 0,
+                    st_blocks: 0,
+                    st_atime: 0,
+                    st_mtime: 0,
+                    st_ctime: 0,
+                });
+            } else {
+                return Err(HostIoError::Errno(HostIoErrno::EBADF));
+            }
+        }
+        let metadata = match self.files.get((fd - FD_RESERVED) as usize) {
+            Some(Some(file)) => file.metadata()?,
+            _ => return Err(HostIoError::Errno(HostIoErrno::EBADF)),
+        };
+
+        macro_rules! time_to_secs {
+            ($time:expr) => {
+                $time
+                    .map_err(|_| HostIoError::Errno(HostIoErrno::EACCES))?
+                    .duration_since(std::time::SystemTime::UNIX_EPOCH)
+                    .map_err(|_| HostIoError::Errno(HostIoErrno::EACCES))?
+                    .as_secs() as u32
+            };
+        }
+        let atime = time_to_secs!(metadata.accessed());
+        let mtime = time_to_secs!(metadata.modified());
+        let ctime = time_to_secs!(metadata.created());
+
+        Ok(HostIoStat {
+            st_dev: 0,
+            st_ino: 0,
+            st_mode: HostIoOpenMode::empty(),
+            st_nlink: 0,
+            st_uid: 0,
+            st_gid: 0,
+            st_rdev: 0,
+            st_size: metadata.len(),
+            st_blksize: 0,
+            st_blocks: 0,
+            st_atime: atime,
+            st_mtime: mtime,
+            st_ctime: ctime,
+        })
+    }
+}
+
+impl target::ext::host_io::HostIoUnlink for Emu {
+    fn unlink(&mut self, filename: &[u8]) -> HostIoResult<(), Self> {
+        let path =
+            std::str::from_utf8(filename).map_err(|_| HostIoError::Errno(HostIoErrno::ENOENT))?;
+        std::fs::remove_file(path)?;
+        Ok(())
+    }
+}
+
+impl target::ext::host_io::HostIoReadlink for Emu {
+    fn readlink<'a>(&mut self, filename: &[u8], buf: &mut [u8]) -> HostIoResult<usize, Self> {
+        if filename == b"/proc/1/exe" {
+            // Support `info proc exe` command
+            let exe = b"/test.elf";
+            return Ok(copy_to_buf(exe, buf));
+        } else if filename == b"/proc/1/cwd" {
+            // Support `info proc cwd` command
+            let cwd = b"/";
+            return Ok(copy_to_buf(cwd, buf));
+        } else if filename.starts_with(b"/proc") {
+            return Err(HostIoError::Errno(HostIoErrno::ENOENT));
+        }
+
+        let path =
+            std::str::from_utf8(filename).map_err(|_| HostIoError::Errno(HostIoErrno::ENOENT))?;
+        let link = std::fs::read_link(path)?;
+        let data = link
+            .to_str()
+            .ok_or(HostIoError::Errno(HostIoErrno::ENOENT))?
+            .as_bytes();
+        if data.len() <= buf.len() {
+            Ok(copy_to_buf(data, buf))
+        } else {
+            Err(HostIoError::Errno(HostIoErrno::ENAMETOOLONG))
+        }
+    }
+}
+
+impl target::ext::host_io::HostIoSetfs for Emu {
+    fn setfs(&mut self, _fs: FsKind) -> HostIoResult<(), Self> {
+        Ok(())
+    }
+}
diff --git a/examples/armv4t/gdb/memory_map.rs b/examples/armv4t/gdb/memory_map.rs
new file mode 100644
index 0000000..ddddb65
--- /dev/null
+++ b/examples/armv4t/gdb/memory_map.rs
@@ -0,0 +1,27 @@
+use gdbstub::target;
+use gdbstub::target::TargetResult;
+
+use super::copy_range_to_buf;
+use crate::emu::Emu;
+
+impl target::ext::memory_map::MemoryMap for Emu {
+    fn memory_map_xml(
+        &self,
+        offset: u64,
+        length: usize,
+        buf: &mut [u8],
+    ) -> TargetResult<usize, Self> {
+        // Sample memory map, with RAM coverying the whole
+        // memory space.
+        let memory_map = r#"<?xml version="1.0"?>
+<!DOCTYPE memory-map
+    PUBLIC "+//IDN gnu.org//DTD GDB Memory Map V1.0//EN"
+            "http://sourceware.org/gdb/gdb-memory-map.dtd">
+<memory-map>
+    <memory type="ram" start="0x0" length="0x100000000"/>
+</memory-map>"#
+            .trim()
+            .as_bytes();
+        Ok(copy_range_to_buf(memory_map, offset, length, buf))
+    }
+}
diff --git a/examples/armv4t/gdb/mod.rs b/examples/armv4t/gdb/mod.rs
index bb9a0e7..155fff3 100644
--- a/examples/armv4t/gdb/mod.rs
+++ b/examples/armv4t/gdb/mod.rs
@@ -1,21 +1,23 @@
 use core::convert::TryInto;
 
 use armv4t_emu::{reg, Memory};
+use gdbstub::common::Signal;
 use gdbstub::target;
-use gdbstub::target::ext::base::singlethread::{
-    GdbInterrupt, ResumeAction, SingleThreadOps, SingleThreadReverseContOps,
-    SingleThreadReverseStepOps, StopReason,
-};
-use gdbstub::target::ext::breakpoints::WatchKind;
+use gdbstub::target::ext::base::singlethread::{SingleThreadBase, SingleThreadResume};
 use gdbstub::target::{Target, TargetError, TargetResult};
 use gdbstub_arch::arm::reg::id::ArmCoreRegId;
 
-use crate::emu::{Emu, Event};
+use crate::emu::{Emu, ExecMode};
 
 // Additional GDB extensions
 
+mod auxv;
 mod breakpoints;
+mod catch_syscalls;
+mod exec_file;
 mod extended_mode;
+mod host_io;
+mod memory_map;
 mod monitor_cmd;
 mod section_offsets;
 mod target_description_xml_override;
@@ -32,8 +34,40 @@
     }
 }
 
+/// Copy all bytes of `data` to `buf`.
+/// Return the size of data copied.
+pub fn copy_to_buf(data: &[u8], buf: &mut [u8]) -> usize {
+    let len = buf.len().min(data.len());
+    buf[..len].copy_from_slice(&data[..len]);
+    len
+}
+
+/// Copy a range of `data` (start at `offset` with a size of `length`) to `buf`.
+/// Return the size of data copied. Returns 0 if `offset >= buf.len()`.
+///
+/// Mainly used by qXfer:_object_:read commands.
+pub fn copy_range_to_buf(data: &[u8], offset: u64, length: usize, buf: &mut [u8]) -> usize {
+    let offset = offset as usize;
+    if offset > data.len() {
+        return 0;
+    }
+
+    let start = offset;
+    let end = (offset + length).min(data.len());
+    copy_to_buf(&data[start..end], buf)
+}
+
 impl Target for Emu {
-    type Arch = gdbstub_arch::arm::Armv4t;
+    // As an example, I've defined a custom architecture based off
+    // `gdbstub_arch::arm::Armv4t`. The implementation is in the `custom_arch`
+    // module at the bottom of this file.
+    //
+    // unless you're working with a particularly funky architecture that uses custom
+    // registers, you should probably stick to using the simple `target.xml`
+    // implementations from the `gdbstub_arch` repo (i.e: `target.xml` files that
+    // only specify the <architecture> and <feature>s of the arch, instead of
+    // listing out all the registers out manually).
+    type Arch = custom_arch::Armv4tCustom;
     type Error = &'static str;
 
     // --------------- IMPORTANT NOTE ---------------
@@ -42,126 +76,117 @@
     // implementations, resulting in unnecessary binary bloat.
 
     #[inline(always)]
-    fn base_ops(&mut self) -> target::ext::base::BaseOps<Self::Arch, Self::Error> {
+    fn base_ops(&mut self) -> target::ext::base::BaseOps<'_, Self::Arch, Self::Error> {
         target::ext::base::BaseOps::SingleThread(self)
     }
 
     #[inline(always)]
-    fn breakpoints(&mut self) -> Option<target::ext::breakpoints::BreakpointsOps<Self>> {
-        Some(self)
-    }
-
-    #[inline(always)]
-    fn extended_mode(&mut self) -> Option<target::ext::extended_mode::ExtendedModeOps<Self>> {
-        Some(self)
-    }
-
-    #[inline(always)]
-    fn monitor_cmd(&mut self) -> Option<target::ext::monitor_cmd::MonitorCmdOps<Self>> {
-        Some(self)
-    }
-
-    #[inline(always)]
-    fn section_offsets(&mut self) -> Option<target::ext::section_offsets::SectionOffsetsOps<Self>> {
-        Some(self)
-    }
-
-    #[inline(always)]
-    fn target_description_xml_override(
+    fn support_breakpoints(
         &mut self,
-    ) -> Option<target::ext::target_description_xml_override::TargetDescriptionXmlOverrideOps<Self>>
-    {
+    ) -> Option<target::ext::breakpoints::BreakpointsOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_extended_mode(
+        &mut self,
+    ) -> Option<target::ext::extended_mode::ExtendedModeOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_monitor_cmd(&mut self) -> Option<target::ext::monitor_cmd::MonitorCmdOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_section_offsets(
+        &mut self,
+    ) -> Option<target::ext::section_offsets::SectionOffsetsOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_target_description_xml_override(
+        &mut self,
+    ) -> Option<
+        target::ext::target_description_xml_override::TargetDescriptionXmlOverrideOps<'_, Self>,
+    > {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_memory_map(&mut self) -> Option<target::ext::memory_map::MemoryMapOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_catch_syscalls(
+        &mut self,
+    ) -> Option<target::ext::catch_syscalls::CatchSyscallsOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_host_io(&mut self) -> Option<target::ext::host_io::HostIoOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_exec_file(&mut self) -> Option<target::ext::exec_file::ExecFileOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_auxv(&mut self) -> Option<target::ext::auxv::AuxvOps<'_, Self>> {
         Some(self)
     }
 }
 
-impl Emu {
-    fn inner_resume(
-        &mut self,
-        action: ResumeAction,
-        mut check_gdb_interrupt: impl FnMut() -> bool,
-    ) -> Result<StopReason<u32>, &'static str> {
-        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);
-                    }
-                }
-            }
-            _ => return Err("cannot resume with signal"),
-        };
-
-        Ok(match event {
-            Event::Halted => StopReason::Terminated(19), // SIGSTOP
-            Event::Break => StopReason::SwBreak,
-            Event::WatchWrite(addr) => StopReason::Watch {
-                kind: WatchKind::Write,
-                addr,
-            },
-            Event::WatchRead(addr) => StopReason::Watch {
-                kind: WatchKind::Read,
-                addr,
-            },
-        })
-    }
-}
-
-impl SingleThreadOps for Emu {
-    fn resume(
-        &mut self,
-        action: ResumeAction,
-        gdb_interrupt: GdbInterrupt<'_>,
-    ) -> Result<StopReason<u32>, Self::Error> {
-        let mut gdb_interrupt = gdb_interrupt.no_async();
-        self.inner_resume(action, || gdb_interrupt.pending())
-    }
-
+impl SingleThreadBase for Emu {
     fn read_registers(
         &mut self,
-        regs: &mut gdbstub_arch::arm::reg::ArmCoreRegs,
+        regs: &mut custom_arch::ArmCoreRegsCustom,
     ) -> TargetResult<(), Self> {
         let mode = self.cpu.mode();
 
         for i in 0..13 {
-            regs.r[i] = self.cpu.reg_get(mode, i as u8);
+            regs.core.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);
+        regs.core.sp = self.cpu.reg_get(mode, reg::SP);
+        regs.core.lr = self.cpu.reg_get(mode, reg::LR);
+        regs.core.pc = self.cpu.reg_get(mode, reg::PC);
+        regs.core.cpsr = self.cpu.reg_get(mode, reg::CPSR);
+
+        regs.custom = self.custom_reg;
 
         Ok(())
     }
 
-    fn write_registers(
-        &mut self,
-        regs: &gdbstub_arch::arm::reg::ArmCoreRegs,
-    ) -> TargetResult<(), Self> {
+    fn write_registers(&mut self, regs: &custom_arch::ArmCoreRegsCustom) -> 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, i, regs.core.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);
+        self.cpu.reg_set(mode, reg::SP, regs.core.sp);
+        self.cpu.reg_set(mode, reg::LR, regs.core.lr);
+        self.cpu.reg_set(mode, reg::PC, regs.core.pc);
+        self.cpu.reg_set(mode, reg::CPSR, regs.core.cpsr);
+
+        self.custom_reg = regs.custom;
 
         Ok(())
     }
 
+    #[inline(always)]
+    fn support_single_register_access(
+        &mut self,
+    ) -> Option<target::ext::base::single_register_access::SingleRegisterAccessOps<'_, (), Self>>
+    {
+        Some(self)
+    }
+
     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)
@@ -177,108 +202,293 @@
     }
 
     #[inline(always)]
-    fn single_register_access(
+    fn support_resume(
         &mut self,
-    ) -> Option<target::ext::base::SingleRegisterAccessOps<(), Self>> {
-        Some(self)
-    }
-
-    #[inline(always)]
-    fn support_reverse_cont(&mut self) -> Option<SingleThreadReverseContOps<Self>> {
-        Some(self)
-    }
-
-    #[inline(always)]
-    fn support_reverse_step(&mut self) -> Option<SingleThreadReverseStepOps<Self>> {
-        Some(self)
-    }
-
-    #[inline(always)]
-    fn support_resume_range_step(
-        &mut self,
-    ) -> Option<target::ext::base::singlethread::SingleThreadRangeSteppingOps<Self>> {
+    ) -> Option<target::ext::base::singlethread::SingleThreadResumeOps<'_, Self>> {
         Some(self)
     }
 }
 
-impl target::ext::base::SingleRegisterAccess<()> for Emu {
+impl SingleThreadResume for Emu {
+    fn resume(&mut self, signal: Option<Signal>) -> Result<(), Self::Error> {
+        // Upon returning from the `resume` method, the target being debugged should be
+        // configured to run according to whatever resume actions the GDB client has
+        // specified (as specified by `set_resume_action`, `resume_range_step`,
+        // `reverse_{step, continue}`, etc...)
+        //
+        // In this basic `armv4t` example, the `resume` method simply sets the exec mode
+        // of the emulator's interpreter loop and returns.
+        //
+        // In more complex implementations, it's likely that the target being debugged
+        // will be running in another thread / process, and will require some kind of
+        // external "orchestration" to set it's execution mode (e.g: modifying the
+        // target's process state via platform specific debugging syscalls).
+
+        if signal.is_some() {
+            return Err("no support for continuing with signal");
+        }
+
+        self.exec_mode = ExecMode::Continue;
+
+        Ok(())
+    }
+
+    #[inline(always)]
+    fn support_reverse_cont(
+        &mut self,
+    ) -> Option<target::ext::base::reverse_exec::ReverseContOps<'_, (), Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_reverse_step(
+        &mut self,
+    ) -> Option<target::ext::base::reverse_exec::ReverseStepOps<'_, (), Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_single_step(
+        &mut self,
+    ) -> Option<target::ext::base::singlethread::SingleThreadSingleStepOps<'_, Self>> {
+        Some(self)
+    }
+
+    #[inline(always)]
+    fn support_range_step(
+        &mut self,
+    ) -> Option<target::ext::base::singlethread::SingleThreadRangeSteppingOps<'_, Self>> {
+        Some(self)
+    }
+}
+
+impl target::ext::base::singlethread::SingleThreadSingleStep for Emu {
+    fn step(&mut self, signal: Option<Signal>) -> Result<(), Self::Error> {
+        if signal.is_some() {
+            return Err("no support for stepping with signal");
+        }
+
+        self.exec_mode = ExecMode::Step;
+
+        Ok(())
+    }
+}
+
+impl target::ext::base::single_register_access::SingleRegisterAccess<()> for Emu {
     fn read_register(
         &mut self,
         _tid: (),
-        reg_id: gdbstub_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())
+        reg_id: custom_arch::ArmCoreRegIdCustom,
+        buf: &mut [u8],
+    ) -> TargetResult<usize, Self> {
+        match reg_id {
+            custom_arch::ArmCoreRegIdCustom::Core(reg_id) => {
+                if let Some(i) = cpu_reg_id(reg_id) {
+                    let w = self.cpu.reg_get(self.cpu.mode(), i);
+                    buf.copy_from_slice(&w.to_le_bytes());
+                    Ok(buf.len())
+                } else {
+                    Err(().into())
+                }
+            }
+            custom_arch::ArmCoreRegIdCustom::Custom => {
+                buf.copy_from_slice(&self.custom_reg.to_le_bytes());
+                Ok(buf.len())
+            }
+            custom_arch::ArmCoreRegIdCustom::Time => {
+                buf.copy_from_slice(
+                    &(std::time::SystemTime::now()
+                        .duration_since(std::time::UNIX_EPOCH)
+                        .unwrap()
+                        .as_millis() as u32)
+                        .to_le_bytes(),
+                );
+                Ok(buf.len())
+            }
         }
     }
 
     fn write_register(
         &mut self,
         _tid: (),
-        reg_id: gdbstub_arch::arm::reg::id::ArmCoreRegId,
+        reg_id: custom_arch::ArmCoreRegIdCustom,
         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())
+        match reg_id {
+            custom_arch::ArmCoreRegIdCustom::Core(reg_id) => {
+                if let Some(i) = cpu_reg_id(reg_id) {
+                    self.cpu.reg_set(self.cpu.mode(), i, w);
+                    Ok(())
+                } else {
+                    Err(().into())
+                }
+            }
+            custom_arch::ArmCoreRegIdCustom::Custom => {
+                self.custom_reg = w;
+                Ok(())
+            }
+            // ignore writes
+            custom_arch::ArmCoreRegIdCustom::Time => Ok(()),
         }
     }
 }
 
-impl target::ext::base::singlethread::SingleThreadReverseCont for Emu {
-    fn reverse_cont(
-        &mut self,
-        gdb_interrupt: GdbInterrupt<'_>,
-    ) -> Result<StopReason<u32>, Self::Error> {
+impl target::ext::base::reverse_exec::ReverseCont<()> for Emu {
+    fn reverse_cont(&mut self) -> Result<(), Self::Error> {
         // FIXME: actually implement reverse step
         eprintln!(
             "FIXME: Not actually reverse-continuing. Performing forwards continue instead..."
         );
-        self.resume(ResumeAction::Continue, gdb_interrupt)
+        self.exec_mode = ExecMode::Continue;
+        Ok(())
     }
 }
 
-impl target::ext::base::singlethread::SingleThreadReverseStep for Emu {
-    fn reverse_step(
-        &mut self,
-        gdb_interrupt: GdbInterrupt<'_>,
-    ) -> Result<StopReason<u32>, Self::Error> {
+impl target::ext::base::reverse_exec::ReverseStep<()> for Emu {
+    fn reverse_step(&mut self, _tid: ()) -> Result<(), Self::Error> {
         // FIXME: actually implement reverse step
         eprintln!(
             "FIXME: Not actually reverse-stepping. Performing single forwards step instead..."
         );
-        self.resume(ResumeAction::Step, gdb_interrupt)
+        self.exec_mode = ExecMode::Step;
+        Ok(())
     }
 }
 
 impl target::ext::base::singlethread::SingleThreadRangeStepping for Emu {
-    fn resume_range_step(
-        &mut self,
-        start: u32,
-        end: u32,
-        gdb_interrupt: GdbInterrupt<'_>,
-    ) -> Result<StopReason<u32>, Self::Error> {
-        let mut gdb_interrupt = gdb_interrupt.no_async();
-        loop {
-            match self.inner_resume(ResumeAction::Step, || gdb_interrupt.pending())? {
-                StopReason::DoneStep => {}
-                stop_reason => return Ok(stop_reason),
+    fn resume_range_step(&mut self, start: u32, end: u32) -> Result<(), Self::Error> {
+        self.exec_mode = ExecMode::RangeStep(start, end);
+        Ok(())
+    }
+}
+
+mod custom_arch {
+    use core::num::NonZeroUsize;
+
+    use gdbstub::arch::{Arch, RegId, Registers, SingleStepGdbBehavior};
+
+    use gdbstub_arch::arm::reg::id::ArmCoreRegId;
+    use gdbstub_arch::arm::reg::ArmCoreRegs;
+    use gdbstub_arch::arm::ArmBreakpointKind;
+
+    /// Implements `Arch` for ARMv4T
+    pub enum Armv4tCustom {}
+
+    #[derive(Debug, Default, Clone, Eq, PartialEq)]
+    pub struct ArmCoreRegsCustom {
+        pub core: ArmCoreRegs,
+        pub custom: u32,
+    }
+
+    impl Registers for ArmCoreRegsCustom {
+        type ProgramCounter = u32;
+
+        fn pc(&self) -> Self::ProgramCounter {
+            self.core.pc
+        }
+
+        fn gdb_serialize(&self, mut write_byte: impl FnMut(Option<u8>)) {
+            self.core.gdb_serialize(&mut write_byte);
+
+            macro_rules! write_bytes {
+                ($bytes:expr) => {
+                    for b in $bytes {
+                        write_byte(Some(*b))
+                    }
+                };
             }
 
-            if !(start..end).contains(&self.cpu.reg_get(self.cpu.mode(), reg::PC)) {
-                return Ok(StopReason::DoneStep);
+            write_bytes!(&self.custom.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()));
+
+            // copied from ArmCoreRegs
+            {
+                for reg in self.core.r.iter_mut() {
+                    *reg = regs.next().ok_or(())?
+                }
+                self.core.sp = regs.next().ok_or(())?;
+                self.core.lr = regs.next().ok_or(())?;
+                self.core.pc = regs.next().ok_or(())?;
+
+                // Floating point registers (unused)
+                for _ in 0..25 {
+                    regs.next().ok_or(())?;
+                }
+
+                self.core.cpsr = regs.next().ok_or(())?;
+            }
+
+            self.custom = regs.next().ok_or(())?;
+
+            if regs.next().is_some() {
+                return Err(());
+            }
+
+            Ok(())
+        }
+    }
+
+    #[derive(Debug)]
+    pub enum ArmCoreRegIdCustom {
+        Core(ArmCoreRegId),
+        Custom,
+        // not sent as part of `struct ArmCoreRegsCustom`, and only accessible via the single
+        // register read/write functions
+        Time,
+    }
+
+    impl RegId for ArmCoreRegIdCustom {
+        fn from_raw_id(id: usize) -> Option<(Self, Option<NonZeroUsize>)> {
+            let reg = match id {
+                26 => Self::Custom,
+                27 => Self::Time,
+                _ => {
+                    let (reg, size) = ArmCoreRegId::from_raw_id(id)?;
+                    return Some((Self::Core(reg), size));
+                }
+            };
+            Some((reg, Some(NonZeroUsize::new(4)?)))
+        }
+    }
+
+    impl Arch for Armv4tCustom {
+        type Usize = u32;
+        type Registers = ArmCoreRegsCustom;
+        type RegId = ArmCoreRegIdCustom;
+        type BreakpointKind = ArmBreakpointKind;
+
+        // for _purely demonstrative purposes_, i'll return dummy data from this
+        // function, as it will be overwritten by TargetDescriptionXmlOverride.
+        //
+        // See `examples/armv4t/gdb/target_description_xml_override.rs`
+        //
+        // in an actual implementation, you'll want to return an actual string here!
+        fn target_description_xml() -> Option<&'static str> {
+            Some("never gets returned")
+        }
+
+        // armv4t supports optional single stepping.
+        //
+        // notably, x86 is an example of an arch that does _not_ support
+        // optional single stepping.
+        #[inline(always)]
+        fn single_step_gdb_behavior() -> SingleStepGdbBehavior {
+            SingleStepGdbBehavior::Optional
         }
     }
 }
diff --git a/examples/armv4t/gdb/target_description_xml_override.rs b/examples/armv4t/gdb/target_description_xml_override.rs
index 843697e..294b513 100644
--- a/examples/armv4t/gdb/target_description_xml_override.rs
+++ b/examples/armv4t/gdb/target_description_xml_override.rs
@@ -1,9 +1,100 @@
 use gdbstub::target;
+use gdbstub::target::TargetError;
+use gdbstub::target::TargetResult;
 
+use super::copy_range_to_buf;
 use crate::emu::Emu;
 
 impl target::ext::target_description_xml_override::TargetDescriptionXmlOverride for Emu {
-    fn target_description_xml(&self) -> &str {
-        r#"<target version="1.0"><!-- custom override string --><architecture>armv4t</architecture></target>"#
+    fn target_description_xml(
+        &self,
+        annex: &[u8],
+        offset: u64,
+        length: usize,
+        buf: &mut [u8],
+    ) -> TargetResult<usize, Self> {
+        let xml = match annex {
+            b"target.xml" => TARGET_XML.trim(),
+            b"extra.xml" => EXTRA_XML.trim(),
+            _ => return Err(TargetError::NonFatal),
+        };
+
+        Ok(copy_range_to_buf(
+            xml.trim().as_bytes(),
+            offset,
+            length,
+            buf,
+        ))
     }
 }
+
+const TARGET_XML: &str = r#"
+<?xml version="1.0"?>
+<!DOCTYPE target SYSTEM "gdb-target.dtd">
+<target version="1.0">
+    <architecture>armv4t</architecture>
+    <feature name="org.gnu.gdb.arm.core">
+        <vector id="padding" type="uint32" count="25"/>
+
+        <reg name="r0" bitsize="32" type="uint32"/>
+        <reg name="r1" bitsize="32" type="uint32"/>
+        <reg name="r2" bitsize="32" type="uint32"/>
+        <reg name="r3" bitsize="32" type="uint32"/>
+        <reg name="r4" bitsize="32" type="uint32"/>
+        <reg name="r5" bitsize="32" type="uint32"/>
+        <reg name="r6" bitsize="32" type="uint32"/>
+        <reg name="r7" bitsize="32" type="uint32"/>
+        <reg name="r8" bitsize="32" type="uint32"/>
+        <reg name="r9" bitsize="32" type="uint32"/>
+        <reg name="r10" bitsize="32" type="uint32"/>
+        <reg name="r11" bitsize="32" type="uint32"/>
+        <reg name="r12" bitsize="32" type="uint32"/>
+        <reg name="sp" bitsize="32" type="data_ptr"/>
+        <reg name="lr" bitsize="32"/>
+        <reg name="pc" bitsize="32" type="code_ptr"/>
+
+        <!--
+            For some reason, my version of `gdb-multiarch` doesn't seem to
+            respect "regnum", and will not parse this custom target.xml unless I
+            manually include the padding bytes in the target description.
+
+            On the bright side, AFAIK, there aren't all that many architectures
+            that use padding bytes. Heck, the only reason armv4t uses padding is
+            for historical reasons (see comment below).
+
+            Odds are if you're defining your own custom arch, you won't run into
+            this issue, since you can just lay out all the registers in the
+            correct order.
+        -->
+        <reg name="padding" type="padding" bitsize="32"/>
+
+        <!-- The CPSR is register 25, rather than register 16, because
+        the FPA registers historically were placed between the PC
+        and the CPSR in the "g" packet. -->
+        <reg name="cpsr" bitsize="32" regnum="25"/>
+    </feature>
+    <xi:include href="extra.xml"/>
+</target>
+"#;
+
+const EXTRA_XML: &str = r#"
+<?xml version="1.0"?>
+<!DOCTYPE target SYSTEM "gdb-target.dtd">
+<feature name="custom-armv4t-extension">
+    <!--
+        maps to a simple scratch register within the emulator. the GDB
+        client can read the register using `p $custom` and set it using
+        `set $custom=1337`
+    -->
+    <reg name="custom" bitsize="32" type="uint32"/>
+
+    <!--
+        pseudo-register that return the current time when read.
+
+        notably, i've set up the target to NOT send this register as part of
+        the regular register list, which means that GDB will fetch/update
+        this register via the 'p' and 'P' packets respectively
+    -->
+    <reg name="time" bitsize="32" type="uint32"/>
+</feature>
+"#;
diff --git a/examples/armv4t/main.rs b/examples/armv4t/main.rs
index 4364819..91d1a4f 100644
--- a/examples/armv4t/main.rs
+++ b/examples/armv4t/main.rs
@@ -1,13 +1,19 @@
+#![deny(rust_2018_idioms, future_incompatible, nonstandard_style)]
+
 use std::net::{TcpListener, TcpStream};
 
 #[cfg(unix)]
 use std::os::unix::net::{UnixListener, UnixStream};
 
-use gdbstub::{Connection, DisconnectReason, GdbStub};
+use gdbstub::common::Signal;
+use gdbstub::conn::{Connection, ConnectionExt};
+use gdbstub::stub::SingleThreadStopReason;
+use gdbstub::stub::{run_blocking, DisconnectReason, GdbStub, GdbStubError};
+use gdbstub::target::Target;
 
 pub type DynResult<T> = Result<T, Box<dyn std::error::Error>>;
 
-static TEST_PROGRAM_ELF: &[u8] = include_bytes!("test_bin/test.elf");
+pub static TEST_PROGRAM_ELF: &[u8] = include_bytes!("test_bin/test.elf");
 
 mod emu;
 mod gdb;
@@ -43,12 +49,102 @@
     Ok(stream)
 }
 
+enum EmuGdbEventLoop {}
+
+impl run_blocking::BlockingEventLoop for EmuGdbEventLoop {
+    type Target = emu::Emu;
+    type Connection = Box<dyn ConnectionExt<Error = std::io::Error>>;
+    type StopReason = SingleThreadStopReason<u32>;
+
+    #[allow(clippy::type_complexity)]
+    fn wait_for_stop_reason(
+        target: &mut emu::Emu,
+        conn: &mut Self::Connection,
+    ) -> Result<
+        run_blocking::Event<SingleThreadStopReason<u32>>,
+        run_blocking::WaitForStopReasonError<
+            <Self::Target as Target>::Error,
+            <Self::Connection as Connection>::Error,
+        >,
+    > {
+        // The `armv4t` example runs the emulator in the same thread as the GDB state
+        // machine loop. As such, it uses a simple poll-based model to check for
+        // interrupt events, whereby the emulator will check if there is any incoming
+        // data over the connection, and pause execution with a synthetic
+        // `RunEvent::IncomingData` event.
+        //
+        // In more complex integrations, the target will probably be running in a
+        // separate thread, and instead of using a poll-based model to check for
+        // incoming data, you'll want to use some kind of "select" based model to
+        // simultaneously wait for incoming GDB data coming over the connection, along
+        // with any target-reported stop events.
+        //
+        // The specifics of how this "select" mechanism work + how the target reports
+        // stop events will entirely depend on your project's architecture.
+        //
+        // Some ideas on how to implement this `select` mechanism:
+        //
+        // - A mpsc channel
+        // - epoll/kqueue
+        // - Running the target + stopping every so often to peek the connection
+        // - Driving `GdbStub` from various interrupt handlers
+
+        let poll_incoming_data = || {
+            // gdbstub takes ownership of the underlying connection, so the `borrow_conn`
+            // method is used to borrow the underlying connection back from the stub to
+            // check for incoming data.
+            conn.peek().map(|b| b.is_some()).unwrap_or(true)
+        };
+
+        match target.run(poll_incoming_data) {
+            emu::RunEvent::IncomingData => {
+                let byte = conn
+                    .read()
+                    .map_err(run_blocking::WaitForStopReasonError::Connection)?;
+                Ok(run_blocking::Event::IncomingData(byte))
+            }
+            emu::RunEvent::Event(event) => {
+                use gdbstub::target::ext::breakpoints::WatchKind;
+
+                // translate emulator stop reason into GDB stop reason
+                let stop_reason = match event {
+                    emu::Event::DoneStep => SingleThreadStopReason::DoneStep,
+                    emu::Event::Halted => SingleThreadStopReason::Terminated(Signal::SIGSTOP),
+                    emu::Event::Break => SingleThreadStopReason::SwBreak(()),
+                    emu::Event::WatchWrite(addr) => SingleThreadStopReason::Watch {
+                        tid: (),
+                        kind: WatchKind::Write,
+                        addr,
+                    },
+                    emu::Event::WatchRead(addr) => SingleThreadStopReason::Watch {
+                        tid: (),
+                        kind: WatchKind::Read,
+                        addr,
+                    },
+                };
+
+                Ok(run_blocking::Event::TargetStopped(stop_reason))
+            }
+        }
+    }
+
+    fn on_interrupt(
+        _target: &mut emu::Emu,
+    ) -> Result<Option<SingleThreadStopReason<u32>>, <emu::Emu as Target>::Error> {
+        // Because this emulator runs as part of the GDB stub loop, there isn't any
+        // special action that needs to be taken to interrupt the underlying target. It
+        // is implicitly paused whenever the stub isn't within the
+        // `wait_for_stop_reason` callback.
+        Ok(Some(SingleThreadStopReason::Signal(Signal::SIGINT)))
+    }
+}
+
 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>> = {
+    let connection: Box<dyn ConnectionExt<Error = std::io::Error>> = {
         if std::env::args().nth(1) == Some("--uds".to_string()) {
             #[cfg(not(unix))]
             {
@@ -63,21 +159,27 @@
         }
     };
 
-    // hook-up debugger
-    let mut debugger = GdbStub::new(connection);
+    let gdb = GdbStub::new(connection);
 
-    match debugger.run(&mut emu)? {
-        DisconnectReason::Disconnect => {
-            // run to completion
-            while emu.step() != Some(emu::Event::Halted) {}
+    match gdb.run_blocking::<EmuGdbEventLoop>(&mut emu) {
+        Ok(disconnect_reason) => match disconnect_reason {
+            DisconnectReason::Disconnect => {
+                println!("GDB client has disconnected. Running to completion...");
+                while emu.step() != Some(emu::Event::Halted) {}
+            }
+            DisconnectReason::TargetExited(code) => {
+                println!("Target exited with code {}!", code)
+            }
+            DisconnectReason::TargetTerminated(sig) => {
+                println!("Target terminated with signal {}!", sig)
+            }
+            DisconnectReason::Kill => println!("GDB sent a kill command!"),
+        },
+        Err(GdbStubError::TargetError(e)) => {
+            println!("target encountered a fatal error: {}", e)
         }
-        DisconnectReason::TargetExited(code) => println!("Target exited with code {}!", code),
-        DisconnectReason::TargetTerminated(sig) => {
-            println!("Target terminated with signal {}!", sig)
-        }
-        DisconnectReason::Kill => {
-            println!("GDB sent a kill command!");
-            return Ok(());
+        Err(e) => {
+            println!("gdbstub encountered a fatal error: {}", e)
         }
     }
 
diff --git a/examples/armv4t/test_bin/.gdbinit b/examples/armv4t/test_bin/.gdbinit
index 6f9d0b8..767af20 100644
--- a/examples/armv4t/test_bin/.gdbinit
+++ b/examples/armv4t/test_bin/.gdbinit
@@ -1,2 +1,3 @@
-file test.elf
+# set remote multiprocess-feature-packet off
+
 target extended-remote :9001
diff --git a/examples/armv4t_multicore/emu.rs b/examples/armv4t_multicore/emu.rs
index f0570fa..1eed74c 100644
--- a/examples/armv4t_multicore/emu.rs
+++ b/examples/armv4t_multicore/emu.rs
@@ -18,7 +18,7 @@
 
 const HLE_RETURN_ADDR: u32 = 0x12345678;
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 pub enum CpuId {
     Cpu,
     Cop,
@@ -26,20 +26,25 @@
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 pub enum Event {
+    DoneStep,
     Halted,
     Break,
     WatchWrite(u32),
     WatchRead(u32),
 }
 
+pub enum ExecMode {
+    Step,
+    Continue,
+}
+
 /// incredibly barebones armv4t-based emulator
 pub struct Emu {
     pub(crate) cpu: Cpu,
     pub(crate) cop: Cpu,
     pub(crate) mem: ExampleMem,
 
-    // FIXME: properly handle multiple actions
-    pub(crate) resume_action_is_step: Option<bool>,
+    pub(crate) exec_mode: HashMap<CpuId, ExecMode>,
 
     pub(crate) watchpoints: Vec<u32>,
     /// (read, write)
@@ -69,12 +74,12 @@
         for h in sections {
             eprintln!(
                 "loading section {:?} into memory from [{:#010x?}..{:#010x?}]",
-                elf_header.shdr_strtab.get(h.sh_name).unwrap().unwrap(),
+                elf_header.shdr_strtab.get_at(h.sh_name).unwrap(),
                 h.sh_addr,
                 h.sh_addr + h.sh_size,
             );
 
-            for (i, b) in program_elf[h.file_range()].iter().enumerate() {
+            for (i, b) in program_elf[h.file_range().unwrap()].iter().enumerate() {
                 mem.w8(h.sh_addr as u32 + i as u32, *b);
             }
         }
@@ -92,7 +97,7 @@
             cop,
             mem,
 
-            resume_action_is_step: None,
+            exec_mode: HashMap::new(),
 
             watchpoints: Vec::new(),
             watchpoint_kind: HashMap::new(),
@@ -188,4 +193,50 @@
 
         evt
     }
+
+    pub fn run(&mut self, mut poll_incoming_data: impl FnMut() -> bool) -> RunEvent {
+        // the underlying armv4t_multicore emulator runs both cores in lock step, so
+        // when GDB requests a specific core to single-step, all we need to do is jot
+        // down that we want to single-step the system, as there is no way to
+        // single-step a single core while the other runs.
+        //
+        // In more complex emulators / implementations, this simplification is _not_
+        // valid, and you should track which specific TID the GDB client requested to be
+        // single-stepped, and run them appropriately.
+
+        let should_single_step = matches!(
+            self.exec_mode
+                .get(&CpuId::Cpu)
+                .or_else(|| self.exec_mode.get(&CpuId::Cop)),
+            Some(&ExecMode::Step)
+        );
+
+        match should_single_step {
+            true => match self.step() {
+                Some((event, id)) => RunEvent::Event(event, id),
+                None => RunEvent::Event(Event::DoneStep, CpuId::Cpu),
+            },
+            false => {
+                let mut cycles = 0;
+                loop {
+                    if cycles % 1024 == 0 {
+                        // poll for incoming data
+                        if poll_incoming_data() {
+                            break RunEvent::IncomingData;
+                        }
+                    }
+                    cycles += 1;
+
+                    if let Some((event, id)) = self.step() {
+                        break RunEvent::Event(event, id);
+                    };
+                }
+            }
+        }
+    }
+}
+
+pub enum RunEvent {
+    Event(Event, CpuId),
+    IncomingData,
 }
diff --git a/examples/armv4t_multicore/gdb.rs b/examples/armv4t_multicore/gdb.rs
index 0c8f6dc..9559d3d 100644
--- a/examples/armv4t_multicore/gdb.rs
+++ b/examples/armv4t_multicore/gdb.rs
@@ -1,34 +1,14 @@
 use armv4t_emu::{reg, Memory};
 
-use gdbstub::common::Tid;
+use gdbstub::common::{Signal, Tid};
 use gdbstub::target;
-use gdbstub::target::ext::base::multithread::{
-    GdbInterrupt, MultiThreadOps, ResumeAction, ThreadStopReason,
-};
+use gdbstub::target::ext::base::multithread::{MultiThreadBase, MultiThreadResume};
 use gdbstub::target::ext::breakpoints::WatchKind;
 use gdbstub::target::{Target, TargetError, TargetResult};
 
-use crate::emu::{CpuId, Emu, Event};
+use crate::emu::{CpuId, Emu, ExecMode};
 
-fn event_to_stopreason(e: Event, id: CpuId) -> ThreadStopReason<u32> {
-    let tid = cpuid_to_tid(id);
-    match e {
-        Event::Halted => ThreadStopReason::Terminated(19), // SIGSTOP
-        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 {
+pub fn cpuid_to_tid(id: CpuId) -> Tid {
     match id {
         CpuId::Cpu => Tid::new(1).unwrap(),
         CpuId::Cop => Tid::new(2).unwrap(),
@@ -48,83 +28,19 @@
     type Error = &'static str;
 
     #[inline(always)]
-    fn base_ops(&mut self) -> target::ext::base::BaseOps<Self::Arch, Self::Error> {
+    fn base_ops(&mut self) -> target::ext::base::BaseOps<'_, Self::Arch, Self::Error> {
         target::ext::base::BaseOps::MultiThread(self)
     }
 
     #[inline(always)]
-    fn breakpoints(&mut self) -> Option<target::ext::breakpoints::BreakpointsOps<Self>> {
+    fn support_breakpoints(
+        &mut self,
+    ) -> Option<target::ext::breakpoints::BreakpointsOps<'_, Self>> {
         Some(self)
     }
 }
 
-impl MultiThreadOps for Emu {
-    fn resume(
-        &mut self,
-        default_resume_action: ResumeAction,
-        gdb_interrupt: GdbInterrupt<'_>,
-    ) -> Result<ThreadStopReason<u32>, Self::Error> {
-        // 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.
-
-        let default_resume_action_is_step = match default_resume_action {
-            ResumeAction::Step => true,
-            ResumeAction::Continue => false,
-            _ => return Err("no support for resuming with signal"),
-        };
-
-        match self
-            .resume_action_is_step
-            .unwrap_or(default_resume_action_is_step)
-        {
-            true => match self.step() {
-                Some((event, id)) => Ok(event_to_stopreason(event, id)),
-                None => Ok(ThreadStopReason::DoneStep),
-            },
-            false => {
-                let mut gdb_interrupt = gdb_interrupt.no_async();
-                let mut cycles: usize = 0;
-                loop {
-                    // check for GDB interrupt every 1024 instructions
-                    if cycles % 1024 == 0 && gdb_interrupt.pending() {
-                        return Ok(ThreadStopReason::GdbInterrupt);
-                    }
-                    cycles += 1;
-
-                    if let Some((event, id)) = self.step() {
-                        return Ok(event_to_stopreason(event, id));
-                    };
-                }
-            }
-        }
-    }
-
-    // FIXME: properly handle multiple actions
-    fn clear_resume_actions(&mut self) -> Result<(), Self::Error> {
-        self.resume_action_is_step = None;
-        Ok(())
-    }
-
-    // FIXME: properly handle multiple actions
-    fn set_resume_action(&mut self, _tid: Tid, action: ResumeAction) -> Result<(), Self::Error> {
-        // in this emulator, each core runs in lock-step, so we don't actually care
-        // about the specific tid. In real integrations, you very much should!
-
-        if self.resume_action_is_step.is_some() {
-            return Ok(());
-        }
-
-        self.resume_action_is_step = match action {
-            ResumeAction::Step => Some(true),
-            ResumeAction::Continue => Some(false),
-            _ => return Err("no support for resuming with signal"),
-        };
-
-        Ok(())
-    }
-
+impl MultiThreadBase for Emu {
     fn read_registers(
         &mut self,
         regs: &mut gdbstub_arch::arm::reg::ArmCoreRegs,
@@ -203,14 +119,88 @@
         register_thread(cpuid_to_tid(CpuId::Cop));
         Ok(())
     }
+
+    #[inline(always)]
+    fn support_resume(
+        &mut self,
+    ) -> Option<target::ext::base::multithread::MultiThreadResumeOps<'_, Self>> {
+        Some(self)
+    }
 }
 
-impl target::ext::breakpoints::Breakpoints for Emu {
-    fn sw_breakpoint(&mut self) -> Option<target::ext::breakpoints::SwBreakpointOps<Self>> {
+impl MultiThreadResume for Emu {
+    fn resume(&mut self) -> Result<(), Self::Error> {
+        // Upon returning from the `resume` method, the target being debugged should be
+        // configured to run according to whatever resume actions the GDB client has
+        // specified (as specified by `set_resume_action`, `set_resume_range_step`,
+        // `set_reverse_{step, continue}`, etc...)
+        //
+        // In this basic `armv4t_multicore` example, the `resume` method is actually a
+        // no-op, as the execution mode of the emulator's interpreter loop has already
+        // been modified via the various `set_X` methods.
+        //
+        // In more complex implementations, it's likely that the target being debugged
+        // will be running in another thread / process, and will require some kind of
+        // external "orchestration" to set it's execution mode (e.g: modifying the
+        // target's process state via platform specific debugging syscalls).
+
+        Ok(())
+    }
+
+    fn clear_resume_actions(&mut self) -> Result<(), Self::Error> {
+        self.exec_mode.clear();
+        Ok(())
+    }
+
+    #[inline(always)]
+    fn support_single_step(
+        &mut self,
+    ) -> Option<target::ext::base::multithread::MultiThreadSingleStepOps<'_, Self>> {
         Some(self)
     }
 
-    fn hw_watchpoint(&mut self) -> Option<target::ext::breakpoints::HwWatchpointOps<Self>> {
+    fn set_resume_action_continue(
+        &mut self,
+        tid: Tid,
+        signal: Option<Signal>,
+    ) -> Result<(), Self::Error> {
+        if signal.is_some() {
+            return Err("no support for continuing with signal");
+        }
+
+        self.exec_mode
+            .insert(tid_to_cpuid(tid)?, ExecMode::Continue);
+
+        Ok(())
+    }
+}
+
+impl target::ext::base::multithread::MultiThreadSingleStep for Emu {
+    fn set_resume_action_step(
+        &mut self,
+        tid: Tid,
+        signal: Option<Signal>,
+    ) -> Result<(), Self::Error> {
+        if signal.is_some() {
+            return Err("no support for stepping with signal");
+        }
+
+        self.exec_mode.insert(tid_to_cpuid(tid)?, ExecMode::Step);
+
+        Ok(())
+    }
+}
+
+impl target::ext::breakpoints::Breakpoints for Emu {
+    fn support_sw_breakpoint(
+        &mut self,
+    ) -> Option<target::ext::breakpoints::SwBreakpointOps<'_, Self>> {
+        Some(self)
+    }
+
+    fn support_hw_watchpoint(
+        &mut self,
+    ) -> Option<target::ext::breakpoints::HwWatchpointOps<'_, Self>> {
         Some(self)
     }
 }
@@ -240,7 +230,12 @@
 }
 
 impl target::ext::breakpoints::HwWatchpoint for Emu {
-    fn add_hw_watchpoint(&mut self, addr: u32, kind: WatchKind) -> TargetResult<bool, Self> {
+    fn add_hw_watchpoint(
+        &mut self,
+        addr: u32,
+        _len: u32, // TODO: properly handle `len` parameter
+        kind: WatchKind,
+    ) -> TargetResult<bool, Self> {
         self.watchpoints.push(addr);
 
         let entry = self.watchpoint_kind.entry(addr).or_insert((false, false));
@@ -253,7 +248,12 @@
         Ok(true)
     }
 
-    fn remove_hw_watchpoint(&mut self, addr: u32, kind: WatchKind) -> TargetResult<bool, Self> {
+    fn remove_hw_watchpoint(
+        &mut self,
+        addr: u32,
+        _len: u32, // TODO: properly handle `len` parameter
+        kind: WatchKind,
+    ) -> TargetResult<bool, Self> {
         let entry = self.watchpoint_kind.entry(addr).or_insert((false, false));
         match kind {
             WatchKind::Write => entry.1 = false,
diff --git a/examples/armv4t_multicore/main.rs b/examples/armv4t_multicore/main.rs
index f51c018..7e3cd04 100644
--- a/examples/armv4t_multicore/main.rs
+++ b/examples/armv4t_multicore/main.rs
@@ -1,9 +1,14 @@
+#![deny(rust_2018_idioms, future_incompatible, nonstandard_style)]
+
 use std::net::{TcpListener, TcpStream};
 
 #[cfg(unix)]
 use std::os::unix::net::{UnixListener, UnixStream};
 
-use gdbstub::{Connection, DisconnectReason, GdbStub};
+use gdbstub::common::Signal;
+use gdbstub::conn::{Connection, ConnectionExt};
+use gdbstub::stub::{run_blocking, DisconnectReason, GdbStub, GdbStubError, MultiThreadStopReason};
+use gdbstub::target::Target;
 
 pub type DynResult<T> = Result<T, Box<dyn std::error::Error>>;
 
@@ -43,12 +48,103 @@
     Ok(stream)
 }
 
+enum EmuGdbEventLoop {}
+
+impl run_blocking::BlockingEventLoop for EmuGdbEventLoop {
+    type Target = emu::Emu;
+    type Connection = Box<dyn ConnectionExt<Error = std::io::Error>>;
+    type StopReason = MultiThreadStopReason<u32>;
+
+    #[allow(clippy::type_complexity)]
+    fn wait_for_stop_reason(
+        target: &mut emu::Emu,
+        conn: &mut Self::Connection,
+    ) -> Result<
+        run_blocking::Event<Self::StopReason>,
+        run_blocking::WaitForStopReasonError<
+            <Self::Target as Target>::Error,
+            <Self::Connection as Connection>::Error,
+        >,
+    > {
+        // The `armv4t_multicore` example runs the emulator in the same thread as the
+        // GDB state machine loop. As such, it uses a simple poll-based model to
+        // check for interrupt events, whereby the emulator will check if there
+        // is any incoming data over the connection, and pause execution with a
+        // synthetic `RunEvent::IncomingData` event.
+        //
+        // In more complex integrations, the target will probably be running in a
+        // separate thread, and instead of using a poll-based model to check for
+        // incoming data, you'll want to use some kind of "select" based model to
+        // simultaneously wait for incoming GDB data coming over the connection, along
+        // with any target-reported stop events.
+        //
+        // The specifics of how this "select" mechanism work + how the target reports
+        // stop events will entirely depend on your project's architecture.
+        //
+        // Some ideas on how to implement this `select` mechanism:
+        //
+        // - A mpsc channel
+        // - epoll/kqueue
+        // - Running the target + stopping every so often to peek the connection
+        // - Driving `GdbStub` from various interrupt handlers
+
+        let poll_incoming_data = || {
+            // gdbstub takes ownership of the underlying connection, so the `borrow_conn`
+            // method is used to borrow the underlying connection back from the stub to
+            // check for incoming data.
+            conn.peek().map(|b| b.is_some()).unwrap_or(true)
+        };
+
+        match target.run(poll_incoming_data) {
+            emu::RunEvent::IncomingData => {
+                let byte = conn
+                    .read()
+                    .map_err(run_blocking::WaitForStopReasonError::Connection)?;
+                Ok(run_blocking::Event::IncomingData(byte))
+            }
+            emu::RunEvent::Event(event, cpuid) => {
+                use gdbstub::target::ext::breakpoints::WatchKind;
+
+                // translate emulator stop reason into GDB stop reason
+                let tid = gdb::cpuid_to_tid(cpuid);
+                let stop_reason = match event {
+                    emu::Event::DoneStep => MultiThreadStopReason::DoneStep,
+                    emu::Event::Halted => MultiThreadStopReason::Terminated(Signal::SIGSTOP),
+                    emu::Event::Break => MultiThreadStopReason::SwBreak(tid),
+                    emu::Event::WatchWrite(addr) => MultiThreadStopReason::Watch {
+                        tid,
+                        kind: WatchKind::Write,
+                        addr,
+                    },
+                    emu::Event::WatchRead(addr) => MultiThreadStopReason::Watch {
+                        tid,
+                        kind: WatchKind::Read,
+                        addr,
+                    },
+                };
+
+                Ok(run_blocking::Event::TargetStopped(stop_reason))
+            }
+        }
+    }
+
+    fn on_interrupt(
+        _target: &mut emu::Emu,
+    ) -> Result<Option<MultiThreadStopReason<u32>>, <emu::Emu as Target>::Error> {
+        // Because this emulator runs as part of the GDB stub loop, there isn't any
+        // special action that needs to be taken to interrupt the underlying target. It
+        // is implicitly paused whenever the stub isn't within the
+        // `wait_for_stop_reason` callback.
+        Ok(Some(MultiThreadStopReason::Signal(Signal::SIGINT)))
+    }
+}
+
 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>> = {
+    let connection: Box<dyn ConnectionExt<Error = std::io::Error>> = {
         if std::env::args().nth(1) == Some("--uds".to_string()) {
             #[cfg(not(unix))]
             {
@@ -63,21 +159,27 @@
         }
     };
 
-    // hook-up debugger
-    let mut debugger = GdbStub::new(connection);
+    let gdb = GdbStub::new(connection);
 
-    match debugger.run(&mut emu)? {
-        DisconnectReason::Disconnect => {
-            // run to completion
-            while emu.step() != Some((emu::Event::Halted, emu::CpuId::Cpu)) {}
+    match gdb.run_blocking::<EmuGdbEventLoop>(&mut emu) {
+        Ok(disconnect_reason) => match disconnect_reason {
+            DisconnectReason::Disconnect => {
+                println!("GDB client has disconnected. Running to completion...");
+                while emu.step() != Some((emu::Event::Halted, emu::CpuId::Cpu)) {}
+            }
+            DisconnectReason::TargetExited(code) => {
+                println!("Target exited with code {}!", code)
+            }
+            DisconnectReason::TargetTerminated(sig) => {
+                println!("Target terminated with signal {}!", sig)
+            }
+            DisconnectReason::Kill => println!("GDB sent a kill command!"),
+        },
+        Err(GdbStubError::TargetError(e)) => {
+            println!("target encountered a fatal error: {}", e)
         }
-        DisconnectReason::TargetExited(code) => println!("Target exited with code {}!", code),
-        DisconnectReason::TargetTerminated(sig) => {
-            println!("Target terminated with signal {}!", sig)
-        }
-        DisconnectReason::Kill => {
-            println!("GDB sent a kill command!");
-            return Ok(());
+        Err(e) => {
+            println!("gdbstub encountered a fatal error: {}", e)
         }
     }
 
diff --git a/src/arch.rs b/src/arch.rs
index dbb21fa..06cb49e 100644
--- a/src/arch.rs
+++ b/src/arch.rs
@@ -16,6 +16,7 @@
 //! crate helps minimize any unnecessary "version churn" in `gdbstub` core.
 
 use core::fmt::Debug;
+use core::num::NonZeroUsize;
 
 use num_traits::{FromPrimitive, PrimInt, Unsigned};
 
@@ -26,17 +27,23 @@
 /// These identifiers are used by GDB to signal which register to read/wite when
 /// performing [single register accesses].
 ///
-/// [single register accesses]: crate::target::ext::base::SingleRegisterAccess
+/// [single register accesses]:
+/// crate::target::ext::base::single_register_access::SingleRegisterAccess
 pub trait RegId: Sized + Debug {
-    /// Map raw GDB register number corresponding `RegId` and register size.
+    /// Map raw GDB register number to a corresponding `RegId` and optional
+    /// register size.
+    ///
+    /// If the register size is specified here, gdbstub will include a runtime
+    /// check that ensures target implementations do not send back more
+    /// bytes than the register allows.
     ///
     /// Returns `None` if the register is not available.
-    fn from_raw_id(id: usize) -> Option<(Self, usize)>;
+    fn from_raw_id(id: usize) -> Option<(Self, Option<NonZeroUsize>)>;
 }
 
 /// Stub implementation -- Returns `None` for all raw IDs.
 impl RegId for () {
-    fn from_raw_id(_id: usize) -> Option<(Self, usize)> {
+    fn from_raw_id(_id: usize) -> Option<(Self, Option<NonZeroUsize>)> {
         None
     }
 }
@@ -113,7 +120,7 @@
 /// explicitly instantiated.
 pub trait Arch {
     /// The architecture's pointer size (e.g: `u32` on a 32-bit system).
-    type Usize: FromPrimitive + PrimInt + Unsigned + BeBytes + LeBytes;
+    type Usize: Debug + FromPrimitive + PrimInt + Unsigned + BeBytes + LeBytes;
 
     /// The architecture's register file. See [`Registers`] for more details.
     type Registers: Registers<ProgramCounter = Self::Usize>;
@@ -147,7 +154,104 @@
     ///
     /// See the [GDB docs](https://sourceware.org/gdb/current/onlinedocs/gdb/Target-Description-Format.html)
     /// for details on the target description XML format.
+    #[inline(always)]
     fn target_description_xml() -> Option<&'static str> {
         None
     }
+
+    /// Encode how the mainline GDB client handles target support for
+    /// single-step on this particular architecture.
+    ///
+    /// # Context
+    ///
+    /// According to the spec, supporting single step _should_ be quite
+    /// straightforward:
+    ///
+    /// - The GDB client sends a `vCont?` packet to enumerate supported
+    ///   resumption modes
+    /// - If the target supports single-step, it responds with the `s;S`
+    ///   capability as part of the response, omitting it if it is not
+    ///   supported.
+    /// - Later, when the user attempts to `stepi`, the GDB client sends a `s`
+    ///   resumption reason if it is supported, falling back to setting a
+    ///   temporary breakpoint + continue to "emulate" the single step.
+    ///
+    /// Unfortunately, the reality is that the mainline GDB client does _not_ do
+    /// this on all architectures...
+    ///
+    /// - On certain architectures (e.g: x86), GDB will _unconditionally_ assume
+    ///   single-step support, regardless whether or not the target reports
+    ///   supports it.
+    /// - On certain architectures (e.g: MIPS), GDB will _never_ use single-step
+    ///   support, even in the target has explicitly reported support for it.
+    ///
+    /// This is a bug, and has been reported at
+    /// <https://sourceware.org/bugzilla/show_bug.cgi?id=28440>.
+    ///
+    /// For a easy repro of this behavior, also see
+    /// <https://github.com/daniel5151/gdb-optional-step-bug>.
+    ///
+    /// # Implications
+    ///
+    /// Unfortunately, even if these idiosyncratic behaviors get fixed in the
+    /// mainline GDB client, it will be quite a while until the typical
+    /// user's distro-provided GDB client includes this bugfix.
+    ///
+    /// As such, `gdbstub` has opted to include this method as a "guard rail" to
+    /// preemptively detect cases of this idiosyncratic behavior, and throw a
+    /// pre-init error that informs the user of the potential issues they may
+    /// run into.
+    ///
+    /// # Writing a proper implementation
+    ///
+    /// To check whether or not a particular architecture exhibits this
+    /// behavior, an implementation should temporarily override this method to
+    /// return [`SingleStepGdbBehavior::Optional`], toggle target support for
+    /// single-step on/off, and observe the behavior of the GDB client after
+    /// invoking `stepi`.
+    ///
+    /// If single-stepping was **disabled**, yet the client nonetheless sent a
+    /// `vCont` packet with a `s` resume action, then this architecture
+    /// _does not_ support optional single stepping, and this method should
+    /// return [`SingleStepGdbBehavior::Required`].
+    ///
+    /// If single-stepping was **disabled**, and the client attempted to set a
+    /// temporary breakpoint (using the `z` packet), and then sent a `vCont`
+    /// packet with a `c` resume action, then this architecture _does_
+    /// support optional single stepping, and this method should return
+    /// [`SingleStepGdbBehavior::Optional`].
+    ///
+    /// If single-stepping was **enabled**, yet the client did _not_ send a
+    /// `vCont` packet with a `s` resume action, then this architecture
+    /// _ignores_ single stepping entirely, and this method should return
+    /// [`SingleStepGdbBehavior::Ignored`].
+    fn single_step_gdb_behavior() -> SingleStepGdbBehavior;
+}
+
+/// Encodes how the mainline GDB client handles target support for single-step
+/// on a particular architecture.
+///
+/// See [Arch::single_step_gdb_behavior] for details.
+#[non_exhaustive]
+#[derive(Debug, Clone, Copy)]
+pub enum SingleStepGdbBehavior {
+    /// GDB will use single-stepping if available, falling back to using
+    /// a temporary breakpoint + continue if unsupported.
+    ///
+    /// e.g: ARM
+    Optional,
+    /// GDB will unconditionally send single-step packets, _requiring_ the
+    /// target to handle these requests.
+    ///
+    /// e.g: x86/x64
+    Required,
+    /// GDB will never use single-stepping, regardless if it's supported by the
+    /// stub. It will always use a temporary breakpoint + continue.
+    ///
+    /// e.g: MIPS
+    Ignored,
+    /// Unknown behavior - no one has tested this platform yet. If possible,
+    /// please conduct a test + upstream your findings to `gdbstub_arch`.
+    #[doc(hidden)]
+    Unknown,
 }
diff --git a/src/common.rs b/src/common.rs
deleted file mode 100644
index d9ba55c..0000000
--- a/src/common.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-//! Common types and definitions.
-
-/// Thread ID
-pub type Tid = core::num::NonZeroUsize;
-
-/// Process ID
-pub type Pid = core::num::NonZeroUsize;
diff --git a/src/common/mod.rs b/src/common/mod.rs
new file mode 100644
index 0000000..d7c9136
--- /dev/null
+++ b/src/common/mod.rs
@@ -0,0 +1,11 @@
+//! Common types and definitions used across `gdbstub`.
+
+mod signal;
+
+pub use self::signal::Signal;
+
+/// Thread ID
+pub type Tid = core::num::NonZeroUsize;
+
+/// Process ID
+pub type Pid = core::num::NonZeroUsize;
diff --git a/src/common/signal.rs b/src/common/signal.rs
new file mode 100644
index 0000000..652752a
--- /dev/null
+++ b/src/common/signal.rs
@@ -0,0 +1,516 @@
+/// Cross-platform signal numbers defined by the GDB Remote Serial Protocol.
+///
+/// Transcribed from <https://github.com/bminor/binutils-gdb/blob/master/include/gdb/signals.def>
+#[repr(u8)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[allow(clippy::upper_case_acronyms)]
+#[allow(non_camel_case_types)]
+#[rustfmt::skip]
+#[non_exhaustive]
+pub enum Signal {
+    #[doc = "Signal 0 (shouldn't be used)"]    SIGZERO = 0,
+    #[doc = "Hangup"]                          SIGHUP = 1,
+    #[doc = "Interrupt"]                       SIGINT = 2,
+    #[doc = "Quit"]                            SIGQUIT = 3,
+    #[doc = "Illegal instruction"]             SIGILL = 4,
+    #[doc = "Trace/breakpoint trap"]           SIGTRAP = 5,
+    #[doc = "Aborted"]                         SIGABRT = 6,
+    #[doc = "Emulation trap"]                  SIGEMT = 7,
+    #[doc = "Arithmetic exception"]            SIGFPE = 8,
+    #[doc = "Killed"]                          SIGKILL = 9,
+    #[doc = "Bus error"]                       SIGBUS = 10,
+    #[doc = "Segmentation fault"]              SIGSEGV = 11,
+    #[doc = "Bad system call"]                 SIGSYS = 12,
+    #[doc = "Broken pipe"]                     SIGPIPE = 13,
+    #[doc = "Alarm clock"]                     SIGALRM = 14,
+    #[doc = "Terminated"]                      SIGTERM = 15,
+    #[doc = "Urgent I/O condition"]            SIGURG = 16,
+    #[doc = "Stopped (signal)"]                SIGSTOP = 17,
+    #[doc = "Stopped (user)"]                  SIGTSTP = 18,
+    #[doc = "Continued"]                       SIGCONT = 19,
+    #[doc = "Child status changed"]            SIGCHLD = 20,
+    #[doc = "Stopped (tty input)"]             SIGTTIN = 21,
+    #[doc = "Stopped (tty output)"]            SIGTTOU = 22,
+    #[doc = "I/O possible"]                    SIGIO = 23,
+    #[doc = "CPU time limit exceeded"]         SIGXCPU = 24,
+    #[doc = "File size limit exceeded"]        SIGXFSZ = 25,
+    #[doc = "Virtual timer expired"]           SIGVTALRM = 26,
+    #[doc = "Profiling timer expired"]         SIGPROF = 27,
+    #[doc = "Window size changed"]             SIGWINCH = 28,
+    #[doc = "Resource lost"]                   SIGLOST = 29,
+    #[doc = "User defined signal 1"]           SIGUSR1 = 30,
+    #[doc = "User defined signal 2"]           SIGUSR2 = 31,
+    #[doc = "Power fail/restart"]              SIGPWR = 32,
+    /* Similar to SIGIO.  Perhaps they should have the same number. */
+    #[doc = "Pollable event occurred"]         SIGPOLL = 33,
+    #[doc = "SIGWIND"]                         SIGWIND = 34,
+    #[doc = "SIGPHONE"]                        SIGPHONE = 35,
+    #[doc = "Process's LWPs are blocked"]      SIGWAITING = 36,
+    #[doc = "Signal LWP"]                      SIGLWP = 37,
+    #[doc = "Swap space dangerously low"]      SIGDANGER = 38,
+    #[doc = "Monitor mode granted"]            SIGGRANT = 39,
+    #[doc = "Need to relinquish monitor mode"] SIGRETRACT = 40,
+    #[doc = "Monitor mode data available"]     SIGMSG = 41,
+    #[doc = "Sound completed"]                 SIGSOUND = 42,
+    #[doc = "Secure attention"]                SIGSAK = 43,
+    #[doc = "SIGPRIO"]                         SIGPRIO = 44,
+    #[doc = "Real-time event 33"]              SIG33 = 45,
+    #[doc = "Real-time event 34"]              SIG34 = 46,
+    #[doc = "Real-time event 35"]              SIG35 = 47,
+    #[doc = "Real-time event 36"]              SIG36 = 48,
+    #[doc = "Real-time event 37"]              SIG37 = 49,
+    #[doc = "Real-time event 38"]              SIG38 = 50,
+    #[doc = "Real-time event 39"]              SIG39 = 51,
+    #[doc = "Real-time event 40"]              SIG40 = 52,
+    #[doc = "Real-time event 41"]              SIG41 = 53,
+    #[doc = "Real-time event 42"]              SIG42 = 54,
+    #[doc = "Real-time event 43"]              SIG43 = 55,
+    #[doc = "Real-time event 44"]              SIG44 = 56,
+    #[doc = "Real-time event 45"]              SIG45 = 57,
+    #[doc = "Real-time event 46"]              SIG46 = 58,
+    #[doc = "Real-time event 47"]              SIG47 = 59,
+    #[doc = "Real-time event 48"]              SIG48 = 60,
+    #[doc = "Real-time event 49"]              SIG49 = 61,
+    #[doc = "Real-time event 50"]              SIG50 = 62,
+    #[doc = "Real-time event 51"]              SIG51 = 63,
+    #[doc = "Real-time event 52"]              SIG52 = 64,
+    #[doc = "Real-time event 53"]              SIG53 = 65,
+    #[doc = "Real-time event 54"]              SIG54 = 66,
+    #[doc = "Real-time event 55"]              SIG55 = 67,
+    #[doc = "Real-time event 56"]              SIG56 = 68,
+    #[doc = "Real-time event 57"]              SIG57 = 69,
+    #[doc = "Real-time event 58"]              SIG58 = 70,
+    #[doc = "Real-time event 59"]              SIG59 = 71,
+    #[doc = "Real-time event 60"]              SIG60 = 72,
+    #[doc = "Real-time event 61"]              SIG61 = 73,
+    #[doc = "Real-time event 62"]              SIG62 = 74,
+    #[doc = "Real-time event 63"]              SIG63 = 75,
+    /* Used internally by Solaris threads.  See signal(5) on Solaris. */
+    #[doc = "LWP internal signal"]             SIGCANCEL = 76,
+    /* Yes, this pains me, too.  But LynxOS didn't have SIG32, and now
+    GNU/Linux does, and we can't disturb the numbering, since it's
+    part of the remote protocol.  Note that in some GDB's
+    GDB_SIGNAL_REALTIME_32 is number 76.  */
+    #[doc = "Real-time event 32"]              SIG32 = 77,
+    /* Yet another pain, IRIX 6 has SIG64. */
+    #[doc = "Real-time event 64"]              SIG64 = 78,
+    /* Yet another pain, GNU/Linux MIPS might go up to 128. */
+    #[doc = "Real-time event 65"]              SIG65 = 79,
+    #[doc = "Real-time event 66"]              SIG66 = 80,
+    #[doc = "Real-time event 67"]              SIG67 = 81,
+    #[doc = "Real-time event 68"]              SIG68 = 82,
+    #[doc = "Real-time event 69"]              SIG69 = 83,
+    #[doc = "Real-time event 70"]              SIG70 = 84,
+    #[doc = "Real-time event 71"]              SIG71 = 85,
+    #[doc = "Real-time event 72"]              SIG72 = 86,
+    #[doc = "Real-time event 73"]              SIG73 = 87,
+    #[doc = "Real-time event 74"]              SIG74 = 88,
+    #[doc = "Real-time event 75"]              SIG75 = 89,
+    #[doc = "Real-time event 76"]              SIG76 = 90,
+    #[doc = "Real-time event 77"]              SIG77 = 91,
+    #[doc = "Real-time event 78"]              SIG78 = 92,
+    #[doc = "Real-time event 79"]              SIG79 = 93,
+    #[doc = "Real-time event 80"]              SIG80 = 94,
+    #[doc = "Real-time event 81"]              SIG81 = 95,
+    #[doc = "Real-time event 82"]              SIG82 = 96,
+    #[doc = "Real-time event 83"]              SIG83 = 97,
+    #[doc = "Real-time event 84"]              SIG84 = 98,
+    #[doc = "Real-time event 85"]              SIG85 = 99,
+    #[doc = "Real-time event 86"]              SIG86 = 100,
+    #[doc = "Real-time event 87"]              SIG87 = 101,
+    #[doc = "Real-time event 88"]              SIG88 = 102,
+    #[doc = "Real-time event 89"]              SIG89 = 103,
+    #[doc = "Real-time event 90"]              SIG90 = 104,
+    #[doc = "Real-time event 91"]              SIG91 = 105,
+    #[doc = "Real-time event 92"]              SIG92 = 106,
+    #[doc = "Real-time event 93"]              SIG93 = 107,
+    #[doc = "Real-time event 94"]              SIG94 = 108,
+    #[doc = "Real-time event 95"]              SIG95 = 109,
+    #[doc = "Real-time event 96"]              SIG96 = 110,
+    #[doc = "Real-time event 97"]              SIG97 = 111,
+    #[doc = "Real-time event 98"]              SIG98 = 112,
+    #[doc = "Real-time event 99"]              SIG99 = 113,
+    #[doc = "Real-time event 100"]             SIG100 = 114,
+    #[doc = "Real-time event 101"]             SIG101 = 115,
+    #[doc = "Real-time event 102"]             SIG102 = 116,
+    #[doc = "Real-time event 103"]             SIG103 = 117,
+    #[doc = "Real-time event 104"]             SIG104 = 118,
+    #[doc = "Real-time event 105"]             SIG105 = 119,
+    #[doc = "Real-time event 106"]             SIG106 = 120,
+    #[doc = "Real-time event 107"]             SIG107 = 121,
+    #[doc = "Real-time event 108"]             SIG108 = 122,
+    #[doc = "Real-time event 109"]             SIG109 = 123,
+    #[doc = "Real-time event 110"]             SIG110 = 124,
+    #[doc = "Real-time event 111"]             SIG111 = 125,
+    #[doc = "Real-time event 112"]             SIG112 = 126,
+    #[doc = "Real-time event 113"]             SIG113 = 127,
+    #[doc = "Real-time event 114"]             SIG114 = 128,
+    #[doc = "Real-time event 115"]             SIG115 = 129,
+    #[doc = "Real-time event 116"]             SIG116 = 130,
+    #[doc = "Real-time event 117"]             SIG117 = 131,
+    #[doc = "Real-time event 118"]             SIG118 = 132,
+    #[doc = "Real-time event 119"]             SIG119 = 133,
+    #[doc = "Real-time event 120"]             SIG120 = 134,
+    #[doc = "Real-time event 121"]             SIG121 = 135,
+    #[doc = "Real-time event 122"]             SIG122 = 136,
+    #[doc = "Real-time event 123"]             SIG123 = 137,
+    #[doc = "Real-time event 124"]             SIG124 = 138,
+    #[doc = "Real-time event 125"]             SIG125 = 139,
+    #[doc = "Real-time event 126"]             SIG126 = 140,
+    #[doc = "Real-time event 127"]             SIG127 = 141,
+
+    #[doc = "Information request"]             SIGINFO = 142,
+
+    /* Some signal we don't know about. */
+    #[doc = "Unknown signal"]                  UNKNOWN = 143,
+
+    /* Use whatever signal we use when one is not specifically specified
+    (for passing to proceed and so on).  */
+    #[doc = "Internal error: printing GDB_SIGNAL_DEFAULT"] INTERNAL_DEFAULT = 144,
+
+    /* Mach exceptions.  In versions of GDB before 5.2, these were just before
+    GDB_SIGNAL_INFO if you were compiling on a Mach host (and missing
+    otherwise).  */
+    #[doc = "Could not access memory"]         EXC_BAD_ACCESS = 145,
+    #[doc = "Illegal instruction/operand"]     EXC_BAD_INSTRUCTION = 146,
+    #[doc = "Arithmetic exception"]            EXC_ARITHMETIC = 147,
+    #[doc = "Emulation instruction"]           EXC_EMULATION = 148,
+    #[doc = "Software generated exception"]    EXC_SOFTWARE = 149,
+    #[doc = "Breakpoint"]                      EXC_BREAKPOINT = 150,
+
+    #[doc = "librt internal signal"]           SIGLIBRT = 151,
+}
+
+impl core::fmt::Display for Signal {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        #[rustfmt::skip]
+        let s = match self {
+            Signal::SIGZERO => "SIGZERO - Signal 0",
+            Signal::SIGHUP => "SIGHUP - Hangup",
+            Signal::SIGINT => "SIGINT - Interrupt",
+            Signal::SIGQUIT => "SIGQUIT - Quit",
+            Signal::SIGILL => "SIGILL - Illegal instruction",
+            Signal::SIGTRAP => "SIGTRAP - Trace/breakpoint trap",
+            Signal::SIGABRT => "SIGABRT - Aborted",
+            Signal::SIGEMT => "SIGEMT - Emulation trap",
+            Signal::SIGFPE => "SIGFPE - Arithmetic exception",
+            Signal::SIGKILL => "SIGKILL - Killed",
+            Signal::SIGBUS => "SIGBUS - Bus error",
+            Signal::SIGSEGV => "SIGSEGV - Segmentation fault",
+            Signal::SIGSYS => "SIGSYS - Bad system call",
+            Signal::SIGPIPE => "SIGPIPE - Broken pipe",
+            Signal::SIGALRM => "SIGALRM - Alarm clock",
+            Signal::SIGTERM => "SIGTERM - Terminated",
+            Signal::SIGURG => "SIGURG - Urgent I/O condition",
+            Signal::SIGSTOP => "SIGSTOP - Stopped (signal)",
+            Signal::SIGTSTP => "SIGTSTP - Stopped (user)",
+            Signal::SIGCONT => "SIGCONT - Continued",
+            Signal::SIGCHLD => "SIGCHLD - Child status changed",
+            Signal::SIGTTIN => "SIGTTIN - Stopped (tty input)",
+            Signal::SIGTTOU => "SIGTTOU - Stopped (tty output)",
+            Signal::SIGIO => "SIGIO - I/O possible",
+            Signal::SIGXCPU => "SIGXCPU - CPU time limit exceeded",
+            Signal::SIGXFSZ => "SIGXFSZ - File size limit exceeded",
+            Signal::SIGVTALRM => "SIGVTALRM - Virtual timer expired",
+            Signal::SIGPROF => "SIGPROF - Profiling timer expired",
+            Signal::SIGWINCH => "SIGWINCH - Window size changed",
+            Signal::SIGLOST => "SIGLOST - Resource lost",
+            Signal::SIGUSR1 => "SIGUSR1 - User defined signal 1",
+            Signal::SIGUSR2 => "SIGUSR2 - User defined signal 2",
+            Signal::SIGPWR => "SIGPWR - Power fail/restart",
+            Signal::SIGPOLL => "SIGPOLL - Pollable event occurred",
+            Signal::SIGWIND => "SIGWIND - SIGWIND",
+            Signal::SIGPHONE => "SIGPHONE - SIGPHONE",
+            Signal::SIGWAITING => "SIGWAITING - Process's LWPs are blocked",
+            Signal::SIGLWP => "SIGLWP - Signal LWP",
+            Signal::SIGDANGER => "SIGDANGER - Swap space dangerously low",
+            Signal::SIGGRANT => "SIGGRANT - Monitor mode granted",
+            Signal::SIGRETRACT => "SIGRETRACT - Need to relinquish monitor mode",
+            Signal::SIGMSG => "SIGMSG - Monitor mode data available",
+            Signal::SIGSOUND => "SIGSOUND - Sound completed",
+            Signal::SIGSAK => "SIGSAK - Secure attention",
+            Signal::SIGPRIO => "SIGPRIO - SIGPRIO",
+            Signal::SIG33 => "SIG33 - Real-time event 33",
+            Signal::SIG34 => "SIG34 - Real-time event 34",
+            Signal::SIG35 => "SIG35 - Real-time event 35",
+            Signal::SIG36 => "SIG36 - Real-time event 36",
+            Signal::SIG37 => "SIG37 - Real-time event 37",
+            Signal::SIG38 => "SIG38 - Real-time event 38",
+            Signal::SIG39 => "SIG39 - Real-time event 39",
+            Signal::SIG40 => "SIG40 - Real-time event 40",
+            Signal::SIG41 => "SIG41 - Real-time event 41",
+            Signal::SIG42 => "SIG42 - Real-time event 42",
+            Signal::SIG43 => "SIG43 - Real-time event 43",
+            Signal::SIG44 => "SIG44 - Real-time event 44",
+            Signal::SIG45 => "SIG45 - Real-time event 45",
+            Signal::SIG46 => "SIG46 - Real-time event 46",
+            Signal::SIG47 => "SIG47 - Real-time event 47",
+            Signal::SIG48 => "SIG48 - Real-time event 48",
+            Signal::SIG49 => "SIG49 - Real-time event 49",
+            Signal::SIG50 => "SIG50 - Real-time event 50",
+            Signal::SIG51 => "SIG51 - Real-time event 51",
+            Signal::SIG52 => "SIG52 - Real-time event 52",
+            Signal::SIG53 => "SIG53 - Real-time event 53",
+            Signal::SIG54 => "SIG54 - Real-time event 54",
+            Signal::SIG55 => "SIG55 - Real-time event 55",
+            Signal::SIG56 => "SIG56 - Real-time event 56",
+            Signal::SIG57 => "SIG57 - Real-time event 57",
+            Signal::SIG58 => "SIG58 - Real-time event 58",
+            Signal::SIG59 => "SIG59 - Real-time event 59",
+            Signal::SIG60 => "SIG60 - Real-time event 60",
+            Signal::SIG61 => "SIG61 - Real-time event 61",
+            Signal::SIG62 => "SIG62 - Real-time event 62",
+            Signal::SIG63 => "SIG63 - Real-time event 63",
+            Signal::SIGCANCEL => "SIGCANCEL - LWP internal signal",
+            Signal::SIG32 => "SIG32 - Real-time event 32",
+            Signal::SIG64 => "SIG64 - Real-time event 64",
+            Signal::SIG65 => "SIG65 - Real-time event 65",
+            Signal::SIG66 => "SIG66 - Real-time event 66",
+            Signal::SIG67 => "SIG67 - Real-time event 67",
+            Signal::SIG68 => "SIG68 - Real-time event 68",
+            Signal::SIG69 => "SIG69 - Real-time event 69",
+            Signal::SIG70 => "SIG70 - Real-time event 70",
+            Signal::SIG71 => "SIG71 - Real-time event 71",
+            Signal::SIG72 => "SIG72 - Real-time event 72",
+            Signal::SIG73 => "SIG73 - Real-time event 73",
+            Signal::SIG74 => "SIG74 - Real-time event 74",
+            Signal::SIG75 => "SIG75 - Real-time event 75",
+            Signal::SIG76 => "SIG76 - Real-time event 76",
+            Signal::SIG77 => "SIG77 - Real-time event 77",
+            Signal::SIG78 => "SIG78 - Real-time event 78",
+            Signal::SIG79 => "SIG79 - Real-time event 79",
+            Signal::SIG80 => "SIG80 - Real-time event 80",
+            Signal::SIG81 => "SIG81 - Real-time event 81",
+            Signal::SIG82 => "SIG82 - Real-time event 82",
+            Signal::SIG83 => "SIG83 - Real-time event 83",
+            Signal::SIG84 => "SIG84 - Real-time event 84",
+            Signal::SIG85 => "SIG85 - Real-time event 85",
+            Signal::SIG86 => "SIG86 - Real-time event 86",
+            Signal::SIG87 => "SIG87 - Real-time event 87",
+            Signal::SIG88 => "SIG88 - Real-time event 88",
+            Signal::SIG89 => "SIG89 - Real-time event 89",
+            Signal::SIG90 => "SIG90 - Real-time event 90",
+            Signal::SIG91 => "SIG91 - Real-time event 91",
+            Signal::SIG92 => "SIG92 - Real-time event 92",
+            Signal::SIG93 => "SIG93 - Real-time event 93",
+            Signal::SIG94 => "SIG94 - Real-time event 94",
+            Signal::SIG95 => "SIG95 - Real-time event 95",
+            Signal::SIG96 => "SIG96 - Real-time event 96",
+            Signal::SIG97 => "SIG97 - Real-time event 97",
+            Signal::SIG98 => "SIG98 - Real-time event 98",
+            Signal::SIG99 => "SIG99 - Real-time event 99",
+            Signal::SIG100 => "SIG100 - Real-time event 100",
+            Signal::SIG101 => "SIG101 - Real-time event 101",
+            Signal::SIG102 => "SIG102 - Real-time event 102",
+            Signal::SIG103 => "SIG103 - Real-time event 103",
+            Signal::SIG104 => "SIG104 - Real-time event 104",
+            Signal::SIG105 => "SIG105 - Real-time event 105",
+            Signal::SIG106 => "SIG106 - Real-time event 106",
+            Signal::SIG107 => "SIG107 - Real-time event 107",
+            Signal::SIG108 => "SIG108 - Real-time event 108",
+            Signal::SIG109 => "SIG109 - Real-time event 109",
+            Signal::SIG110 => "SIG110 - Real-time event 110",
+            Signal::SIG111 => "SIG111 - Real-time event 111",
+            Signal::SIG112 => "SIG112 - Real-time event 112",
+            Signal::SIG113 => "SIG113 - Real-time event 113",
+            Signal::SIG114 => "SIG114 - Real-time event 114",
+            Signal::SIG115 => "SIG115 - Real-time event 115",
+            Signal::SIG116 => "SIG116 - Real-time event 116",
+            Signal::SIG117 => "SIG117 - Real-time event 117",
+            Signal::SIG118 => "SIG118 - Real-time event 118",
+            Signal::SIG119 => "SIG119 - Real-time event 119",
+            Signal::SIG120 => "SIG120 - Real-time event 120",
+            Signal::SIG121 => "SIG121 - Real-time event 121",
+            Signal::SIG122 => "SIG122 - Real-time event 122",
+            Signal::SIG123 => "SIG123 - Real-time event 123",
+            Signal::SIG124 => "SIG124 - Real-time event 124",
+            Signal::SIG125 => "SIG125 - Real-time event 125",
+            Signal::SIG126 => "SIG126 - Real-time event 126",
+            Signal::SIG127 => "SIG127 - Real-time event 127",
+            Signal::SIGINFO => "SIGINFO - Information request",
+            Signal::UNKNOWN => "UNKNOWN - Unknown signal",
+            Signal::INTERNAL_DEFAULT => "INTERNAL_DEFAULT - Internal error: printing GDB_SIGNAL_DEFAULT",
+            Signal::EXC_BAD_ACCESS => "EXC_BAD_ACCESS - Could not access memory",
+            Signal::EXC_BAD_INSTRUCTION => "EXC_BAD_INSTRUCTION - Illegal instruction/operand",
+            Signal::EXC_ARITHMETIC => "EXC_ARITHMETIC - Arithmetic exception",
+            Signal::EXC_EMULATION => "EXC_EMULATION - Emulation instruction",
+            Signal::EXC_SOFTWARE => "EXC_SOFTWARE - Software generated exception",
+            Signal::EXC_BREAKPOINT => "EXC_BREAKPOINT - Breakpoint",
+            Signal::SIGLIBRT => "SIGLIBRT - librt internal signal",
+        };
+
+        write!(f, "{}", s)
+    }
+}
+
+impl Signal {
+    #[cfg(not(feature = "paranoid_unsafe"))]
+    pub(crate) fn from_protocol_u8(val: u8) -> Signal {
+        if val <= 151 {
+            // SAFETY: Signal is repr(u8), and `val` was confirmed to fall in valid range
+            unsafe { core::mem::transmute(val) }
+        } else {
+            Signal::UNKNOWN
+        }
+    }
+
+    #[cfg(feature = "paranoid_unsafe")]
+    pub(crate) fn from_protocol_u8(val: u8) -> Signal {
+        match val {
+            0 => Signal::SIGZERO,
+            1 => Signal::SIGHUP,
+            2 => Signal::SIGINT,
+            3 => Signal::SIGQUIT,
+            4 => Signal::SIGILL,
+            5 => Signal::SIGTRAP,
+            6 => Signal::SIGABRT,
+            7 => Signal::SIGEMT,
+            8 => Signal::SIGFPE,
+            9 => Signal::SIGKILL,
+            10 => Signal::SIGBUS,
+            11 => Signal::SIGSEGV,
+            12 => Signal::SIGSYS,
+            13 => Signal::SIGPIPE,
+            14 => Signal::SIGALRM,
+            15 => Signal::SIGTERM,
+            16 => Signal::SIGURG,
+            17 => Signal::SIGSTOP,
+            18 => Signal::SIGTSTP,
+            19 => Signal::SIGCONT,
+            20 => Signal::SIGCHLD,
+            21 => Signal::SIGTTIN,
+            22 => Signal::SIGTTOU,
+            23 => Signal::SIGIO,
+            24 => Signal::SIGXCPU,
+            25 => Signal::SIGXFSZ,
+            26 => Signal::SIGVTALRM,
+            27 => Signal::SIGPROF,
+            28 => Signal::SIGWINCH,
+            29 => Signal::SIGLOST,
+            30 => Signal::SIGUSR1,
+            31 => Signal::SIGUSR2,
+            32 => Signal::SIGPWR,
+            33 => Signal::SIGPOLL,
+            34 => Signal::SIGWIND,
+            35 => Signal::SIGPHONE,
+            36 => Signal::SIGWAITING,
+            37 => Signal::SIGLWP,
+            38 => Signal::SIGDANGER,
+            39 => Signal::SIGGRANT,
+            40 => Signal::SIGRETRACT,
+            41 => Signal::SIGMSG,
+            42 => Signal::SIGSOUND,
+            43 => Signal::SIGSAK,
+            44 => Signal::SIGPRIO,
+            45 => Signal::SIG33,
+            46 => Signal::SIG34,
+            47 => Signal::SIG35,
+            48 => Signal::SIG36,
+            49 => Signal::SIG37,
+            50 => Signal::SIG38,
+            51 => Signal::SIG39,
+            52 => Signal::SIG40,
+            53 => Signal::SIG41,
+            54 => Signal::SIG42,
+            55 => Signal::SIG43,
+            56 => Signal::SIG44,
+            57 => Signal::SIG45,
+            58 => Signal::SIG46,
+            59 => Signal::SIG47,
+            60 => Signal::SIG48,
+            61 => Signal::SIG49,
+            62 => Signal::SIG50,
+            63 => Signal::SIG51,
+            64 => Signal::SIG52,
+            65 => Signal::SIG53,
+            66 => Signal::SIG54,
+            67 => Signal::SIG55,
+            68 => Signal::SIG56,
+            69 => Signal::SIG57,
+            70 => Signal::SIG58,
+            71 => Signal::SIG59,
+            72 => Signal::SIG60,
+            73 => Signal::SIG61,
+            74 => Signal::SIG62,
+            75 => Signal::SIG63,
+            76 => Signal::SIGCANCEL,
+            77 => Signal::SIG32,
+            78 => Signal::SIG64,
+            79 => Signal::SIG65,
+            80 => Signal::SIG66,
+            81 => Signal::SIG67,
+            82 => Signal::SIG68,
+            83 => Signal::SIG69,
+            84 => Signal::SIG70,
+            85 => Signal::SIG71,
+            86 => Signal::SIG72,
+            87 => Signal::SIG73,
+            88 => Signal::SIG74,
+            89 => Signal::SIG75,
+            90 => Signal::SIG76,
+            91 => Signal::SIG77,
+            92 => Signal::SIG78,
+            93 => Signal::SIG79,
+            94 => Signal::SIG80,
+            95 => Signal::SIG81,
+            96 => Signal::SIG82,
+            97 => Signal::SIG83,
+            98 => Signal::SIG84,
+            99 => Signal::SIG85,
+            100 => Signal::SIG86,
+            101 => Signal::SIG87,
+            102 => Signal::SIG88,
+            103 => Signal::SIG89,
+            104 => Signal::SIG90,
+            105 => Signal::SIG91,
+            106 => Signal::SIG92,
+            107 => Signal::SIG93,
+            108 => Signal::SIG94,
+            109 => Signal::SIG95,
+            110 => Signal::SIG96,
+            111 => Signal::SIG97,
+            112 => Signal::SIG98,
+            113 => Signal::SIG99,
+            114 => Signal::SIG100,
+            115 => Signal::SIG101,
+            116 => Signal::SIG102,
+            117 => Signal::SIG103,
+            118 => Signal::SIG104,
+            119 => Signal::SIG105,
+            120 => Signal::SIG106,
+            121 => Signal::SIG107,
+            122 => Signal::SIG108,
+            123 => Signal::SIG109,
+            124 => Signal::SIG110,
+            125 => Signal::SIG111,
+            126 => Signal::SIG112,
+            127 => Signal::SIG113,
+            128 => Signal::SIG114,
+            129 => Signal::SIG115,
+            130 => Signal::SIG116,
+            131 => Signal::SIG117,
+            132 => Signal::SIG118,
+            133 => Signal::SIG119,
+            134 => Signal::SIG120,
+            135 => Signal::SIG121,
+            136 => Signal::SIG122,
+            137 => Signal::SIG123,
+            138 => Signal::SIG124,
+            139 => Signal::SIG125,
+            140 => Signal::SIG126,
+            141 => Signal::SIG127,
+            142 => Signal::SIGINFO,
+            143 => Signal::UNKNOWN,
+            144 => Signal::INTERNAL_DEFAULT,
+            145 => Signal::EXC_BAD_ACCESS,
+            146 => Signal::EXC_BAD_INSTRUCTION,
+            147 => Signal::EXC_ARITHMETIC,
+            148 => Signal::EXC_EMULATION,
+            149 => Signal::EXC_SOFTWARE,
+            150 => Signal::EXC_BREAKPOINT,
+            151 => Signal::SIGLIBRT,
+
+            _ => Signal::UNKNOWN,
+        }
+    }
+}
diff --git a/src/conn/impls/boxed.rs b/src/conn/impls/boxed.rs
new file mode 100644
index 0000000..2de92c0
--- /dev/null
+++ b/src/conn/impls/boxed.rs
@@ -0,0 +1,54 @@
+use crate::conn::Connection;
+use crate::conn::ConnectionExt;
+
+use alloc::boxed::Box;
+
+impl<E> Connection for Box<dyn Connection<Error = E>> {
+    type Error = E;
+
+    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 flush(&mut self) -> Result<(), Self::Error> {
+        (**self).flush()
+    }
+
+    fn on_session_start(&mut self) -> Result<(), Self::Error> {
+        (**self).on_session_start()
+    }
+}
+
+impl<E> Connection for Box<dyn ConnectionExt<Error = E>> {
+    type Error = E;
+
+    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 flush(&mut self) -> Result<(), Self::Error> {
+        (**self).flush()
+    }
+
+    fn on_session_start(&mut self) -> Result<(), Self::Error> {
+        (**self).on_session_start()
+    }
+}
+
+impl<E> ConnectionExt for Box<dyn ConnectionExt<Error = E>> {
+    fn read(&mut self) -> Result<u8, Self::Error> {
+        (**self).read()
+    }
+
+    fn peek(&mut self) -> Result<Option<u8>, Self::Error> {
+        (**self).peek()
+    }
+}
diff --git a/src/connection/impls/mod.rs b/src/conn/impls/mod.rs
similarity index 61%
rename from src/connection/impls/mod.rs
rename to src/conn/impls/mod.rs
index 8d13f70..97d7eb1 100644
--- a/src/connection/impls/mod.rs
+++ b/src/conn/impls/mod.rs
@@ -10,19 +10,12 @@
 #[cfg(all(feature = "std", unix))]
 mod unixstream;
 
-use super::Connection;
+use crate::conn::Connection;
+use crate::conn::ConnectionExt;
 
 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)
     }
@@ -31,8 +24,24 @@
         (**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()
+    }
+}
+
+impl<E> Connection for &mut dyn ConnectionExt<Error = E> {
+    type Error = E;
+
+    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 flush(&mut self) -> Result<(), Self::Error> {
@@ -43,3 +52,13 @@
         (**self).on_session_start()
     }
 }
+
+impl<E> ConnectionExt for &mut dyn ConnectionExt<Error = E> {
+    fn read(&mut self) -> Result<u8, Self::Error> {
+        (**self).read()
+    }
+
+    fn peek(&mut self) -> Result<Option<u8>, Self::Error> {
+        (**self).peek()
+    }
+}
diff --git a/src/connection/impls/tcpstream.rs b/src/conn/impls/tcpstream.rs
similarity index 85%
rename from src/connection/impls/tcpstream.rs
rename to src/conn/impls/tcpstream.rs
index 52d3772..969b7ff 100644
--- a/src/connection/impls/tcpstream.rs
+++ b/src/conn/impls/tcpstream.rs
@@ -1,41 +1,11 @@
 use std::net::TcpStream;
 
-use crate::Connection;
+use crate::conn::Connection;
+use crate::conn::ConnectionExt;
 
 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;
 
@@ -59,3 +29,28 @@
         self.set_nodelay(true)
     }
 }
+
+impl ConnectionExt for TcpStream {
+    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 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),
+        }
+    }
+}
diff --git a/src/connection/impls/unixstream.rs b/src/conn/impls/unixstream.rs
similarity index 88%
rename from src/connection/impls/unixstream.rs
rename to src/conn/impls/unixstream.rs
index 81854ee..7f8191c 100644
--- a/src/connection/impls/unixstream.rs
+++ b/src/conn/impls/unixstream.rs
@@ -3,9 +3,10 @@
 use std::os::unix::io::AsRawFd;
 use std::os::unix::net::UnixStream;
 
-use crate::Connection;
+use crate::conn::Connection;
+use crate::conn::ConnectionExt;
 
-// TODO: Remove PeekExt once `gdbstub`'s MSRV >1.48 (rust-lang/rust#73761)
+// TODO: Remove PeekExt once rust-lang/rust#73761 is stabilized
 trait PeekExt {
     fn peek(&self, buf: &mut [u8]) -> io::Result<usize>;
 }
@@ -49,37 +50,6 @@
 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;
 
@@ -98,3 +68,28 @@
         Write::flush(self)
     }
 }
+
+impl ConnectionExt for UnixStream {
+    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 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),
+        }
+    }
+}
diff --git a/src/connection/mod.rs b/src/conn/mod.rs
similarity index 75%
rename from src/connection/mod.rs
rename to src/conn/mod.rs
index 7dbd4ea..9bd640e 100644
--- a/src/connection/mod.rs
+++ b/src/conn/mod.rs
@@ -1,3 +1,5 @@
+//! Traits to perform in-order, serial, byte-wise I/O.
+
 mod impls;
 
 /// A trait to perform in-order, serial, byte-wise I/O.
@@ -9,22 +11,6 @@
     /// 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>;
 
@@ -41,10 +27,6 @@
         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.
     ///
@@ -69,3 +51,26 @@
         Ok(())
     }
 }
+
+/// Extends [`Connection`] with `read` and `peek` methods.
+///
+/// This trait is used as part of `gdbstub`'s quickstart
+/// [`GdbStub::run_blocking`](crate::stub::GdbStub::run_blocking) API.
+///
+/// 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).
+///
+/// [`gdbstub_run::Callbacks::read_byte`]:
+/// crate::gdbstub_run::Callbacks::read_byte
+pub trait ConnectionExt: Connection {
+    /// Read a single byte.
+    fn read(&mut self) -> Result<u8, Self::Error>;
+
+    /// Peek a single byte. This MUST be a **non-blocking** operation, returning
+    /// `None` if no byte is available.
+    ///
+    /// Returns a byte (if one is available) without removing that byte from the
+    /// queue. Subsequent calls to `peek` MUST return the same byte.
+    fn peek(&mut self) -> Result<Option<u8>, Self::Error>;
+}
diff --git a/src/connection/impls/boxed.rs b/src/connection/impls/boxed.rs
deleted file mode 100644
index 8acdb36..0000000
--- a/src/connection/impls/boxed.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-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/gdbstub_impl/error.rs b/src/gdbstub_impl/error.rs
deleted file mode 100644
index 40d2317..0000000
--- a/src/gdbstub_impl/error.rs
+++ /dev/null
@@ -1,88 +0,0 @@
-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(C),
-    /// Client nack'd the last packet, but `gdbstub` doesn't implement
-    /// re-transmission.
-    ClientSentNack,
-    /// Packet cannot fit in the provided packet buffer.
-    PacketBufferOverflow,
-    /// Could not parse the packet into a valid command.
-    PacketParse(PacketParseError),
-    /// GDB client sent an unexpected packet. This should never happen!
-    /// Please file an issue at https://github.com/daniel5151/gdbstub/issues
-    PacketUnexpected,
-    /// GDB client sent a packet with too much data for the given target.
-    TargetMismatch,
-    /// Target encountered a fatal error.
-    TargetError(T),
-    /// Target responded with an unsupported stop reason.
-    ///
-    /// Certain stop reasons can only be used when their associated protocol
-    /// feature has been implemented. e.g: a Target cannot return a
-    /// `StopReason::HwBreak` if the hardware breakpoints IDET hasn't been
-    /// implemented.
-    UnsupportedStopReason,
-    /// Target didn't report any active threads when there should have been at
-    /// least one running.
-    NoActiveThreads,
-    /// Internal - A non-fatal error occurred (with errno-style error code)
-    ///
-    /// This "dummy" error is required as part of the internal
-    /// `TargetResultExt::handle_error()` machinery, and will never be
-    /// propagated up to the end user.
-    #[doc(hidden)]
-    NonFatalError(u8),
-}
-
-impl<T, C> From<ResponseWriterError<C>> for GdbStubError<T, C> {
-    fn from(e: ResponseWriterError<C>) -> Self {
-        GdbStubError::ConnectionWrite(e.0)
-    }
-}
-
-impl<A, T, C> From<CapacityError<A>> for GdbStubError<T, C> {
-    fn from(_: CapacityError<A>) -> Self {
-        GdbStubError::PacketBufferOverflow
-    }
-}
-
-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."),
-            PacketBufferOverflow => 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. This should never happen! Please file an issue at https://github.com/daniel5151/gdbstub/issues"),
-            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),
-            UnsupportedStopReason => write!(f, "Target responded with an unsupported stop reason."),
-            NoActiveThreads => write!(f, "Target didn't report any active threads when there should have been at least one running."),
-            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/ext/base.rs b/src/gdbstub_impl/ext/base.rs
deleted file mode 100644
index d130422..0000000
--- a/src/gdbstub_impl/ext/base.rs
+++ /dev/null
@@ -1,743 +0,0 @@
-use super::prelude::*;
-use crate::protocol::commands::ext::Base;
-
-use crate::arch::{Arch, Registers};
-use crate::protocol::{IdKind, SpecificIdKind, SpecificThreadId};
-use crate::target::ext::base::multithread::ThreadStopReason;
-use crate::target::ext::base::{BaseOps, GdbInterrupt, ReplayLogPosition, ResumeAction};
-use crate::{FAKE_PID, SINGLE_THREAD_TID};
-
-impl<T: Target, C: Connection> GdbStubImpl<T, C> {
-    #[inline(always)]
-    fn get_sane_any_tid(&mut self, target: &mut T) -> Result<Tid, Error<T::Error, C::Error>> {
-        let 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)?;
-                // Note that `Error::NoActiveThreads` shouldn't ever occur, since this method is
-                // called from the `H` packet handler, which AFAIK is only sent after the GDB
-                // client has confirmed that a thread / process exists.
-                //
-                // If it does, that really sucks, and will require rethinking how to handle "any
-                // thread" messages.
-                first_tid.ok_or(Error::NoActiveThreads)?
-            }
-        };
-        Ok(tid)
-    }
-
-    pub(crate) fn handle_base<'a>(
-        &mut self,
-        res: &mut ResponseWriter<C>,
-        target: &mut T,
-        command: Base<'a>,
-    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
-        let handler_status = match command {
-            // ------------------ Handshaking and Queries ------------------- //
-            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(cmd.packet_buffer_len)?;
-
-                res.write_str(";vContSupported+")?;
-                res.write_str(";multiprocess+")?;
-                res.write_str(";QStartNoAckMode+")?;
-
-                let (reverse_cont, reverse_step) = match target.base_ops() {
-                    BaseOps::MultiThread(ops) => (
-                        ops.support_reverse_cont().is_some(),
-                        ops.support_reverse_step().is_some(),
-                    ),
-                    BaseOps::SingleThread(ops) => (
-                        ops.support_reverse_cont().is_some(),
-                        ops.support_reverse_step().is_some(),
-                    ),
-                };
-
-                if reverse_cont {
-                    res.write_str(";ReverseContinue+")?;
-                }
-
-                if reverse_step {
-                    res.write_str(";ReverseStep+")?;
-                }
-
-                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+")?;
-                    }
-                }
-
-                if let Some(ops) = target.breakpoints() {
-                    if ops.sw_breakpoint().is_some() {
-                        res.write_str(";swbreak+")?;
-                    }
-
-                    if ops.hw_breakpoint().is_some() || ops.hw_watchpoint().is_some() {
-                        res.write_str(";hwbreak+")?;
-                    }
-                }
-
-                if T::Arch::target_description_xml().is_some()
-                    || target.target_description_xml_override().is_some()
-                {
-                    res.write_str(";qXfer:features:read+")?;
-                }
-
-                HandlerStatus::Handled
-            }
-            Base::QStartNoAckMode(_) => {
-                self.no_ack_mode = true;
-                HandlerStatus::NeedsOk
-            }
-            Base::qXferFeaturesRead(cmd) => {
-                #[allow(clippy::redundant_closure)]
-                let xml = target
-                    .target_description_xml_override()
-                    .map(|ops| ops.target_description_xml())
-                    .or_else(|| T::Arch::target_description_xml());
-
-                match 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.
-            // this will be particularly relevant when working on non-stop mode.
-            Base::QuestionMark(_) => {
-                res.write_str("S05")?;
-                HandlerStatus::Handled
-            }
-            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)?;
-                        ops.query_if_attached(pid).handle_error()?.was_attached()
-                    }
-                };
-                res.write_str(if is_attached { "1" } else { "0" })?;
-                HandlerStatus::Handled
-            }
-            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
-            }
-            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
-            }
-            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
-            }
-            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
-            }
-            Base::k(_) | 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 {
-                            Base::vKill(cmd) => Some(cmd.pid),
-                            _ => None,
-                        };
-
-                        let should_terminate = ops.kill(pid).handle_error()?;
-                        if should_terminate.into_bool() {
-                            // manually write OK, since we need to return a DisconnectReason
-                            res.write_str("OK")?;
-                            HandlerStatus::Disconnect(DisconnectReason::Kill)
-                        } else {
-                            HandlerStatus::NeedsOk
-                        }
-                    }
-                }
-            }
-            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)
-            }
-            Base::vCont(cmd) => {
-                use crate::protocol::commands::_vCont::vCont;
-                match cmd {
-                    vCont::Query => {
-                        res.write_str("vCont;c;C;s;S")?;
-                        if match target.base_ops() {
-                            BaseOps::SingleThread(ops) => ops.support_resume_range_step().is_some(),
-                            BaseOps::MultiThread(ops) => ops.support_range_step().is_some(),
-                        } {
-                            res.write_str(";r")?;
-                        }
-                        HandlerStatus::Handled
-                    }
-                    vCont::Actions(actions) => self.do_vcont(res, target, actions)?,
-                }
-            }
-            // TODO?: support custom resume addr in 'c' and 's'
-            //
-            // unfortunately, this wouldn't be a particularly easy thing to implement, since the
-            // vCont packet doesn't natively support custom resume addresses. This leaves a few
-            // options for the implementation:
-            //
-            // 1. Adding new ResumeActions (i.e: ContinueWithAddr(U) and StepWithAddr(U))
-            // 2. Automatically calling `read_registers`, updating the `pc`, and calling
-            //    `write_registers` prior to resuming.
-            //    - will require adding some sort of `get_pc_mut` method to the `Registers` trait.
-            //
-            // Option 1 is easier to implement, but puts more burden on the implementor. Option 2
-            // will require more effort to implement (and will be less performant), but it will hide
-            // this protocol wart from the end user.
-            //
-            // Oh, one more thought - there's a subtle pitfall to watch out for if implementing
-            // Option 1: if the target is using conditional breakpoints, `do_vcont` has to be
-            // modified to only pass the resume with address variants on the _first_ iteration
-            // through the loop.
-            Base::c(_) => {
-                use crate::protocol::commands::_vCont::Actions;
-
-                self.do_vcont(
-                    res,
-                    target,
-                    Actions::new_continue(SpecificThreadId {
-                        pid: None,
-                        tid: self.current_resume_tid,
-                    }),
-                )?
-            }
-            Base::s(_) => {
-                use crate::protocol::commands::_vCont::Actions;
-
-                self.do_vcont(
-                    res,
-                    target,
-                    Actions::new_step(SpecificThreadId {
-                        pid: None,
-                        tid: self.current_resume_tid,
-                    }),
-                )?
-            }
-
-            // ------------------- Multi-threading Support ------------------ //
-            Base::H(cmd) => {
-                use crate::protocol::commands::_h_upcase::Op;
-                match cmd.kind {
-                    Op::Other => match cmd.thread.tid {
-                        IdKind::Any => self.current_mem_tid = self.get_sane_any_tid(target)?,
-                        // "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 => {
-                            self.current_resume_tid =
-                                SpecificIdKind::WithId(self.get_sane_any_tid(target)?)
-                        }
-                        IdKind::All => self.current_resume_tid = SpecificIdKind::All,
-                        IdKind::WithId(tid) => {
-                            self.current_resume_tid = SpecificIdKind::WithId(tid)
-                        }
-                    },
-                }
-                HandlerStatus::NeedsOk
-            }
-            Base::qfThreadInfo(_) => {
-                res.write_str("m")?;
-
-                match target.base_ops() {
-                    BaseOps::SingleThread(_) => res.write_specific_thread_id(SpecificThreadId {
-                        pid: Some(SpecificIdKind::WithId(FAKE_PID)),
-                        tid: SpecificIdKind::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_specific_thread_id(SpecificThreadId {
-                                    pid: Some(SpecificIdKind::WithId(FAKE_PID)),
-                                    tid: SpecificIdKind::WithId(tid),
-                                })?;
-                                Ok(())
-                            })();
-
-                            if let Err(e) = e {
-                                err = Err(e)
-                            }
-                        })
-                        .map_err(Error::TargetError)?;
-                        err?;
-                    }
-                }
-
-                HandlerStatus::Handled
-            }
-            Base::qsThreadInfo(_) => {
-                res.write_str("l")?;
-                HandlerStatus::Handled
-            }
-            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)
-    }
-
-    #[allow(clippy::type_complexity)]
-    fn do_vcont_single_thread(
-        ops: &mut dyn crate::target::ext::base::singlethread::SingleThreadOps<
-            Arch = T::Arch,
-            Error = T::Error,
-        >,
-        res: &mut ResponseWriter<C>,
-        actions: &crate::protocol::commands::_vCont::Actions,
-    ) -> Result<ThreadStopReason<<T::Arch as Arch>::Usize>, Error<T::Error, C::Error>> {
-        use crate::protocol::commands::_vCont::VContKind;
-
-        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 mut actions = actions.iter();
-        let first_action = actions
-            .next()
-            .ok_or(Error::PacketParse(
-                crate::protocol::PacketParseError::MalformedCommand,
-            ))?
-            .ok_or(Error::PacketParse(
-                crate::protocol::PacketParseError::MalformedCommand,
-            ))?;
-
-        let invalid_second_action = match actions.next() {
-            None => false,
-            Some(act) => match act {
-                None => {
-                    return Err(Error::PacketParse(
-                        crate::protocol::PacketParseError::MalformedCommand,
-                    ))
-                }
-                Some(act) => !matches!(act.kind, VContKind::Continue),
-            },
-        };
-
-        if invalid_second_action || actions.next().is_some() {
-            return Err(Error::PacketUnexpected);
-        }
-
-        let action = match first_action.kind {
-            VContKind::Step => ResumeAction::Step,
-            VContKind::Continue => ResumeAction::Continue,
-            VContKind::StepWithSig(sig) => ResumeAction::StepWithSignal(sig),
-            VContKind::ContinueWithSig(sig) => ResumeAction::ContinueWithSignal(sig),
-            VContKind::RangeStep(start, end) => {
-                if let Some(ops) = ops.support_resume_range_step() {
-                    let start = start.decode().map_err(|_| Error::TargetMismatch)?;
-                    let end = end.decode().map_err(|_| Error::TargetMismatch)?;
-
-                    let ret = ops
-                        .resume_range_step(start, end, GdbInterrupt::new(&mut check_gdb_interrupt))
-                        .map_err(Error::TargetError)?
-                        .into();
-                    err?;
-                    return Ok(ret);
-                } else {
-                    return Err(Error::PacketUnexpected);
-                }
-            }
-            // TODO: update this case when non-stop mode is implemented
-            VContKind::Stop => return Err(Error::PacketUnexpected),
-        };
-
-        let ret = ops
-            .resume(action, GdbInterrupt::new(&mut check_gdb_interrupt))
-            .map_err(Error::TargetError)?
-            .into();
-        err?;
-        Ok(ret)
-    }
-
-    #[allow(clippy::type_complexity)]
-    fn do_vcont_multi_thread(
-        ops: &mut dyn crate::target::ext::base::multithread::MultiThreadOps<
-            Arch = T::Arch,
-            Error = T::Error,
-        >,
-        res: &mut ResponseWriter<C>,
-        actions: &crate::protocol::commands::_vCont::Actions,
-    ) -> Result<ThreadStopReason<<T::Arch as Arch>::Usize>, Error<T::Error, C::Error>> {
-        // this is a pretty arbitrary choice, but it seems reasonable for most cases.
-        let mut default_resume_action = ResumeAction::Continue;
-
-        ops.clear_resume_actions().map_err(Error::TargetError)?;
-
-        for action in actions.iter() {
-            use crate::protocol::commands::_vCont::VContKind;
-
-            let action = action.ok_or(Error::PacketParse(
-                crate::protocol::PacketParseError::MalformedCommand,
-            ))?;
-
-            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.
-                VContKind::StepWithSig(sig) => ResumeAction::StepWithSignal(sig),
-                VContKind::ContinueWithSig(sig) => ResumeAction::ContinueWithSignal(sig),
-                VContKind::RangeStep(start, end) => {
-                    if let Some(ops) = ops.support_range_step() {
-                        match action.thread.map(|thread| thread.tid) {
-                            // An action with no thread-id matches all threads
-                            None | Some(SpecificIdKind::All) => {
-                                return Err(Error::PacketUnexpected)
-                            }
-                            Some(SpecificIdKind::WithId(tid)) => {
-                                let start = start.decode().map_err(|_| Error::TargetMismatch)?;
-                                let end = end.decode().map_err(|_| Error::TargetMismatch)?;
-
-                                ops.set_resume_action_range_step(tid, start, end)
-                                    .map_err(Error::TargetError)?;
-                                continue;
-                            }
-                        };
-                    } else {
-                        return Err(Error::PacketUnexpected);
-                    }
-                }
-                // TODO: update this case when non-stop mode is implemented
-                VContKind::Stop => return Err(Error::PacketUnexpected),
-            };
-
-            match action.thread.map(|thread| thread.tid) {
-                // An action with no thread-id matches all threads
-                None | Some(SpecificIdKind::All) => default_resume_action = resume_action,
-                Some(SpecificIdKind::WithId(tid)) => ops
-                    .set_resume_action(tid, resume_action)
-                    .map_err(Error::TargetError)?,
-            };
-        }
-
-        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 ret = ops
-            .resume(
-                default_resume_action,
-                GdbInterrupt::new(&mut check_gdb_interrupt),
-            )
-            .map_err(Error::TargetError)?;
-
-        err?;
-
-        Ok(ret)
-    }
-
-    fn do_vcont(
-        &mut self,
-        res: &mut ResponseWriter<C>,
-        target: &mut T,
-        actions: crate::protocol::commands::_vCont::Actions,
-    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
-        loop {
-            let stop_reason = match target.base_ops() {
-                BaseOps::SingleThread(ops) => Self::do_vcont_single_thread(ops, res, &actions)?,
-                BaseOps::MultiThread(ops) => Self::do_vcont_multi_thread(ops, res, &actions)?,
-            };
-
-            match self.finish_exec(res, target, stop_reason)? {
-                Some(status) => break Ok(status),
-                None => continue,
-            }
-        }
-    }
-
-    fn write_break_common(
-        &mut self,
-        res: &mut ResponseWriter<C>,
-        tid: Tid,
-    ) -> Result<(), Error<T::Error, C::Error>> {
-        self.current_mem_tid = tid;
-        self.current_resume_tid = SpecificIdKind::WithId(tid);
-
-        res.write_str("T05")?;
-
-        res.write_str("thread:")?;
-        res.write_specific_thread_id(SpecificThreadId {
-            pid: Some(SpecificIdKind::WithId(FAKE_PID)),
-            tid: SpecificIdKind::WithId(tid),
-        })?;
-        res.write_str(";")?;
-
-        Ok(())
-    }
-
-    pub(super) fn finish_exec(
-        &mut self,
-        res: &mut ResponseWriter<C>,
-        target: &mut T,
-        stop_reason: ThreadStopReason<<T::Arch as Arch>::Usize>,
-    ) -> Result<Option<HandlerStatus>, Error<T::Error, C::Error>> {
-        macro_rules! guard_reverse_exec {
-            () => {{
-                let (reverse_cont, reverse_step) = match target.base_ops() {
-                    BaseOps::MultiThread(ops) => (
-                        ops.support_reverse_cont().is_some(),
-                        ops.support_reverse_step().is_some(),
-                    ),
-                    BaseOps::SingleThread(ops) => (
-                        ops.support_reverse_cont().is_some(),
-                        ops.support_reverse_step().is_some(),
-                    ),
-                };
-                reverse_cont || reverse_step
-            }};
-        }
-
-        macro_rules! guard_break {
-            ($op:ident) => {
-                target.breakpoints().and_then(|ops| ops.$op()).is_some()
-            };
-        }
-
-        let status = match stop_reason {
-            ThreadStopReason::DoneStep | ThreadStopReason::GdbInterrupt => {
-                res.write_str("S05")?;
-                HandlerStatus::Handled
-            }
-            ThreadStopReason::Signal(sig) => {
-                res.write_str("S")?;
-                res.write_num(sig)?;
-                HandlerStatus::Handled
-            }
-            ThreadStopReason::Exited(code) => {
-                res.write_str("W")?;
-                res.write_num(code)?;
-                HandlerStatus::Disconnect(DisconnectReason::TargetExited(code))
-            }
-            ThreadStopReason::Terminated(sig) => {
-                res.write_str("X")?;
-                res.write_num(sig)?;
-                HandlerStatus::Disconnect(DisconnectReason::TargetTerminated(sig))
-            }
-            ThreadStopReason::SwBreak(tid) if guard_break!(sw_breakpoint) => {
-                crate::__dead_code_marker!("sw_breakpoint", "stop_reason");
-
-                self.write_break_common(res, tid)?;
-                res.write_str("swbreak:;")?;
-                HandlerStatus::Handled
-            }
-            ThreadStopReason::HwBreak(tid) if guard_break!(hw_breakpoint) => {
-                crate::__dead_code_marker!("hw_breakpoint", "stop_reason");
-
-                self.write_break_common(res, tid)?;
-                res.write_str("hwbreak:;")?;
-                HandlerStatus::Handled
-            }
-            ThreadStopReason::Watch { tid, kind, addr } if guard_break!(hw_watchpoint) => {
-                crate::__dead_code_marker!("hw_watchpoint", "stop_reason");
-
-                self.write_break_common(res, tid)?;
-
-                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)?;
-                res.write_str(";")?;
-                HandlerStatus::Handled
-            }
-            ThreadStopReason::ReplayLog(pos) if guard_reverse_exec!() => {
-                crate::__dead_code_marker!("reverse_exec", "stop_reason");
-
-                res.write_str("T05")?;
-
-                res.write_str("replaylog:")?;
-                res.write_str(match pos {
-                    ReplayLogPosition::Begin => "begin",
-                    ReplayLogPosition::End => "end",
-                })?;
-                res.write_str(";")?;
-
-                HandlerStatus::Handled
-            }
-            _ => return Err(Error::UnsupportedStopReason),
-        };
-
-        Ok(Some(status))
-    }
-}
-
-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::Exited(code) => ThreadStopReason::Exited(code),
-            StopReason::Terminated(sig) => ThreadStopReason::Terminated(sig),
-            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),
-            StopReason::ReplayLog(pos) => ThreadStopReason::ReplayLog(pos),
-        }
-    }
-}
diff --git a/src/gdbstub_impl/ext/breakpoints.rs b/src/gdbstub_impl/ext/breakpoints.rs
deleted file mode 100644
index 438ead0..0000000
--- a/src/gdbstub_impl/ext/breakpoints.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-use super::prelude::*;
-use crate::protocol::commands::ext::Breakpoints;
-
-use crate::arch::{Arch, BreakpointKind};
-
-enum CmdKind {
-    Add,
-    Remove,
-}
-
-impl<T: Target, C: Connection> GdbStubImpl<T, C> {
-    #[inline(always)]
-    fn handle_breakpoint_common(
-        &mut self,
-        ops: crate::target::ext::breakpoints::BreakpointsOps<T>,
-        cmd: crate::protocol::commands::breakpoint::BasicBreakpoint<'_>,
-        cmd_kind: CmdKind,
-    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
-        let addr =
-            <T::Arch as Arch>::Usize::from_be_bytes(cmd.addr).ok_or(Error::TargetMismatch)?;
-        let kind =
-            <T::Arch as Arch>::BreakpointKind::from_usize(cmd.kind).ok_or(Error::TargetMismatch)?;
-
-        let handler_status = match cmd_kind {
-            CmdKind::Add => {
-                use crate::target::ext::breakpoints::WatchKind::*;
-                let supported = match cmd.type_ {
-                    0 => (ops.sw_breakpoint()).map(|op| op.add_sw_breakpoint(addr, kind)),
-                    1 => (ops.hw_breakpoint()).map(|op| op.add_hw_breakpoint(addr, kind)),
-                    2 => (ops.hw_watchpoint()).map(|op| op.add_hw_watchpoint(addr, Write)),
-                    3 => (ops.hw_watchpoint()).map(|op| op.add_hw_watchpoint(addr, Read)),
-                    4 => (ops.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)),
-                }
-            }
-            CmdKind::Remove => {
-                use crate::target::ext::breakpoints::WatchKind::*;
-                let supported = match cmd.type_ {
-                    0 => (ops.sw_breakpoint()).map(|op| op.remove_sw_breakpoint(addr, kind)),
-                    1 => (ops.hw_breakpoint()).map(|op| op.remove_hw_breakpoint(addr, kind)),
-                    2 => (ops.hw_watchpoint()).map(|op| op.remove_hw_watchpoint(addr, Write)),
-                    3 => (ops.hw_watchpoint()).map(|op| op.remove_hw_watchpoint(addr, Read)),
-                    4 => (ops.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)),
-                }
-            }
-        };
-
-        Ok(handler_status)
-    }
-
-    pub(crate) fn handle_breakpoints<'a>(
-        &mut self,
-        _res: &mut ResponseWriter<C>,
-        target: &mut T,
-        command: Breakpoints<'a>,
-    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
-        let ops = match target.breakpoints() {
-            Some(ops) => ops,
-            None => return Ok(HandlerStatus::Handled),
-        };
-
-        crate::__dead_code_marker!("breakpoints", "impl");
-
-        let handler_status = match command {
-            Breakpoints::z(cmd) => self.handle_breakpoint_common(ops, cmd, CmdKind::Remove)?,
-            Breakpoints::Z(cmd) => self.handle_breakpoint_common(ops, cmd, CmdKind::Add)?,
-            Breakpoints::ZWithBytecode(cmd) => {
-                warn!("Client sent breakpoint packet with bytecode even though target didn't support agent expressions");
-                self.handle_breakpoint_common(ops, cmd.base, CmdKind::Add)?
-            }
-        };
-        Ok(handler_status)
-    }
-}
diff --git a/src/gdbstub_impl/ext/mod.rs b/src/gdbstub_impl/ext/mod.rs
deleted file mode 100644
index 58a99df..0000000
--- a/src/gdbstub_impl/ext/mod.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-mod prelude {
-    pub use crate::common::*;
-    pub use crate::connection::Connection;
-    pub use crate::internal::*;
-    pub use crate::target::Target;
-
-    pub(crate) use crate::protocol::ResponseWriter;
-
-    pub(super) use super::super::error::GdbStubError as Error;
-    pub(super) use super::super::target_result_ext::TargetResultExt;
-    pub(super) use super::super::{DisconnectReason, GdbStubImpl, HandlerStatus};
-}
-
-mod base;
-mod breakpoints;
-mod extended_mode;
-mod monitor_cmd;
-mod reverse_exec;
-mod section_offsets;
-mod single_register_access;
diff --git a/src/gdbstub_impl/ext/reverse_exec.rs b/src/gdbstub_impl/ext/reverse_exec.rs
deleted file mode 100644
index b4035c0..0000000
--- a/src/gdbstub_impl/ext/reverse_exec.rs
+++ /dev/null
@@ -1,139 +0,0 @@
-use super::prelude::*;
-use crate::protocol::commands::ext::{ReverseCont, ReverseStep};
-
-use crate::arch::Arch;
-use crate::protocol::SpecificIdKind;
-use crate::target::ext::base::multithread::{MultiThreadReverseCont, MultiThreadReverseStep};
-use crate::target::ext::base::singlethread::{SingleThreadReverseCont, SingleThreadReverseStep};
-use crate::target::ext::base::{BaseOps, GdbInterrupt};
-
-enum ReverseContOps<'a, A: Arch, E> {
-    SingleThread(&'a mut dyn SingleThreadReverseCont<Arch = A, Error = E>),
-    MultiThread(&'a mut dyn MultiThreadReverseCont<Arch = A, Error = E>),
-}
-
-enum ReverseStepOps<'a, A: Arch, E> {
-    SingleThread(&'a mut dyn SingleThreadReverseStep<Arch = A, Error = E>),
-    MultiThread(&'a mut dyn MultiThreadReverseStep<Arch = A, Error = E>),
-}
-
-impl<T: Target, C: Connection> GdbStubImpl<T, C> {
-    pub(crate) fn handle_reverse_cont(
-        &mut self,
-        res: &mut ResponseWriter<C>,
-        target: &mut T,
-        command: ReverseCont,
-    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
-        // Resolve the reverse-continue operations. Error out if the target does not
-        // support it.
-        let ops = match target.base_ops() {
-            BaseOps::MultiThread(ops) => match ops.support_reverse_cont() {
-                Some(ops) => ReverseContOps::MultiThread(ops),
-                None => return Ok(HandlerStatus::Handled),
-            },
-            BaseOps::SingleThread(ops) => match ops.support_reverse_cont() {
-                Some(ops) => ReverseContOps::SingleThread(ops),
-                None => return Ok(HandlerStatus::Handled),
-            },
-        };
-
-        crate::__dead_code_marker!("reverse_cont", "impl");
-
-        let handler_status = match command {
-            ReverseCont::bc(_) => {
-                // FIXME: This block is duplicated from the vCont code.
-                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 ops {
-                    ReverseContOps::MultiThread(ops) => ops
-                        .reverse_cont(GdbInterrupt::new(&mut check_gdb_interrupt))
-                        .map_err(Error::TargetError)?,
-                    ReverseContOps::SingleThread(ops) => ops
-                        .reverse_cont(GdbInterrupt::new(&mut check_gdb_interrupt))
-                        .map_err(Error::TargetError)?
-                        .into(),
-                };
-
-                err?;
-
-                // FIXME: properly handle None case
-                self.finish_exec(res, target, stop_reason)?
-                    .ok_or(Error::PacketUnexpected)?
-            }
-        };
-
-        Ok(handler_status)
-    }
-
-    // FIXME: De-duplicate with above code?
-    pub(crate) fn handle_reverse_step(
-        &mut self,
-        res: &mut ResponseWriter<C>,
-        target: &mut T,
-        command: ReverseStep,
-    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
-        // Resolve the reverse-step operations. Error out if the target does not
-        // support it.
-        let ops = match target.base_ops() {
-            BaseOps::MultiThread(ops) => match ops.support_reverse_step() {
-                Some(ops) => ReverseStepOps::MultiThread(ops),
-                None => return Ok(HandlerStatus::Handled),
-            },
-            BaseOps::SingleThread(ops) => match ops.support_reverse_step() {
-                Some(ops) => ReverseStepOps::SingleThread(ops),
-                None => return Ok(HandlerStatus::Handled),
-            },
-        };
-
-        crate::__dead_code_marker!("reverse_step", "impl");
-
-        let handler_status = match command {
-            ReverseStep::bs(_) => {
-                let tid = match self.current_resume_tid {
-                    // NOTE: Can't single-step all cores.
-                    SpecificIdKind::All => return Err(Error::PacketUnexpected),
-                    SpecificIdKind::WithId(tid) => tid,
-                };
-
-                // FIXME: This block is duplicated from the vCont code.
-                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 ops {
-                    ReverseStepOps::MultiThread(ops) => ops
-                        .reverse_step(tid, GdbInterrupt::new(&mut check_gdb_interrupt))
-                        .map_err(Error::TargetError)?,
-                    ReverseStepOps::SingleThread(ops) => ops
-                        .reverse_step(GdbInterrupt::new(&mut check_gdb_interrupt))
-                        .map_err(Error::TargetError)?
-                        .into(),
-                };
-
-                err?;
-
-                // FIXME: properly handle None case
-                self.finish_exec(res, target, stop_reason)?
-                    .ok_or(Error::PacketUnexpected)?
-            }
-        };
-
-        Ok(handler_status)
-    }
-}
diff --git a/src/gdbstub_impl/ext/single_register_access.rs b/src/gdbstub_impl/ext/single_register_access.rs
deleted file mode 100644
index 04ef9f2..0000000
--- a/src/gdbstub_impl/ext/single_register_access.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-use super::prelude::*;
-use crate::protocol::commands::ext::SingleRegisterAccess;
-
-use crate::arch::{Arch, RegId};
-use crate::target::ext::base::BaseOps;
-
-impl<T: Target, C: Connection> GdbStubImpl<T, C> {
-    fn inner<Id>(
-        res: &mut ResponseWriter<C>,
-        ops: crate::target::ext::base::SingleRegisterAccessOps<Id, T>,
-        command: SingleRegisterAccess<'_>,
-        id: Id,
-    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
-        let handler_status = match command {
-            SingleRegisterAccess::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 {
-                    // empty packet indicates unrecognized query
-                    None => return Ok(HandlerStatus::Handled),
-                    Some(v) => v,
-                };
-                let dst = &mut dst[0..reg_size];
-                ops.read_register(id, reg_id, dst).handle_error()?;
-
-                res.write_hex_buf(dst)?;
-                HandlerStatus::Handled
-            }
-            SingleRegisterAccess::P(p) => {
-                let reg = <T::Arch as Arch>::RegId::from_raw_id(p.reg_id);
-                match reg {
-                    // empty packet indicates unrecognized query
-                    None => return Ok(HandlerStatus::Handled),
-                    Some((reg_id, _)) => ops.write_register(id, reg_id, p.val).handle_error()?,
-                }
-                HandlerStatus::NeedsOk
-            }
-        };
-
-        Ok(handler_status)
-    }
-
-    pub(crate) fn handle_single_register_access<'a>(
-        &mut self,
-        res: &mut ResponseWriter<C>,
-        target: &mut T,
-        command: SingleRegisterAccess<'a>,
-    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
-        match target.base_ops() {
-            BaseOps::SingleThread(ops) => match ops.single_register_access() {
-                None => Ok(HandlerStatus::Handled),
-                Some(ops) => Self::inner(res, ops, command, ()),
-            },
-            BaseOps::MultiThread(ops) => match ops.single_register_access() {
-                None => Ok(HandlerStatus::Handled),
-                Some(ops) => Self::inner(res, ops, command, self.current_mem_tid),
-            },
-        }
-    }
-}
diff --git a/src/gdbstub_impl/mod.rs b/src/gdbstub_impl/mod.rs
deleted file mode 100644
index 364da0e..0000000
--- a/src/gdbstub_impl/mod.rs
+++ /dev/null
@@ -1,232 +0,0 @@
-use core::marker::PhantomData;
-
-use managed::ManagedSlice;
-
-use crate::common::*;
-use crate::connection::Connection;
-use crate::protocol::{commands::Command, Packet, ResponseWriter, SpecificIdKind};
-use crate::target::Target;
-use crate::util::managed_vec::ManagedVec;
-use crate::SINGLE_THREAD_TID;
-
-mod builder;
-mod error;
-mod ext;
-mod target_result_ext;
-
-pub use builder::{GdbStubBuilder, GdbStubBuilderError};
-pub use error::GdbStubError;
-
-use GdbStubError as Error;
-
-/// Describes why the GDB session ended.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum DisconnectReason {
-    /// Target exited with given status code
-    TargetExited(u8),
-    /// Target terminated with given signal
-    TargetTerminated(u8),
-    /// 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>,
-
-    current_mem_tid: Tid,
-    current_resume_tid: SpecificIdKind,
-    no_ack_mode: bool,
-}
-
-enum HandlerStatus {
-    Handled,
-    NeedsOk,
-    Disconnect(DisconnectReason),
-}
-
-impl<T: Target, C: Connection> GdbStubImpl<T, C> {
-    fn new() -> GdbStubImpl<T, C> {
-        GdbStubImpl {
-            _target: PhantomData,
-            _connection: PhantomData,
-
-            // NOTE: `current_mem_tid` and `current_resume_tid` are never queried prior to being set
-            // by the GDB client (via the 'H' packet), so it's fine to use dummy values here.
-            //
-            // The alternative would be to use `Option`, and while this would be more "correct", it
-            // would introduce a _lot_ of noisy and heavy error handling logic all over the place.
-            //
-            // Plus, even if the GDB client is acting strangely and doesn't overwrite these values,
-            // the target will simply return a non-fatal error, which is totally fine.
-            current_mem_tid: SINGLE_THREAD_TID,
-            current_resume_tid: SpecificIdKind::WithId(SINGLE_THREAD_TID),
-            no_ack_mode: false,
-        }
-    }
-
-    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)?;
-
-        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)?)?;
-        }
-
-        trace!(
-            "<-- {}",
-            core::str::from_utf8(buf.as_slice()).unwrap_or("<invalid packet>")
-        );
-
-        drop(buf);
-
-        Packet::from_buf(target, pkt_buf.as_mut()).map_err(Error::PacketParse)
-    }
-
-    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) => {
-                // cmd must be ASCII, as the slice originated from a PacketBuf, which checks for
-                // ASCII as part of the initial validation.
-                info!("Unknown command: {}", core::str::from_utf8(cmd).unwrap());
-                Ok(HandlerStatus::Handled)
-            }
-            // `handle_X` methods are defined in the `ext` module
-            Command::Base(cmd) => self.handle_base(res, target, cmd),
-            Command::SingleRegisterAccess(cmd) => {
-                self.handle_single_register_access(res, target, cmd)
-            }
-            Command::Breakpoints(cmd) => self.handle_breakpoints(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),
-            Command::ReverseCont(cmd) => self.handle_reverse_cont(res, target, cmd),
-            Command::ReverseStep(cmd) => self.handle_reverse_step(res, target, cmd),
-        }
-    }
-}
diff --git a/src/gdbstub_impl/target_result_ext.rs b/src/gdbstub_impl/target_result_ext.rs
deleted file mode 100644
index 386205b..0000000
--- a/src/gdbstub_impl/target_result_ext.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-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/mod.rs b/src/internal/mod.rs
index 5a7580a..c755fa3 100644
--- a/src/internal/mod.rs
+++ b/src/internal/mod.rs
@@ -1,10 +1,8 @@
-//! Types / traits which are not expected to be directly implemented by
-//! `gdbstub` users.
+//! Types / traits which are part of `gdbstub`'s public API, but don't need to
+//! be implemented by consumers of the library.
 
 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
index fc9ce17..065e7ce 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,20 +2,45 @@
 //! [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, both 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`.
+//! - `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` variant to simplify `std::io::Error` handling
+//!       from Target methods.
+//! - `paranoid_unsafe`
+//!     - Please refer to the [`unsafe` in `gdbstub`](https://github.com/daniel5151/gdbstub#unsafe-in-gdbstub)
+//!       section of the README.md for more details.
+//!
 //! ## 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.
 //!
-//! At a high level, there are only two things that are required to get up and
-//! running with `gdbstub`: a [`Connection`](#the-connection-trait), and a
-//! [`Target`](#the-target-trait)
+//! At a high level, there are only three things that are required to get up and
+//! running with `gdbstub`: a [`Connection`](#the-connection-trait), a
+//! [`Target`](#the-target-trait), and a [event loop](#the-event-loop).
 //!
 //! > _Note:_ I _highly recommended_ referencing some of the
-//! [examples](https://github.com/daniel5151/gdbstub/blob/master/README.md#examples)
-//! listed in the project README when integrating `gdbstub` into a project for
-//! the first time.
+//! [examples](https://github.com/daniel5151/gdbstub#examples) listed in the
+//! project README when integrating `gdbstub` into a project for the first time.
 //!
 //! > In particular, the in-tree
 //! [`armv4t`](https://github.com/daniel5151/gdbstub/tree/master/examples/armv4t)
@@ -27,7 +52,7 @@
 //!
 //! First things first: `gdbstub` needs some way to communicate with a GDB
 //! client. To facilitate this communication, `gdbstub` uses a custom
-//! [`Connection`] trait.
+//! [`Connection`](conn::Connection) trait.
 //!
 //! `Connection` is automatically implemented for common `std` types such as
 //! [`TcpStream`](std::net::TcpStream) and
@@ -78,11 +103,14 @@
 //! instructions on how to implement [`Target`](target::Target) for a particular
 //! platform.
 //!
-//! ### Starting the debugging session using `GdbStub`
+//! ## The Event Loop
 //!
-//! Once a [`Connection`](#the-connection-trait) has been established and
-//! [`Target`](#the-target-trait) has been all wired up, all that's left is to
-//! hand things off to [`gdbstub::GdbStub`](GdbStub) and let it do the rest!
+//! Once a [`Connection`](#the-connection-trait) has been established and the
+//! [`Target`](#the-target-trait) has been initialized, all that's left is to
+//! wire things up and decide what kind of event loop will be used to drive the
+//! debugging session!
+//!
+//! First things first, let's get an instance of `GdbStub` ready to run:
 //!
 //! ```rust,ignore
 //! // Set-up a valid `Target`
@@ -93,51 +121,205 @@
 //!
 //! // Create a new `gdbstub::GdbStub` using the established `Connection`.
 //! let mut debugger = gdbstub::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);
-//!         // `gdbstub` will not immediate close the debugging session if a
-//!         // fatal error occurs, enabling "post mortem" debugging if required.
-//!         debugger.run(&mut target)?;
-//!     }
-//!     Err(e) => return Err(e.into())
-//! }
 //! ```
 //!
-//! ## Feature flags
+//! Cool, but how do you actually start the debugging session?
+// use an explicit doc attribute to avoid automatic rustfmt wrapping
+#![doc = "### `GdbStub::run_blocking`: The quick and easy way to get up and running with `gdbstub`"]
 //!
-//! By default, both the `std` and `alloc` features are enabled.
+//! If you've got an extra thread to spare, the quickest way to get up and
+//! running with `gdbstub` is by using the
+//! [`GdbStub::run_blocking`](stub::run_blocking) API alongside the
+//! [`BlockingEventLoop`] trait.
 //!
-//! When using `gdbstub` in `#![no_std]` contexts, make sure to set
-//! `default-features = false`.
+//! If you are on a more resource constrained platform, and/or don't wish to
+//! dedicate an entire thread to `gdbstub`, feel free to skip ahead to the
+//! [following
+//! section](#gdbstubstatemachine-driving-gdbstub-in-an-async-event-loop--via-interrupt-handlers).
 //!
-//! - `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`.
-//! - `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.
+//! A basic integration of `gdbstub` into a project using the
+//! `GdbStub::run_blocking` API might look something like this:
+//!
+//! ```rust
+//! # use gdbstub::target::ext::base::BaseOps;
+//! #
+//! # struct MyTarget;
+//! #
+//! # impl Target for MyTarget {
+//! #     type Error = &'static str;
+//! #     type Arch = gdbstub_arch::arm::Armv4t; // as an example
+//! #     fn base_ops(&mut self) -> BaseOps<Self::Arch, Self::Error> { todo!() }
+//! # }
+//! #
+//! # impl MyTarget {
+//! #     fn run_and_check_for_incoming_data(
+//! #         &mut self,
+//! #         conn: &mut impl Connection
+//! #     ) -> MyTargetEvent { todo!() }
+//! #
+//! #     fn stop_in_response_to_ctrl_c_interrupt(
+//! #         &mut self
+//! #     ) -> Result<(), &'static str> { todo!() }
+//! # }
+//! #
+//! # enum MyTargetEvent {
+//! #     IncomingData,
+//! #     StopReason(SingleThreadStopReason<u32>),
+//! # }
+//! #
+//! use gdbstub::common::Signal;
+//! use gdbstub::conn::{Connection, ConnectionExt}; // note the use of `ConnectionExt`
+//! use gdbstub::stub::{run_blocking, DisconnectReason, GdbStub, GdbStubError};
+//! use gdbstub::stub::SingleThreadStopReason;
+//! use gdbstub::target::Target;
+//!
+//! enum MyGdbBlockingEventLoop {}
+//!
+//! // The `run_blocking::BlockingEventLoop` groups together various callbacks
+//! // the `GdbStub::run_blocking` event loop requires you to implement.
+//! impl run_blocking::BlockingEventLoop for MyGdbBlockingEventLoop {
+//!     type Target = MyTarget;
+//!     type Connection = Box<dyn ConnectionExt<Error = std::io::Error>>;
+//!
+//!     // or MultiThreadStopReason on multi threaded targets
+//!     type StopReason = SingleThreadStopReason<u32>;
+//!
+//!     // Invoked immediately after the target's `resume` method has been
+//!     // called. The implementation should block until either the target
+//!     // reports a stop reason, or if new data was sent over the connection.
+//!     fn wait_for_stop_reason(
+//!         target: &mut MyTarget,
+//!         conn: &mut Self::Connection,
+//!     ) -> Result<
+//!         run_blocking::Event<SingleThreadStopReason<u32>>,
+//!         run_blocking::WaitForStopReasonError<
+//!             <Self::Target as Target>::Error,
+//!             <Self::Connection as Connection>::Error,
+//!         >,
+//!     > {
+//!         // the specific mechanism to "select" between incoming data and target
+//!         // events will depend on your project's architecture.
+//!         //
+//!         // some examples of how you might implement this method include: `epoll`,
+//!         // `select!` across multiple event channels, periodic polling, etc...
+//!         //
+//!         // in this example, lets assume the target has a magic method that handles
+//!         // this for us.
+//!         let event = match target.run_and_check_for_incoming_data(conn) {
+//!             MyTargetEvent::IncomingData => {
+//!                 let byte = conn
+//!                     .read() // method provided by the `ConnectionExt` trait
+//!                     .map_err(run_blocking::WaitForStopReasonError::Connection)?;
+//!
+//!                 run_blocking::Event::IncomingData(byte)
+//!             }
+//!             MyTargetEvent::StopReason(reason) => {
+//!                 run_blocking::Event::TargetStopped(reason)
+//!             }
+//!         };
+//!
+//!         Ok(event)
+//!     }
+//!
+//!     // Invoked when the GDB client sends a Ctrl-C interrupt.
+//!     fn on_interrupt(
+//!         target: &mut MyTarget,
+//!     ) -> Result<Option<SingleThreadStopReason<u32>>, <MyTarget as Target>::Error> {
+//!         // notify the target that a ctrl-c interrupt has occurred.
+//!         target.stop_in_response_to_ctrl_c_interrupt()?;
+//!
+//!         // a pretty typical stop reason in response to a Ctrl-C interrupt is to
+//!         // report a "Signal::SIGINT".
+//!         Ok(Some(SingleThreadStopReason::Signal(Signal::SIGINT).into()))
+//!     }
+//! }
+//!
+//! fn gdb_event_loop_thread(
+//!     debugger: GdbStub<MyTarget, Box<dyn ConnectionExt<Error = std::io::Error>>>,
+//!     mut target: MyTarget
+//! ) {
+//!     match debugger.run_blocking::<MyGdbBlockingEventLoop>(&mut target) {
+//!         Ok(disconnect_reason) => match disconnect_reason {
+//!             DisconnectReason::Disconnect => {
+//!                 println!("Client disconnected")
+//!             }
+//!             DisconnectReason::TargetExited(code) => {
+//!                 println!("Target exited with code {}", code)
+//!             }
+//!             DisconnectReason::TargetTerminated(sig) => {
+//!                 println!("Target terminated with signal {}", sig)
+//!             }
+//!             DisconnectReason::Kill => println!("GDB sent a kill command"),
+//!         },
+//!         Err(GdbStubError::TargetError(e)) => {
+//!             println!("target encountered a fatal error: {}", e)
+//!         }
+//!         Err(e) => {
+//!             println!("gdbstub encountered a fatal error: {}", e)
+//!         }
+//!     }
+//! }
+//! ```
+// use an explicit doc attribute to avoid automatic rustfmt wrapping
+#![doc = "### `GdbStubStateMachine`: Driving `gdbstub` in an async event loop / via interrupt handlers"]
+//!
+//! `GdbStub::run_blocking` requires that the target implement the
+//! [`BlockingEventLoop`] trait, which as the name implies, uses _blocking_ IO
+//! when handling certain events. Blocking the thread is a totally reasonable
+//! approach in most implementations, as one can simply spin up a separate
+//! thread to run the GDB stub (or in certain emulator implementations, run the
+//! emulator as part of the `wait_for_stop_reason` method).
+//!
+//! Unfortunately, this blocking behavior can be a non-starter when integrating
+//! `gdbstub` in projects that don't support / wish to avoid the traditional
+//! thread-based execution model, such as projects using `async/await`, or
+//! bare-metal `no_std` projects running on embedded hardware.
+//!
+//! In these cases, `gdbstub` provides access to the underlying
+//! [`GdbStubStateMachine`] API, which gives implementations full control over
+//! the GDB stub's "event loop". This API requires implementations to "push"
+//! data to the `gdbstub` implementation whenever new data becomes available
+//! (e.g: when a UART interrupt handler receives a byte, when the target hits a
+//! breakpoint, etc...), as opposed to the `GdbStub::run_blocking` API, which
+//! "pulls" these events in a blocking manner.
+//!
+//! See the [`GdbStubStateMachine`] docs for more details on how to use this
+//! API.
+//!
+//! <br>
+//!
+//! * * *
+//!
+//! <br>
+//!
+//! And with that lengthy introduction, I wish you the best of luck in your
+//! debugging adventures!
+//!
+//! If you have any suggestions, feature requests, or run into any problems,
+//! please start a discussion / open an issue over on the
+//! [`gdbstub` GitHub repo](https://github.com/daniel5151/gdbstub/).
+//!
+//! [`GdbStubStateMachine`]: stub::state_machine::GdbStubStateMachine
+//! [`BlockingEventLoop`]: stub::run_blocking::BlockingEventLoop
 
 #![cfg_attr(not(feature = "std"), no_std)]
 #![deny(missing_docs)]
+#![deny(rust_2018_idioms, future_incompatible, nonstandard_style)]
+// Primarily due to rust-lang/rust#8995
+//
+// If this ever gets fixed, it's be possible to rewrite complex types using inherent associated type
+// aliases.
+//
+// For example, instead of writing this monstrosity:
+//
+// Result<Option<MultiThreadStopReason<<Self::Arch as Arch>::Usize>>, Self::Error>
+//
+// ...it could be rewritten as:
+//
+// type StopReason = MultiThreadStopReason<<Self::Arch as Arch>::Usize>>;
+//
+// Result<Option<StopReason>, Self::Error>
+#![allow(clippy::type_complexity)]
 
 #[cfg(feature = "alloc")]
 extern crate alloc;
@@ -145,8 +327,6 @@
 #[macro_use]
 extern crate log;
 
-mod connection;
-mod gdbstub_impl;
 mod protocol;
 mod util;
 
@@ -155,14 +335,20 @@
 
 pub mod arch;
 pub mod common;
+pub mod conn;
+pub mod stub;
 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) };
+
+pub(crate) mod is_valid_tid {
+    pub trait IsValidTid {}
+
+    impl IsValidTid for () {}
+    impl IsValidTid for crate::common::Tid {}
+}
diff --git a/src/protocol/commands.rs b/src/protocol/commands.rs
index 1fd1810..a0fcf80 100644
--- a/src/protocol/commands.rs
+++ b/src/protocol/commands.rs
@@ -3,16 +3,16 @@
 use crate::protocol::packet::PacketBuf;
 use crate::target::Target;
 
+/// Common imports used by >50% of all packet parsers.
+///
+/// Do not clutter this prelude with types only used by a few packets.
 pub(self) mod prelude {
-    pub use super::ParseCommand;
-    pub use crate::common::*;
-    pub use crate::protocol::common::hex::{decode_hex, decode_hex_buf, is_hex, HexString};
-    pub use crate::protocol::common::thread_id::{
-        IdKind, SpecificIdKind, SpecificThreadId, ThreadId,
-    };
-    pub use crate::protocol::common::Bstr;
-    pub use crate::protocol::packet::PacketBuf;
     pub use core::convert::{TryFrom, TryInto};
+
+    pub use crate::protocol::commands::ParseCommand;
+    pub use crate::protocol::common::hex::{decode_hex, decode_hex_buf};
+    pub use crate::protocol::packet::PacketBuf;
+    pub use crate::util::no_panic_iter::SliceExt;
 }
 
 pub trait ParseCommand<'a>: Sized {
@@ -20,13 +20,6 @@
     fn from_packet(buf: PacketBuf<'a>) -> Option<Self>;
 }
 
-// Breakpoint packets are special-cased, as the "Z" packet is parsed differently
-// depending on whether or not the target implements the `Agent` extension.
-//
-// While it's entirely possible to eagerly parse the "Z" packet for bytecode,
-// doing so would unnecessary bloat implementations that do not support
-// evaluating agent expressions.
-
 macro_rules! commands {
     (
         $(
@@ -35,6 +28,21 @@
             }
         )*
     ) => {paste! {
+        // Most packets follow a consistent model of "only enabled when a
+        // particular IDET is implemented", but there are some exceptions to
+        // this rule that need to be special-cased:
+        //
+        // # Breakpoint packets (z, Z)
+        //
+        // Breakpoint packets are special-cased, as the "Z" packet is parsed
+        // differently depending on whether or not the target implements the
+        // `Agent` extension.
+        //
+        // While it's entirely possible to eagerly parse the "Z" packet for
+        // bytecode, doing so would unnecessary bloat implementations that do
+        // not support evaluating agent expressions.
+
+
         $($(
             #[allow(non_snake_case, non_camel_case_types)]
             pub mod $mod;
@@ -77,38 +85,65 @@
                 // that aren't top-level `Target` IDETs to split-up the packet
                 // parsing code.
                 trait Hack {
-                    fn base(&mut self) -> Option<()>;
-                    fn single_register_access(&mut self) -> Option<()>;
-                    fn reverse_step(&mut self) -> Option<()>;
-                    fn reverse_cont(&mut self) -> Option<()>;
+                    fn support_base(&mut self) -> Option<()>;
+                    fn support_target_xml(&mut self) -> Option<()>;
+                    fn support_resume(&mut self) -> Option<()>;
+                    fn support_single_register_access(&mut self) -> Option<()>;
+                    fn support_reverse_step(&mut self) -> Option<()>;
+                    fn support_reverse_cont(&mut self) -> Option<()>;
+                    fn support_x_upcase_packet(&mut self) -> Option<()>;
                 }
 
                 impl<T: Target> Hack for T {
-                    fn base(&mut self) -> Option<()> {
+                    fn support_base(&mut self) -> Option<()> {
                         Some(())
                     }
 
-                    fn single_register_access(&mut self) -> Option<()> {
-                        use crate::target::ext::base::BaseOps;
-                        match self.base_ops() {
-                            BaseOps::SingleThread(ops) => ops.single_register_access().map(drop),
-                            BaseOps::MultiThread(ops) => ops.single_register_access().map(drop),
+                    fn support_target_xml(&mut self) -> Option<()> {
+                        use crate::arch::Arch;
+                        if self.use_target_description_xml()
+                            && (T::Arch::target_description_xml().is_some()
+                                || self.support_target_description_xml_override().is_some())
+                        {
+                            Some(())
+                        } else {
+                            None
                         }
                     }
 
-                    fn reverse_step(&mut self) -> Option<()> {
+                    fn support_resume(&mut self) -> Option<()> {
+                        self.base_ops().resume_ops().map(drop)
+                    }
+
+                    fn support_single_register_access(&mut self) -> Option<()> {
                         use crate::target::ext::base::BaseOps;
                         match self.base_ops() {
-                            BaseOps::SingleThread(ops) => ops.support_reverse_step().map(drop),
-                            BaseOps::MultiThread(ops) => ops.support_reverse_step().map(drop),
+                            BaseOps::SingleThread(ops) => ops.support_single_register_access().map(drop),
+                            BaseOps::MultiThread(ops) => ops.support_single_register_access().map(drop),
                         }
                     }
 
-                    fn reverse_cont(&mut self) -> Option<()> {
-                        use crate::target::ext::base::BaseOps;
-                        match self.base_ops() {
-                            BaseOps::SingleThread(ops) => ops.support_reverse_cont().map(drop),
-                            BaseOps::MultiThread(ops) => ops.support_reverse_cont().map(drop),
+                    fn support_reverse_step(&mut self) -> Option<()> {
+                        use crate::target::ext::base::ResumeOps;
+                        match self.base_ops().resume_ops()? {
+                            ResumeOps::SingleThread(ops) => ops.support_reverse_step().map(drop),
+                            ResumeOps::MultiThread(ops) => ops.support_reverse_step().map(drop),
+                        }
+                    }
+
+                    fn support_reverse_cont(&mut self) -> Option<()> {
+                        use crate::target::ext::base::ResumeOps;
+                        match self.base_ops().resume_ops()? {
+                            ResumeOps::SingleThread(ops) => ops.support_reverse_cont().map(drop),
+                            ResumeOps::MultiThread(ops) => ops.support_reverse_cont().map(drop),
+                        }
+                    }
+
+                    fn support_x_upcase_packet(&mut self) -> Option<()> {
+                        if self.use_x_upcase_packet() {
+                            Some(())
+                        } else {
+                            None
                         }
                     }
                 }
@@ -117,7 +152,7 @@
 
                 $(
                 #[allow(clippy::string_lit_as_bytes)]
-                if target.$ext().is_some() {
+                if target.[< support_ $ext >]().is_some() {
                     $(
                     if buf.strip_prefix($name.as_bytes()) {
                         crate::__dead_code_marker!($name, "prefix_match");
@@ -134,7 +169,7 @@
                 }
                 )*
 
-                if let Some(_breakpoint_ops) = target.breakpoints() {
+                if let Some(_breakpoint_ops) = target.support_breakpoints() {
                     use breakpoint::{BasicBreakpoint, BytecodeBreakpoint};
 
                     if buf.strip_prefix(b"z") {
@@ -145,7 +180,7 @@
                     if buf.strip_prefix(b"Z") {
                         // TODO: agent bytecode currently unimplemented
                         if true {
-                           let cmd = BasicBreakpoint::from_slice(buf.into_body())?;
+                            let cmd = BasicBreakpoint::from_slice(buf.into_body())?;
                             return Some(Command::Breakpoints(ext::Breakpoints::Z(cmd)))
                         } else {
                             let cmd = BytecodeBreakpoint::from_slice(buf.into_body())?;
@@ -163,7 +198,6 @@
 commands! {
     base use 'a {
         "?" => question_mark::QuestionMark,
-        "c" => _c::c<'a>,
         "D" => _d_upcase::D,
         "g" => _g::g,
         "G" => _g_upcase::G<'a>,
@@ -176,15 +210,26 @@
         "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,
     }
 
+    target_xml use 'a {
+        "qXfer:features:read" => _qXfer_features_read::qXferFeaturesRead<'a>,
+    }
+
+    resume use 'a {
+        "c" => _c::c<'a>,
+        "s" => _s::s<'a>,
+        "vCont" => _vCont::vCont<'a>,
+    }
+
+    x_upcase_packet use 'a {
+        "X" => _x_upcase::X<'a>,
+    }
+
     single_register_access use 'a {
-        "p" => _p::p,
+        "p" => _p::p<'a>,
         "P" => _p_upcase::P<'a>,
     }
 
@@ -216,4 +261,31 @@
     reverse_step {
         "bs" => _bs::bs,
     }
+
+    memory_map use 'a {
+        "qXfer:memory-map:read" => _qXfer_memory_map::qXferMemoryMapRead<'a>,
+    }
+
+    auxv use 'a {
+        "qXfer:auxv:read" => _qXfer_auxv_read::qXferAuxvRead<'a>,
+    }
+
+    exec_file use 'a {
+        "qXfer:exec-file:read" => _qXfer_exec_file::qXferExecFileRead<'a>,
+    }
+
+    host_io use 'a {
+        "vFile:open" => _vFile_open::vFileOpen<'a>,
+        "vFile:close" => _vFile_close::vFileClose,
+        "vFile:pread" => _vFile_pread::vFilePread<'a>,
+        "vFile:pwrite" => _vFile_pwrite::vFilePwrite<'a>,
+        "vFile:fstat" => _vFile_fstat::vFileFstat,
+        "vFile:unlink" => _vFile_unlink::vFileUnlink<'a>,
+        "vFile:readlink" => _vFile_readlink::vFileReadlink<'a>,
+        "vFile:setfs" => _vFile_setfs::vFileSetfs,
+    }
+
+    catch_syscalls use 'a {
+        "QCatchSyscalls" => _QCatchSyscalls::QCatchSyscalls<'a>,
+    }
 }
diff --git a/src/protocol/commands/_QCatchSyscalls.rs b/src/protocol/commands/_QCatchSyscalls.rs
new file mode 100644
index 0000000..873f39a
--- /dev/null
+++ b/src/protocol/commands/_QCatchSyscalls.rs
@@ -0,0 +1,25 @@
+use super::prelude::*;
+
+use crate::protocol::common::lists::ArgListHex;
+
+#[derive(Debug)]
+pub enum QCatchSyscalls<'a> {
+    Disable,
+    Enable(ArgListHex<'a>),
+    EnableAll,
+}
+
+impl<'a> ParseCommand<'a> for QCatchSyscalls<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+
+        match body {
+            [b':', b'0'] => Some(QCatchSyscalls::Disable),
+            [b':', b'1', b';', sysno @ ..] => {
+                Some(QCatchSyscalls::Enable(ArgListHex::from_packet(sysno)?))
+            }
+            [b':', b'1'] => Some(QCatchSyscalls::EnableAll),
+            _ => None,
+        }
+    }
+}
diff --git a/src/protocol/commands/_d_upcase.rs b/src/protocol/commands/_d_upcase.rs
index 8f047b6..5003f4e 100644
--- a/src/protocol/commands/_d_upcase.rs
+++ b/src/protocol/commands/_d_upcase.rs
@@ -1,5 +1,7 @@
 use super::prelude::*;
 
+use crate::common::Pid;
+
 #[derive(Debug)]
 pub struct D {
     pub pid: Option<Pid>,
diff --git a/src/protocol/commands/_h_upcase.rs b/src/protocol/commands/_h_upcase.rs
index 101d0d0..0fdb3ac 100644
--- a/src/protocol/commands/_h_upcase.rs
+++ b/src/protocol/commands/_h_upcase.rs
@@ -1,5 +1,7 @@
 use super::prelude::*;
 
+use crate::protocol::common::thread_id::ThreadId;
+
 #[derive(Debug)]
 pub enum Op {
     StepContinue,
diff --git a/src/protocol/commands/_m.rs b/src/protocol/commands/_m.rs
index f7570ad..4b879f4 100644
--- a/src/protocol/commands/_m.rs
+++ b/src/protocol/commands/_m.rs
@@ -29,11 +29,9 @@
         // +------+------------------+------------------------------------------------+
 
         let (buf, body_range) = buf.into_raw_buf();
-        let body = &mut buf[body_range.start..];
+        let body = buf.get_mut(body_range.start..body_range.end)?;
 
-        // 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 mut body = body.split_mut_no_panic(|b| *b == b',');
 
         let addr = decode_hex_buf(body.next()?).ok()?;
         let addr_len = addr.len();
@@ -41,8 +39,13 @@
 
         drop(body);
 
+        // ensures that `split_at_mut` doesn't panic
+        if buf.len() < body_range.start + addr_len {
+            return None;
+        }
+
         let (addr, buf) = buf.split_at_mut(body_range.start + addr_len);
-        let addr = &addr[b"$m".len()..];
+        let addr = addr.get(b"$m".len()..)?;
 
         Some(m { addr, len, buf })
     }
diff --git a/src/protocol/commands/_m_upcase.rs b/src/protocol/commands/_m_upcase.rs
index 49773aa..0e4f9a6 100644
--- a/src/protocol/commands/_m_upcase.rs
+++ b/src/protocol/commands/_m_upcase.rs
@@ -11,15 +11,11 @@
     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 mut body = body.split_mut_no_panic(|&b| b == b',' || b == b':');
         let addr = decode_hex_buf(body.next()?).ok()?;
         let len = decode_hex(body.next()?).ok()?;
-        let val = body.next()?;
+        let val = decode_hex_buf(body.next()?).ok()?;
 
-        Some(M {
-            addr,
-            len,
-            val: decode_hex_buf(val).ok()?,
-        })
+        Some(M { addr, len, val })
     }
 }
diff --git a/src/protocol/commands/_p.rs b/src/protocol/commands/_p.rs
index 08e13b5..6405622 100644
--- a/src/protocol/commands/_p.rs
+++ b/src/protocol/commands/_p.rs
@@ -1,13 +1,23 @@
 use super::prelude::*;
 
 #[derive(Debug)]
-pub struct p {
+pub struct p<'a> {
     pub reg_id: usize,
+
+    pub buf: &'a mut [u8],
 }
 
-impl<'a> ParseCommand<'a> for p {
+impl<'a> ParseCommand<'a> for p<'a> {
     fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
-        let reg_id = decode_hex(buf.into_body()).ok()?;
-        Some(p { reg_id })
+        let (buf, body_range) = buf.into_raw_buf();
+        let body = buf.get(body_range.start..body_range.end)?;
+
+        if body.is_empty() {
+            return None;
+        }
+
+        let reg_id = decode_hex(body).ok()?;
+
+        Some(p { reg_id, buf })
     }
 }
diff --git a/src/protocol/commands/_qAttached.rs b/src/protocol/commands/_qAttached.rs
index 01edf37..a5429aa 100644
--- a/src/protocol/commands/_qAttached.rs
+++ b/src/protocol/commands/_qAttached.rs
@@ -1,5 +1,7 @@
 use super::prelude::*;
 
+use crate::common::Pid;
+
 #[derive(Debug)]
 pub struct qAttached {
     pub pid: Option<Pid>,
diff --git a/src/protocol/commands/_qSupported.rs b/src/protocol/commands/_qSupported.rs
index 97e8ff2..368566d 100644
--- a/src/protocol/commands/_qSupported.rs
+++ b/src/protocol/commands/_qSupported.rs
@@ -9,16 +9,14 @@
 impl<'a> ParseCommand<'a> for qSupported<'a> {
     fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
         let packet_buffer_len = buf.full_len();
-
         let body = buf.into_body();
-        if body.is_empty() {
-            return None;
+        match body {
+            [b':', body @ ..] => Some(qSupported {
+                packet_buffer_len,
+                features: Features(body),
+            }),
+            _ => None,
         }
-
-        Some(qSupported {
-            packet_buffer_len,
-            features: Features(body),
-        })
     }
 }
 
@@ -26,41 +24,30 @@
 pub struct Features<'a>(&'a [u8]);
 
 impl<'a> Features<'a> {
-    pub fn into_iter(self) -> impl Iterator<Item = Option<Feature<'a>>> + 'a {
+    pub fn into_iter(self) -> impl Iterator<Item = Result<Option<(Feature, bool)>, ()>> + '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,
-                })
-            }
+            None => Err(()),
+            Some(&c) => match c {
+                b'+' | b'-' => {
+                    let feature = match &s[..s.len() - 1] {
+                        b"multiprocess" => Feature::Multiprocess,
+                        // TODO: implementing other features will require IDET plumbing
+                        _ => return Ok(None),
+                    };
+                    Ok(Some((feature, c == b'+')))
+                }
+                _ => {
+                    // TODO: add support for "xmlRegisters="
+                    // that's the only feature packet that uses an '=', and AFAIK, it's not really
+                    // used anymore...
+                    Ok(None)
+                }
+            },
         })
     }
 }
 
 #[derive(Debug)]
-pub enum FeatureSupported {
-    Yes,
-    No,
-    Maybe,
-}
-
-#[derive(Debug)]
-pub struct Feature<'a> {
-    name: Bstr<'a>,
-    val: Option<Bstr<'a>>,
-    status: FeatureSupported,
+pub enum Feature {
+    Multiprocess,
 }
diff --git a/src/protocol/commands/_qXfer_auxv_read.rs b/src/protocol/commands/_qXfer_auxv_read.rs
new file mode 100644
index 0000000..1ed4967
--- /dev/null
+++ b/src/protocol/commands/_qXfer_auxv_read.rs
@@ -0,0 +1,18 @@
+// use super::prelude::*; // unused
+
+use crate::protocol::common::qxfer::{ParseAnnex, QXferReadBase};
+
+pub type qXferAuxvRead<'a> = QXferReadBase<'a, AuxvAnnex>;
+
+#[derive(Debug)]
+pub struct AuxvAnnex;
+
+impl<'a> ParseAnnex<'a> for AuxvAnnex {
+    fn from_buf(buf: &[u8]) -> Option<Self> {
+        if buf != b"" {
+            return None;
+        }
+
+        Some(AuxvAnnex)
+    }
+}
diff --git a/src/protocol/commands/_qXfer_exec_file.rs b/src/protocol/commands/_qXfer_exec_file.rs
new file mode 100644
index 0000000..df1aa1d
--- /dev/null
+++ b/src/protocol/commands/_qXfer_exec_file.rs
@@ -0,0 +1,22 @@
+use super::prelude::*;
+
+use crate::common::Pid;
+use crate::protocol::common::qxfer::{ParseAnnex, QXferReadBase};
+
+pub type qXferExecFileRead<'a> = QXferReadBase<'a, ExecFileAnnex>;
+
+#[derive(Debug)]
+pub struct ExecFileAnnex {
+    pub pid: Option<Pid>,
+}
+
+impl<'a> ParseAnnex<'a> for ExecFileAnnex {
+    fn from_buf(buf: &[u8]) -> Option<Self> {
+        let pid = match buf {
+            [] => None,
+            buf => Some(Pid::new(decode_hex(buf).ok()?)?),
+        };
+
+        Some(ExecFileAnnex { pid })
+    }
+}
diff --git a/src/protocol/commands/_qXfer_features_read.rs b/src/protocol/commands/_qXfer_features_read.rs
index 6e6d377..8fe9650 100644
--- a/src/protocol/commands/_qXfer_features_read.rs
+++ b/src/protocol/commands/_qXfer_features_read.rs
@@ -1,29 +1,16 @@
-use super::prelude::*;
+// use super::prelude::*; // unused
+
+use crate::protocol::common::qxfer::{ParseAnnex, QXferReadBase};
+
+pub type qXferFeaturesRead<'a> = QXferReadBase<'a, FeaturesAnnex<'a>>;
 
 #[derive(Debug)]
-pub struct qXferFeaturesRead {
-    pub offset: usize,
-    pub len: usize,
+pub struct FeaturesAnnex<'a> {
+    pub name: &'a [u8],
 }
 
-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 })
+impl<'a> ParseAnnex<'a> for FeaturesAnnex<'a> {
+    fn from_buf(buf: &'a [u8]) -> Option<Self> {
+        Some(FeaturesAnnex { name: buf })
     }
 }
diff --git a/src/protocol/commands/_qXfer_memory_map.rs b/src/protocol/commands/_qXfer_memory_map.rs
new file mode 100644
index 0000000..01aa21a
--- /dev/null
+++ b/src/protocol/commands/_qXfer_memory_map.rs
@@ -0,0 +1,18 @@
+// use super::prelude::*; // unused
+
+use crate::protocol::common::qxfer::{ParseAnnex, QXferReadBase};
+
+pub type qXferMemoryMapRead<'a> = QXferReadBase<'a, MemoryMapAnnex>;
+
+#[derive(Debug)]
+pub struct MemoryMapAnnex;
+
+impl<'a> ParseAnnex<'a> for MemoryMapAnnex {
+    fn from_buf(buf: &[u8]) -> Option<Self> {
+        if buf != b"" {
+            return None;
+        }
+
+        Some(MemoryMapAnnex)
+    }
+}
diff --git a/src/protocol/commands/_t_upcase.rs b/src/protocol/commands/_t_upcase.rs
index c7b48ef..7ae257c 100644
--- a/src/protocol/commands/_t_upcase.rs
+++ b/src/protocol/commands/_t_upcase.rs
@@ -1,5 +1,7 @@
 use super::prelude::*;
 
+use crate::protocol::common::thread_id::ThreadId;
+
 #[derive(Debug)]
 pub struct T {
     pub thread: ThreadId,
diff --git a/src/protocol/commands/_vAttach.rs b/src/protocol/commands/_vAttach.rs
index 6329552..81bc924 100644
--- a/src/protocol/commands/_vAttach.rs
+++ b/src/protocol/commands/_vAttach.rs
@@ -1,5 +1,7 @@
 use super::prelude::*;
 
+use crate::common::Pid;
+
 #[derive(Debug)]
 pub struct vAttach {
     pub pid: Pid,
diff --git a/src/protocol/commands/_vCont.rs b/src/protocol/commands/_vCont.rs
index 932c7db..0a8d3c0 100644
--- a/src/protocol/commands/_vCont.rs
+++ b/src/protocol/commands/_vCont.rs
@@ -1,9 +1,13 @@
 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.
+use crate::common::Signal;
+use crate::protocol::common::hex::HexString;
+use crate::protocol::common::thread_id::{SpecificThreadId, ThreadId};
+
+// TODO?: instead of lazily parsing data, 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.
 //
 // The interesting part would be to see whether or not the simplified error
 // handing code will compensate for all the new code required to pre-validate
@@ -89,23 +93,23 @@
 #[derive(Debug, Copy, Clone)]
 pub enum VContKind<'a> {
     Continue,
-    ContinueWithSig(u8),
+    ContinueWithSig(Signal),
     RangeStep(HexString<'a>, HexString<'a>),
     Step,
-    StepWithSig(u8),
+    StepWithSig(Signal),
     Stop,
 }
 
 impl<'a> VContKind<'a> {
-    fn from_bytes(s: &[u8]) -> Option<VContKind> {
+    fn from_bytes(s: &[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'C', sig @ ..] => ContinueWithSig(Signal::from_protocol_u8(decode_hex(sig).ok()?)),
+            [b'S', sig @ ..] => StepWithSig(Signal::from_protocol_u8(decode_hex(sig).ok()?)),
             [b'r', range @ ..] => {
                 let mut range = range.split(|b| *b == b',');
                 RangeStep(HexString(range.next()?), HexString(range.next()?))
diff --git a/src/protocol/commands/_vFile_close.rs b/src/protocol/commands/_vFile_close.rs
new file mode 100644
index 0000000..5bddfd2
--- /dev/null
+++ b/src/protocol/commands/_vFile_close.rs
@@ -0,0 +1,23 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct vFileClose {
+    pub fd: u32,
+}
+
+impl<'a> ParseCommand<'a> for vFileClose {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        if body.is_empty() {
+            return None;
+        }
+
+        match body {
+            [b':', body @ ..] => {
+                let fd = decode_hex(body).ok()?;
+                Some(vFileClose { fd })
+            },
+            _ => None,
+        }
+    }
+}
diff --git a/src/protocol/commands/_vFile_fstat.rs b/src/protocol/commands/_vFile_fstat.rs
new file mode 100644
index 0000000..63e5bbd
--- /dev/null
+++ b/src/protocol/commands/_vFile_fstat.rs
@@ -0,0 +1,23 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct vFileFstat {
+    pub fd: u32,
+}
+
+impl<'a> ParseCommand<'a> for vFileFstat {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        if body.is_empty() {
+            return None;
+        }
+
+        match body {
+            [b':', body @ ..] => {
+                let fd = decode_hex(body).ok()?;
+                Some(vFileFstat { fd })
+            },
+            _ => None,
+        }
+    }
+}
diff --git a/src/protocol/commands/_vFile_open.rs b/src/protocol/commands/_vFile_open.rs
new file mode 100644
index 0000000..290772d
--- /dev/null
+++ b/src/protocol/commands/_vFile_open.rs
@@ -0,0 +1,30 @@
+use super::prelude::*;
+
+use crate::target::ext::host_io::{HostIoOpenFlags, HostIoOpenMode};
+
+#[derive(Debug)]
+pub struct vFileOpen<'a> {
+    pub filename: &'a [u8],
+    pub flags: HostIoOpenFlags,
+    pub mode: HostIoOpenMode,
+}
+
+impl<'a> ParseCommand<'a> for vFileOpen<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        if body.is_empty() {
+            return None;
+        }
+
+        match body {
+            [b':', body @ ..] => {
+                let mut body = body.splitn_mut_no_panic(3, |b| *b == b',');
+                let filename = decode_hex_buf(body.next()?).ok()?;
+                let flags = HostIoOpenFlags::from_bits(decode_hex(body.next()?).ok()?)?;
+                let mode = HostIoOpenMode::from_bits(decode_hex(body.next()?).ok()?)?;
+                Some(vFileOpen { filename, flags, mode })
+            },
+            _ => None,
+        }
+    }
+}
diff --git a/src/protocol/commands/_vFile_pread.rs b/src/protocol/commands/_vFile_pread.rs
new file mode 100644
index 0000000..e8fc743
--- /dev/null
+++ b/src/protocol/commands/_vFile_pread.rs
@@ -0,0 +1,35 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct vFilePread<'a> {
+    pub fd: u32,
+    pub count: usize,
+    pub offset: u64,
+
+    pub buf: &'a mut [u8],
+}
+
+impl<'a> ParseCommand<'a> for vFilePread<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let (buf, body_range) = buf.into_raw_buf();
+        let body = buf.get_mut(body_range.start..body_range.end)?;
+
+        if body.is_empty() {
+            return None;
+        }
+
+        match body {
+            [b':', body @ ..] => {
+                let mut body = body.splitn_mut_no_panic(3, |b| *b == b',');
+                let fd = decode_hex(body.next()?).ok()?;
+                let count = decode_hex(body.next()?).ok()?;
+                let offset = decode_hex(body.next()?).ok()?;
+
+                drop(body);
+
+                Some(vFilePread { fd, count, offset, buf })
+            },
+            _ => None,
+        }
+    }
+}
diff --git a/src/protocol/commands/_vFile_pwrite.rs b/src/protocol/commands/_vFile_pwrite.rs
new file mode 100644
index 0000000..62f2ea9
--- /dev/null
+++ b/src/protocol/commands/_vFile_pwrite.rs
@@ -0,0 +1,30 @@
+use super::prelude::*;
+
+use crate::protocol::common::hex::decode_bin_buf;
+
+#[derive(Debug)]
+pub struct vFilePwrite<'a> {
+    pub fd: u32,
+    pub offset: &'a [u8],
+    pub data: &'a [u8],
+}
+
+impl<'a> ParseCommand<'a> for vFilePwrite<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        if body.is_empty() {
+            return None;
+        }
+
+        match body {
+            [b':', body @ ..] => {
+                let mut body = body.splitn_mut_no_panic(3, |b| *b == b',');
+                let fd = decode_hex(body.next()?).ok()?;
+                let offset = decode_hex_buf(body.next()?).ok()?;
+                let data = decode_bin_buf(body.next()?).ok()?;
+                Some(vFilePwrite { fd, offset, data })
+            }
+            _ => None,
+        }
+    }
+}
diff --git a/src/protocol/commands/_vFile_readlink.rs b/src/protocol/commands/_vFile_readlink.rs
new file mode 100644
index 0000000..9a0ae76
--- /dev/null
+++ b/src/protocol/commands/_vFile_readlink.rs
@@ -0,0 +1,28 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct vFileReadlink<'a> {
+    pub filename: &'a [u8],
+
+    pub buf: &'a mut [u8],
+}
+
+impl<'a> ParseCommand<'a> for vFileReadlink<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let (buf, body_range) = buf.into_raw_buf();
+        // TODO: rewrite to avoid panic
+        let (body, buf) = buf[body_range.start..].split_at_mut(body_range.end - body_range.start);
+
+        if body.is_empty() {
+            return None;
+        }
+
+        match body {
+            [b':', body @ ..] => {
+                let filename = decode_hex_buf(body).ok()?;
+                Some(vFileReadlink { filename, buf })
+            },
+            _ => None,
+        }
+    }
+}
diff --git a/src/protocol/commands/_vFile_setfs.rs b/src/protocol/commands/_vFile_setfs.rs
new file mode 100644
index 0000000..c027d6e
--- /dev/null
+++ b/src/protocol/commands/_vFile_setfs.rs
@@ -0,0 +1,28 @@
+use super::prelude::*;
+
+use crate::target::ext::host_io::FsKind;
+
+#[derive(Debug)]
+pub struct vFileSetfs {
+    pub fs: FsKind,
+}
+
+impl<'a> ParseCommand<'a> for vFileSetfs {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        if body.is_empty() {
+            return None;
+        }
+
+        match body {
+            [b':', body @ ..] => {
+                let fs = match crate::common::Pid::new(decode_hex(body).ok()?) {
+                    None => FsKind::Stub,
+                    Some(pid) => FsKind::Pid(pid),
+                };
+                Some(vFileSetfs { fs })
+            },
+            _ => None,
+        }
+    }
+}
diff --git a/src/protocol/commands/_vFile_unlink.rs b/src/protocol/commands/_vFile_unlink.rs
new file mode 100644
index 0000000..2a5a4e6
--- /dev/null
+++ b/src/protocol/commands/_vFile_unlink.rs
@@ -0,0 +1,23 @@
+use super::prelude::*;
+
+#[derive(Debug)]
+pub struct vFileUnlink<'a> {
+    pub filename: &'a [u8],
+}
+
+impl<'a> ParseCommand<'a> for vFileUnlink<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+        if body.is_empty() {
+            return None;
+        }
+
+        match body {
+            [b':', body @ ..] => {
+                let filename = decode_hex_buf(body).ok()?;
+                Some(vFileUnlink { filename })
+            },
+            _ => None,
+        }
+    }
+}
diff --git a/src/protocol/commands/_vKill.rs b/src/protocol/commands/_vKill.rs
index 9378aad..c7cf462 100644
--- a/src/protocol/commands/_vKill.rs
+++ b/src/protocol/commands/_vKill.rs
@@ -1,5 +1,7 @@
 use super::prelude::*;
 
+use crate::common::Pid;
+
 #[derive(Debug)]
 pub struct vKill {
     pub pid: Pid,
diff --git a/src/protocol/commands/_vRun.rs b/src/protocol/commands/_vRun.rs
index 2f1db1b..4683dc4 100644
--- a/src/protocol/commands/_vRun.rs
+++ b/src/protocol/commands/_vRun.rs
@@ -1,31 +1,18 @@
 use super::prelude::*;
 
+use crate::protocol::common::lists::ArgListHex;
+
 #[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())
-    }
+    pub args: ArgListHex<'a>,
 }
 
 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 mut body = body.splitn_mut_no_panic(3, |b| *b == b';');
 
         let _first_semi = body.next()?;
         let filename = match decode_hex_buf(body.next()?).ok()? {
@@ -34,15 +21,9 @@
         };
         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),
+            args: ArgListHex::from_packet(args)?,
         })
     }
 }
diff --git a/src/protocol/commands/_x_upcase.rs b/src/protocol/commands/_x_upcase.rs
new file mode 100644
index 0000000..2be4c8e
--- /dev/null
+++ b/src/protocol/commands/_x_upcase.rs
@@ -0,0 +1,23 @@
+use super::prelude::*;
+
+use crate::protocol::common::hex::decode_bin_buf;
+
+#[derive(Debug)]
+pub struct X<'a> {
+    pub addr: &'a [u8],
+    pub len: usize,
+    pub val: &'a [u8],
+}
+
+impl<'a> ParseCommand<'a> for X<'a> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let body = buf.into_body();
+
+        let mut body = body.split_mut_no_panic(|&b| b == b',' || b == b':');
+        let addr = decode_hex_buf(body.next()?).ok()?;
+        let len = decode_hex(body.next()?).ok()?;
+        let val = decode_bin_buf(body.next()?).ok()?;
+
+        Some(X { addr, len, val })
+    }
+}
diff --git a/src/protocol/commands/breakpoint.rs b/src/protocol/commands/breakpoint.rs
index 101d69e..46b9bb6 100644
--- a/src/protocol/commands/breakpoint.rs
+++ b/src/protocol/commands/breakpoint.rs
@@ -1,7 +1,5 @@
-pub use crate::common::*;
-pub use crate::protocol::common::hex::{decode_hex, decode_hex_buf};
-pub use crate::protocol::packet::PacketBuf;
-pub use core::convert::{TryFrom, TryInto};
+use crate::protocol::common::hex::{decode_hex, decode_hex_buf};
+use crate::util::no_panic_iter::SliceExt;
 
 // Breakpoint packets are split up like this:
 //
@@ -21,15 +19,15 @@
     pub type_: u8,
     pub addr: &'a [u8],
     /// architecture dependent
-    pub kind: usize,
+    pub kind: &'a [u8],
 }
 
 impl<'a> BasicBreakpoint<'a> {
     pub fn from_slice(body: &'a mut [u8]) -> Option<BasicBreakpoint<'a>> {
-        let mut body = body.splitn_mut(4, |b| matches!(*b, b',' | b';'));
+        let mut body = body.splitn_mut_no_panic(4, |b| matches!(*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()?;
+        let kind = decode_hex_buf(body.next()?).ok()?;
 
         Some(BasicBreakpoint { type_, addr, kind })
     }
@@ -44,7 +42,7 @@
 
 impl<'a> BytecodeBreakpoint<'a> {
     pub fn from_slice(body: &'a mut [u8]) -> Option<BytecodeBreakpoint<'a>> {
-        let mut body = body.splitn_mut(2, |b| *b == b';');
+        let mut body = body.splitn_mut_no_panic(2, |b| *b == b';');
 
         let base = BasicBreakpoint::from_slice(body.next()?)?;
 
diff --git a/src/protocol/common/hex.rs b/src/protocol/common/hex.rs
index 4c3170b..e30d88c 100644
--- a/src/protocol/common/hex.rs
+++ b/src/protocol/common/hex.rs
@@ -8,7 +8,7 @@
     InvalidOutput,
 }
 
-/// Decode a GDB dex string into the specified integer.
+/// Decode a GDB hex string into the specified integer.
 ///
 /// GDB hex strings may include "xx", which represent "missing" data. This
 /// method simply treats "xx" as 0x00.
@@ -35,7 +35,7 @@
     Ok(result)
 }
 
-/// Wrapper around a raw hex string. Enabled "late" calls to `decode` from
+/// Wrapper around a raw hex string. Enables "late" calls to `decode` from
 /// outside the `crate::protocol` module.
 #[derive(Debug, Clone, Copy)]
 pub struct HexString<'a>(pub &'a [u8]);
@@ -64,7 +64,7 @@
     }
 }
 
-/// Check if the byte `c` is a valid GDB hex digit `[0-9][a-f][A-F][xX]`
+/// Check if the byte `c` is a valid GDB hex digit `[0-9a-fA-FxX]`
 #[allow(clippy::match_like_matches_macro)]
 pub fn is_hex(c: u8) -> bool {
     match c {
@@ -81,7 +81,74 @@
 /// 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
+#[cfg(not(feature = "paranoid_unsafe"))]
+pub fn decode_hex_buf(base_buf: &mut [u8]) -> Result<&mut [u8], DecodeHexBufError> {
+    use DecodeHexBufError::*;
+
+    if base_buf.is_empty() {
+        return Ok(&mut []);
+    }
+
+    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 {
+        // SAFETY: rustc isn't smart enough to automatically elide these bound checks.
+        //
+        // If buf.len() == 0 or 1: trivially safe, since the for block is never taken
+        // If buf.len() >= 2: the range of values for `i` is 0..(buf.len() / 2 - 1)
+        let (hi, lo, b) = unsafe {
+            (
+                //    (buf.len() / 2 - 1) * 2
+                // == (buf.len() - 2)
+                // since buf.len() is >2, this is in-bounds
+                *buf.get_unchecked(i * 2),
+                //    (buf.len() / 2 - 1) * 2 + 1
+                // == (buf.len() - 1)
+                // since buf.len() is >2, this is in-bounds
+                *buf.get_unchecked(i * 2 + 1),
+                // since buf.len() is >2, (buf.len() / 2 - 1) is always in-bounds
+                buf.get_unchecked_mut(i),
+            )
+        };
+
+        let hi = ascii2byte(hi).ok_or(NotAscii)?;
+        let lo = ascii2byte(lo).ok_or(NotAscii)?;
+        *b = hi << 4 | lo;
+    }
+
+    // SAFETY: rustc isn't smart enough to automatically elide this bound check.
+    //
+    // Consider the different values (decoded_len + odd_adust) can take:
+    //
+    //  buf.len() | (decoded_len + odd_adust)
+    // -----------|---------------------------
+    //      0     | (0 + 0) == 0
+    //      1     | (0 + 1) == 1
+    //      2     | (1 + 0) == 1
+    //      3     | (1 + 1) == 2
+    //      4     | (2 + 0) == 2
+    //      5     | (2 + 1) == 3
+    //
+    // Note that the computed index is always in-bounds.
+    //
+    // If I were still in undergrad, I could probably have whipped up a proper
+    // mathematical proof by induction or whatnot, but hopefully this "proof by
+    // example" ought to suffice.
+    unsafe { Ok(base_buf.get_unchecked_mut(..decoded_len + odd_adust)) }
+}
+
+/// 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?
+#[cfg(feature = "paranoid_unsafe")]
 pub fn decode_hex_buf(base_buf: &mut [u8]) -> Result<&mut [u8], DecodeHexBufError> {
     use DecodeHexBufError::*;
 
@@ -101,7 +168,43 @@
     Ok(&mut base_buf[..decoded_len + odd_adust])
 }
 
-#[allow(dead_code)]
+#[derive(Debug)]
+pub enum DecodeBinBufError {
+    UnexpectedEnd,
+}
+
+/// Decode GDB escaped binary bytes into origin bytes _in place_.
+pub fn decode_bin_buf(buf: &mut [u8]) -> Result<&mut [u8], DecodeBinBufError> {
+    use DecodeBinBufError::*;
+    let mut i = 0;
+    let mut j = 0;
+    let len = buf.len();
+    while i < len && j < len {
+        if buf[i] == b'}' {
+            if i + 1 >= len {
+                return Err(UnexpectedEnd);
+            } else {
+                buf[j] = buf[i + 1] ^ 0x20;
+                i += 1;
+            }
+        } else {
+            buf[j] = buf[i];
+        }
+        i += 1;
+        j += 1;
+    }
+
+    // SAFETY: by inspection, the value of j will never exceed buf.len().
+    // Unfortunately, the LLVM optimizer isn't smart enough to see this, so
+    // we have to manually elide the bounds check...
+    if cfg!(feature = "paranoid_unsafe") {
+        Ok(&mut buf[..j])
+    } else {
+        debug_assert!(j <= len);
+        unsafe { Ok(buf.get_unchecked_mut(..j)) }
+    }
+}
+
 #[derive(Debug)]
 pub enum EncodeHexBufError {
     SmallBuffer,
@@ -201,4 +304,11 @@
         let res = decode_hex_buf(&mut payload).unwrap();
         assert_eq!(res, [0x1]);
     }
+
+    #[test]
+    fn decode_bin_buf_escaped() {
+        let mut payload = b"}\x03}\x04}]}\n".to_vec();
+        let res = decode_bin_buf(&mut payload).unwrap();
+        assert_eq!(res, [0x23, 0x24, 0x7d, 0x2a]);
+    }
 }
diff --git a/src/protocol/common/lists.rs b/src/protocol/common/lists.rs
new file mode 100644
index 0000000..5dfdab7
--- /dev/null
+++ b/src/protocol/common/lists.rs
@@ -0,0 +1,26 @@
+use crate::protocol::common::hex::{decode_hex_buf, is_hex};
+
+/// A wrapper type around a list of hex encoded arguments separated by `;`.
+#[derive(Debug)]
+pub struct ArgListHex<'a>(&'a mut [u8]);
+
+impl<'a> ArgListHex<'a> {
+    pub fn from_packet(args: &'a mut [u8]) -> Option<Self> {
+        // 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(Self(args))
+    }
+
+    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())
+    }
+}
diff --git a/src/protocol/common/mod.rs b/src/protocol/common/mod.rs
index 50895a2..81c0fd8 100644
--- a/src/protocol/common/mod.rs
+++ b/src/protocol/common/mod.rs
@@ -1,26 +1,4 @@
 pub mod hex;
+pub mod lists;
+pub mod qxfer;
 pub mod 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/qxfer.rs b/src/protocol/common/qxfer.rs
new file mode 100644
index 0000000..1d14aff
--- /dev/null
+++ b/src/protocol/common/qxfer.rs
@@ -0,0 +1,52 @@
+use crate::protocol::commands::ParseCommand;
+use crate::protocol::common::hex::decode_hex;
+use crate::protocol::packet::PacketBuf;
+
+/// Parse the `annex` field of a qXfer packet. Used in conjunction with
+/// `QXferBase` to cut keep qXfer packet parsing DRY.
+pub trait ParseAnnex<'a>: Sized {
+    fn from_buf(buf: &'a [u8]) -> Option<Self>;
+}
+
+#[derive(Debug)]
+pub struct QXferReadBase<'a, T: ParseAnnex<'a>> {
+    pub annex: T,
+    pub offset: u64,
+    pub length: usize,
+
+    pub buf: &'a mut [u8],
+}
+
+impl<'a, T: ParseAnnex<'a>> ParseCommand<'a> for QXferReadBase<'a, T> {
+    fn from_packet(buf: PacketBuf<'a>) -> Option<Self> {
+        let (buf, body_range) = buf.into_raw_buf();
+
+        // this looks a bit wacky, but the compiler is dumb and won't elide bounds
+        // checks without it.
+        let (body, buf) = {
+            let buf = buf.get_mut(body_range.start..)?;
+            if body_range.end - body_range.start > buf.len() {
+                return None;
+            }
+            buf.split_at_mut(body_range.end - body_range.start)
+        };
+
+        if body.is_empty() {
+            return None;
+        }
+
+        let mut body = body.split(|b| *b == b':').skip(1);
+        let annex = T::from_buf(body.next()?)?;
+
+        let mut body = body.next()?.split(|b| *b == b',');
+        let offset = decode_hex(body.next()?).ok()?;
+        let length = decode_hex(body.next()?).ok()?;
+
+        Some(QXferReadBase {
+            annex,
+            offset,
+            length,
+            buf,
+        })
+    }
+}
diff --git a/src/protocol/common/thread_id.rs b/src/protocol/common/thread_id.rs
index 2677132..d3afe5b 100644
--- a/src/protocol/common/thread_id.rs
+++ b/src/protocol/common/thread_id.rs
@@ -1,7 +1,7 @@
 use core::convert::{TryFrom, TryInto};
 use core::num::NonZeroUsize;
 
-use super::hex::decode_hex;
+use crate::protocol::common::hex::decode_hex;
 
 /// Tid/Pid Selector.
 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
diff --git a/src/protocol/console_output.rs b/src/protocol/console_output.rs
index 9918b26..36c0ce9 100644
--- a/src/protocol/console_output.rs
+++ b/src/protocol/console_output.rs
@@ -74,7 +74,7 @@
 #[macro_export]
 macro_rules! output {
     ($console_output:expr, $($args:tt)*) => {{
-        use std::fmt::Write;
+        use core::fmt::Write;
         let _ = write!($console_output, $($args)*);
     }};
 }
diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs
index e7116d4..1c85778 100644
--- a/src/protocol/mod.rs
+++ b/src/protocol/mod.rs
@@ -1,9 +1,15 @@
+//! GDB protocol internals.
+//!
+//! These types should _not_ leak into the public interface (with a few
+//! exceptions, as listed below).
+
 mod common;
 mod console_output;
 mod packet;
 mod response_writer;
 
 pub(crate) mod commands;
+pub(crate) mod recv_packet;
 
 pub(crate) use common::thread_id::{IdKind, SpecificIdKind, SpecificThreadId};
 pub(crate) use packet::Packet;
diff --git a/src/protocol/packet.rs b/src/protocol/packet.rs
index 9fbf977..84dd07b 100644
--- a/src/protocol/packet.rs
+++ b/src/protocol/packet.rs
@@ -10,7 +10,6 @@
     MissingChecksum,
     MalformedChecksum,
     MalformedCommand,
-    NotAscii,
     UnexpectedHeader(u8),
 }
 
@@ -22,6 +21,18 @@
     Command(Command<'a>),
 }
 
+/// Wrapper around a byte buffer containing a GDB packet, while also tracking
+/// the range of the buffer containing the packet's "body".
+///
+/// A newly constructed `PacketBuf` will have a body that spans the entire data
+/// portion of the packet (i.e: `b"$data#checksum"`), but this range can be
+/// further restricted as part of packet parsing.
+///
+/// Notably, `PacketBuf` will _always_ maintain a mutable reference back to the
+/// _entire_ underlying packet buffer. This makes it possible to re-use any
+/// unused buffer space as "scratch" space. One notable example of this use-case
+/// is the 'm' packet, which recycles unused packet buffer space as a buffer for
+/// the target's `read_memory` method.
 pub struct PacketBuf<'a> {
     buf: &'a mut [u8],
     body_range: core::ops::Range<usize>,
@@ -29,7 +40,7 @@
 
 impl<'a> PacketBuf<'a> {
     /// Validate the contents of the raw packet buffer, checking for checksum
-    /// consistency, structural correctness, and ASCII validation.
+    /// consistency and structural correctness.
     pub fn new(pkt_buf: &'a mut [u8]) -> Result<PacketBuf<'a>, PacketParseError> {
         if pkt_buf.is_empty() {
             return Err(PacketParseError::EmptyBuf);
@@ -38,18 +49,13 @@
         // split buffer into body and checksum components
         let mut parts = pkt_buf[1..].split(|b| *b == b'#');
 
-        let body = parts.next().unwrap(); // spit iter always returns at least one elem
+        let body = parts.next().unwrap(); // spit iter always returns at least one element
         let checksum = parts
             .next()
             .ok_or(PacketParseError::MissingChecksum)?
             .get(..2)
             .ok_or(PacketParseError::MalformedChecksum)?;
 
-        // validate that the body is valid ASCII
-        if !body.is_ascii() {
-            return Err(PacketParseError::NotAscii);
-        }
-
         // validate the checksum
         let checksum = decode_hex(checksum).map_err(|_| PacketParseError::MalformedChecksum)?;
         let calculated = body.iter().fold(0u8, |a, x| a.wrapping_add(*x));
@@ -60,23 +66,18 @@
             });
         }
 
-        let end_of_body = 1 + body.len();
+        let body_range = 1..(body.len() + 1); // compensate for the leading '$'
 
         Ok(PacketBuf {
             buf: pkt_buf,
-            body_range: 1..end_of_body,
+            body_range,
         })
     }
 
     /// (used for tests) Create a packet buffer from a raw body buffer, skipping
-    /// the header/checksum trimming stage. ASCII validation is still performed.
+    /// the header/checksum trimming stage.
     #[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,
@@ -84,8 +85,24 @@
         })
     }
 
+    /// Strip the specified prefix from the packet buffer, returning `true` if
+    /// there was a prefix match.
     pub fn strip_prefix(&mut self, prefix: &[u8]) -> bool {
-        if self.buf[self.body_range.clone()].starts_with(prefix) {
+        let body = {
+            // SAFETY: The public interface of `PacketBuf` ensures that `self.body_range`
+            // always stays within the bounds of the provided buffer.
+            #[cfg(not(feature = "paranoid_unsafe"))]
+            unsafe {
+                self.buf.get_unchecked_mut(self.body_range.clone())
+            }
+
+            #[cfg(feature = "paranoid_unsafe")]
+            &mut self.buf[self.body_range.clone()]
+        };
+
+        if body.starts_with(prefix) {
+            // SAFETY: if the current buffer range `starts_with` the specified prefix, then
+            // it is safe to bump `body_range.start` by the prefix length.
             self.body_range = (self.body_range.start + prefix.len())..self.body_range.end;
             true
         } else {
@@ -93,14 +110,22 @@
         }
     }
 
-    /// Return a mut reference to slice of the packet buffer corresponding to
-    /// the current body.
+    /// Return a mutable 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]
+        // SAFETY: The public interface of `PacketBuf` ensures that `self.body_range`
+        // always stays within the bounds of the provided buffer.
+        #[cfg(not(feature = "paranoid_unsafe"))]
+        unsafe {
+            self.buf.get_unchecked_mut(self.body_range.clone())
+        }
+
+        #[cfg(feature = "paranoid_unsafe")]
+        &mut self.buf[self.body_range.clone()]
     }
 
-    /// Return a mut reference to the _entire_ underlying packet buffer, and the
-    /// current body's range.
+    /// Return a mutable reference to the _entire_ underlying packet buffer, and
+    /// the current body's range.
     pub fn into_raw_buf(self) -> (&'a mut [u8], core::ops::Range<usize>) {
         (self.buf, self.body_range)
     }
@@ -108,7 +133,8 @@
     /// Returns the length of the _entire_ underlying packet buffer - not just
     /// the length of the current range.
     ///
-    /// This method is used when handing the `qSupported` packet.
+    /// This method is used when handing the `qSupported` packet in order to
+    /// obtain the maximum packet size the stub supports.
     pub fn full_len(&self) -> usize {
         self.buf.len()
     }
diff --git a/src/protocol/recv_packet.rs b/src/protocol/recv_packet.rs
new file mode 100644
index 0000000..9f3b567
--- /dev/null
+++ b/src/protocol/recv_packet.rs
@@ -0,0 +1,67 @@
+#[cfg(feature = "trace-pkt")]
+use alloc::string::String;
+
+use managed::ManagedSlice;
+
+use crate::util::managed_vec::{CapacityError, ManagedVec};
+
+enum State {
+    Ready,
+    Body,
+    Checksum1,
+    Checksum2,
+}
+
+/// Receives a packet incrementally using a asynchronous state machine.
+pub struct RecvPacketStateMachine {
+    state: State,
+    idx: usize,
+}
+
+impl RecvPacketStateMachine {
+    pub fn new() -> Self {
+        RecvPacketStateMachine {
+            state: State::Ready,
+            idx: 0,
+        }
+    }
+
+    pub fn pump<'b>(
+        &mut self,
+        packet_buffer: &'b mut ManagedSlice<'_, u8>,
+        byte: u8,
+    ) -> Result<Option<&'b mut [u8]>, CapacityError<u8>> {
+        let mut buf = ManagedVec::new_with_idx(packet_buffer, self.idx);
+        buf.push(byte)?;
+        self.idx += 1;
+
+        match self.state {
+            State::Ready => {
+                if byte == b'$' {
+                    self.state = State::Body;
+                } else {
+                    self.idx = 0;
+                }
+            }
+            State::Body => {
+                if byte == b'#' {
+                    self.state = State::Checksum1;
+                }
+            }
+            State::Checksum1 => self.state = State::Checksum2,
+            State::Checksum2 => {
+                self.state = State::Ready;
+                self.idx = 0;
+            }
+        }
+
+        if matches!(self.state, State::Ready) {
+            #[cfg(feature = "trace-pkt")]
+            trace!("<-- {}", String::from_utf8_lossy(buf.as_slice()));
+
+            Ok(Some(packet_buffer))
+        } else {
+            Ok(None)
+        }
+    }
+}
diff --git a/src/protocol/response_writer.rs b/src/protocol/response_writer.rs
index 484bd60..09ea352 100644
--- a/src/protocol/response_writer.rs
+++ b/src/protocol/response_writer.rs
@@ -1,8 +1,13 @@
+#[cfg(feature = "trace-pkt")]
+use alloc::string::String;
+#[cfg(feature = "trace-pkt")]
+use alloc::vec::Vec;
+
 use num_traits::PrimInt;
 
+use crate::conn::Connection;
 use crate::internal::BeBytes;
 use crate::protocol::{SpecificIdKind, SpecificThreadId};
-use crate::Connection;
 
 /// Newtype around a Connection error. Having a newtype allows implementing a
 /// `From<ResponseWriterError<C>> for crate::Error<T, C>`, which greatly
@@ -12,55 +17,60 @@
 
 /// A wrapper around [`Connection`] that computes the single-byte checksum of
 /// incoming / outgoing data.
-pub struct ResponseWriter<'a, C: Connection + 'a> {
-    // 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)
+pub struct ResponseWriter<'a, C: Connection> {
     inner: &'a mut C,
     started: bool,
     checksum: u8,
-    // TODO?: Make using RLE configurable by the target?
-    // if implemented correctly, targets that disable RLE entirely could have all RLE code
-    // dead-code-eliminated.
+
+    rle_enabled: bool,
     rle_char: u8,
     rle_repeat: u8,
+
     // buffer to log outgoing packets. only allocates if logging is enabled.
-    #[cfg(feature = "std")]
+    #[cfg(feature = "trace-pkt")]
     msg: Vec<u8>,
 }
 
 impl<'a, C: Connection + 'a> ResponseWriter<'a, C> {
     /// Creates a new ResponseWriter
-    pub fn new(inner: &'a mut C) -> Self {
+    pub fn new(inner: &'a mut C, rle_enabled: bool) -> Self {
         Self {
             inner,
             started: false,
             checksum: 0,
+
+            rle_enabled,
             rle_char: 0,
             rle_repeat: 0,
-            #[cfg(feature = "std")]
+
+            #[cfg(feature = "trace-pkt")]
             msg: Vec::new(),
         }
     }
 
     /// Consumes self, writing out the final '#' and checksum
     pub fn flush(mut self) -> Result<(), Error<C::Error>> {
-        self.write(b'#')?;
-
         // don't include the '#' in checksum calculation
-        // (note: even though `self.write` was called, the the '#' char hasn't been
-        // added to the checksum, and is just sitting in the RLE buffer)
-        let checksum = self.checksum;
-
-        #[cfg(feature = "std")]
-        trace!(
-            "--> ${}#{:02x?}",
-            core::str::from_utf8(&self.msg).unwrap(), // buffers are always ascii
+        let checksum = if self.rle_enabled {
+            self.write(b'#')?;
+            // (note: even though `self.write` was called, the the '#' char hasn't been
+            // added to the checksum, and is just sitting in the RLE buffer)
+            self.checksum
+        } else {
+            let checksum = self.checksum;
+            self.write(b'#')?;
             checksum
-        );
+        };
 
         self.write_hex(checksum)?;
+
         // HACK: "write" a dummy char to force an RLE flush
-        self.write(0)?;
+        if self.rle_enabled {
+            self.write(0)?;
+        }
+
+        #[cfg(feature = "trace-pkt")]
+        trace!("--> ${}", String::from_utf8_lossy(&self.msg));
 
         self.inner.flush().map_err(Error)?;
 
@@ -73,17 +83,21 @@
     }
 
     fn inner_write(&mut self, byte: u8) -> Result<(), Error<C::Error>> {
-        #[cfg(feature = "std")]
+        #[cfg(feature = "trace-pkt")]
         if log_enabled!(log::Level::Trace) {
-            match self.msg.as_slice() {
-                [.., c, b'*'] => {
-                    let c = *c;
-                    self.msg.pop();
-                    for _ in 0..(byte - 29) {
-                        self.msg.push(c);
+            if self.rle_enabled {
+                match self.msg.as_slice() {
+                    [.., c, b'*'] => {
+                        let c = *c;
+                        self.msg.pop();
+                        for _ in 0..(byte - 29) {
+                            self.msg.push(c);
+                        }
                     }
+                    _ => self.msg.push(byte),
                 }
-                _ => self.msg.push(byte),
+            } else {
+                self.msg.push(byte)
             }
         }
 
@@ -97,6 +111,10 @@
     }
 
     fn write(&mut self, byte: u8) -> Result<(), Error<C::Error>> {
+        if !self.rle_enabled {
+            return self.inner_write(byte);
+        }
+
         const ASCII_FIRST_PRINT: u8 = b' ';
         const ASCII_LAST_PRINT: u8 = b'~';
 
@@ -147,11 +165,24 @@
 
     /// 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() {
+        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!(),
+                // This match arm is unreachable, but the compiler isn't smart enough to optimize
+                // out the branch. As such, using `unreachable!` here would introduce panicking
+                // code to `gdbstub`.
+                //
+                // In this case, it'd be totally reasonable to use
+                // `unsafe { core::hint::unreachable_unchecked() }`, but i'll be honest, using some
+                // spooky unsafe compiler hints just to eek out a smidge more performance here just
+                // isn't worth the cognitive overhead.
+                //
+                // Moreover, I've played around with this code in godbolt.org, and it turns out that
+                // leaving this match arm as `=> digit` ends up generating the _exact same code_ as
+                // using `unreachable_unchecked` (at least on x86_64 targets compiled using the
+                // latest Rust compiler). YMMV on other platforms.
+                _ => digit,
             };
             self.write(c)?;
         }
diff --git a/src/gdbstub_impl/builder.rs b/src/stub/builder.rs
similarity index 95%
rename from src/gdbstub_impl/builder.rs
rename to src/stub/builder.rs
index d5cb58a..bb1ceba 100644
--- a/src/gdbstub_impl/builder.rs
+++ b/src/stub/builder.rs
@@ -3,7 +3,11 @@
 
 use managed::ManagedSlice;
 
-use super::{Connection, GdbStub, GdbStubImpl, Target};
+use crate::conn::Connection;
+use crate::target::Target;
+
+use super::core_impl::GdbStubImpl;
+use super::GdbStub;
 
 /// An error which may occur when building a [`GdbStub`].
 #[derive(Debug)]
@@ -105,7 +109,7 @@
         Ok(GdbStub {
             conn: self.conn,
             packet_buffer,
-            state: GdbStubImpl::new(),
+            inner: GdbStubImpl::new(),
         })
     }
 }
diff --git a/src/stub/core_impl.rs b/src/stub/core_impl.rs
new file mode 100644
index 0000000..82f3657
--- /dev/null
+++ b/src/stub/core_impl.rs
@@ -0,0 +1,273 @@
+use core::marker::PhantomData;
+
+use crate::common::{Signal, Tid};
+use crate::conn::Connection;
+use crate::protocol::commands::Command;
+use crate::protocol::{Packet, ResponseWriter, SpecificIdKind};
+use crate::stub::GdbStubError as Error;
+use crate::target::Target;
+use crate::SINGLE_THREAD_TID;
+
+/// Common imports used by >50% of all extensions.
+///
+/// Do not clutter this prelude with types only used by a few extensions.
+mod prelude {
+    pub(super) use crate::conn::Connection;
+    pub(super) use crate::internal::BeBytes;
+    pub(super) use crate::protocol::ResponseWriter;
+    pub(super) use crate::stub::core_impl::target_result_ext::TargetResultExt;
+    pub(super) use crate::stub::core_impl::{GdbStubImpl, HandlerStatus};
+    pub(super) use crate::stub::error::GdbStubError as Error;
+    pub(super) use crate::target::Target;
+}
+
+mod auxv;
+mod base;
+mod breakpoints;
+mod catch_syscalls;
+mod exec_file;
+mod extended_mode;
+mod host_io;
+mod memory_map;
+mod monitor_cmd;
+mod resume;
+mod reverse_exec;
+mod section_offsets;
+mod single_register_access;
+mod target_xml;
+mod x_upcase_packet;
+
+pub(crate) use resume::FinishExecStatus;
+
+pub(crate) mod target_result_ext {
+    use crate::stub::GdbStubError;
+    use crate::target::TargetError;
+
+    /// 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))
+        }
+    }
+}
+
+/// Describes why the GDB session ended.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum DisconnectReason {
+    /// Target exited with given status code
+    TargetExited(u8),
+    /// Target terminated with given signal
+    TargetTerminated(Signal),
+    /// GDB issued a disconnect command
+    Disconnect,
+    /// GDB issued a kill command
+    Kill,
+}
+
+pub enum State {
+    Pump,
+    DeferredStopReason,
+    CtrlCInterrupt,
+    Disconnect(DisconnectReason),
+}
+
+pub struct GdbStubImpl<T: Target, C: Connection> {
+    _target: PhantomData<T>,
+    _connection: PhantomData<C>,
+
+    current_mem_tid: Tid,
+    current_resume_tid: SpecificIdKind,
+    features: ProtocolFeatures,
+}
+
+pub enum HandlerStatus {
+    Handled,
+    NeedsOk,
+    DeferredStopReason,
+    Disconnect(DisconnectReason),
+}
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    pub fn new() -> GdbStubImpl<T, C> {
+        GdbStubImpl {
+            _target: PhantomData,
+            _connection: PhantomData,
+
+            // NOTE: `current_mem_tid` and `current_resume_tid` are never queried prior to being set
+            // by the GDB client (via the 'H' packet), so it's fine to use dummy values here.
+            //
+            // The alternative would be to use `Option`, and while this would be more "correct", it
+            // would introduce a _lot_ of noisy and heavy error handling logic all over the place.
+            //
+            // Plus, even if the GDB client is acting strangely and doesn't overwrite these values,
+            // the target will simply return a non-fatal error, which is totally fine.
+            current_mem_tid: SINGLE_THREAD_TID,
+            current_resume_tid: SpecificIdKind::WithId(SINGLE_THREAD_TID),
+            features: ProtocolFeatures::empty(),
+        }
+    }
+
+    pub fn handle_packet(
+        &mut self,
+        target: &mut T,
+        conn: &mut C,
+        packet: Packet<'_>,
+    ) -> Result<State, Error<T::Error, C::Error>> {
+        match packet {
+            Packet::Ack => Ok(State::Pump),
+            Packet::Nack => Err(Error::ClientSentNack),
+            Packet::Interrupt => {
+                debug!("<-- interrupt packet");
+                Ok(State::CtrlCInterrupt)
+            }
+            Packet::Command(command) => {
+                // Acknowledge the command
+                if !self.features.no_ack_mode() {
+                    conn.write(b'+').map_err(Error::ConnectionWrite)?;
+                }
+
+                let mut res = ResponseWriter::new(conn, target.use_rle());
+                let disconnect_reason = match self.handle_command(&mut res, target, command) {
+                    Ok(HandlerStatus::Handled) => None,
+                    Ok(HandlerStatus::NeedsOk) => {
+                        res.write_str("OK")?;
+                        None
+                    }
+                    Ok(HandlerStatus::DeferredStopReason) => return Ok(State::DeferredStopReason),
+                    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(e) => return Err(e),
+                };
+
+                // every response needs to be flushed, _except_ for the response to a kill
+                // packet, but ONLY when extended mode is NOT implemented.
+                let is_kill = matches!(disconnect_reason, Some(DisconnectReason::Kill));
+                if !(target.support_extended_mode().is_none() && is_kill) {
+                    res.flush()?;
+                }
+
+                let state = match disconnect_reason {
+                    Some(reason) => State::Disconnect(reason),
+                    None => State::Pump,
+                };
+
+                Ok(state)
+            }
+        }
+    }
+
+    fn handle_command(
+        &mut self,
+        res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        cmd: Command<'_>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        match cmd {
+            // `handle_X` methods are defined in the `ext` module
+            Command::Base(cmd) => self.handle_base(res, target, cmd),
+            Command::TargetXml(cmd) => self.handle_target_xml(res, target, cmd),
+            Command::Resume(cmd) => self.handle_stop_resume(res, target, cmd),
+            Command::XUpcasePacket(cmd) => self.handle_x_upcase_packet(res, target, cmd),
+            Command::SingleRegisterAccess(cmd) => {
+                self.handle_single_register_access(res, target, cmd)
+            }
+            Command::Breakpoints(cmd) => self.handle_breakpoints(res, target, cmd),
+            Command::CatchSyscalls(cmd) => self.handle_catch_syscalls(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),
+            Command::ReverseCont(cmd) => self.handle_reverse_cont(res, target, cmd),
+            Command::ReverseStep(cmd) => self.handle_reverse_step(res, target, cmd),
+            Command::MemoryMap(cmd) => self.handle_memory_map(res, target, cmd),
+            Command::HostIo(cmd) => self.handle_host_io(res, target, cmd),
+            Command::ExecFile(cmd) => self.handle_exec_file(res, target, cmd),
+            Command::Auxv(cmd) => self.handle_auxv(res, target, cmd),
+            // in the worst case, the command could not be parsed...
+            Command::Unknown(cmd) => {
+                // HACK: if the user accidentally sends a resume command to a
+                // target without resume support, inform them of their mistake +
+                // return a dummy stop reason.
+                if target.base_ops().resume_ops().is_none() && target.use_resume_stub() {
+                    let is_resume_pkt = cmd
+                        .get(0)
+                        .map(|c| matches!(c, b'c' | b'C' | b's' | b'S'))
+                        .unwrap_or(false);
+
+                    if is_resume_pkt {
+                        warn!("attempted to resume target without resume support!");
+
+                        // TODO: omit this message if non-stop mode is active
+                        {
+                            let mut res = ResponseWriter::new(res.as_conn(), target.use_rle());
+                            res.write_str("O")?;
+                            res.write_hex_buf(b"target has not implemented `support_resume()`\n")?;
+                            res.flush()?;
+                        }
+
+                        res.write_str("S05")?;
+                    }
+                }
+
+                info!("Unknown command: {:?}", core::str::from_utf8(cmd));
+                Ok(HandlerStatus::Handled)
+            }
+        }
+    }
+}
+
+// This bitflag is not part of the protocol - it is an internal implementation
+// detail. The alternative would be to use multiple `bool` fields, which wastes
+// space in minimal `gdbstub` configurations.
+bitflags::bitflags! {
+    struct ProtocolFeatures: u8 {
+        const NO_ACK_MODE = 1 << 0;
+        const MULTIPROCESS = 1 << 1;
+    }
+}
+
+impl ProtocolFeatures {
+    #[inline(always)]
+    fn no_ack_mode(&self) -> bool {
+        self.contains(ProtocolFeatures::NO_ACK_MODE)
+    }
+
+    #[inline(always)]
+    fn set_no_ack_mode(&mut self, val: bool) {
+        self.set(ProtocolFeatures::NO_ACK_MODE, val)
+    }
+
+    #[inline(always)]
+    fn multiprocess(&self) -> bool {
+        self.contains(ProtocolFeatures::MULTIPROCESS)
+    }
+
+    #[inline(always)]
+    fn set_multiprocess(&mut self, val: bool) {
+        self.set(ProtocolFeatures::MULTIPROCESS, val)
+    }
+}
diff --git a/src/stub/core_impl/auxv.rs b/src/stub/core_impl/auxv.rs
new file mode 100644
index 0000000..55bd021
--- /dev/null
+++ b/src/stub/core_impl/auxv.rs
@@ -0,0 +1,36 @@
+use super::prelude::*;
+use crate::protocol::commands::ext::Auxv;
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    pub(crate) fn handle_auxv(
+        &mut self,
+        res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: Auxv<'_>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let ops = match target.support_auxv() {
+            Some(ops) => ops,
+            None => return Ok(HandlerStatus::Handled),
+        };
+
+        crate::__dead_code_marker!("auxv", "impl");
+
+        let handler_status = match command {
+            Auxv::qXferAuxvRead(cmd) => {
+                let ret = ops
+                    .get_auxv(cmd.offset, cmd.length, cmd.buf)
+                    .handle_error()?;
+                if ret == 0 {
+                    res.write_str("l")?;
+                } else {
+                    res.write_str("m")?;
+                    // TODO: add more specific error variant?
+                    res.write_binary(cmd.buf.get(..ret).ok_or(Error::PacketBufferOverflow)?)?;
+                }
+                HandlerStatus::Handled
+            }
+        };
+
+        Ok(handler_status)
+    }
+}
diff --git a/src/stub/core_impl/base.rs b/src/stub/core_impl/base.rs
new file mode 100644
index 0000000..8809338
--- /dev/null
+++ b/src/stub/core_impl/base.rs
@@ -0,0 +1,380 @@
+use super::prelude::*;
+use crate::protocol::commands::ext::Base;
+
+use crate::arch::{Arch, Registers};
+use crate::common::Tid;
+use crate::protocol::{IdKind, SpecificIdKind, SpecificThreadId};
+use crate::target::ext::base::{BaseOps, ResumeOps};
+use crate::{FAKE_PID, SINGLE_THREAD_TID};
+
+use super::DisconnectReason;
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    #[inline(always)]
+    fn get_sane_any_tid(&mut self, target: &mut T) -> Result<Tid, Error<T::Error, C::Error>> {
+        let 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)?;
+                // Note that `Error::NoActiveThreads` shouldn't ever occur, since this method is
+                // called from the `H` packet handler, which AFAIK is only sent after the GDB
+                // client has confirmed that a thread / process exists.
+                //
+                // If it does, that really sucks, and will require rethinking how to handle "any
+                // thread" messages.
+                first_tid.ok_or(Error::NoActiveThreads)?
+            }
+        };
+        Ok(tid)
+    }
+
+    pub(crate) fn handle_base<'a>(
+        &mut self,
+        res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: Base<'a>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let handler_status = match command {
+            // ------------------ Handshaking and Queries ------------------- //
+            Base::qSupported(cmd) => {
+                use crate::protocol::commands::_qSupported::Feature;
+
+                // perform incoming feature negotiation
+                for feature in cmd.features.into_iter() {
+                    let (feature, supported) = match feature {
+                        Ok(Some(v)) => v,
+                        Ok(None) => continue,
+                        Err(()) => {
+                            return Err(Error::PacketParse(
+                                crate::protocol::PacketParseError::MalformedCommand,
+                            ))
+                        }
+                    };
+
+                    match feature {
+                        Feature::Multiprocess => self.features.set_multiprocess(supported),
+                    }
+                }
+
+                res.write_str("PacketSize=")?;
+                res.write_num(cmd.packet_buffer_len)?;
+
+                // these are the few features that gdbstub unconditionally supports
+                res.write_str(concat!(
+                    ";vContSupported+",
+                    ";multiprocess+",
+                    ";QStartNoAckMode+",
+                ))?;
+
+                if let Some(resume_ops) = target.base_ops().resume_ops() {
+                    let (reverse_cont, reverse_step) = match resume_ops {
+                        ResumeOps::MultiThread(ops) => (
+                            ops.support_reverse_cont().is_some(),
+                            ops.support_reverse_step().is_some(),
+                        ),
+                        ResumeOps::SingleThread(ops) => (
+                            ops.support_reverse_cont().is_some(),
+                            ops.support_reverse_step().is_some(),
+                        ),
+                    };
+
+                    if reverse_cont {
+                        res.write_str(";ReverseContinue+")?;
+                    }
+
+                    if reverse_step {
+                        res.write_str(";ReverseStep+")?;
+                    }
+                }
+
+                if let Some(ops) = target.support_extended_mode() {
+                    if ops.support_configure_aslr().is_some() {
+                        res.write_str(";QDisableRandomization+")?;
+                    }
+
+                    if ops.support_configure_env().is_some() {
+                        res.write_str(";QEnvironmentHexEncoded+")?;
+                        res.write_str(";QEnvironmentUnset+")?;
+                        res.write_str(";QEnvironmentReset+")?;
+                    }
+
+                    if ops.support_configure_startup_shell().is_some() {
+                        res.write_str(";QStartupWithShell+")?;
+                    }
+
+                    if ops.support_configure_working_dir().is_some() {
+                        res.write_str(";QSetWorkingDir+")?;
+                    }
+                }
+
+                if let Some(ops) = target.support_breakpoints() {
+                    if ops.support_sw_breakpoint().is_some() {
+                        res.write_str(";swbreak+")?;
+                    }
+
+                    if ops.support_hw_breakpoint().is_some()
+                        || ops.support_hw_watchpoint().is_some()
+                    {
+                        res.write_str(";hwbreak+")?;
+                    }
+                }
+
+                if target.support_catch_syscalls().is_some() {
+                    res.write_str(";QCatchSyscalls+")?;
+                }
+
+                if target.use_target_description_xml()
+                    && (T::Arch::target_description_xml().is_some()
+                        || target.support_target_description_xml_override().is_some())
+                {
+                    res.write_str(";qXfer:features:read+")?;
+                }
+
+                if target.support_memory_map().is_some() {
+                    res.write_str(";qXfer:memory-map:read+")?;
+                }
+
+                if target.support_exec_file().is_some() {
+                    res.write_str(";qXfer:exec-file:read+")?;
+                }
+
+                if target.support_auxv().is_some() {
+                    res.write_str(";qXfer:auxv:read+")?;
+                }
+
+                HandlerStatus::Handled
+            }
+            Base::QStartNoAckMode(_) => {
+                self.features.set_no_ack_mode(true);
+                HandlerStatus::NeedsOk
+            }
+
+            // -------------------- "Core" Functionality -------------------- //
+            // TODO: Improve the '?' response based on last-sent stop reason.
+            // this will be particularly relevant when working on non-stop mode.
+            Base::QuestionMark(_) => {
+                res.write_str("S05")?;
+                HandlerStatus::Handled
+            }
+            Base::qAttached(cmd) => {
+                let is_attached = match target.support_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) => {
+                        match cmd.pid {
+                            Some(pid) => ops.query_if_attached(pid).handle_error()?.was_attached(),
+                            None => true, // assume attached to an existing process
+                        }
+                    }
+                };
+                res.write_str(if is_attached { "1" } else { "0" })?;
+                HandlerStatus::Handled
+            }
+            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
+            }
+            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
+            }
+            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
+            }
+            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
+            }
+            Base::k(_) | Base::vKill(_) => {
+                match target.support_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 {
+                            Base::vKill(cmd) => Some(cmd.pid),
+                            _ => None,
+                        };
+
+                        let should_terminate = ops.kill(pid).handle_error()?;
+                        if should_terminate.into_bool() {
+                            // manually write OK, since we need to return a DisconnectReason
+                            res.write_str("OK")?;
+                            HandlerStatus::Disconnect(DisconnectReason::Kill)
+                        } else {
+                            HandlerStatus::NeedsOk
+                        }
+                    }
+                }
+            }
+            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)
+            }
+
+            // ------------------- Multi-threading Support ------------------ //
+            Base::H(cmd) => {
+                use crate::protocol::commands::_h_upcase::Op;
+                match cmd.kind {
+                    Op::Other => match cmd.thread.tid {
+                        IdKind::Any => self.current_mem_tid = self.get_sane_any_tid(target)?,
+                        // "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 => {
+                            self.current_resume_tid =
+                                SpecificIdKind::WithId(self.get_sane_any_tid(target)?)
+                        }
+                        IdKind::All => self.current_resume_tid = SpecificIdKind::All,
+                        IdKind::WithId(tid) => {
+                            self.current_resume_tid = SpecificIdKind::WithId(tid)
+                        }
+                    },
+                }
+                HandlerStatus::NeedsOk
+            }
+            Base::qfThreadInfo(_) => {
+                res.write_str("m")?;
+
+                match target.base_ops() {
+                    BaseOps::SingleThread(_) => res.write_specific_thread_id(SpecificThreadId {
+                        pid: self
+                            .features
+                            .multiprocess()
+                            .then(|| SpecificIdKind::WithId(FAKE_PID)),
+                        tid: SpecificIdKind::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_specific_thread_id(SpecificThreadId {
+                                    pid: self
+                                        .features
+                                        .multiprocess()
+                                        .then(|| SpecificIdKind::WithId(FAKE_PID)),
+                                    tid: SpecificIdKind::WithId(tid),
+                                })?;
+                                Ok(())
+                            })();
+
+                            if let Err(e) = e {
+                                err = Err(e)
+                            }
+                        })
+                        .map_err(Error::TargetError)?;
+                        err?;
+                    }
+                }
+
+                HandlerStatus::Handled
+            }
+            Base::qsThreadInfo(_) => {
+                res.write_str("l")?;
+                HandlerStatus::Handled
+            }
+            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)?
+                        }
+                    },
+                    _ => return Err(Error::PacketUnexpected),
+                };
+                if alive {
+                    HandlerStatus::NeedsOk
+                } else {
+                    // any error code will do
+                    return Err(Error::NonFatalError(1));
+                }
+            }
+        };
+        Ok(handler_status)
+    }
+}
diff --git a/src/stub/core_impl/breakpoints.rs b/src/stub/core_impl/breakpoints.rs
new file mode 100644
index 0000000..aeb176a
--- /dev/null
+++ b/src/stub/core_impl/breakpoints.rs
@@ -0,0 +1,99 @@
+use super::prelude::*;
+use crate::protocol::commands::ext::Breakpoints;
+
+use crate::arch::{Arch, BreakpointKind};
+
+enum CmdKind {
+    Add,
+    Remove,
+}
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    #[inline(always)]
+    fn handle_breakpoint_common(
+        &mut self,
+        ops: crate::target::ext::breakpoints::BreakpointsOps<'_, T>,
+        cmd: crate::protocol::commands::breakpoint::BasicBreakpoint<'_>,
+        cmd_kind: CmdKind,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let addr =
+            <T::Arch as Arch>::Usize::from_be_bytes(cmd.addr).ok_or(Error::TargetMismatch)?;
+
+        macro_rules! bp_kind {
+            () => {
+                BeBytes::from_be_bytes(cmd.kind)
+                    .and_then(<T::Arch as Arch>::BreakpointKind::from_usize)
+                    .ok_or(Error::TargetMismatch)?
+            };
+        }
+
+        let supported = match cmd.type_ {
+            0 if ops.support_sw_breakpoint().is_some() => {
+                let ops = ops.support_sw_breakpoint().unwrap();
+                let bp_kind = bp_kind!();
+                match cmd_kind {
+                    CmdKind::Add => ops.add_sw_breakpoint(addr, bp_kind),
+                    CmdKind::Remove => ops.remove_sw_breakpoint(addr, bp_kind),
+                }
+            }
+            1 if ops.support_hw_breakpoint().is_some() => {
+                let ops = ops.support_hw_breakpoint().unwrap();
+                let bp_kind = bp_kind!();
+                match cmd_kind {
+                    CmdKind::Add => ops.add_hw_breakpoint(addr, bp_kind),
+                    CmdKind::Remove => ops.remove_hw_breakpoint(addr, bp_kind),
+                }
+            }
+            2 | 3 | 4 if ops.support_hw_watchpoint().is_some() => {
+                use crate::target::ext::breakpoints::WatchKind;
+                let kind = match cmd.type_ {
+                    2 => WatchKind::Write,
+                    3 => WatchKind::Read,
+                    4 => WatchKind::ReadWrite,
+                    _ => unreachable!(),
+                };
+                let len = <T::Arch as Arch>::Usize::from_be_bytes(cmd.kind)
+                    .ok_or(Error::TargetMismatch)?;
+                let ops = ops.support_hw_watchpoint().unwrap();
+                match cmd_kind {
+                    CmdKind::Add => ops.add_hw_watchpoint(addr, len, kind),
+                    CmdKind::Remove => ops.remove_hw_watchpoint(addr, len, kind),
+                }
+            }
+            // explicitly handle unguarded variants of known breakpoint types
+            0 | 1 | 2 | 3 | 4 => return Ok(HandlerStatus::Handled),
+            // warn if the GDB client ever sends a type outside the known types
+            other => {
+                warn!("unknown breakpoint type: {}", other);
+                return Ok(HandlerStatus::Handled);
+            }
+        };
+
+        match supported.handle_error()? {
+            true => Ok(HandlerStatus::NeedsOk),
+            false => Err(Error::NonFatalError(22)),
+        }
+    }
+
+    pub(crate) fn handle_breakpoints<'a>(
+        &mut self,
+        _res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: Breakpoints<'a>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let ops = match target.support_breakpoints() {
+            Some(ops) => ops,
+            None => return Ok(HandlerStatus::Handled),
+        };
+
+        crate::__dead_code_marker!("breakpoints", "impl");
+
+        let handler_status = match command {
+            Breakpoints::z(cmd) => self.handle_breakpoint_common(ops, cmd, CmdKind::Remove)?,
+            Breakpoints::Z(cmd) => self.handle_breakpoint_common(ops, cmd, CmdKind::Add)?,
+            // TODO: handle ZWithBytecode once agent expressions are implemented
+            _ => HandlerStatus::Handled,
+        };
+        Ok(handler_status)
+    }
+}
diff --git a/src/stub/core_impl/catch_syscalls.rs b/src/stub/core_impl/catch_syscalls.rs
new file mode 100644
index 0000000..b7dde2e
--- /dev/null
+++ b/src/stub/core_impl/catch_syscalls.rs
@@ -0,0 +1,50 @@
+use super::prelude::*;
+use crate::protocol::commands::ext::CatchSyscalls;
+
+use crate::arch::Arch;
+use crate::protocol::commands::_QCatchSyscalls::QCatchSyscalls;
+use crate::target::ext::catch_syscalls::SyscallNumbers;
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    pub(crate) fn handle_catch_syscalls(
+        &mut self,
+        _res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: CatchSyscalls<'_>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let ops = match target.support_catch_syscalls() {
+            Some(ops) => ops,
+            None => return Ok(HandlerStatus::Handled),
+        };
+
+        crate::__dead_code_marker!("catch_syscalls", "impl");
+
+        let handler_status = match command {
+            CatchSyscalls::QCatchSyscalls(cmd) => {
+                match cmd {
+                    QCatchSyscalls::Disable => ops.disable_catch_syscalls().handle_error()?,
+                    QCatchSyscalls::Enable(sysno) => {
+                        let mut error = false;
+                        let mut filter = sysno
+                            .into_iter()
+                            .map(<T::Arch as Arch>::Usize::from_be_bytes)
+                            .take_while(|x| {
+                                error = x.is_none();
+                                !error
+                            })
+                            .flatten();
+                        ops.enable_catch_syscalls(Some(SyscallNumbers { inner: &mut filter }))
+                            .handle_error()?;
+                        if error {
+                            return Err(Error::TargetMismatch);
+                        }
+                    }
+                    QCatchSyscalls::EnableAll => ops.enable_catch_syscalls(None).handle_error()?,
+                }
+                HandlerStatus::NeedsOk
+            }
+        };
+
+        Ok(handler_status)
+    }
+}
diff --git a/src/stub/core_impl/exec_file.rs b/src/stub/core_impl/exec_file.rs
new file mode 100644
index 0000000..dd5919b
--- /dev/null
+++ b/src/stub/core_impl/exec_file.rs
@@ -0,0 +1,36 @@
+use super::prelude::*;
+use crate::protocol::commands::ext::ExecFile;
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    pub(crate) fn handle_exec_file(
+        &mut self,
+        res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: ExecFile<'_>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let ops = match target.support_exec_file() {
+            Some(ops) => ops,
+            None => return Ok(HandlerStatus::Handled),
+        };
+
+        crate::__dead_code_marker!("exec_file", "impl");
+
+        let handler_status = match command {
+            ExecFile::qXferExecFileRead(cmd) => {
+                let ret = ops
+                    .get_exec_file(cmd.annex.pid, cmd.offset, cmd.length, cmd.buf)
+                    .handle_error()?;
+                if ret == 0 {
+                    res.write_str("l")?;
+                } else {
+                    res.write_str("m")?;
+                    // TODO: add more specific error variant?
+                    res.write_binary(cmd.buf.get(..ret).ok_or(Error::PacketBufferOverflow)?)?;
+                }
+                HandlerStatus::Handled
+            }
+        };
+
+        Ok(handler_status)
+    }
+}
diff --git a/src/gdbstub_impl/ext/extended_mode.rs b/src/stub/core_impl/extended_mode.rs
similarity index 65%
rename from src/gdbstub_impl/ext/extended_mode.rs
rename to src/stub/core_impl/extended_mode.rs
index 2ca5cd5..a87e08f 100644
--- a/src/gdbstub_impl/ext/extended_mode.rs
+++ b/src/stub/core_impl/extended_mode.rs
@@ -4,11 +4,11 @@
 impl<T: Target, C: Connection> GdbStubImpl<T, C> {
     pub(crate) fn handle_extended_mode<'a>(
         &mut self,
-        res: &mut ResponseWriter<C>,
+        res: &mut ResponseWriter<'_, C>,
         target: &mut T,
         command: ExtendedMode<'a>,
     ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
-        let ops = match target.extended_mode() {
+        let ops = match target.support_extended_mode() {
             Some(ops) => ops,
             None => return Ok(HandlerStatus::Handled),
         };
@@ -37,41 +37,44 @@
                     .run(cmd.filename, Args::new(&mut cmd.args.into_iter()))
                     .handle_error()?;
 
-                // TODO: send a more descriptive stop packet?
+                // This is a reasonable response, as the `run` handler must
+                // spawn the process in a stopped state.
                 res.write_str("S05")?;
                 HandlerStatus::Handled
             }
             // --------- ASLR --------- //
-            ExtendedMode::QDisableRandomization(cmd) if ops.configure_aslr().is_some() => {
-                let ops = ops.configure_aslr().unwrap();
+            ExtendedMode::QDisableRandomization(cmd) if ops.support_configure_aslr().is_some() => {
+                let ops = ops.support_configure_aslr().unwrap();
                 ops.cfg_aslr(cmd.value).handle_error()?;
                 HandlerStatus::NeedsOk
             }
             // --------- Environment --------- //
-            ExtendedMode::QEnvironmentHexEncoded(cmd) if ops.configure_env().is_some() => {
-                let ops = ops.configure_env().unwrap();
+            ExtendedMode::QEnvironmentHexEncoded(cmd) if ops.support_configure_env().is_some() => {
+                let ops = ops.support_configure_env().unwrap();
                 ops.set_env(cmd.key, cmd.value).handle_error()?;
                 HandlerStatus::NeedsOk
             }
-            ExtendedMode::QEnvironmentUnset(cmd) if ops.configure_env().is_some() => {
-                let ops = ops.configure_env().unwrap();
+            ExtendedMode::QEnvironmentUnset(cmd) if ops.support_configure_env().is_some() => {
+                let ops = ops.support_configure_env().unwrap();
                 ops.remove_env(cmd.key).handle_error()?;
                 HandlerStatus::NeedsOk
             }
-            ExtendedMode::QEnvironmentReset(_cmd) if ops.configure_env().is_some() => {
-                let ops = ops.configure_env().unwrap();
+            ExtendedMode::QEnvironmentReset(_cmd) if ops.support_configure_env().is_some() => {
+                let ops = ops.support_configure_env().unwrap();
                 ops.reset_env().handle_error()?;
                 HandlerStatus::NeedsOk
             }
             // --------- Working Dir --------- //
-            ExtendedMode::QSetWorkingDir(cmd) if ops.configure_working_dir().is_some() => {
-                let ops = ops.configure_working_dir().unwrap();
+            ExtendedMode::QSetWorkingDir(cmd) if ops.support_configure_working_dir().is_some() => {
+                let ops = ops.support_configure_working_dir().unwrap();
                 ops.cfg_working_dir(cmd.dir).handle_error()?;
                 HandlerStatus::NeedsOk
             }
             // --------- Startup Shell --------- //
-            ExtendedMode::QStartupWithShell(cmd) if ops.configure_startup_shell().is_some() => {
-                let ops = ops.configure_startup_shell().unwrap();
+            ExtendedMode::QStartupWithShell(cmd)
+                if ops.support_configure_startup_shell().is_some() =>
+            {
+                let ops = ops.support_configure_startup_shell().unwrap();
                 ops.cfg_startup_with_shell(cmd.value).handle_error()?;
                 HandlerStatus::NeedsOk
             }
diff --git a/src/stub/core_impl/host_io.rs b/src/stub/core_impl/host_io.rs
new file mode 100644
index 0000000..4ea6b1a
--- /dev/null
+++ b/src/stub/core_impl/host_io.rs
@@ -0,0 +1,140 @@
+use super::prelude::*;
+use crate::protocol::commands::ext::HostIo;
+
+use crate::arch::Arch;
+use crate::target::ext::host_io::{HostIoError, HostIoStat};
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    pub(crate) fn handle_host_io(
+        &mut self,
+        res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: HostIo<'_>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let ops = match target.support_host_io() {
+            Some(ops) => ops,
+            None => return Ok(HandlerStatus::Handled),
+        };
+
+        crate::__dead_code_marker!("host_io", "impl");
+
+        macro_rules! handle_hostio_result {
+            ( if let Ok($val:pat) = $ret:expr => $callback:block ) => {{
+                match $ret {
+                    Ok($val) => $callback,
+                    Err(HostIoError::Errno(errno)) => {
+                        res.write_str("F-1,")?;
+                        res.write_num(errno as u32)?;
+                    }
+                    Err(HostIoError::Fatal(e)) => return Err(Error::TargetError(e)),
+                }
+            }};
+        }
+
+        let handler_status = match command {
+            HostIo::vFileOpen(cmd) if ops.support_open().is_some() => {
+                let ops = ops.support_open().unwrap();
+                handle_hostio_result! {
+                    if let Ok(fd) = ops.open(cmd.filename, cmd.flags, cmd.mode) => {
+                        res.write_str("F")?;
+                        res.write_num(fd)?;
+                    }
+                }
+                HandlerStatus::Handled
+            }
+            HostIo::vFileClose(cmd) if ops.support_close().is_some() => {
+                let ops = ops.support_close().unwrap();
+                handle_hostio_result! {
+                    if let Ok(()) = ops.close(cmd.fd) => {
+                        res.write_str("F0")?;
+                    }
+                }
+                HandlerStatus::Handled
+            }
+            HostIo::vFilePread(cmd) if ops.support_pread().is_some() => {
+                let ops = ops.support_pread().unwrap();
+                handle_hostio_result! {
+                    if let Ok(ret) = ops.pread(cmd.fd, cmd.count, cmd.offset, cmd.buf) => {
+                        res.write_str("F")?;
+                        res.write_num(ret)?;
+                        res.write_str(";")?;
+                        res.write_binary(cmd.buf.get(..ret).ok_or(Error::PacketBufferOverflow)?)?;
+                    }
+                };
+
+                HandlerStatus::Handled
+            }
+            HostIo::vFilePwrite(cmd) if ops.support_pwrite().is_some() => {
+                let offset = <T::Arch as Arch>::Usize::from_be_bytes(cmd.offset)
+                    .ok_or(Error::TargetMismatch)?;
+                let ops = ops.support_pwrite().unwrap();
+                handle_hostio_result! {
+                    if let Ok(ret) = ops.pwrite(cmd.fd, offset, cmd.data) => {
+                        res.write_str("F")?;
+                        res.write_num(ret)?;
+                    }
+                };
+                HandlerStatus::Handled
+            }
+            HostIo::vFileFstat(cmd) if ops.support_fstat().is_some() => {
+                let ops = ops.support_fstat().unwrap();
+                handle_hostio_result! {
+                    if let Ok(stat) = ops.fstat(cmd.fd) => {
+                        let size = core::mem::size_of::<HostIoStat>();
+                        res.write_str("F")?;
+                        res.write_num(size)?;
+                        res.write_str(";")?;
+                        res.write_binary(&stat.st_dev.to_be_bytes())?;
+                        res.write_binary(&stat.st_ino.to_be_bytes())?;
+                        res.write_binary(&(stat.st_mode.bits()).to_be_bytes())?;
+                        res.write_binary(&stat.st_nlink.to_be_bytes())?;
+                        res.write_binary(&stat.st_uid.to_be_bytes())?;
+                        res.write_binary(&stat.st_gid.to_be_bytes())?;
+                        res.write_binary(&stat.st_rdev.to_be_bytes())?;
+                        res.write_binary(&stat.st_size.to_be_bytes())?;
+                        res.write_binary(&stat.st_blksize.to_be_bytes())?;
+                        res.write_binary(&stat.st_blocks.to_be_bytes())?;
+                        res.write_binary(&stat.st_atime.to_be_bytes())?;
+                        res.write_binary(&stat.st_mtime.to_be_bytes())?;
+                        res.write_binary(&stat.st_ctime.to_be_bytes())?;
+                    }
+                };
+                HandlerStatus::Handled
+            }
+            HostIo::vFileUnlink(cmd) if ops.support_unlink().is_some() => {
+                let ops = ops.support_unlink().unwrap();
+                handle_hostio_result! {
+                    if let Ok(()) = ops.unlink(cmd.filename) => {
+                        res.write_str("F0")?;
+                    }
+                };
+                HandlerStatus::Handled
+            }
+            HostIo::vFileReadlink(cmd) if ops.support_readlink().is_some() => {
+                let ops = ops.support_readlink().unwrap();
+                handle_hostio_result! {
+                    if let Ok(ret) = ops.readlink(cmd.filename, cmd.buf) => {
+                        res.write_str("F")?;
+                        res.write_num(ret)?;
+                        res.write_str(";")?;
+                        res.write_binary(cmd.buf.get(..ret).ok_or(Error::PacketBufferOverflow)?)?;
+                    }
+                };
+
+                HandlerStatus::Handled
+            }
+            HostIo::vFileSetfs(cmd) if ops.support_setfs().is_some() => {
+                let ops = ops.support_setfs().unwrap();
+                handle_hostio_result! {
+                    if let Ok(()) = ops.setfs(cmd.fs) => {
+                        res.write_str("F0")?;
+                    }
+                };
+                HandlerStatus::Handled
+            }
+            _ => HandlerStatus::Handled,
+        };
+
+        Ok(handler_status)
+    }
+}
diff --git a/src/stub/core_impl/memory_map.rs b/src/stub/core_impl/memory_map.rs
new file mode 100644
index 0000000..638efb3
--- /dev/null
+++ b/src/stub/core_impl/memory_map.rs
@@ -0,0 +1,36 @@
+use super::prelude::*;
+use crate::protocol::commands::ext::MemoryMap;
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    pub(crate) fn handle_memory_map(
+        &mut self,
+        res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: MemoryMap<'_>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let ops = match target.support_memory_map() {
+            Some(ops) => ops,
+            None => return Ok(HandlerStatus::Handled),
+        };
+
+        crate::__dead_code_marker!("memory_map", "impl");
+
+        let handler_status = match command {
+            MemoryMap::qXferMemoryMapRead(cmd) => {
+                let ret = ops
+                    .memory_map_xml(cmd.offset, cmd.length, cmd.buf)
+                    .handle_error()?;
+                if ret == 0 {
+                    res.write_str("l")?;
+                } else {
+                    res.write_str("m")?;
+                    // TODO: add more specific error variant?
+                    res.write_binary(cmd.buf.get(..ret).ok_or(Error::PacketBufferOverflow)?)?;
+                }
+                HandlerStatus::Handled
+            }
+        };
+
+        Ok(handler_status)
+    }
+}
diff --git a/src/gdbstub_impl/ext/monitor_cmd.rs b/src/stub/core_impl/monitor_cmd.rs
similarity index 89%
rename from src/gdbstub_impl/ext/monitor_cmd.rs
rename to src/stub/core_impl/monitor_cmd.rs
index cfc8821..ea191ab 100644
--- a/src/gdbstub_impl/ext/monitor_cmd.rs
+++ b/src/stub/core_impl/monitor_cmd.rs
@@ -6,11 +6,11 @@
 impl<T: Target, C: Connection> GdbStubImpl<T, C> {
     pub(crate) fn handle_monitor_cmd<'a>(
         &mut self,
-        res: &mut ResponseWriter<C>,
+        res: &mut ResponseWriter<'_, C>,
         target: &mut T,
         command: MonitorCmd<'a>,
     ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
-        let ops = match target.monitor_cmd() {
+        let ops = match target.support_monitor_cmd() {
             Some(ops) => ops,
             None => return Ok(HandlerStatus::Handled),
         };
@@ -19,11 +19,13 @@
 
         let handler_status = match command {
             MonitorCmd::qRcmd(cmd) => {
+                let use_rle = ops.use_rle();
+
                 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());
+                        let mut res = ResponseWriter::new(res.as_conn(), use_rle);
                         res.write_str("O")?;
                         res.write_hex_buf(msg)?;
                         res.flush()?;
diff --git a/src/stub/core_impl/resume.rs b/src/stub/core_impl/resume.rs
new file mode 100644
index 0000000..6f39eab
--- /dev/null
+++ b/src/stub/core_impl/resume.rs
@@ -0,0 +1,432 @@
+use super::prelude::*;
+use crate::protocol::commands::ext::Resume;
+
+use crate::arch::Arch;
+use crate::common::{Signal, Tid};
+use crate::protocol::commands::_vCont::Actions;
+use crate::protocol::{SpecificIdKind, SpecificThreadId};
+use crate::stub::MultiThreadStopReason;
+use crate::target::ext::base::reverse_exec::ReplayLogPosition;
+use crate::target::ext::base::ResumeOps;
+use crate::target::ext::catch_syscalls::CatchSyscallPosition;
+use crate::FAKE_PID;
+
+use super::DisconnectReason;
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    pub(crate) fn handle_stop_resume<'a>(
+        &mut self,
+        res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: Resume<'a>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let mut ops = match target.base_ops().resume_ops() {
+            Some(ops) => ops,
+            None => return Ok(HandlerStatus::Handled),
+        };
+
+        let actions = match command {
+            Resume::vCont(cmd) => {
+                use crate::protocol::commands::_vCont::vCont;
+                match cmd {
+                    vCont::Query => {
+                        // Continue is part of the base protocol
+                        res.write_str("vCont;c;C")?;
+
+                        // Single stepping is optional
+                        if match &mut ops {
+                            ResumeOps::SingleThread(ops) => ops.support_single_step().is_some(),
+                            ResumeOps::MultiThread(ops) => ops.support_single_step().is_some(),
+                        } {
+                            res.write_str(";s;S")?;
+                        }
+
+                        // Range stepping is optional
+                        if match &mut ops {
+                            ResumeOps::SingleThread(ops) => ops.support_range_step().is_some(),
+                            ResumeOps::MultiThread(ops) => ops.support_range_step().is_some(),
+                        } {
+                            res.write_str(";r")?;
+                        }
+
+                        // doesn't actually invoke vCont
+                        return Ok(HandlerStatus::Handled);
+                    }
+                    vCont::Actions(actions) => actions,
+                }
+            }
+            // TODO?: support custom resume addr in 'c' and 's'
+            //
+            // vCont doesn't have a notion of "resume addr", and since the implementation of these
+            // packets reuse vCont infrastructure, supporting this obscure feature will be a bit
+            // annoying...
+            //
+            // TODO: add `support_legacy_s_c_packets` flag (similar to `use_X_packet`)
+            Resume::c(_) => Actions::new_continue(SpecificThreadId {
+                pid: None,
+                tid: self.current_resume_tid,
+            }),
+            Resume::s(_) => Actions::new_step(SpecificThreadId {
+                pid: None,
+                tid: self.current_resume_tid,
+            }),
+        };
+
+        self.do_vcont(ops, actions)
+    }
+
+    fn do_vcont_single_thread(
+        ops: &mut dyn crate::target::ext::base::singlethread::SingleThreadResume<
+            Arch = T::Arch,
+            Error = T::Error,
+        >,
+        actions: &Actions<'_>,
+    ) -> Result<(), Error<T::Error, C::Error>> {
+        use crate::protocol::commands::_vCont::VContKind;
+
+        let mut actions = actions.iter();
+        let first_action = actions
+            .next()
+            .ok_or(Error::PacketParse(
+                crate::protocol::PacketParseError::MalformedCommand,
+            ))?
+            .ok_or(Error::PacketParse(
+                crate::protocol::PacketParseError::MalformedCommand,
+            ))?;
+
+        let invalid_second_action = match actions.next() {
+            None => false,
+            Some(act) => match act {
+                None => {
+                    return Err(Error::PacketParse(
+                        crate::protocol::PacketParseError::MalformedCommand,
+                    ))
+                }
+                Some(act) => !matches!(act.kind, VContKind::Continue),
+            },
+        };
+
+        if invalid_second_action || actions.next().is_some() {
+            return Err(Error::PacketUnexpected);
+        }
+
+        match first_action.kind {
+            VContKind::Continue | VContKind::ContinueWithSig(_) => {
+                let signal = match first_action.kind {
+                    VContKind::ContinueWithSig(sig) => Some(sig),
+                    _ => None,
+                };
+
+                ops.resume(signal).map_err(Error::TargetError)?;
+                Ok(())
+            }
+            VContKind::Step | VContKind::StepWithSig(_) if ops.support_single_step().is_some() => {
+                let ops = ops.support_single_step().unwrap();
+
+                let signal = match first_action.kind {
+                    VContKind::StepWithSig(sig) => Some(sig),
+                    _ => None,
+                };
+
+                ops.step(signal).map_err(Error::TargetError)?;
+                Ok(())
+            }
+            VContKind::RangeStep(start, end) if ops.support_range_step().is_some() => {
+                let ops = ops.support_range_step().unwrap();
+
+                let start = start.decode().map_err(|_| Error::TargetMismatch)?;
+                let end = end.decode().map_err(|_| Error::TargetMismatch)?;
+
+                ops.resume_range_step(start, end)
+                    .map_err(Error::TargetError)?;
+                Ok(())
+            }
+            // TODO: update this case when non-stop mode is implemented
+            VContKind::Stop => Err(Error::PacketUnexpected),
+
+            // Instead of using `_ =>`, explicitly list out any remaining unguarded cases.
+            VContKind::RangeStep(..) | VContKind::Step | VContKind::StepWithSig(..) => {
+                error!("GDB client sent resume action not reported by `vCont?`");
+                Err(Error::PacketUnexpected)
+            }
+        }
+    }
+
+    fn do_vcont_multi_thread(
+        ops: &mut dyn crate::target::ext::base::multithread::MultiThreadResume<
+            Arch = T::Arch,
+            Error = T::Error,
+        >,
+        actions: &Actions<'_>,
+    ) -> Result<(), Error<T::Error, C::Error>> {
+        ops.clear_resume_actions().map_err(Error::TargetError)?;
+
+        for action in actions.iter() {
+            use crate::protocol::commands::_vCont::VContKind;
+
+            let action = action.ok_or(Error::PacketParse(
+                crate::protocol::PacketParseError::MalformedCommand,
+            ))?;
+
+            match action.kind {
+                VContKind::Continue | VContKind::ContinueWithSig(_) => {
+                    let signal = match action.kind {
+                        VContKind::ContinueWithSig(sig) => Some(sig),
+                        _ => None,
+                    };
+
+                    match action.thread.map(|thread| thread.tid) {
+                        // An action with no thread-id matches all threads
+                        None | Some(SpecificIdKind::All) => {
+                            // Target API contract specifies that the default
+                            // resume action for all threads is continue.
+                        }
+                        Some(SpecificIdKind::WithId(tid)) => ops
+                            .set_resume_action_continue(tid, signal)
+                            .map_err(Error::TargetError)?,
+                    }
+                }
+                VContKind::Step | VContKind::StepWithSig(_)
+                    if ops.support_single_step().is_some() =>
+                {
+                    let ops = ops.support_single_step().unwrap();
+
+                    let signal = match action.kind {
+                        VContKind::StepWithSig(sig) => Some(sig),
+                        _ => None,
+                    };
+
+                    match action.thread.map(|thread| thread.tid) {
+                        // An action with no thread-id matches all threads
+                        None | Some(SpecificIdKind::All) => {
+                            error!("GDB client sent 'step' as default resume action");
+                            return Err(Error::PacketUnexpected);
+                        }
+                        Some(SpecificIdKind::WithId(tid)) => {
+                            ops.set_resume_action_step(tid, signal)
+                                .map_err(Error::TargetError)?;
+                        }
+                    };
+                }
+
+                VContKind::RangeStep(start, end) if ops.support_range_step().is_some() => {
+                    let ops = ops.support_range_step().unwrap();
+
+                    match action.thread.map(|thread| thread.tid) {
+                        // An action with no thread-id matches all threads
+                        None | Some(SpecificIdKind::All) => {
+                            error!("GDB client sent 'range step' as default resume action");
+                            return Err(Error::PacketUnexpected);
+                        }
+                        Some(SpecificIdKind::WithId(tid)) => {
+                            let start = start.decode().map_err(|_| Error::TargetMismatch)?;
+                            let end = end.decode().map_err(|_| Error::TargetMismatch)?;
+
+                            ops.set_resume_action_range_step(tid, start, end)
+                                .map_err(Error::TargetError)?;
+                        }
+                    };
+                }
+                // TODO: update this case when non-stop mode is implemented
+                VContKind::Stop => return Err(Error::PacketUnexpected),
+
+                // Instead of using `_ =>`, explicitly list out any remaining unguarded cases.
+                VContKind::RangeStep(..) | VContKind::Step | VContKind::StepWithSig(..) => {
+                    error!("GDB client sent resume action not reported by `vCont?`");
+                    return Err(Error::PacketUnexpected);
+                }
+            }
+        }
+
+        ops.resume().map_err(Error::TargetError)
+    }
+
+    fn do_vcont(
+        &mut self,
+        ops: ResumeOps<'_, T::Arch, T::Error>,
+        actions: Actions<'_>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        match ops {
+            ResumeOps::SingleThread(ops) => Self::do_vcont_single_thread(ops, &actions)?,
+            ResumeOps::MultiThread(ops) => Self::do_vcont_multi_thread(ops, &actions)?,
+        };
+
+        Ok(HandlerStatus::DeferredStopReason)
+    }
+
+    fn write_stop_common(
+        &mut self,
+        res: &mut ResponseWriter<'_, C>,
+        tid: Option<Tid>,
+        signal: Signal,
+    ) -> Result<(), Error<T::Error, C::Error>> {
+        res.write_str("T")?;
+        res.write_num(signal as u8)?;
+
+        if let Some(tid) = tid {
+            self.current_mem_tid = tid;
+            self.current_resume_tid = SpecificIdKind::WithId(tid);
+
+            res.write_str("thread:")?;
+            res.write_specific_thread_id(SpecificThreadId {
+                pid: self
+                    .features
+                    .multiprocess()
+                    .then(|| SpecificIdKind::WithId(FAKE_PID)),
+                tid: SpecificIdKind::WithId(tid),
+            })?;
+            res.write_str(";")?;
+        }
+
+        Ok(())
+    }
+
+    pub(crate) fn finish_exec(
+        &mut self,
+        res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        stop_reason: MultiThreadStopReason<<T::Arch as Arch>::Usize>,
+    ) -> Result<FinishExecStatus, Error<T::Error, C::Error>> {
+        macro_rules! guard_reverse_exec {
+            () => {{
+                if let Some(resume_ops) = target.base_ops().resume_ops() {
+                    let (reverse_cont, reverse_step) = match resume_ops {
+                        ResumeOps::MultiThread(ops) => (
+                            ops.support_reverse_cont().is_some(),
+                            ops.support_reverse_step().is_some(),
+                        ),
+                        ResumeOps::SingleThread(ops) => (
+                            ops.support_reverse_cont().is_some(),
+                            ops.support_reverse_step().is_some(),
+                        ),
+                    };
+
+                    reverse_cont || reverse_step
+                } else {
+                    false
+                }
+            }};
+        }
+
+        macro_rules! guard_break {
+            ($op:ident) => {
+                target
+                    .support_breakpoints()
+                    .and_then(|ops| ops.$op())
+                    .is_some()
+            };
+        }
+
+        macro_rules! guard_catch_syscall {
+            () => {
+                target.support_catch_syscalls().is_some()
+            };
+        }
+
+        let status = match stop_reason {
+            MultiThreadStopReason::DoneStep => {
+                res.write_str("S")?;
+                res.write_num(Signal::SIGTRAP as u8)?;
+                FinishExecStatus::Handled
+            }
+            MultiThreadStopReason::Signal(sig) => {
+                res.write_str("S")?;
+                res.write_num(sig as u8)?;
+                FinishExecStatus::Handled
+            }
+            MultiThreadStopReason::Exited(code) => {
+                res.write_str("W")?;
+                res.write_num(code)?;
+                FinishExecStatus::Disconnect(DisconnectReason::TargetExited(code))
+            }
+            MultiThreadStopReason::Terminated(sig) => {
+                res.write_str("X")?;
+                res.write_num(sig as u8)?;
+                FinishExecStatus::Disconnect(DisconnectReason::TargetTerminated(sig))
+            }
+            MultiThreadStopReason::SignalWithThread { tid, signal } => {
+                self.write_stop_common(res, Some(tid), signal)?;
+                FinishExecStatus::Handled
+            }
+            MultiThreadStopReason::SwBreak(tid) if guard_break!(support_sw_breakpoint) => {
+                crate::__dead_code_marker!("sw_breakpoint", "stop_reason");
+
+                self.write_stop_common(res, Some(tid), Signal::SIGTRAP)?;
+                res.write_str("swbreak:;")?;
+                FinishExecStatus::Handled
+            }
+            MultiThreadStopReason::HwBreak(tid) if guard_break!(support_hw_breakpoint) => {
+                crate::__dead_code_marker!("hw_breakpoint", "stop_reason");
+
+                self.write_stop_common(res, Some(tid), Signal::SIGTRAP)?;
+                res.write_str("hwbreak:;")?;
+                FinishExecStatus::Handled
+            }
+            MultiThreadStopReason::Watch { tid, kind, addr }
+                if guard_break!(support_hw_watchpoint) =>
+            {
+                crate::__dead_code_marker!("hw_watchpoint", "stop_reason");
+
+                self.write_stop_common(res, Some(tid), Signal::SIGTRAP)?;
+
+                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)?;
+                res.write_str(";")?;
+                FinishExecStatus::Handled
+            }
+            MultiThreadStopReason::ReplayLog { tid, pos } if guard_reverse_exec!() => {
+                crate::__dead_code_marker!("reverse_exec", "stop_reason");
+
+                self.write_stop_common(res, tid, Signal::SIGTRAP)?;
+
+                res.write_str("replaylog:")?;
+                res.write_str(match pos {
+                    ReplayLogPosition::Begin => "begin",
+                    ReplayLogPosition::End => "end",
+                })?;
+                res.write_str(";")?;
+
+                FinishExecStatus::Handled
+            }
+            MultiThreadStopReason::CatchSyscall {
+                tid,
+                number,
+                position,
+            } if guard_catch_syscall!() => {
+                crate::__dead_code_marker!("catch_syscall", "stop_reason");
+
+                self.write_stop_common(res, tid, Signal::SIGTRAP)?;
+
+                res.write_str(match position {
+                    CatchSyscallPosition::Entry => "syscall_entry:",
+                    CatchSyscallPosition::Return => "syscall_return:",
+                })?;
+                res.write_num(number)?;
+                res.write_str(";")?;
+
+                FinishExecStatus::Handled
+            }
+            // Explicitly avoid using `_ =>` to handle the "unguarded" variants, as doing so would
+            // squelch the useful compiler error that crops up whenever stop reasons are added.
+            MultiThreadStopReason::SwBreak(_)
+            | MultiThreadStopReason::HwBreak(_)
+            | MultiThreadStopReason::Watch { .. }
+            | MultiThreadStopReason::ReplayLog { .. }
+            | MultiThreadStopReason::CatchSyscall { .. } => {
+                return Err(Error::UnsupportedStopReason);
+            }
+        };
+
+        Ok(status)
+    }
+}
+
+pub(crate) enum FinishExecStatus {
+    Handled,
+    Disconnect(DisconnectReason),
+}
diff --git a/src/stub/core_impl/reverse_exec.rs b/src/stub/core_impl/reverse_exec.rs
new file mode 100644
index 0000000..7b85d38
--- /dev/null
+++ b/src/stub/core_impl/reverse_exec.rs
@@ -0,0 +1,110 @@
+use super::prelude::*;
+use crate::protocol::commands::ext::{ReverseCont, ReverseStep};
+
+use crate::arch::Arch;
+use crate::common::Tid;
+use crate::protocol::SpecificIdKind;
+use crate::target::ext::base::reverse_exec::{
+    ReverseCont as ReverseContTrait, ReverseStep as ReverseStepTrait,
+};
+use crate::target::ext::base::ResumeOps;
+
+macro_rules! defn_ops {
+    ($name:ident, $reverse_trait:ident, $f:ident) => {
+        enum $name<'a, A: Arch, E> {
+            SingleThread(&'a mut dyn $reverse_trait<(), Arch = A, Error = E>),
+            MultiThread(&'a mut dyn $reverse_trait<Tid, Arch = A, Error = E>),
+        }
+
+        impl<'a, A, E> $name<'a, A, E>
+        where
+            A: Arch,
+        {
+            #[inline(always)]
+            fn from_target<T>(target: &mut T) -> Option<$name<'_, T::Arch, T::Error>>
+            where
+                T: Target,
+            {
+                let ops = match target.base_ops().resume_ops()? {
+                    ResumeOps::SingleThread(ops) => $name::SingleThread(ops.$f()?),
+                    ResumeOps::MultiThread(ops) => $name::MultiThread(ops.$f()?),
+                };
+                Some(ops)
+            }
+        }
+    };
+}
+
+defn_ops!(ReverseContOps, ReverseContTrait, support_reverse_cont);
+defn_ops!(ReverseStepOps, ReverseStepTrait, support_reverse_step);
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    pub(crate) fn handle_reverse_cont(
+        &mut self,
+        _res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: ReverseCont,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let ops = match ReverseContOps::<'_, T::Arch, T::Error>::from_target(target) {
+            Some(ops) => ops,
+            None => return Ok(HandlerStatus::Handled),
+        };
+
+        crate::__dead_code_marker!("reverse_cont", "impl");
+
+        let handler_status = match command {
+            ReverseCont::bc(_) => {
+                match ops {
+                    ReverseContOps::MultiThread(ops) => {
+                        ops.reverse_cont().map_err(Error::TargetError)?
+                    }
+                    ReverseContOps::SingleThread(ops) => {
+                        ops.reverse_cont().map_err(Error::TargetError)?
+                    }
+                }
+
+                HandlerStatus::DeferredStopReason
+            }
+        };
+
+        Ok(handler_status)
+    }
+
+    // FIXME: De-duplicate with above code?
+    pub(crate) fn handle_reverse_step(
+        &mut self,
+        _res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: ReverseStep,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        let ops = match ReverseStepOps::<'_, T::Arch, T::Error>::from_target(target) {
+            Some(ops) => ops,
+            None => return Ok(HandlerStatus::Handled),
+        };
+
+        crate::__dead_code_marker!("reverse_step", "impl");
+
+        let handler_status = match command {
+            ReverseStep::bs(_) => {
+                let tid = match self.current_resume_tid {
+                    // NOTE: Can't single-step all cores.
+                    SpecificIdKind::All => return Err(Error::PacketUnexpected),
+                    SpecificIdKind::WithId(tid) => tid,
+                };
+
+                match ops {
+                    ReverseStepOps::MultiThread(ops) => {
+                        ops.reverse_step(tid).map_err(Error::TargetError)?
+                    }
+                    ReverseStepOps::SingleThread(ops) => {
+                        ops.reverse_step(()).map_err(Error::TargetError)?
+                    }
+                }
+
+                HandlerStatus::DeferredStopReason
+            }
+        };
+
+        Ok(handler_status)
+    }
+}
diff --git a/src/gdbstub_impl/ext/section_offsets.rs b/src/stub/core_impl/section_offsets.rs
similarity index 95%
rename from src/gdbstub_impl/ext/section_offsets.rs
rename to src/stub/core_impl/section_offsets.rs
index 3fa7e04..6af9dde 100644
--- a/src/gdbstub_impl/ext/section_offsets.rs
+++ b/src/stub/core_impl/section_offsets.rs
@@ -4,11 +4,11 @@
 impl<T: Target, C: Connection> GdbStubImpl<T, C> {
     pub(crate) fn handle_section_offsets(
         &mut self,
-        res: &mut ResponseWriter<C>,
+        res: &mut ResponseWriter<'_, C>,
         target: &mut T,
         command: SectionOffsets,
     ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
-        let ops = match target.section_offsets() {
+        let ops = match target.support_section_offsets() {
             Some(ops) => ops,
             None => return Ok(HandlerStatus::Handled),
         };
diff --git a/src/stub/core_impl/single_register_access.rs b/src/stub/core_impl/single_register_access.rs
new file mode 100644
index 0000000..fd61c58
--- /dev/null
+++ b/src/stub/core_impl/single_register_access.rs
@@ -0,0 +1,77 @@
+use super::prelude::*;
+use crate::protocol::commands::ext::SingleRegisterAccess;
+
+use crate::arch::{Arch, RegId};
+use crate::target::ext::base::BaseOps;
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    fn inner<Tid>(
+        res: &mut ResponseWriter<'_, C>,
+        ops: crate::target::ext::base::single_register_access::SingleRegisterAccessOps<'_, Tid, T>,
+        command: SingleRegisterAccess<'_>,
+        id: Tid,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>>
+    where
+        Tid: crate::is_valid_tid::IsValidTid,
+    {
+        let handler_status = match command {
+            SingleRegisterAccess::p(p) => {
+                let reg = <T::Arch as Arch>::RegId::from_raw_id(p.reg_id);
+                let (reg_id, reg_size) = match reg {
+                    None => {
+                        warn!("reg id {} does not map onto any known register", p.reg_id);
+                        return Ok(HandlerStatus::Handled);
+                    }
+                    Some(v) => v,
+                };
+                let mut buf = p.buf;
+                if let Some(size) = reg_size {
+                    buf = buf
+                        .get_mut(..size.get())
+                        .ok_or(Error::PacketBufferOverflow)?;
+                }
+
+                let len = ops.read_register(id, reg_id, buf).handle_error()?;
+
+                if let Some(size) = reg_size {
+                    if size.get() != len {
+                        return Err(Error::TargetMismatch);
+                    }
+                } else {
+                    buf = buf.get_mut(..len).ok_or(Error::PacketBufferOverflow)?;
+                }
+                res.write_hex_buf(buf)?;
+                HandlerStatus::Handled
+            }
+            SingleRegisterAccess::P(p) => {
+                let reg = <T::Arch as Arch>::RegId::from_raw_id(p.reg_id);
+                match reg {
+                    // empty packet indicates unrecognized query
+                    None => return Ok(HandlerStatus::Handled),
+                    Some((reg_id, _)) => ops.write_register(id, reg_id, p.val).handle_error()?,
+                }
+                HandlerStatus::NeedsOk
+            }
+        };
+
+        Ok(handler_status)
+    }
+
+    pub(crate) fn handle_single_register_access<'a>(
+        &mut self,
+        res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: SingleRegisterAccess<'a>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        match target.base_ops() {
+            BaseOps::SingleThread(ops) => match ops.support_single_register_access() {
+                None => Ok(HandlerStatus::Handled),
+                Some(ops) => Self::inner(res, ops, command, ()),
+            },
+            BaseOps::MultiThread(ops) => match ops.support_single_register_access() {
+                None => Ok(HandlerStatus::Handled),
+                Some(ops) => Self::inner(res, ops, command, self.current_mem_tid),
+            },
+        }
+    }
+}
diff --git a/src/stub/core_impl/target_xml.rs b/src/stub/core_impl/target_xml.rs
new file mode 100644
index 0000000..15b1c35
--- /dev/null
+++ b/src/stub/core_impl/target_xml.rs
@@ -0,0 +1,63 @@
+use super::prelude::*;
+use crate::protocol::commands::ext::TargetXml;
+
+use crate::arch::Arch;
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    pub(crate) fn handle_target_xml(
+        &mut self,
+        res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: TargetXml<'_>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        if !target.use_target_description_xml() {
+            return Ok(HandlerStatus::Handled);
+        }
+
+        let handler_status = match command {
+            TargetXml::qXferFeaturesRead(cmd) => {
+                let ret = if let Some(ops) = target.support_target_description_xml_override() {
+                    ops.target_description_xml(cmd.annex.name, cmd.offset, cmd.length, cmd.buf)
+                        .handle_error()?
+                } else if let Some(xml) = T::Arch::target_description_xml() {
+                    if cmd.annex.name != b"target.xml" {
+                        // TODO: not the best error... should probably report to the user the
+                        // <xi:include> isn't supported at the Arch level (yet)
+                        return Err(Error::PacketUnexpected);
+                    }
+
+                    let xml = xml.trim().as_bytes();
+                    let xml_len = xml.len();
+
+                    let start = xml_len.min(cmd.offset as usize);
+                    let end = xml_len.min(cmd.offset as usize + cmd.length);
+
+                    // LLVM isn't smart enough to realize that `end` will always be greater than
+                    // `start`, and fails to elide the `slice_index_order_fail` check unless we
+                    // include this seemingly useless call to `max`.
+                    let data = &xml[start..end.max(start)];
+
+                    let n = data.len().min(cmd.buf.len());
+                    cmd.buf[..n].copy_from_slice(&data[..n]);
+                    n
+                } else {
+                    // 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.
+                    return Err(Error::PacketUnexpected);
+                };
+
+                if ret == 0 {
+                    res.write_str("l")?;
+                } else {
+                    res.write_str("m")?;
+                    // TODO: add more specific error variant?
+                    res.write_binary(cmd.buf.get(..ret).ok_or(Error::PacketBufferOverflow)?)?;
+                }
+                HandlerStatus::Handled
+            }
+        };
+
+        Ok(handler_status)
+    }
+}
diff --git a/src/stub/core_impl/x_upcase_packet.rs b/src/stub/core_impl/x_upcase_packet.rs
new file mode 100644
index 0000000..f35f846
--- /dev/null
+++ b/src/stub/core_impl/x_upcase_packet.rs
@@ -0,0 +1,38 @@
+use super::prelude::*;
+use crate::protocol::commands::ext::XUpcasePacket;
+
+use crate::arch::Arch;
+use crate::target::ext::base::BaseOps;
+
+impl<T: Target, C: Connection> GdbStubImpl<T, C> {
+    pub(crate) fn handle_x_upcase_packet<'a>(
+        &mut self,
+        _res: &mut ResponseWriter<'_, C>,
+        target: &mut T,
+        command: XUpcasePacket<'a>,
+    ) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
+        if !target.use_x_upcase_packet() {
+            return Ok(HandlerStatus::Handled);
+        }
+
+        crate::__dead_code_marker!("x_upcase_packet", "impl");
+
+        let handler_status = match command {
+            XUpcasePacket::X(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
+            }
+        };
+        Ok(handler_status)
+    }
+}
diff --git a/src/stub/error.rs b/src/stub/error.rs
new file mode 100644
index 0000000..6d55997
--- /dev/null
+++ b/src/stub/error.rs
@@ -0,0 +1,129 @@
+use core::fmt::{self, Debug, Display};
+
+use crate::arch::SingleStepGdbBehavior;
+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 initializing the session.
+    ConnectionInit(C),
+    /// Connection Error while reading request.
+    ConnectionRead(C),
+    /// Connection Error while writing response.
+    ConnectionWrite(C),
+
+    /// Client nack'd the last packet, but `gdbstub` doesn't implement
+    /// re-transmission.
+    ClientSentNack,
+    /// Packet cannot fit in the provided packet buffer.
+    PacketBufferOverflow,
+    /// Could not parse the packet into a valid command.
+    PacketParse(PacketParseError),
+    /// GDB client sent an unexpected packet. This should never happen!
+    /// Please re-run with `log` trace-level logging enabled and file an issue
+    /// at <https://github.com/daniel5151/gdbstub/issues>
+    PacketUnexpected,
+    /// GDB client sent a packet with too much data for the given target.
+    TargetMismatch,
+    /// Target encountered a fatal error.
+    TargetError(T),
+    /// Target responded with an unsupported stop reason.
+    ///
+    /// Certain stop reasons can only be used when their associated protocol
+    /// feature has been implemented. e.g: a Target cannot return a
+    /// `StopReason::HwBreak` if the hardware breakpoints IDET hasn't been
+    /// implemented.
+    UnsupportedStopReason,
+    /// Target didn't report any active threads when there should have been at
+    /// least one running.
+    NoActiveThreads,
+
+    /// The target has not opted into using implicit software breakpoints.
+    /// See [`Target::guard_rail_implicit_sw_breakpoints`] for more information.
+    ///
+    /// [`Target::guard_rail_implicit_sw_breakpoints`]:
+    /// crate::target::Target::guard_rail_implicit_sw_breakpoints
+    ImplicitSwBreakpoints,
+    /// The target has not indicated support for optional single stepping. See
+    /// [`Target::guard_rail_single_step_gdb_behavior`] for more information.
+    ///
+    /// If you encountered this error while using an `Arch` implementation
+    /// defined in `gdbstub_arch` and believe this is incorrect, please file an
+    /// issue at <https://github.com/daniel5151/gdbstub/issues>.
+    ///
+    /// [`Target::guard_rail_single_step_gdb_behavior`]:
+    /// crate::target::Target::guard_rail_single_step_gdb_behavior
+    SingleStepGdbBehavior(SingleStepGdbBehavior),
+
+    // Internal - A non-fatal error occurred (with errno-style error code)
+    //
+    // This "dummy" error is required as part of the internal
+    // `TargetResultExt::handle_error()` machinery, and will never be
+    // propagated up to the end user.
+    #[doc(hidden)]
+    NonFatalError(u8),
+}
+
+impl<T, C> From<ResponseWriterError<C>> for GdbStubError<T, C> {
+    fn from(e: ResponseWriterError<C>) -> Self {
+        GdbStubError::ConnectionWrite(e.0)
+    }
+}
+
+impl<A, T, C> From<CapacityError<A>> for GdbStubError<T, C> {
+    fn from(_: CapacityError<A>) -> Self {
+        GdbStubError::PacketBufferOverflow
+    }
+}
+
+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 {
+            ConnectionInit(e) => write!(f, "Connection Error while initializing the session: {:?}", e),
+            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."),
+            PacketBufferOverflow => 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. Please re-run with `log` trace-level logging enabled and file an issue at https://github.com/daniel5151/gdbstub/issues"),
+            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),
+            UnsupportedStopReason => write!(f, "Target responded with an unsupported stop reason."),
+            NoActiveThreads => write!(f, "Target didn't report any active threads when there should have been at least one running."),
+
+            ImplicitSwBreakpoints => write!(f, "Warning: The target has not opted into using implicit software breakpoints. See `Target::guard_rail_implicit_sw_breakpoints` for more information."),
+            SingleStepGdbBehavior(behavior) => {
+                use crate::arch::SingleStepGdbBehavior;
+                write!(
+                    f,
+                    "Warning: Mismatch between the targets' single-step support and arch-level single-step behavior: {} ",
+                    match behavior {
+                        SingleStepGdbBehavior::Optional => "", // unreachable, since optional single step will not result in an error
+                        SingleStepGdbBehavior::Required => "GDB requires single-step support on this arch.",
+                        SingleStepGdbBehavior::Ignored => "GDB ignores single-step support on this arch, yet the target has implemented support for it.",
+                        SingleStepGdbBehavior::Unknown => "This arch's single-step behavior hasn't been tested yet: please conduct a test + upstream your findings!",
+                    }
+                )?;
+                write!(f, "See `Target::guard_rail_single_step_gdb_behavior` for more information.")
+            },
+
+            NonFatalError(_) => write!(f, "Internal non-fatal error. End users should never see this! Please file an issue if you do!"),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl<T, C> std::error::Error for GdbStubError<T, C>
+where
+    C: Debug,
+    T: Debug,
+{
+}
diff --git a/src/stub/mod.rs b/src/stub/mod.rs
new file mode 100644
index 0000000..8cf08d9
--- /dev/null
+++ b/src/stub/mod.rs
@@ -0,0 +1,267 @@
+//! The core [`GdbStub`] type, used to drive a GDB debugging session for a
+//! particular [`Target`] over a given [`Connection`].
+
+use managed::ManagedSlice;
+
+use crate::conn::{Connection, ConnectionExt};
+use crate::target::Target;
+
+mod builder;
+mod core_impl;
+mod error;
+mod stop_reason;
+
+pub mod state_machine;
+
+pub use builder::{GdbStubBuilder, GdbStubBuilderError};
+pub use core_impl::DisconnectReason;
+pub use error::GdbStubError;
+pub use stop_reason::{
+    BaseStopReason, IntoStopReason, MultiThreadStopReason, SingleThreadStopReason,
+};
+
+use GdbStubError as Error;
+
+/// Types and traits related to the [`GdbStub::run_blocking`] interface.
+pub mod run_blocking {
+    use super::*;
+
+    use crate::conn::ConnectionExt;
+
+    /// A set of user-provided methods required to run a GDB debugging session
+    /// using the [`GdbStub::run_blocking`] method.
+    ///
+    /// Reminder: to use `gdbstub` in a non-blocking manner (e.g: via
+    /// async/await, unix polling, from an interrupt handler, etc...) you will
+    /// need to interface with the
+    /// [`GdbStubStateMachine`](state_machine::GdbStubStateMachine) API
+    /// directly.
+    pub trait BlockingEventLoop {
+        /// The Target being driven.
+        type Target: Target;
+        /// Connection being used to drive the target.
+        type Connection: ConnectionExt;
+
+        /// Which variant of the `StopReason` type should be used. Single
+        /// threaded targets should use [`SingleThreadStopReason`], whereas
+        /// multi threaded targets should use [`MultiThreadStopReason`].
+        ///
+        /// [`SingleThreadStopReason`]: crate::stub::SingleThreadStopReason
+        /// [`MultiThreadStopReason`]: crate::stub::MultiThreadStopReason
+        type StopReason: IntoStopReason<Self::Target>;
+
+        /// Invoked immediately after the target's `resume` method has been
+        /// called. The implementation should block until either the target
+        /// reports a stop reason, or if new data was sent over the connection.
+        ///
+        /// The specific mechanism to "select" between these two events is
+        /// implementation specific. Some examples might include: `epoll`,
+        /// `select!` across multiple event channels, periodic polling, etc...
+        fn wait_for_stop_reason(
+            target: &mut Self::Target,
+            conn: &mut Self::Connection,
+        ) -> Result<
+            Event<Self::StopReason>,
+            WaitForStopReasonError<
+                <Self::Target as Target>::Error,
+                <Self::Connection as Connection>::Error,
+            >,
+        >;
+
+        /// Invoked when the GDB client sends a Ctrl-C interrupt.
+        ///
+        /// Depending on how the target is implemented, it may or may not make
+        /// sense to immediately return a stop reason as part of handling the
+        /// Ctrl-C interrupt. e.g: in some cases, it may be better to send the
+        /// target a signal upon receiving a Ctrl-C interrupt _without_
+        /// immediately sending a stop reason, and instead deferring the stop
+        /// reason to some later point in the target's execution.
+        ///
+        /// _Suggestion_: If you're unsure which stop reason to report,
+        /// [`BaseStopReason::Signal(Signal::SIGINT)`] is a sensible default.
+        ///
+        /// [`BaseStopReason::Signal(Signal::SIGINT)`]:
+        /// crate::stub::BaseStopReason::Signal
+        fn on_interrupt(
+            target: &mut Self::Target,
+        ) -> Result<Option<Self::StopReason>, <Self::Target as Target>::Error>;
+    }
+
+    /// Returned by the `wait_for_stop_reason` closure in
+    /// [`GdbStub::run_blocking`]
+    pub enum Event<StopReason> {
+        /// GDB Client sent data while the target was running.
+        IncomingData(u8),
+        /// The target has stopped.
+        TargetStopped(StopReason),
+    }
+
+    /// Error value returned by the `wait_for_stop_reason` closure in
+    /// [`GdbStub::run_blocking`]
+    pub enum WaitForStopReasonError<T, C> {
+        /// A fatal target error has occurred.
+        Target(T),
+        /// A fatal connection error has occurred.
+        Connection(C),
+    }
+}
+
+/// 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>,
+    inner: core_impl::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.
+    ///
+    /// _Note:_ `new` is only available when the `alloc` feature is enabled, as
+    /// it will use a dynamically allocated `Vec` as a packet buffer.
+    ///
+    /// For fine-grained control over various `GdbStub` options, including the
+    /// ability to specify a fixed-size buffer, use the [`GdbStub::builder`]
+    /// method instead.
+    #[cfg(feature = "alloc")]
+    pub fn new(conn: C) -> GdbStub<'a, T, C> {
+        GdbStubBuilder::new(conn).build().unwrap()
+    }
+
+    /// (Quickstart) Start a GDB remote debugging session using a blocking event
+    /// loop.
+    ///
+    /// This method provides a quick and easy way to get up and running with
+    /// `gdbstub` without directly having to immediately interface with the
+    /// lower-level [state-machine](state_machine::GdbStubStateMachine)
+    /// based interface.
+    ///
+    /// Instead, an implementation simply needs to provide a implementation of
+    /// [`run_blocking::BlockingEventLoop`], which is a simplified set
+    /// of methods describing how to drive the target.
+    ///
+    /// `GdbStub::run_blocking` returns once the GDB client closes the debugging
+    /// session, or if the target triggers a disconnect.
+    ///
+    /// Note that this implementation is **blocking**, which many not be
+    /// preferred (or suitable) in all cases. To use `gdbstub` in a non-blocking
+    /// manner (e.g: via async/await, unix polling, from an interrupt handler,
+    /// etc...) you will need to interface with the underlying
+    /// [`GdbStubStateMachine`](state_machine::GdbStubStateMachine) API
+    /// directly.
+    pub fn run_blocking<E>(
+        self,
+        target: &mut T,
+    ) -> Result<DisconnectReason, Error<T::Error, C::Error>>
+    where
+        C: ConnectionExt,
+        E: run_blocking::BlockingEventLoop<Target = T, Connection = C>,
+    {
+        let mut gdb = self.run_state_machine(target)?;
+        loop {
+            gdb = match gdb {
+                state_machine::GdbStubStateMachine::Idle(mut gdb) => {
+                    // needs more data, so perform a blocking read on the connection
+                    let byte = gdb.borrow_conn().read().map_err(Error::ConnectionRead)?;
+                    gdb.incoming_data(target, byte)?
+                }
+
+                state_machine::GdbStubStateMachine::Disconnected(gdb) => {
+                    // run_blocking keeps things simple, and doesn't expose a way to re-use the
+                    // state machine
+                    break Ok(gdb.get_reason());
+                }
+
+                state_machine::GdbStubStateMachine::CtrlCInterrupt(gdb) => {
+                    // defer to the implementation on how it wants to handle the interrupt
+                    let stop_reason = E::on_interrupt(target).map_err(Error::TargetError)?;
+                    gdb.interrupt_handled(target, stop_reason)?
+                }
+
+                state_machine::GdbStubStateMachine::Running(mut gdb) => {
+                    use run_blocking::{Event as BlockingEventLoopEvent, WaitForStopReasonError};
+
+                    // block waiting for the target to return a stop reason
+                    let event = E::wait_for_stop_reason(target, gdb.borrow_conn());
+                    match event {
+                        Ok(BlockingEventLoopEvent::TargetStopped(stop_reason)) => {
+                            gdb.report_stop(target, stop_reason)?
+                        }
+
+                        Ok(BlockingEventLoopEvent::IncomingData(byte)) => {
+                            gdb.incoming_data(target, byte)?
+                        }
+
+                        Err(WaitForStopReasonError::Target(e)) => {
+                            break Err(Error::TargetError(e));
+                        }
+                        Err(WaitForStopReasonError::Connection(e)) => {
+                            break Err(Error::ConnectionRead(e));
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /// Starts a GDB remote debugging session, converting this instance of
+    /// `GdbStub` into a
+    /// [`GdbStubStateMachine`](state_machine::GdbStubStateMachine) that is
+    /// ready to receive data.
+    pub fn run_state_machine(
+        mut self,
+        target: &mut T,
+    ) -> Result<state_machine::GdbStubStateMachine<'a, T, C>, Error<T::Error, C::Error>> {
+        // Check if the target hasn't explicitly opted into implicit sw breakpoints
+        {
+            let support_software_breakpoints = target
+                .support_breakpoints()
+                .map(|ops| ops.support_sw_breakpoint().is_some())
+                .unwrap_or(false);
+
+            if !support_software_breakpoints && !target.guard_rail_implicit_sw_breakpoints() {
+                return Err(Error::ImplicitSwBreakpoints);
+            }
+        }
+
+        // Check how the target's arch handles single stepping
+        {
+            use crate::arch::SingleStepGdbBehavior;
+            use crate::target::ext::base::ResumeOps;
+
+            if let Some(ops) = target.base_ops().resume_ops() {
+                let support_single_step = match ops {
+                    ResumeOps::SingleThread(ops) => ops.support_single_step().is_some(),
+                    ResumeOps::MultiThread(ops) => ops.support_single_step().is_some(),
+                };
+
+                let behavior = target.guard_rail_single_step_gdb_behavior();
+
+                let return_error = match behavior {
+                    SingleStepGdbBehavior::Optional => false,
+                    SingleStepGdbBehavior::Required => !support_single_step,
+                    SingleStepGdbBehavior::Ignored => support_single_step,
+                    SingleStepGdbBehavior::Unknown => true,
+                };
+
+                if return_error {
+                    return Err(Error::SingleStepGdbBehavior(behavior));
+                }
+            }
+        }
+
+        // Perform any connection initialization
+        {
+            self.conn
+                .on_session_start()
+                .map_err(Error::ConnectionInit)?;
+        }
+
+        Ok(state_machine::GdbStubStateMachineInner::from_plain_gdbstub(self).into())
+    }
+}
diff --git a/src/stub/state_machine.rs b/src/stub/state_machine.rs
new file mode 100644
index 0000000..766d343
--- /dev/null
+++ b/src/stub/state_machine.rs
@@ -0,0 +1,370 @@
+//! Low-level state-machine interface that underpins [`GdbStub`].
+//
+// TODO: write some proper documentation + examples of how to interface with
+// this API.
+//!
+//! # Hey, what gives? Where are all the docs!?
+//!
+//! Yep, sorry about that!
+//!
+//! `gdbstub` 0.6 turned out ot be a pretty massive release, and documenting
+//! everything has proven to be a somewhat gargantuan task that's kept delaying
+//! the release data further and further back...
+//!
+//! To avoid blocking the release any further, I've decided to leave this bit of
+//! the API sparsely documented.
+//!
+//! If you're interested in using this API directly (e.g: to integrate `gdbstub`
+//! into a `no_std` project, or to use `gdbstub` in a non-blocking manner
+//! alongside `async/await` / a project specific event loop), your best bet
+//! would be to review the following bits of code to get a feel for the API:
+//!
+//! - The implementation of [`GdbStub::run_blocking`]
+//! - Implementations of [`BlockingEventLoop`] used alongside
+//!   `GdbStub::run_blocking` (e.g: the in-tree `armv4t` / `armv4t_multicore`
+//!   examples)
+//! - Real-world projects using the API
+//!     - The best example of this (at the time of writing) is the code at
+//!     [`vmware-labs/node-replicated-kernel`](https://github.com/vmware-labs/node-replicated-kernel/blob/4326704aaf3c0052e614dcde2a788a8483224394/kernel/src/arch/x86_64/gdb/mod.rs#L106)
+//!
+//! If you have any questions, feel free to open a discussion thread over at the
+//! [`gdbstub` GitHub repo](https://github.com/daniel5151/gdbstub/).
+//!
+//! [`BlockingEventLoop`]: super::run_blocking::BlockingEventLoop
+//! [`GdbStub::run_blocking`]: super::GdbStub::run_blocking
+
+use managed::ManagedSlice;
+
+use crate::arch::Arch;
+use crate::conn::Connection;
+use crate::protocol::recv_packet::RecvPacketStateMachine;
+use crate::protocol::{Packet, ResponseWriter};
+use crate::stub::error::GdbStubError as Error;
+use crate::stub::stop_reason::IntoStopReason;
+use crate::target::Target;
+
+use super::core_impl::{FinishExecStatus, GdbStubImpl, State};
+use super::{DisconnectReason, GdbStub};
+
+/// State-machine interface to `GdbStub`.
+///
+/// See the [module level documentation](self) for more details.
+pub enum GdbStubStateMachine<'a, T, C>
+where
+    T: Target,
+    C: Connection,
+{
+    /// The target is completely stopped, and the GDB stub is waiting for
+    /// additional input.
+    Idle(GdbStubStateMachineInner<'a, state::Idle<T>, T, C>),
+    /// The target is currently running, and the GDB client is waiting for
+    /// the target to report a stop reason.
+    ///
+    /// Note that the client may still send packets to the target
+    /// (e.g: to trigger a Ctrl-C interrupt).
+    Running(GdbStubStateMachineInner<'a, state::Running, T, C>),
+    /// The GDB client has sent a Ctrl-C interrupt to the target.
+    CtrlCInterrupt(GdbStubStateMachineInner<'a, state::CtrlCInterrupt, T, C>),
+    /// The GDB client has disconnected.
+    Disconnected(GdbStubStateMachineInner<'a, state::Disconnected, T, C>),
+}
+
+/// State machine typestates.
+///
+/// The types in this module are used to parameterize instances of
+/// [`GdbStubStateMachineInner`], thereby enforcing that certain API methods
+/// can only be called while the stub is in a certain state.
+// As an internal implementation detail, they _also_ carry state-specific
+// payloads, which are used when transitioning between states.
+pub mod state {
+    use super::*;
+
+    use crate::stub::stop_reason::MultiThreadStopReason;
+
+    // used internally when logging state transitions
+    pub(crate) const MODULE_PATH: &str = concat!(module_path!(), "::");
+
+    /// Typestate corresponding to the "Idle" state.
+    #[non_exhaustive]
+    pub struct Idle<T: Target> {
+        pub(crate) deferred_ctrlc_stop_reason:
+            Option<MultiThreadStopReason<<<T as Target>::Arch as Arch>::Usize>>,
+    }
+
+    /// Typestate corresponding to the "Running" state.
+    #[non_exhaustive]
+    pub struct Running {}
+
+    /// Typestate corresponding to the "CtrlCInterrupt" state.
+    #[non_exhaustive]
+    pub struct CtrlCInterrupt {
+        pub(crate) from_idle: bool,
+    }
+
+    /// Typestate corresponding to the "Disconnected" state.
+    #[non_exhaustive]
+    pub struct Disconnected {
+        pub(crate) reason: DisconnectReason,
+    }
+}
+
+/// Internal helper macro to convert between a particular inner state into
+/// its corresponding `GdbStubStateMachine` variant.
+macro_rules! impl_from_inner {
+        ($state:ident $($tt:tt)*) => {
+            impl<'a, T, C> From<GdbStubStateMachineInner<'a, state::$state $($tt)*, T, C>>
+                for GdbStubStateMachine<'a, T, C>
+            where
+                T: Target,
+                C: Connection,
+            {
+                fn from(inner: GdbStubStateMachineInner<'a, state::$state $($tt)*, T, C>) -> Self {
+                    GdbStubStateMachine::$state(inner)
+                }
+            }
+        };
+    }
+
+impl_from_inner!(Idle<T>);
+impl_from_inner!(Running);
+impl_from_inner!(CtrlCInterrupt);
+impl_from_inner!(Disconnected);
+
+/// Internal helper trait to cut down on boilerplate required to transition
+/// between states.
+trait Transition<'a, T, C>
+where
+    T: Target,
+    C: Connection,
+{
+    /// Transition between different state machine states
+    fn transition<S2>(self, state: S2) -> GdbStubStateMachineInner<'a, S2, T, C>;
+}
+
+impl<'a, S1, T, C> Transition<'a, T, C> for GdbStubStateMachineInner<'a, S1, T, C>
+where
+    T: Target,
+    C: Connection,
+{
+    #[inline(always)]
+    fn transition<S2>(self, state: S2) -> GdbStubStateMachineInner<'a, S2, T, C> {
+        if log::log_enabled!(log::Level::Trace) {
+            let s1 = core::any::type_name::<S1>();
+            let s2 = core::any::type_name::<S2>();
+            log::trace!(
+                "transition: {:?} --> {:?}",
+                s1.strip_prefix(state::MODULE_PATH).unwrap_or(s1),
+                s2.strip_prefix(state::MODULE_PATH).unwrap_or(s2)
+            );
+        }
+        GdbStubStateMachineInner { i: self.i, state }
+    }
+}
+
+// split off `GdbStubStateMachineInner`'s non state-dependant data into separate
+// struct for code bloat optimization (i.e: `transition` will generate better
+// code when the struct is cleaved this way).
+struct GdbStubStateMachineReallyInner<'a, T: Target, C: Connection> {
+    conn: C,
+    packet_buffer: ManagedSlice<'a, u8>,
+    recv_packet: RecvPacketStateMachine,
+    inner: GdbStubImpl<T, C>,
+}
+
+/// Core state machine implementation that is parameterized by various
+/// [states](state). Can be converted back into the appropriate
+/// [`GdbStubStateMachine`] variant via [`Into::into`].
+pub struct GdbStubStateMachineInner<'a, S, T: Target, C: Connection> {
+    i: GdbStubStateMachineReallyInner<'a, T, C>,
+    state: S,
+}
+
+/// Methods which can be called regardless of the current state.
+impl<'a, S, T: Target, C: Connection> GdbStubStateMachineInner<'a, S, T, C> {
+    /// Return a mutable reference to the underlying connection.
+    pub fn borrow_conn(&mut self) -> &mut C {
+        &mut self.i.conn
+    }
+}
+
+/// Methods which can only be called from the [`GdbStubStateMachine::Idle`]
+/// state.
+impl<'a, T: Target, C: Connection> GdbStubStateMachineInner<'a, state::Idle<T>, T, C> {
+    /// Internal entrypoint into the state machine.
+    pub(crate) fn from_plain_gdbstub(
+        stub: GdbStub<'a, T, C>,
+    ) -> GdbStubStateMachineInner<'a, state::Idle<T>, T, C> {
+        GdbStubStateMachineInner {
+            i: GdbStubStateMachineReallyInner {
+                conn: stub.conn,
+                packet_buffer: stub.packet_buffer,
+                recv_packet: RecvPacketStateMachine::new(),
+                inner: stub.inner,
+            },
+            state: state::Idle {
+                deferred_ctrlc_stop_reason: None,
+            },
+        }
+    }
+
+    /// Pass a byte to the GDB stub.
+    pub fn incoming_data(
+        mut self,
+        target: &mut T,
+        byte: u8,
+    ) -> Result<GdbStubStateMachine<'a, T, C>, Error<T::Error, C::Error>> {
+        let packet_buffer = match self.i.recv_packet.pump(&mut self.i.packet_buffer, byte)? {
+            Some(buf) => buf,
+            None => return Ok(self.into()),
+        };
+
+        let packet = Packet::from_buf(target, packet_buffer).map_err(Error::PacketParse)?;
+        let state = self
+            .i
+            .inner
+            .handle_packet(target, &mut self.i.conn, packet)?;
+        Ok(match state {
+            State::Pump => self.into(),
+            State::Disconnect(reason) => self.transition(state::Disconnected { reason }).into(),
+            State::DeferredStopReason => {
+                match self.state.deferred_ctrlc_stop_reason {
+                    // if we were interrupted while idle, immediately report the deferred stop
+                    // reason after transitioning into the running state
+                    Some(reason) => {
+                        return self
+                            .transition(state::Running {})
+                            .report_stop(target, reason)
+                    }
+                    // otherwise, just transition into the running state as usual
+                    None => self.transition(state::Running {}).into(),
+                }
+            }
+            State::CtrlCInterrupt => self
+                .transition(state::CtrlCInterrupt { from_idle: true })
+                .into(),
+        })
+    }
+}
+
+/// Methods which can only be called from the
+/// [`GdbStubStateMachine::Running`] state.
+impl<'a, T: Target, C: Connection> GdbStubStateMachineInner<'a, state::Running, T, C> {
+    /// Report a target stop reason back to GDB.
+    pub fn report_stop(
+        mut self,
+        target: &mut T,
+        reason: impl IntoStopReason<T>,
+    ) -> Result<GdbStubStateMachine<'a, T, C>, Error<T::Error, C::Error>> {
+        let mut res = ResponseWriter::new(&mut self.i.conn, target.use_rle());
+        let event = self.i.inner.finish_exec(&mut res, target, reason.into())?;
+        res.flush()?;
+
+        Ok(match event {
+            FinishExecStatus::Handled => self
+                .transition(state::Idle {
+                    deferred_ctrlc_stop_reason: None,
+                })
+                .into(),
+            FinishExecStatus::Disconnect(reason) => {
+                self.transition(state::Disconnected { reason }).into()
+            }
+        })
+    }
+
+    /// Pass a byte to the GDB stub.
+    ///
+    /// NOTE: unlike the `incoming_data` method in the `state::Idle` state,
+    /// this method does not perform any state transitions, and will
+    /// return a `GdbStubStateMachineInner` in the `state::Running` state.
+    pub fn incoming_data(
+        mut self,
+        target: &mut T,
+        byte: u8,
+    ) -> Result<GdbStubStateMachine<'a, T, C>, Error<T::Error, C::Error>> {
+        let packet_buffer = match self.i.recv_packet.pump(&mut self.i.packet_buffer, byte)? {
+            Some(buf) => buf,
+            None => return Ok(self.into()),
+        };
+
+        let packet = Packet::from_buf(target, packet_buffer).map_err(Error::PacketParse)?;
+        let state = self
+            .i
+            .inner
+            .handle_packet(target, &mut self.i.conn, packet)?;
+        Ok(match state {
+            State::Pump => self.transition(state::Running {}).into(),
+            State::Disconnect(reason) => self.transition(state::Disconnected { reason }).into(),
+            State::DeferredStopReason => self.transition(state::Running {}).into(),
+            State::CtrlCInterrupt => self
+                .transition(state::CtrlCInterrupt { from_idle: false })
+                .into(),
+        })
+    }
+}
+
+/// Methods which can only be called from the
+/// [`GdbStubStateMachine::CtrlCInterrupt`] state.
+impl<'a, T: Target, C: Connection> GdbStubStateMachineInner<'a, state::CtrlCInterrupt, T, C> {
+    /// Acknowledge the Ctrl-C interrupt.
+    ///
+    /// Passing `None` as a stop reason will return the state machine to
+    /// whatever state it was in pre-interruption, without immediately returning
+    /// a stop reason.
+    ///
+    /// Depending on how the target is implemented, it may or may not make sense
+    /// to immediately return a stop reason as part of handling the Ctrl-C
+    /// interrupt. e.g: in some cases, it may be better to send the target a
+    /// signal upon receiving a Ctrl-C interrupt _without_ immediately sending a
+    /// stop reason, and instead deferring the stop reason to some later point
+    /// in the target's execution.
+    ///
+    /// Some notes on handling Ctrl-C interrupts:
+    ///
+    /// - Stubs are not required to recognize these interrupt mechanisms, and
+    ///   the precise meaning associated with receipt of the interrupt is
+    ///   implementation defined.
+    /// - If the target supports debugging of multiple threads and/or processes,
+    ///   it should attempt to interrupt all currently-executing threads and
+    ///   processes.
+    /// - If the stub is successful at interrupting the running program, it
+    ///   should send one of the stop reply packets (see Stop Reply Packets) to
+    ///   GDB as a result of successfully stopping the program
+    pub fn interrupt_handled(
+        self,
+        target: &mut T,
+        stop_reason: Option<impl IntoStopReason<T>>,
+    ) -> Result<GdbStubStateMachine<'a, T, C>, Error<T::Error, C::Error>> {
+        if self.state.from_idle {
+            // target is stopped - we cannot report the stop reason yet
+            Ok(self
+                .transition(state::Idle {
+                    deferred_ctrlc_stop_reason: stop_reason.map(Into::into),
+                })
+                .into())
+        } else {
+            // target is running - we can immediately report the stop reason
+            let gdb = self.transition(state::Running {});
+            match stop_reason {
+                Some(reason) => gdb.report_stop(target, reason),
+                None => Ok(gdb.into()),
+            }
+        }
+    }
+}
+
+/// Methods which can only be called from the
+/// [`GdbStubStateMachine::Disconnected`] state.
+impl<'a, T: Target, C: Connection> GdbStubStateMachineInner<'a, state::Disconnected, T, C> {
+    /// Inspect why the GDB client disconnected.
+    pub fn get_reason(&self) -> DisconnectReason {
+        self.state.reason
+    }
+
+    /// Reuse the existing state machine instance, reentering the idle loop.
+    pub fn return_to_idle(self) -> GdbStubStateMachine<'a, T, C> {
+        self.transition(state::Idle {
+            deferred_ctrlc_stop_reason: None,
+        })
+        .into()
+    }
+}
diff --git a/src/stub/stop_reason.rs b/src/stub/stop_reason.rs
new file mode 100644
index 0000000..70ed15a
--- /dev/null
+++ b/src/stub/stop_reason.rs
@@ -0,0 +1,162 @@
+//! Stop reasons reported back to the GDB client.
+
+use crate::arch::Arch;
+use crate::common::Signal;
+use crate::common::Tid;
+use crate::target::ext::base::reverse_exec::ReplayLogPosition;
+use crate::target::ext::breakpoints::WatchKind;
+use crate::target::ext::catch_syscalls::CatchSyscallPosition;
+use crate::target::Target;
+
+/// Describes why a thread stopped.
+///
+/// Single threaded targets should set `Tid` to `()`, whereas multi threaded
+/// targets should set `Tid` to [`Tid`]. To make things easier, it is
+/// recommended to use the [`SingleThreadStopReason`] and
+/// [`MultiThreadStopReason`] when possible.
+///
+///
+///
+/// Targets MUST only respond with stop reasons that correspond to IDETs that
+/// target has implemented. Not doing so will result in a runtime error.
+///
+/// e.g: A target which has not implemented the [`HwBreakpoint`] IDET must not
+/// return a `HwBreak` stop reason. While this is not enforced at compile time,
+/// doing so will result in a runtime `UnsupportedStopReason` error.
+///
+/// [`HwBreakpoint`]: crate::target::ext::breakpoints::HwBreakpoint
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum BaseStopReason<Tid, U> {
+    /// Completed the single-step request.
+    DoneStep,
+    /// The process exited with the specified exit status.
+    Exited(u8),
+    /// The process terminated with the specified signal number.
+    Terminated(Signal),
+    /// The program received a signal.
+    Signal(Signal),
+    /// A specific thread received a signal.
+    SignalWithThread {
+        /// Tid of the associated thread
+        tid: Tid,
+        /// The signal
+        signal: Signal,
+    },
+    /// A thread hit a software breakpoint (e.g. due to a trap instruction).
+    ///
+    /// Requires: [`SwBreakpoint`].
+    ///
+    /// NOTE: This does not necessarily have to be a breakpoint configured by
+    /// the client/user of the current GDB session.
+    ///
+    /// [`SwBreakpoint`]: crate::target::ext::breakpoints::SwBreakpoint
+    SwBreak(Tid),
+    /// A thread hit a hardware breakpoint.
+    ///
+    /// Requires: [`HwBreakpoint`].
+    ///
+    /// [`HwBreakpoint`]: crate::target::ext::breakpoints::HwBreakpoint
+    HwBreak(Tid),
+    /// A thread hit a watchpoint.
+    ///
+    /// Requires: [`HwWatchpoint`].
+    ///
+    /// [`HwWatchpoint`]: crate::target::ext::breakpoints::HwWatchpoint
+    Watch {
+        /// Tid of the associated thread
+        tid: Tid,
+        /// Kind of watchpoint that was hit
+        kind: WatchKind,
+        /// Address of watched memory
+        addr: U,
+    },
+    /// The program has reached the end of the logged replay events.
+    ///
+    /// Requires: [`ReverseCont`] or [`ReverseStep`].
+    ///
+    /// This is used for GDB's reverse execution. When playing back a recording,
+    /// you may hit the end of the buffer of recorded events, and as such no
+    /// further execution can be done. This stop reason tells GDB that this has
+    /// occurred.
+    ///
+    /// [`ReverseCont`]: crate::target::ext::base::reverse_exec::ReverseCont
+    /// [`ReverseStep`]: crate::target::ext::base::reverse_exec::ReverseStep
+    ReplayLog {
+        /// (optional) Tid of the associated thread.
+        tid: Option<Tid>,
+        /// The point reached in a replay log (i.e: beginning vs. end).
+        pos: ReplayLogPosition,
+    },
+    /// The program has reached a syscall entry or return location.
+    ///
+    /// Requires: [`CatchSyscalls`].
+    ///
+    /// [`CatchSyscalls`]: crate::target::ext::catch_syscalls::CatchSyscalls
+    CatchSyscall {
+        /// (optional) Tid of the associated thread.
+        tid: Option<Tid>,
+        /// The syscall number.
+        number: U,
+        /// The location the event occurred at.
+        position: CatchSyscallPosition,
+    },
+}
+
+/// A stop reason for a single threaded target.
+///
+/// Threads are identified using the unit type `()` (as there is only a single
+/// possible thread-id).
+pub type SingleThreadStopReason<U> = BaseStopReason<(), U>;
+
+/// A stop reason for a multi threaded target.
+///
+/// Threads are identified using a [`Tid`].
+pub type MultiThreadStopReason<U> = BaseStopReason<Tid, U>;
+
+impl<U> From<BaseStopReason<(), U>> for BaseStopReason<Tid, U> {
+    fn from(st_stop_reason: BaseStopReason<(), U>) -> BaseStopReason<Tid, U> {
+        match st_stop_reason {
+            BaseStopReason::DoneStep => BaseStopReason::DoneStep,
+            BaseStopReason::Exited(code) => BaseStopReason::Exited(code),
+            BaseStopReason::Terminated(sig) => BaseStopReason::Terminated(sig),
+            BaseStopReason::SignalWithThread { signal, .. } => BaseStopReason::SignalWithThread {
+                tid: crate::SINGLE_THREAD_TID,
+                signal,
+            },
+            BaseStopReason::SwBreak(_) => BaseStopReason::SwBreak(crate::SINGLE_THREAD_TID),
+            BaseStopReason::HwBreak(_) => BaseStopReason::HwBreak(crate::SINGLE_THREAD_TID),
+            BaseStopReason::Watch { kind, addr, .. } => BaseStopReason::Watch {
+                tid: crate::SINGLE_THREAD_TID,
+                kind,
+                addr,
+            },
+            BaseStopReason::Signal(sig) => BaseStopReason::Signal(sig),
+            BaseStopReason::ReplayLog { pos, .. } => BaseStopReason::ReplayLog { tid: None, pos },
+            BaseStopReason::CatchSyscall {
+                number, position, ..
+            } => BaseStopReason::CatchSyscall {
+                tid: None,
+                number,
+                position,
+            },
+        }
+    }
+}
+
+mod private {
+    pub trait Sealed {}
+
+    impl<U> Sealed for super::SingleThreadStopReason<U> {}
+    impl<U> Sealed for super::MultiThreadStopReason<U> {}
+}
+
+/// A marker trait implemented by [`SingleThreadStopReason`] and
+/// [`MultiThreadStopReason`].
+pub trait IntoStopReason<T: Target>:
+    private::Sealed + Into<MultiThreadStopReason<<<T as Target>::Arch as Arch>::Usize>>
+{
+}
+
+impl<T: Target> IntoStopReason<T> for SingleThreadStopReason<<<T as Target>::Arch as Arch>::Usize> {}
+impl<T: Target> IntoStopReason<T> for MultiThreadStopReason<<<T as Target>::Arch as Arch>::Usize> {}
diff --git a/src/target/ext/auxv.rs b/src/target/ext/auxv.rs
new file mode 100644
index 0000000..c801af2
--- /dev/null
+++ b/src/target/ext/auxv.rs
@@ -0,0 +1,16 @@
+//! Access the target’s auxiliary vector.
+use crate::target::{Target, TargetResult};
+
+/// Target Extension - Access the target’s auxiliary vector.
+pub trait Auxv: Target {
+    /// Get auxiliary vector from the target.
+    ///
+    /// Return the number of bytes written into `buf` (which may be less than
+    /// `length`).
+    ///
+    /// If `offset` is greater than the length of the underlying data, return
+    /// `Ok(0)`.
+    fn get_auxv(&self, offset: u64, length: usize, buf: &mut [u8]) -> TargetResult<usize, Self>;
+}
+
+define_ext!(AuxvOps, Auxv);
diff --git a/src/target/ext/base/mod.rs b/src/target/ext/base/mod.rs
index 04bcbd9..ce31270 100644
--- a/src/target/ext/base/mod.rs
+++ b/src/target/ext/base/mod.rs
@@ -1,107 +1,40 @@
-//! Base operations required to debug any target (read/write memory/registers,
-//! step/resume, etc...)
+//! Base operations required to debug most targets (e.g: read/write
+//! memory/registers, step/resume, etc...)
 //!
-//! It is recommended that single threaded targets implement the simplified
-//! `singlethread` API, as `gdbstub` includes optimized implementations of
-//! certain internal routines when operating in singlethreaded mode.
+//! It is **highly recommended** that single threaded targets implement the
+//! simplified `singlethread` API, as `gdbstub` includes optimized
+//! implementations of certain internal routines when operating in single
+//! threaded mode.
+
+use crate::arch::Arch;
 
 pub mod multithread;
+pub mod reverse_exec;
+pub mod single_register_access;
 pub mod singlethread;
 
-mod single_register_access;
-
-pub use single_register_access::{SingleRegisterAccess, SingleRegisterAccessOps};
-
-/// Base operations for single/multi threaded targets.
+/// Base required 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>),
+    SingleThread(&'a mut dyn singlethread::SingleThreadBase<Arch = A, Error = E>),
     /// Multi-threaded target
-    MultiThread(&'a mut dyn multithread::MultiThreadOps<Arch = A, Error = E>),
+    MultiThread(&'a mut dyn multithread::MultiThreadBase<Arch = A, Error = E>),
 }
 
-/// Describes how the target should be resumed.
-///
-/// Due to a quirk / bug in the mainline GDB client, targets are required to
-/// handle the `WithSignal` variants of `Step` and `Continue` regardless of
-/// whether or not they have a concept of "signals".
-///
-/// If your target does not support signals (e.g: the target is a bare-metal
-/// microcontroller / emulator), the recommended behavior is to either return a
-/// target-specific fatal error, or to handle `{Step,Continue}WithSignal` the
-/// same way as their non-`WithSignal` variants.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum ResumeAction {
-    /// Continue execution, stopping once a
-    /// [`StopReason`](singlethread::StopReason) occurs.
-    Continue,
-    /// Step execution.
-    Step,
-    /// Continue with signal.
-    ContinueWithSignal(u8),
-    /// Step with signal.
-    StepWithSignal(u8),
+pub(crate) enum ResumeOps<'a, A, E> {
+    /// Single-threaded target
+    SingleThread(&'a mut dyn singlethread::SingleThreadResume<Arch = A, Error = E>),
+    /// Multi-threaded target
+    MultiThread(&'a mut dyn multithread::MultiThreadResume<Arch = A, Error = E>),
 }
 
-/// Describes the point reached in a replay log for the corresponding stop
-/// reason.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum ReplayLogPosition {
-    /// Reached the beginning of the replay log.
-    Begin,
-    /// Reached the end of the replay log.
-    End,
-}
-
-/// A handle to check for incoming GDB interrupts.
-///
-/// At the moment, checking for incoming interrupts requires periodically
-/// polling for pending interrupts. e.g:
-///
-/// ```ignore
-/// let interrupts = gdb_interrupt.no_async();
-/// loop {
-///     if interrupts.pending() {
-///         return Ok(StopReason::GdbInterrupt)
-///     }
-///
-///     // execute some number of clock cycles
-///     for _ in 0..1024 {
-///         match self.system.step() { .. }
-///     }
-/// }
-/// ```
-///
-/// There is an outstanding issue to add a non-blocking interface to
-/// `GdbInterrupt` (see [daniel5151/gdbstub#36](https://github.com/daniel5151/gdbstub/issues/36)).
-/// Please comment on the issue if this is something you'd like to see
-/// implemented and/or would like to help out with!
-pub struct GdbInterrupt<'a> {
-    inner: &'a mut dyn FnMut() -> bool,
-}
-
-impl<'a> GdbInterrupt<'a> {
-    pub(crate) fn new(inner: &'a mut dyn FnMut() -> bool) -> GdbInterrupt<'a> {
-        GdbInterrupt { inner }
-    }
-
-    /// Returns a [`GdbInterruptNoAsync`] struct which can be polled using a
-    /// simple non-blocking [`pending(&mut self) ->
-    /// bool`](GdbInterruptNoAsync::pending) method.
-    pub fn no_async(self) -> GdbInterruptNoAsync<'a> {
-        GdbInterruptNoAsync { inner: self.inner }
-    }
-}
-
-/// A simplified interface to [`GdbInterrupt`] for projects without
-/// async/await infrastructure.
-pub struct GdbInterruptNoAsync<'a> {
-    inner: &'a mut dyn FnMut() -> bool,
-}
-
-impl<'a> GdbInterruptNoAsync<'a> {
-    /// Checks if there is a pending GDB interrupt.
-    pub fn pending(&mut self) -> bool {
-        (self.inner)()
+impl<'a, A: Arch, E> BaseOps<'a, A, E> {
+    #[inline(always)]
+    pub(crate) fn resume_ops(self) -> Option<ResumeOps<'a, A, E>> {
+        let ret = match self {
+            BaseOps::SingleThread(ops) => ResumeOps::SingleThread(ops.support_resume()?),
+            BaseOps::MultiThread(ops) => ResumeOps::MultiThread(ops.support_resume()?),
+        };
+        Some(ret)
     }
 }
diff --git a/src/target/ext/base/multithread.rs b/src/target/ext/base/multithread.rs
index eba9d79..423f57b 100644
--- a/src/target/ext/base/multithread.rs
+++ b/src/target/ext/base/multithread.rs
@@ -1,127 +1,12 @@
 //! Base debugging operations for multi threaded targets.
 
 use crate::arch::Arch;
-use crate::common::*;
-use crate::target::ext::breakpoints::WatchKind;
+use crate::common::Signal;
+use crate::common::Tid;
 use crate::target::{Target, TargetResult};
 
-use super::{ReplayLogPosition, SingleRegisterAccessOps};
-
-// Convenient re-exports
-pub use super::{GdbInterrupt, ResumeAction};
-
-/// Base debugging operations for multi threaded targets.
-#[allow(clippy::type_complexity)]
-pub trait MultiThreadOps: Target {
-    /// Resume execution on the target.
-    ///
-    /// Prior to calling `resume`, `gdbstub` will call `clear_resume_actions`,
-    /// followed by zero or more calls to `set_resume_action`, specifying any
-    /// thread-specific resume actions.
-    ///
-    /// The `default_action` parameter specifies the "fallback" resume action
-    /// for any threads that did not have a specific resume action set via
-    /// `set_resume_action`. The GDB client typically sets this to
-    /// `ResumeAction::Continue`, though this is not guaranteed.
-    ///
-    /// 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 working correctly.
-    ///
-    /// # Additional Considerations
-    ///
-    /// ### 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.
-    ///
-    /// ### Running in "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 must be stopped when returning from `resume`
-    /// (not just the thread associated with the `ThreadStopReason`).
-    fn resume(
-        &mut self,
-        default_resume_action: ResumeAction,
-        gdb_interrupt: GdbInterrupt<'_>,
-    ) -> Result<ThreadStopReason<<Self::Arch as Arch>::Usize>, Self::Error>;
-
-    /// Clear all previously set resume actions.
-    fn clear_resume_actions(&mut self) -> Result<(), Self::Error>;
-
-    /// Specify what action each thread should take when
-    /// [`resume`](Self::resume) is called.
-    ///
-    /// A simple implementation of this method would simply update an internal
-    /// `HashMap<Tid, ResumeAction>`.
-    ///
-    /// Aside from the four "base" resume actions handled by this method (i.e:
-    /// `Step`, `Continue`, `StepWithSignal`, and `ContinueWithSignal`),
-    /// there are also two additional resume actions which are only set if the
-    /// target implements their corresponding protocol extension:
-    ///
-    /// Action                     | Protocol Extension
-    /// ---------------------------|---------------------------
-    /// Optimized [Range Stepping] | See [`support_range_step()`]
-    /// "Stop"                     | Used in "Non-Stop" mode \*
-    ///
-    /// \* "Non-Stop" mode is currently unimplemented
-    ///
-    /// [Range Stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#range-stepping
-    /// [`support_range_step()`]: Self::support_range_step
-    fn set_resume_action(&mut self, tid: Tid, action: ResumeAction) -> Result<(), Self::Error>;
-
-    /// Support for the optimized [range stepping] resume action.
-    ///
-    /// [range stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#range-stepping
-    #[inline(always)]
-    fn support_range_step(&mut self) -> Option<MultiThreadRangeSteppingOps<Self>> {
-        None
-    }
-
-    /// Support for [reverse stepping] a target.
-    ///
-    /// [reverse stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
-    #[inline(always)]
-    fn support_reverse_step(&mut self) -> Option<MultiThreadReverseStepOps<Self>> {
-        None
-    }
-
-    /// Support for [reverse continuing] a target.
-    ///
-    /// [reverse continuing]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
-    #[inline(always)]
-    fn support_reverse_cont(&mut self) -> Option<MultiThreadReverseContOps<Self>> {
-        None
-    }
-
+/// Base required debugging operations for multi threaded targets.
+pub trait MultiThreadBase: Target {
     /// Read the target's registers.
     ///
     /// If the registers could not be accessed, an appropriate non-fatal error
@@ -143,14 +28,18 @@
     ) -> TargetResult<(), Self>;
 
     /// Support for single-register access.
-    /// See [`SingleRegisterAccess`](super::SingleRegisterAccess) for more
-    /// details.
+    /// See [`SingleRegisterAccess`] for more details.
     ///
     /// While this is an optional feature, it is **highly recommended** to
     /// implement it when possible, as it can significantly improve performance
     /// on certain architectures.
+    ///
+    /// [`SingleRegisterAccess`]:
+    /// super::single_register_access::SingleRegisterAccess
     #[inline(always)]
-    fn single_register_access(&mut self) -> Option<SingleRegisterAccessOps<Tid, Self>> {
+    fn support_single_register_access(
+        &mut self,
+    ) -> Option<super::single_register_access::SingleRegisterAccessOps<'_, Tid, Self>> {
         None
     }
 
@@ -202,59 +91,174 @@
         })?;
         Ok(found)
     }
+
+    /// Support for resuming the target (e.g: via `continue` or `step`)
+    #[inline(always)]
+    fn support_resume(&mut self) -> Option<MultiThreadResumeOps<'_, Self>> {
+        None
+    }
 }
 
-/// Target Extension - [Reverse continue] for multi threaded targets.
-///
-/// Reverse continue allows the target to run backwards until it reaches the end
-/// of the replay log.
-///
-/// [Reverse continue]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
-pub trait MultiThreadReverseCont: Target + MultiThreadOps {
-    /// Reverse-continue the target.
-    fn reverse_cont(
-        &mut self,
-        gdb_interrupt: GdbInterrupt<'_>,
-    ) -> Result<ThreadStopReason<<Self::Arch as Arch>::Usize>, Self::Error>;
-}
+/// Target extension - support for resuming multi threaded targets.
+pub trait MultiThreadResume: Target {
+    /// Resume execution on the target.
+    ///
+    /// Prior to calling `resume`, `gdbstub` will call `clear_resume_actions`,
+    /// followed by zero or more calls to the `set_resume_action_XXX` methods,
+    /// specifying any thread-specific resume actions.
+    ///
+    /// Upon returning from the `resume` method, the target being debugged
+    /// should be configured to run according to whatever resume actions the
+    /// GDB client had specified using any of the `set_resume_action_XXX`
+    /// methods.
+    ///
+    /// Any thread that wasn't explicitly resumed by a `set_resume_action_XXX`
+    /// method should be resumed as though it was resumed with
+    /// `set_resume_action_continue`.
+    ///
+    /// A basic target implementation only needs to implement support for
+    /// `set_resume_action_continue`, with all other resume actions requiring
+    /// their corresponding protocol extension to be implemented:
+    ///
+    /// Action                      | Protocol Extension
+    /// ----------------------------|------------------------------
+    /// Optimized [Single Stepping] | See [`support_single_step()`]
+    /// Optimized [Range Stepping]  | See [`support_range_step()`]
+    /// "Stop"                      | Used in "Non-Stop" mode \*
+    ///
+    /// \* "Non-Stop" mode is currently unimplemented in `gdbstub`
+    ///
+    /// [Single stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#index-stepi
+    /// [Range Stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#range-stepping
+    /// [`support_single_step()`]: Self::support_single_step
+    /// [`support_range_step()`]: Self::support_range_step
+    ///
+    /// # Additional Considerations
+    ///
+    /// ### 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.
+    ///
+    /// ### 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) -> Result<(), Self::Error>;
 
-define_ext!(MultiThreadReverseContOps, MultiThreadReverseCont);
+    /// Clear all previously set resume actions.
+    fn clear_resume_actions(&mut self) -> Result<(), Self::Error>;
 
-/// Target Extension - [Reverse stepping] for multi threaded targets.
-///
-/// Reverse stepping allows the target to run backwards by one step.
-///
-/// [Reverse stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
-pub trait MultiThreadReverseStep: Target + MultiThreadOps {
-    /// Reverse-step the specified [`Tid`].
-    fn reverse_step(
+    /// Continue the specified thread.
+    ///
+    /// See the [`resume`](Self::resume) docs for information on when this is
+    /// called.
+    ///
+    /// The GDB client may also include a `signal` which should be passed to the
+    /// target.
+    fn set_resume_action_continue(
         &mut self,
         tid: Tid,
-        gdb_interrupt: GdbInterrupt<'_>,
-    ) -> Result<ThreadStopReason<<Self::Arch as Arch>::Usize>, Self::Error>;
+        signal: Option<Signal>,
+    ) -> Result<(), Self::Error>;
+
+    /// Support for optimized [single stepping].
+    ///
+    /// [single stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#index-stepi
+    #[inline(always)]
+    fn support_single_step(&mut self) -> Option<MultiThreadSingleStepOps<'_, Self>> {
+        None
+    }
+
+    /// Support for optimized [range stepping].
+    ///
+    /// [range stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#range-stepping
+    #[inline(always)]
+    fn support_range_step(&mut self) -> Option<MultiThreadRangeSteppingOps<'_, Self>> {
+        None
+    }
+
+    /// Support for [reverse stepping] a target.
+    ///
+    /// [reverse stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
+    #[inline(always)]
+    fn support_reverse_step(
+        &mut self,
+    ) -> Option<super::reverse_exec::ReverseStepOps<'_, Tid, Self>> {
+        None
+    }
+
+    /// Support for [reverse continuing] a target.
+    ///
+    /// [reverse continuing]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
+    #[inline(always)]
+    fn support_reverse_cont(
+        &mut self,
+    ) -> Option<super::reverse_exec::ReverseContOps<'_, Tid, Self>> {
+        None
+    }
 }
 
-define_ext!(MultiThreadReverseStepOps, MultiThreadReverseStep);
+define_ext!(MultiThreadResumeOps, MultiThreadResume);
 
-/// Target Extension - Optimized [range stepping] for multi threaded targets.
-/// See [`MultiThreadOps::support_range_step`].
-///
-/// Range Stepping will step the target once, and keep stepping the target as
-/// long as execution remains between the specified start (inclusive) and end
-/// (exclusive) addresses, or another stop condition is met (e.g: a breakpoint
-/// it hit).
-///
-/// If the range is empty (`start` == `end`), then the action becomes
-/// equivalent to the ‘s’ action. In other words, single-step once, and
-/// report the stop (even if the stepped instruction jumps to start).
-///
-/// _Note:_ A stop reply may be sent at any point even if the PC is still
-/// within the stepping range; for example, it is valid to implement range
-/// stepping in a degenerate way as a single instruction step operation.
-///
-/// [range stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#range-stepping
-pub trait MultiThreadRangeStepping: Target + MultiThreadOps {
-    /// See [`MultiThreadOps::set_resume_action`].
+/// Target Extension - Optimized single stepping for multi threaded targets.
+/// See [`MultiThreadResume::support_single_step`].
+pub trait MultiThreadSingleStep: Target + MultiThreadResume {
+    /// [Single step] the specified target thread.
+    ///
+    /// Single stepping will step the target a single "step" - typically a
+    /// single instruction.
+    ///
+    /// The GDB client may also include a `signal` which should be passed to the
+    /// target.
+    ///
+    /// If your target does not support signals (e.g: the target is a bare-metal
+    /// microcontroller / emulator), the recommended behavior is to return a
+    /// target-specific fatal error
+    ///
+    /// [Single step]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#index-stepi
+    fn set_resume_action_step(
+        &mut self,
+        tid: Tid,
+        signal: Option<Signal>,
+    ) -> Result<(), Self::Error>;
+}
+
+define_ext!(MultiThreadSingleStepOps, MultiThreadSingleStep);
+
+/// Target Extension - Optimized range stepping for multi threaded targets.
+/// See [`MultiThreadResume::support_range_step`].
+pub trait MultiThreadRangeStepping: Target + MultiThreadResume {
+    /// [Range step] the specified target thread.
+    ///
+    /// Range Stepping will step the target once, and keep stepping the target
+    /// as long as execution remains between the specified start (inclusive)
+    /// and end (exclusive) addresses, or another stop condition is met
+    /// (e.g: a breakpoint it hit).
+    ///
+    /// If the range is empty (`start` == `end`), then the action becomes
+    /// equivalent to the ‘s’ action. In other words, single-step once, and
+    /// report the stop (even if the stepped instruction jumps to start).
+    ///
+    /// _Note:_ A stop reply may be sent at any point even if the PC is still
+    /// within the stepping range; for example, it is valid to implement range
+    /// stepping in a degenerate way as a single instruction step operation.
+    ///
+    /// [Range step]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#range-stepping
     fn set_resume_action_range_step(
         &mut self,
         tid: Tid,
@@ -264,65 +268,3 @@
 }
 
 define_ext!(MultiThreadRangeSteppingOps, MultiThreadRangeStepping);
-
-/// Describes why a thread stopped.
-///
-/// Targets MUST only respond with stop reasons that correspond to IDETs that
-/// target has implemented.
-///
-/// e.g: A target which has not implemented the [`HwBreakpoint`] IDET must not
-/// return a `HwBreak` stop reason. While this is not enforced at compile time,
-/// doing so will result in a runtime `UnsupportedStopReason` error.
-///
-/// [`HwBreakpoint`]: crate::target::ext::breakpoints::HwBreakpoint
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-#[non_exhaustive]
-pub enum ThreadStopReason<U> {
-    /// Completed the single-step request.
-    DoneStep,
-    /// `check_gdb_interrupt` returned `true`.
-    GdbInterrupt,
-    /// The process exited with the specified exit status.
-    Exited(u8),
-    /// The process terminated with the specified signal number.
-    Terminated(u8),
-    /// The program received a signal.
-    Signal(u8),
-    /// A thread hit a software breakpoint (e.g. due to a trap instruction).
-    ///
-    /// Requires: [`SwBreakpoint`].
-    ///
-    /// NOTE: This does not necessarily have to be a breakpoint configured by
-    /// the client/user of the current GDB session.
-    ///
-    /// [`SwBreakpoint`]: crate::target::ext::breakpoints::SwBreakpoint
-    SwBreak(Tid),
-    /// A thread hit a hardware breakpoint.
-    ///
-    /// Requires: [`HwBreakpoint`].
-    ///
-    /// [`HwBreakpoint`]: crate::target::ext::breakpoints::HwBreakpoint
-    HwBreak(Tid),
-    /// A thread hit a watchpoint.
-    ///
-    /// Requires: [`HwWatchpoint`].
-    ///
-    /// [`HwWatchpoint`]: crate::target::ext::breakpoints::HwWatchpoint
-    Watch {
-        /// Which thread hit the watchpoint
-        tid: Tid,
-        /// Kind of watchpoint that was hit
-        kind: WatchKind,
-        /// Address of watched memory
-        addr: U,
-    },
-    /// The program has reached the end of the logged replay events.
-    ///
-    /// Requires: [`MultiThreadReverseCont`] or [`MultiThreadReverseStep`].
-    ///
-    /// This is used for GDB's reverse execution. When playing back a recording,
-    /// you may hit the end of the buffer of recorded events, and as such no
-    /// further execution can be done. This stop reason tells GDB that this has
-    /// occurred.
-    ReplayLog(ReplayLogPosition),
-}
diff --git a/src/target/ext/base/reverse_exec.rs b/src/target/ext/base/reverse_exec.rs
new file mode 100644
index 0000000..badd7a2
--- /dev/null
+++ b/src/target/ext/base/reverse_exec.rs
@@ -0,0 +1,51 @@
+//! Support for reverse debugging targets.
+
+use crate::target::Target;
+
+/// Target Extension - Reverse continue for targets.
+pub trait ReverseCont<Tid>: Target
+where
+    Tid: crate::is_valid_tid::IsValidTid,
+{
+    /// [Reverse continue] the target.
+    ///
+    /// Reverse continue allows the target to run backwards until it reaches the
+    /// end of the replay log.
+    ///
+    /// [Reverse continue]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
+    fn reverse_cont(&mut self) -> Result<(), Self::Error>;
+}
+
+/// See [`ReverseCont`]
+pub type ReverseContOps<'a, Tid, T> =
+    &'a mut dyn ReverseCont<Tid, Arch = <T as Target>::Arch, Error = <T as Target>::Error>;
+
+/// Target Extension - Reverse stepping for targets.
+pub trait ReverseStep<Tid>: Target
+where
+    Tid: crate::is_valid_tid::IsValidTid,
+{
+    /// [Reverse step] the specified `Tid`.
+    ///
+    /// On single threaded targets, `tid` is set to `()` and can be ignored.
+    ///
+    /// Reverse stepping allows the target to run backwards by one "step" -
+    /// typically a single instruction.
+    ///
+    /// [Reverse step]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
+    fn reverse_step(&mut self, tid: Tid) -> Result<(), Self::Error>;
+}
+
+/// See [`ReverseStep`]
+pub type ReverseStepOps<'a, Tid, T> =
+    &'a mut dyn ReverseStep<Tid, Arch = <T as Target>::Arch, Error = <T as Target>::Error>;
+
+/// Describes the point reached in a replay log (used alongside
+/// [`BaseStopReason::ReplayLog`](crate::stub::BaseStopReason::ReplayLog))
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ReplayLogPosition {
+    /// Reached the beginning of the replay log.
+    Begin,
+    /// Reached the end of the replay log.
+    End,
+}
diff --git a/src/target/ext/base/single_register_access.rs b/src/target/ext/base/single_register_access.rs
index bf94c80..f7a9b16 100644
--- a/src/target/ext/base/single_register_access.rs
+++ b/src/target/ext/base/single_register_access.rs
@@ -1,3 +1,5 @@
+//! Support for single-register read/write access.
+
 use crate::arch::Arch;
 use crate::target::{Target, TargetResult};
 
@@ -15,28 +17,28 @@
 /// part of the default default register file used by the `read/write_registers`
 /// methods, and can only be accessed via this extension (e.g: the RISC-V
 /// Control and Status registers).
-pub trait SingleRegisterAccess<Id>: Target {
+pub trait SingleRegisterAccess<Tid>: Target
+where
+    Tid: crate::is_valid_tid::IsValidTid,
+{
     /// Read to a single register on the target.
     ///
     /// The `tid` field identifies which thread the value should be read from.
     /// On single threaded targets, `tid` is set to `()` and can be ignored.
     ///
     /// Implementations should write the value of the register using target's
-    /// native byte order in the buffer `dst`.
+    /// native byte order in the buffer `buf`.
+    ///
+    /// Return the number of bytes written into `buf`.
     ///
     /// 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,
-        tid: Id,
+        tid: Tid,
         reg_id: <Self::Arch as Arch>::RegId,
-        dst: &mut [u8],
-    ) -> TargetResult<(), Self>;
+        buf: &mut [u8],
+    ) -> TargetResult<usize, Self>;
 
     /// Write from a single register on the target.
     ///
@@ -49,19 +51,14 @@
     ///
     /// 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,
-        tid: Id,
+        tid: Tid,
         reg_id: <Self::Arch as Arch>::RegId,
         val: &[u8],
     ) -> TargetResult<(), Self>;
 }
 
 /// See [`SingleRegisterAccess`]
-pub type SingleRegisterAccessOps<'a, Id, T> =
-    &'a mut dyn SingleRegisterAccess<Id, Arch = <T as Target>::Arch, Error = <T as Target>::Error>;
+pub type SingleRegisterAccessOps<'a, Tid, T> =
+    &'a mut dyn SingleRegisterAccess<Tid, Arch = <T as Target>::Arch, Error = <T as Target>::Error>;
diff --git a/src/target/ext/base/singlethread.rs b/src/target/ext/base/singlethread.rs
index f9ecaba..c949886 100644
--- a/src/target/ext/base/singlethread.rs
+++ b/src/target/ext/base/singlethread.rs
@@ -1,76 +1,11 @@
 //! Base debugging operations for single threaded targets.
 
 use crate::arch::Arch;
-use crate::target::ext::breakpoints::WatchKind;
+use crate::common::Signal;
 use crate::target::{Target, TargetResult};
 
-use super::{ReplayLogPosition, SingleRegisterAccessOps};
-
-// Convenient re-exports
-pub use super::{GdbInterrupt, 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: step or
-    /// 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,
-        gdb_interrupt: GdbInterrupt<'_>,
-    ) -> Result<StopReason<<Self::Arch as Arch>::Usize>, Self::Error>;
-
-    /// Support for the optimized [range stepping] resume action.
-    ///
-    /// [range stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#range-stepping
-    #[inline(always)]
-    fn support_resume_range_step(&mut self) -> Option<SingleThreadRangeSteppingOps<Self>> {
-        None
-    }
-
-    /// Support for [reverse stepping] a target.
-    ///
-    /// [reverse stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
-    #[inline(always)]
-    fn support_reverse_step(&mut self) -> Option<SingleThreadReverseStepOps<Self>> {
-        None
-    }
-
-    /// Support for [reverse continuing] a target.
-    ///
-    /// [reverse continuing]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
-    #[inline(always)]
-    fn support_reverse_cont(&mut self) -> Option<SingleThreadReverseContOps<Self>> {
-        None
-    }
-
+/// Base required debugging operations for single threaded targets.
+pub trait SingleThreadBase: Target {
     /// Read the target's registers.
     fn read_registers(
         &mut self,
@@ -82,14 +17,18 @@
         -> TargetResult<(), Self>;
 
     /// Support for single-register access.
-    /// See [`SingleRegisterAccess`](super::SingleRegisterAccess) for more
-    /// details.
+    /// See [`SingleRegisterAccess`] for more details.
     ///
     /// While this is an optional feature, it is **highly recommended** to
     /// implement it when possible, as it can significantly improve performance
     /// on certain architectures.
+    ///
+    /// [`SingleRegisterAccess`]:
+    /// super::single_register_access::SingleRegisterAccess
     #[inline(always)]
-    fn single_register_access(&mut self) -> Option<SingleRegisterAccessOps<(), Self>> {
+    fn support_single_register_access(
+        &mut self,
+    ) -> Option<super::single_register_access::SingleRegisterAccessOps<'_, (), Self>> {
         None
     }
 
@@ -114,126 +53,117 @@
         start_addr: <Self::Arch as Arch>::Usize,
         data: &[u8],
     ) -> TargetResult<(), Self>;
+
+    /// Support for resuming the target (e.g: via `continue` or `step`)
+    #[inline(always)]
+    fn support_resume(&mut self) -> Option<SingleThreadResumeOps<'_, Self>> {
+        None
+    }
 }
 
-/// Target Extension - [Reverse continue] for single threaded targets.
-///
-/// Reverse continue allows the target to run backwards until it reaches the end
-/// of the replay log.
-///
-/// [Reverse continue]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
-pub trait SingleThreadReverseCont: Target + SingleThreadOps {
-    /// Reverse-continue the target.
-    fn reverse_cont(
+/// Target extension - support for resuming single threaded targets.
+pub trait SingleThreadResume: Target {
+    /// Resume execution on the target.
+    ///
+    /// The GDB client may also include a `signal` which should be passed to the
+    /// target.
+    ///
+    /// # Additional Considerations
+    ///
+    /// ### 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, signal: Option<Signal>) -> Result<(), Self::Error>;
+
+    /// Support for optimized [single stepping].
+    ///
+    /// [single stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#index-stepi
+    #[inline(always)]
+    fn support_single_step(&mut self) -> Option<SingleThreadSingleStepOps<'_, Self>> {
+        None
+    }
+
+    /// Support for optimized [range stepping].
+    ///
+    /// [range stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#range-stepping
+    #[inline(always)]
+    fn support_range_step(&mut self) -> Option<SingleThreadRangeSteppingOps<'_, Self>> {
+        None
+    }
+
+    /// Support for [reverse stepping] a target.
+    ///
+    /// [reverse stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
+    #[inline(always)]
+    fn support_reverse_step(
         &mut self,
-        gdb_interrupt: GdbInterrupt<'_>,
-    ) -> Result<StopReason<<Self::Arch as Arch>::Usize>, Self::Error>;
-}
+    ) -> Option<super::reverse_exec::ReverseStepOps<'_, (), Self>> {
+        None
+    }
 
-define_ext!(SingleThreadReverseContOps, SingleThreadReverseCont);
-
-/// Target Extension - [Reverse stepping] for single threaded targets.
-///
-/// Reverse stepping allows the target to run backwards by one step.
-///
-/// [Reverse stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
-pub trait SingleThreadReverseStep: Target + SingleThreadOps {
-    /// Reverse-step the target.
-    fn reverse_step(
+    /// Support for [reverse continuing] a target.
+    ///
+    /// [reverse continuing]: https://sourceware.org/gdb/current/onlinedocs/gdb/Reverse-Execution.html
+    #[inline(always)]
+    fn support_reverse_cont(
         &mut self,
-        gdb_interrupt: GdbInterrupt<'_>,
-    ) -> Result<StopReason<<Self::Arch as Arch>::Usize>, Self::Error>;
+    ) -> Option<super::reverse_exec::ReverseContOps<'_, (), Self>> {
+        None
+    }
 }
 
-define_ext!(SingleThreadReverseStepOps, SingleThreadReverseStep);
+define_ext!(SingleThreadResumeOps, SingleThreadResume);
 
-/// Target Extension - Optimized [range stepping] for single threaded targets.
-/// See [`SingleThreadOps::support_resume_range_step`].
-///
-/// Range Stepping will step the target once, and keep stepping the target as
-/// long as execution remains between the specified start (inclusive) and end
-/// (exclusive) addresses, or another stop condition is met (e.g: a breakpoint
-/// it hit).
-///
-/// If the range is empty (`start` == `end`), then the action becomes
-/// equivalent to the ‘s’ action. In other words, single-step once, and
-/// report the stop (even if the stepped instruction jumps to start).
-///
-/// _Note:_ A stop reply may be sent at any point even if the PC is still
-/// within the stepping range; for example, it is valid to implement range
-/// stepping in a degenerate way as a single instruction step operation.
-///
-/// [range stepping]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#range-stepping
-pub trait SingleThreadRangeStepping: Target + SingleThreadOps {
-    /// See [`SingleThreadOps::resume`].
+/// Target Extension - Optimized single stepping for single threaded targets.
+/// See [`SingleThreadResume::support_single_step`].
+pub trait SingleThreadSingleStep: Target + SingleThreadResume {
+    /// [Single step] the target.
+    ///
+    /// Single stepping will step the target a single "step" - typically a
+    /// single instruction.
+    /// The GDB client may also include a `signal` which should be passed to the
+    /// target.
+    ///
+    /// [Single step]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#index-stepi
+    fn step(&mut self, signal: Option<Signal>) -> Result<(), Self::Error>;
+}
+
+define_ext!(SingleThreadSingleStepOps, SingleThreadSingleStep);
+
+/// Target Extension - Optimized range stepping for single threaded targets.
+/// See [`SingleThreadResume::support_range_step`].
+pub trait SingleThreadRangeStepping: Target + SingleThreadResume {
+    /// [Range step] the target.
+    ///
+    /// Range Stepping will step the target once, and keep stepping the target
+    /// as long as execution remains between the specified start (inclusive)
+    /// and end (exclusive) addresses, or another stop condition is met
+    /// (e.g: a breakpoint it hit).
+    ///
+    /// If the range is empty (`start` == `end`), then the action becomes
+    /// equivalent to the ‘s’ action. In other words, single-step once, and
+    /// report the stop (even if the stepped instruction jumps to start).
+    ///
+    /// _Note:_ A stop reply may be sent at any point even if the PC is still
+    /// within the stepping range; for example, it is valid to implement range
+    /// stepping in a degenerate way as a single instruction step operation.
+    ///
+    /// [Range step]: https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#range-stepping
     fn resume_range_step(
         &mut self,
         start: <Self::Arch as Arch>::Usize,
         end: <Self::Arch as Arch>::Usize,
-        gdb_interrupt: GdbInterrupt<'_>,
-    ) -> Result<StopReason<<Self::Arch as Arch>::Usize>, Self::Error>;
+    ) -> Result<(), Self::Error>;
 }
 
 define_ext!(SingleThreadRangeSteppingOps, SingleThreadRangeStepping);
-
-/// Describes why the target stopped.
-///
-/// Targets MUST only respond with stop reasons that correspond to IDETs that
-/// target has implemented.
-///
-/// e.g: A target which has not implemented the [`HwBreakpoint`] IDET must not
-/// return a `HwBreak` stop reason. While this is not enforced at compile time,
-/// doing so will result in a runtime `UnsupportedStopReason` error.
-///
-/// [`HwBreakpoint`]: crate::target::ext::breakpoints::HwBreakpoint
-// 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,
-    /// The process exited with the specified exit status.
-    Exited(u8),
-    /// The process terminated with the specified signal number.
-    Terminated(u8),
-    /// The program received a signal.
-    Signal(u8),
-    /// Hit a software breakpoint (e.g. due to a trap instruction).
-    ///
-    /// Requires: [`SwBreakpoint`].
-    ///
-    /// NOTE: This does not necessarily have to be a breakpoint configured by
-    /// the client/user of the current GDB session.
-    ///
-    /// [`SwBreakpoint`]: crate::target::ext::breakpoints::SwBreakpoint
-    SwBreak,
-    /// Hit a hardware breakpoint.
-    ///
-    /// Requires: [`HwBreakpoint`].
-    ///
-    /// [`HwBreakpoint`]: crate::target::ext::breakpoints::HwBreakpoint
-    HwBreak,
-    /// Hit a watchpoint.
-    ///
-    /// Requires: [`HwWatchpoint`].
-    ///
-    /// [`HwWatchpoint`]: crate::target::ext::breakpoints::HwWatchpoint
-    Watch {
-        /// Kind of watchpoint that was hit
-        kind: WatchKind,
-        /// Address of watched memory
-        addr: U,
-    },
-    /// The program has reached the end of the logged replay events.
-    ///
-    /// Requires: [`SingleThreadReverseCont`] or [`SingleThreadReverseStep`].
-    ///
-    /// This is used for GDB's reverse execution. When playing back a recording,
-    /// you may hit the end of the buffer of recorded events, and as such no
-    /// further execution can be done. This stop reason tells GDB that this has
-    /// occurred.
-    ReplayLog(ReplayLogPosition),
-}
diff --git a/src/target/ext/breakpoints.rs b/src/target/ext/breakpoints.rs
index 98fb5a0..8b40a04 100644
--- a/src/target/ext/breakpoints.rs
+++ b/src/target/ext/breakpoints.rs
@@ -5,21 +5,21 @@
 
 /// Target Extension - Set/Remove Breakpoints.
 pub trait Breakpoints: Target {
-    /// Set/Remote software breakpoints.
+    /// Support for setting / removing software breakpoints.
     #[inline(always)]
-    fn sw_breakpoint(&mut self) -> Option<SwBreakpointOps<Self>> {
+    fn support_sw_breakpoint(&mut self) -> Option<SwBreakpointOps<'_, Self>> {
         None
     }
 
-    /// Set/Remote hardware breakpoints.
+    /// Support for setting / removing hardware breakpoints.
     #[inline(always)]
-    fn hw_breakpoint(&mut self) -> Option<HwBreakpointOps<Self>> {
+    fn support_hw_breakpoint(&mut self) -> Option<HwBreakpointOps<'_, Self>> {
         None
     }
 
-    /// Set/Remote hardware watchpoints.
+    /// Support for setting / removing hardware watchpoints.
     #[inline(always)]
-    fn hw_watchpoint(&mut self) -> Option<HwWatchpointOps<Self>> {
+    fn support_hw_watchpoint(&mut self) -> Option<HwWatchpointOps<'_, Self>> {
         None
     }
 }
@@ -37,6 +37,7 @@
 /// CPU cycle, ignoring the specified breakpoint `kind` entirely.
 pub trait SwBreakpoint: Target + Breakpoints {
     /// Add a new software breakpoint.
+    ///
     /// Return `Ok(false)` if the operation could not be completed.
     fn add_sw_breakpoint(
         &mut self,
@@ -45,6 +46,7 @@
     ) -> TargetResult<bool, Self>;
 
     /// Remove an existing software breakpoint.
+    ///
     /// Return `Ok(false)` if the operation could not be completed.
     fn remove_sw_breakpoint(
         &mut self,
@@ -66,6 +68,7 @@
 /// just-as-fast).
 pub trait HwBreakpoint: Target + Breakpoints {
     /// Add a new hardware breakpoint.
+    ///
     /// Return `Ok(false)` if the operation could not be completed.
     fn add_hw_breakpoint(
         &mut self,
@@ -74,6 +77,7 @@
     ) -> TargetResult<bool, Self>;
 
     /// Remove an existing hardware breakpoint.
+    ///
     /// Return `Ok(false)` if the operation could not be completed.
     fn remove_hw_breakpoint(
         &mut self,
@@ -106,18 +110,24 @@
 /// location after each step).
 pub trait HwWatchpoint: Target + Breakpoints {
     /// Add a new hardware watchpoint.
+    /// The number of bytes to watch is specified by `len`.
+    ///
     /// Return `Ok(false)` if the operation could not be completed.
     fn add_hw_watchpoint(
         &mut self,
         addr: <Self::Arch as Arch>::Usize,
+        len: <Self::Arch as Arch>::Usize,
         kind: WatchKind,
     ) -> TargetResult<bool, Self>;
 
     /// Remove an existing hardware watchpoint.
+    /// The number of bytes to watch is specified by `len`.
+    ///
     /// Return `Ok(false)` if the operation could not be completed.
     fn remove_hw_watchpoint(
         &mut self,
         addr: <Self::Arch as Arch>::Usize,
+        len: <Self::Arch as Arch>::Usize,
         kind: WatchKind,
     ) -> TargetResult<bool, Self>;
 }
diff --git a/src/target/ext/catch_syscalls.rs b/src/target/ext/catch_syscalls.rs
new file mode 100644
index 0000000..694d8ad
--- /dev/null
+++ b/src/target/ext/catch_syscalls.rs
@@ -0,0 +1,52 @@
+//! Enable or disable catching syscalls from the inferior process.
+
+use crate::arch::Arch;
+use crate::target::{Target, TargetResult};
+
+/// Target Extension - Enable and disable catching syscalls from the inferior
+/// process.
+///
+/// Implementing this extension allows the target to support the `catch syscall`
+/// GDB client command. See [GDB documentation](https://sourceware.org/gdb/onlinedocs/gdb/Set-Catchpoints.html)
+/// for further details.
+pub trait CatchSyscalls: Target {
+    /// Enables catching syscalls from the inferior process.
+    ///
+    /// If `filter` is `None`, then all syscalls should be reported to GDB. If a
+    /// filter is provided, only the syscalls listed in the filter should be
+    /// reported to GDB.
+    ///
+    /// Note: filters are not combined, subsequent calls this method should
+    /// replace any existing syscall filtering.
+    fn enable_catch_syscalls(
+        &mut self,
+        filter: Option<SyscallNumbers<'_, <Self::Arch as Arch>::Usize>>,
+    ) -> TargetResult<(), Self>;
+
+    /// Disables catching syscalls from the inferior process.
+    fn disable_catch_syscalls(&mut self) -> TargetResult<(), Self>;
+}
+
+define_ext!(CatchSyscallsOps, CatchSyscalls);
+
+/// Describes where the syscall catchpoint was triggered at.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum CatchSyscallPosition {
+    /// Reached the entry location of the syscall.
+    Entry,
+    /// Reached the return location of the syscall.
+    Return,
+}
+
+/// Iterator of syscall numbers that should be reported to GDB.
+pub struct SyscallNumbers<'a, U> {
+    pub(crate) inner: &'a mut dyn Iterator<Item = U>,
+}
+
+impl<U> Iterator for SyscallNumbers<'_, U> {
+    type Item = U;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.inner.next()
+    }
+}
diff --git a/src/target/ext/exec_file.rs b/src/target/ext/exec_file.rs
new file mode 100644
index 0000000..0db9874
--- /dev/null
+++ b/src/target/ext/exec_file.rs
@@ -0,0 +1,32 @@
+//! Provide exec-file path for the target.
+use crate::target::{Target, TargetResult};
+
+use crate::common::Pid;
+
+/// Target Extension - Provide current exec-file.
+///
+/// NOTE: this extension is primarily intended to be used alongside the [`Host
+/// I/O Extensions`](crate::target::ext::host_io), which enables the GDB client
+/// to read the executable file directly from the target
+pub trait ExecFile: Target {
+    /// Get full absolute path of the file that was executed to create
+    /// process `pid` running on the remote system.
+    ///
+    /// If `pid` is `None`, return the filename corresponding to the
+    /// currently executing process.
+    ///
+    /// Return the number of bytes written into `buf` (which may be less than
+    /// `length`).
+    ///
+    /// If `offset` is greater than the length of the underlying data, return
+    /// `Ok(0)`.
+    fn get_exec_file(
+        &self,
+        pid: Option<Pid>,
+        offset: u64,
+        length: usize,
+        buf: &mut [u8],
+    ) -> TargetResult<usize, Self>;
+}
+
+define_ext!(ExecFileOps, ExecFile);
diff --git a/src/target/ext/extended_mode.rs b/src/target/ext/extended_mode.rs
index ce1e90e..817bec0 100644
--- a/src/target/ext/extended_mode.rs
+++ b/src/target/ext/extended_mode.rs
@@ -88,7 +88,7 @@
     /// `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>;
+    fn run(&mut self, filename: Option<&[u8]>, args: Args<'_, '_>) -> TargetResult<Pid, Self>;
 
     /// Attach to a new process with the specified PID.
     ///
@@ -146,27 +146,29 @@
         Ok(())
     }
 
-    /// Enable/Disable ASLR for spawned processes.
+    /// Support for enabling / disabling ASLR for spawned processes.
     #[inline(always)]
-    fn configure_aslr(&mut self) -> Option<ConfigureAslrOps<Self>> {
+    fn support_configure_aslr(&mut self) -> Option<ConfigureAslrOps<'_, Self>> {
         None
     }
 
-    /// Set/Remove/Reset Environment variables for spawned processes.
+    /// Support for setting / removing / resetting environment variables for
+    /// spawned processes.
     #[inline(always)]
-    fn configure_env(&mut self) -> Option<ConfigureEnvOps<Self>> {
+    fn support_configure_env(&mut self) -> Option<ConfigureEnvOps<'_, Self>> {
         None
     }
 
-    /// Configure if spawned processes should be spawned using a shell.
+    /// Support for configuring if spawned processes should be spawned using a
+    /// shell.
     #[inline(always)]
-    fn configure_startup_shell(&mut self) -> Option<ConfigureStartupShellOps<Self>> {
+    fn support_configure_startup_shell(&mut self) -> Option<ConfigureStartupShellOps<'_, Self>> {
         None
     }
 
-    /// Configure the working directory for spawned processes.
+    /// Support for configuring the working directory for spawned processes.
     #[inline(always)]
-    fn configure_working_dir(&mut self) -> Option<ConfigureWorkingDirOps<Self>> {
+    fn support_configure_working_dir(&mut self) -> Option<ConfigureWorkingDirOps<'_, Self>> {
         None
     }
 }
diff --git a/src/target/ext/host_io.rs b/src/target/ext/host_io.rs
new file mode 100644
index 0000000..77dbed0
--- /dev/null
+++ b/src/target/ext/host_io.rs
@@ -0,0 +1,381 @@
+//! Provide Host I/O operations for the target.
+use bitflags::bitflags;
+
+use crate::arch::Arch;
+use crate::target::Target;
+
+bitflags! {
+    /// Host flags for opening files.
+    ///
+    /// Extracted from the GDB documentation at
+    /// [Open Flags](https://sourceware.org/gdb/current/onlinedocs/gdb/Open-Flags.html#Open-Flags),
+    /// and the LLDB source code at
+    /// [`lldb/include/lldb/Host/File.h`](https://github.com/llvm/llvm-project/blob/ec642ceebc1aacc8b16249df7734b8cf90ae2963/lldb/include/lldb/Host/File.h#L47-L66)
+    pub struct HostIoOpenFlags: u32 {
+        /// A read-only file.
+        const O_RDONLY = 0x0;
+        /// A write-only file.
+        const O_WRONLY = 0x1;
+        /// A read-write file.
+        const O_RDWR = 0x2;
+        /// Append to an existing file.
+        const O_APPEND = 0x8;
+        /// Create a non-existent file.
+        const O_CREAT = 0x200;
+        /// Truncate an existing file.
+        const O_TRUNC = 0x400;
+        /// Exclusive access.
+        const O_EXCL = 0x800;
+
+        /// LLDB extension: Do not block.
+        const O_NONBLOCK = 1 << 28;
+        /// LLDB extension: Do not follow symlinks.
+        const O_DONT_FOLLOW_SYMLINKS = 1 << 29;
+        /// LLDB extension: Close the file when executing a new process.
+        const O_CLOSE_ON_EXEC = 1 << 30;
+        /// LLDB extension: Invalid value.
+        const O_INVALID = 1 << 31;
+    }
+}
+
+bitflags! {
+    /// Host file permissions.
+    ///
+    /// Extracted from the GDB documentation at
+    /// [mode_t Values](https://sourceware.org/gdb/current/onlinedocs/gdb/mode_005ft-Values.html#mode_005ft-Values)
+    pub struct HostIoOpenMode: u32 {
+        /// A regular file.
+        const S_IFREG = 0o100000;
+        /// A directory.
+        const S_IFDIR = 0o40000;
+        /// User read permissions.
+        const S_IRUSR = 0o400;
+        /// User write permissions.
+        const S_IWUSR = 0o200;
+        /// User execute permissions.
+        const S_IXUSR = 0o100;
+        /// Group read permissions.
+        const S_IRGRP = 0o40;
+        /// Group write permissions
+        const S_IWGRP = 0o20;
+        /// Group execute permissions.
+        const S_IXGRP = 0o10;
+        /// World read permissions.
+        const S_IROTH = 0o4;
+        /// World write permissions
+        const S_IWOTH = 0o2;
+        /// World execute permissions.
+        const S_IXOTH = 0o1;
+    }
+}
+
+/// Data returned by a host fstat request.
+///
+/// Extracted from the GDB documentation at
+/// [struct stat](https://sourceware.org/gdb/current/onlinedocs/gdb/struct-stat.html#struct-stat)
+#[derive(Debug)]
+pub struct HostIoStat {
+    /// The device.
+    pub st_dev: u32,
+    /// The inode.
+    pub st_ino: u32,
+    /// Protection bits.
+    pub st_mode: HostIoOpenMode,
+    /// The number of hard links.
+    pub st_nlink: u32,
+    /// The user id of the owner.
+    pub st_uid: u32,
+    /// The group id of the owner.
+    pub st_gid: u32,
+    /// The device type, if an inode device.
+    pub st_rdev: u32,
+    /// The size of the file in bytes.
+    pub st_size: u64,
+    /// The blocksize for the filesystem.
+    pub st_blksize: u64,
+    /// The number of blocks allocated.
+    pub st_blocks: u64,
+    /// The last time the file was accessed, in seconds since the epoch.
+    pub st_atime: u32,
+    /// The last time the file was modified, in seconds since the epoch.
+    pub st_mtime: u32,
+    /// The last time the file was changed, in seconds since the epoch.
+    pub st_ctime: u32,
+}
+
+/// Select the filesystem vFile operations will operate on. Used by vFile setfs
+/// command.
+#[derive(Debug)]
+pub enum FsKind {
+    /// Select the filesystem as seen by the remote stub.
+    Stub,
+    /// Select the filesystem as seen by process pid.
+    Pid(crate::common::Pid),
+}
+
+/// Errno values for Host I/O operations.
+///
+/// Extracted from the GDB documentation at
+/// <https://sourceware.org/gdb/onlinedocs/gdb/Errno-Values.html>
+#[derive(Debug)]
+pub enum HostIoErrno {
+    /// Operation not permitted (POSIX.1-2001).
+    EPERM = 1,
+    /// No such file or directory (POSIX.1-2001).
+    ///
+    /// Typically, this error results when a specified pathname does not exist,
+    /// or one of the components in the directory prefix of a pathname does not
+    /// exist, or the specified pathname is a dangling symbolic link.
+    ENOENT = 2,
+    /// Interrupted function call (POSIX.1-2001); see signal(7).
+    EINTR = 4,
+    /// Bad file descriptor (POSIX.1-2001).
+    EBADF = 9,
+    /// Permission denied (POSIX.1-2001).
+    EACCES = 13,
+    /// Bad address (POSIX.1-2001).
+    EFAULT = 14,
+    /// Device or resource busy (POSIX.1-2001).
+    EBUSY = 16,
+    /// File exists (POSIX.1-2001).
+    EEXIST = 17,
+    /// No such device (POSIX.1-2001).
+    ENODEV = 19,
+    /// Not a directory (POSIX.1-2001).
+    ENOTDIR = 20,
+    /// Is a directory (POSIX.1-2001).
+    EISDIR = 21,
+    /// Invalid argument (POSIX.1-2001).
+    EINVAL = 22,
+    /// Too many open files in system (POSIX.1-2001). On Linux, this is probably
+    /// a result of encountering the /proc/sys/fs/file-max limit (see proc(5)).
+    ENFILE = 23,
+    /// Too many open files (POSIX.1-2001). Commonly caused by exceeding the
+    /// RLIMIT_NOFILE resource limit described in getrlimit(2).
+    EMFILE = 24,
+    /// File too large (POSIX.1-2001).
+    EFBIG = 27,
+    /// No space left on device (POSIX.1-2001).
+    ENOSPC = 28,
+    /// Invalid seek (POSIX.1-2001).
+    ESPIPE = 29,
+    /// Read-only filesystem (POSIX.1-2001).
+    EROFS = 30,
+    /// Filename too long (POSIX.1-2001).
+    ENAMETOOLONG = 91,
+    /// Unknown errno - there may not be a GDB mapping for this value
+    EUNKNOWN = 9999,
+}
+
+/// The error type for Host I/O operations.
+pub enum HostIoError<E> {
+    /// An operation-specific non-fatal error code.
+    ///
+    /// See [`HostIoErrno`] for more details.
+    Errno(HostIoErrno),
+    /// A target-specific fatal error.
+    ///
+    /// **WARNING:** Returning this error will immediately halt the target's
+    /// execution and return a `GdbStubError::TargetError`!
+    ///
+    /// Note that returning this error will _not_ notify the GDB client that the
+    /// debugging session has been terminated, making it possible to resume
+    /// execution after resolving the error and/or setting up a post-mortem
+    /// debugging environment.
+    Fatal(E),
+}
+
+/// When the `std` feature is enabled, `HostIoError` implements
+/// `From<std::io::Error>`, mapping [`std::io::ErrorKind`] to the appropriate
+/// [`HostIoErrno`] when possible, and falling back to [`HostIoErrno::EUNKNOWN`]
+/// when no mapping exists.
+#[cfg(feature = "std")]
+impl<E> From<std::io::Error> for HostIoError<E> {
+    fn from(e: std::io::Error) -> HostIoError<E> {
+        use std::io::ErrorKind::*;
+        let errno = match e.kind() {
+            PermissionDenied => HostIoErrno::EPERM,
+            NotFound => HostIoErrno::ENOENT,
+            Interrupted => HostIoErrno::EINTR,
+            AlreadyExists => HostIoErrno::EEXIST,
+            InvalidInput => HostIoErrno::EINVAL,
+            _ => HostIoErrno::EUNKNOWN,
+        };
+        HostIoError::Errno(errno)
+    }
+}
+
+/// A specialized `Result` type for Host I/O operations. Supports reporting
+/// non-fatal errors back to the GDB client.
+///
+/// See [`HostIoError`] for more details.
+pub type HostIoResult<T, Tgt> = Result<T, HostIoError<<Tgt as Target>::Error>>;
+
+/// Target Extension - Perform I/O operations on host
+pub trait HostIo: Target {
+    /// Support `open` operation.
+    #[inline(always)]
+    fn support_open(&mut self) -> Option<HostIoOpenOps<'_, Self>> {
+        None
+    }
+
+    /// Support `close` operation.
+    #[inline(always)]
+    fn support_close(&mut self) -> Option<HostIoCloseOps<'_, Self>> {
+        None
+    }
+
+    /// Support `pread` operation.
+    #[inline(always)]
+    fn support_pread(&mut self) -> Option<HostIoPreadOps<'_, Self>> {
+        None
+    }
+
+    /// Support `pwrite` operation.
+    #[inline(always)]
+    fn support_pwrite(&mut self) -> Option<HostIoPwriteOps<'_, Self>> {
+        None
+    }
+
+    /// Support `fstat` operation.
+    #[inline(always)]
+    fn support_fstat(&mut self) -> Option<HostIoFstatOps<'_, Self>> {
+        None
+    }
+
+    /// Support `unlink` operation.
+    #[inline(always)]
+    fn support_unlink(&mut self) -> Option<HostIoUnlinkOps<'_, Self>> {
+        None
+    }
+
+    /// Support `readlink` operation.
+    #[inline(always)]
+    fn support_readlink(&mut self) -> Option<HostIoReadlinkOps<'_, Self>> {
+        None
+    }
+
+    /// Support `setfs` operation.
+    #[inline(always)]
+    fn support_setfs(&mut self) -> Option<HostIoSetfsOps<'_, Self>> {
+        None
+    }
+}
+
+define_ext!(HostIoOps, HostIo);
+
+/// Nested Target Extension - Host I/O open operation.
+pub trait HostIoOpen: HostIo {
+    /// Open a file at `filename` and return a file descriptor for it, or return
+    /// [`HostIoError::Errno`] if an error occurs.
+    ///
+    /// `flags` are the flags used when opening the file (see
+    /// [`HostIoOpenFlags`]), and `mode` is the mode used if the file is
+    /// created (see [`HostIoOpenMode`]).
+    fn open(
+        &mut self,
+        filename: &[u8],
+        flags: HostIoOpenFlags,
+        mode: HostIoOpenMode,
+    ) -> HostIoResult<u32, Self>;
+}
+
+define_ext!(HostIoOpenOps, HostIoOpen);
+
+/// Nested Target Extension - Host I/O close operation.
+pub trait HostIoClose: HostIo {
+    /// Close the open file corresponding to `fd`.
+    fn close(&mut self, fd: u32) -> HostIoResult<(), Self>;
+}
+
+define_ext!(HostIoCloseOps, HostIoClose);
+
+/// Nested Target Extension - Host I/O pread operation.
+pub trait HostIoPread: HostIo {
+    /// Read data from the open file corresponding to `fd`.
+    ///
+    /// Up to `count` bytes will be read from the file, starting at `offset`
+    /// relative to the start of the file.
+    ///
+    /// Return the number of bytes written into `buf` (which may be less than
+    /// `count`).
+    ///
+    /// If `offset` is greater than the length of the underlying data, return
+    /// `Ok(0)`.
+    fn pread(
+        &mut self,
+        fd: u32,
+        count: usize,
+        offset: u64,
+        buf: &mut [u8],
+    ) -> HostIoResult<usize, Self>;
+}
+
+define_ext!(HostIoPreadOps, HostIoPread);
+
+/// Nested Target Extension - Host I/O pwrite operation.
+pub trait HostIoPwrite: HostIo {
+    /// Write `data` to the open file corresponding to `fd`.
+    ///
+    /// Start the write at `offset` from the start of the file.
+    ///
+    /// Return the number of bytes written, which may be shorter
+    /// than the length of data, or [`HostIoError::Errno`] if an error occurred.
+    fn pwrite(
+        &mut self,
+        fd: u32,
+        offset: <Self::Arch as Arch>::Usize,
+        data: &[u8],
+    ) -> HostIoResult<<Self::Arch as Arch>::Usize, Self>;
+}
+
+define_ext!(HostIoPwriteOps, HostIoPwrite);
+
+/// Nested Target Extension - Host I/O fstat operation.
+pub trait HostIoFstat: HostIo {
+    /// Get information about the open file corresponding to `fd`.
+    ///
+    /// On success return a [`HostIoStat`] struct.
+    /// Return [`HostIoError::Errno`] if an error occurs.
+    fn fstat(&mut self, fd: u32) -> HostIoResult<HostIoStat, Self>;
+}
+
+define_ext!(HostIoFstatOps, HostIoFstat);
+
+/// Nested Target Extension - Host I/O unlink operation.
+pub trait HostIoUnlink: HostIo {
+    /// Delete the file at `filename` on the target.
+    fn unlink(&mut self, filename: &[u8]) -> HostIoResult<(), Self>;
+}
+
+define_ext!(HostIoUnlinkOps, HostIoUnlink);
+
+/// Nested Target Extension - Host I/O readlink operation.
+pub trait HostIoReadlink: HostIo {
+    /// Read value of symbolic link `filename` on the target.
+    ///
+    /// Return the number of bytes written into `buf`.
+    ///
+    /// Unlike most other Host IO handlers, if the resolved file path exceeds
+    /// the length of the provided `buf`, the target should NOT return a
+    /// partial response, and MUST return a `Err(HostIoErrno::ENAMETOOLONG)`.
+    fn readlink(&mut self, filename: &[u8], buf: &mut [u8]) -> HostIoResult<usize, Self>;
+}
+
+define_ext!(HostIoReadlinkOps, HostIoReadlink);
+
+/// Nested Target Extension - Host I/O setfs operation.
+pub trait HostIoSetfs: HostIo {
+    /// Select the filesystem on which vFile operations with filename arguments
+    /// will operate. This is required for GDB to be able to access files on
+    /// remote targets where the remote stub does not share a common filesystem
+    /// with the inferior(s).
+    ///
+    /// See [`FsKind`] for the meaning of `fs`.
+    ///
+    /// If setfs indicates success, the selected filesystem remains selected
+    /// until the next successful setfs operation.
+    fn setfs(&mut self, fs: FsKind) -> HostIoResult<(), Self>;
+}
+
+define_ext!(HostIoSetfsOps, HostIoSetfs);
diff --git a/src/target/ext/memory_map.rs b/src/target/ext/memory_map.rs
new file mode 100644
index 0000000..46ffd2a
--- /dev/null
+++ b/src/target/ext/memory_map.rs
@@ -0,0 +1,25 @@
+//! Provide a memory map for the target.
+use crate::target::{Target, TargetResult};
+
+/// Target Extension - Read the target's memory map.
+pub trait MemoryMap: Target {
+    /// Get memory map XML file from the target.
+    ///
+    /// See the [GDB Documentation] for a description of the format.
+    ///
+    /// [GDB Documentation]: https://sourceware.org/gdb/onlinedocs/gdb/Memory-Map-Format.html
+    ///
+    /// Return the number of bytes written into `buf` (which may be less than
+    /// `length`).
+    ///
+    /// If `offset` is greater than the length of the underlying data, return
+    /// `Ok(0)`.
+    fn memory_map_xml(
+        &self,
+        offset: u64,
+        length: usize,
+        buf: &mut [u8],
+    ) -> TargetResult<usize, Self>;
+}
+
+define_ext!(MemoryMapOps, MemoryMap);
diff --git a/src/target/ext/mod.rs b/src/target/ext/mod.rs
index 80342de..8198936 100644
--- a/src/target/ext/mod.rs
+++ b/src/target/ext/mod.rs
@@ -8,7 +8,8 @@
 //!
 //! If there's a GDB protocol extensions you're interested in that hasn't been
 //! implemented in `gdbstub` yet, (e.g: remote filesystem access, tracepoint
-//! support, etc...), consider opening an issue / filing a PR on GitHub!
+//! support, etc...), consider opening an issue / filing a PR on the
+//! [`gdbstub` GitHub repo](https://github.com/daniel5151/gdbstub/).
 //!
 //! 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
@@ -18,9 +19,9 @@
 //!
 //! The GDB protocol is massive, and contains all sorts of optional
 //! functionality. In the early versions of `gdbstub`, the `Target` trait
-//! directly had a method for _every single protocol extension_, which if taken
-//! to the extreme, would have resulted in literally _hundreds_ of associated
-//! methods!
+//! directly implemented a method for _every single protocol extension_. If this
+//! trend continued, there would've been literally _hundreds_ of associated
+//! methods - of which only a small subset were ever used at once!
 //!
 //! Aside from the cognitive complexity of having so many methods on a single
 //! trait, this approach had numerous other drawbacks as well:
@@ -28,8 +29,8 @@
 //!  - 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.
-//!  - `GdbStub`'s internal implementation needed to include _runtime_ checks to
-//!    deal with incorrectly implemented `Target`s.
+//!  - `GdbStub`'s internal implementation needed to include a large number of
+//!    _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
@@ -41,7 +42,7 @@
 //!
 //! At first blush, it seems the the solution to all these issues is obvious:
 //! simply tie each protocol extension to a `cargo` feature! And yes, while
-//! would would indeed work, there would be several serious ergonomic drawbacks:
+//! this would indeed work, there would be several serious ergonomic drawbacks:
 //!
 //! - There would be _hundreds_ of individual feature flags that would need to
 //!   be toggled by end users.
@@ -110,7 +111,7 @@
 //!
 //!     // Optional extension
 //!     #[inline(always)]
-//!     fn get_protocol_ext(&mut self) -> Option<ProtocolExtOps<Self>> {
+//!     fn support_protocol_ext(&mut self) -> Option<ProtocolExtOps<Self>> {
 //!         // disabled by default
 //!         None
 //!     }
@@ -140,14 +141,14 @@
 //! ```
 //!
 //! - (user) Implements the base `Protocol` trait, overriding the
-//!   `get_protocol_ext` method to return `Some(self)`, which will effectively
-//!   "enable" the extension.
+//!   `support_protocol_ext` method to return `Some(self)`, which will
+//!   effectively "enable" the extension.
 //!
 //! ```rust,ignore
 //! impl Protocol for MyTarget {
 //!     // Optional extension
 //!     #[inline(always)]
-//!     fn get_protocol_ext(&mut self) -> Option<ProtocolExtOps<Self>> {
+//!     fn support_protocol_ext(&mut self) -> Option<ProtocolExtOps<Self>> {
 //!         Some(self) // will not compile unless `MyTarget` also implements `ProtocolExt`
 //!     }
 //!
@@ -166,8 +167,8 @@
 //!
 //! Now, here's where IDETs really shine: If the user didn't implement
 //! `ProtocolExt`, but _did_ try to enable the feature by overriding
-//! `get_protocol_ext` to return `Some(self)`, they'll get a compile-time error
-//! that looks something like this:
+//! `support_protocol_ext` to return `Some(self)`, they'll get a compile-time
+//! error that looks something like this:
 //!
 //! ```text
 //! error[E0277]: the trait bound `MyTarget: ProtocolExt` is not satisfied
@@ -187,7 +188,7 @@
 //!
 //! ```rust,ignore
 //! fn execute_protocol(mut target: impl Target) {
-//!     match target.get_protocol_ext() {
+//!     match target.support_protocol_ext() {
 //!         Some(ops) => ops.foo(),
 //!         None => { /* fallback when not enabled */ }
 //!     }
@@ -197,11 +198,12 @@
 //! This is already pretty cool, but what's _even cooler_ is that if you take a
 //! look at the generated assembly of a monomorphized `execute_protocol` method
 //! (e.g: using godbolt.org), you'll find that the compiler is able to
-//! efficiently inline and devirtualize _all_ the calls to `get_protocol_ext`
-//! method, which in-turn allows the dead-code-eliminator to work its magic, and
-//! remove the unused branches from the generated code! i.e: If a target
-//! implemention didn't implement the `ProtocolExt` extension, then that `match`
-//! statement in `execute_protocol` would simply turn into a noop!
+//! efficiently inline and devirtualize _all_ the calls to
+//! `support_protocol_ext` method, which in-turn allows the dead-code-eliminator
+//! to work its magic, and remove the unused branches from the generated code!
+//! i.e: If a target implemention didn't implement the `ProtocolExt` extension,
+//! then that `match` statement in `execute_protocol` would simply turn into a
+//! noop!
 //!
 //! If IDETs are something you're interested in, consider checking out
 //! [daniel5151/optional-trait-methods](https://github.com/daniel5151/optional-trait-methods)
@@ -233,9 +235,9 @@
 //!      "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.get_protocol_ext().is_some()` block, it's possible to specify
-//!       _arbitrary_ blocks of code to be feature-dependent!
+//!     - This is a really awesome trick: by wrapping code in an `if
+//!       target.support_protocol_ext().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.
 
@@ -256,9 +258,14 @@
     };
 }
 
+pub mod auxv;
 pub mod base;
 pub mod breakpoints;
+pub mod catch_syscalls;
+pub mod exec_file;
 pub mod extended_mode;
+pub mod host_io;
+pub mod memory_map;
 pub mod monitor_cmd;
 pub mod section_offsets;
 pub mod target_description_xml_override;
diff --git a/src/target/ext/target_description_xml_override.rs b/src/target/ext/target_description_xml_override.rs
index 7cac034..7cb9011 100644
--- a/src/target/ext/target_description_xml_override.rs
+++ b/src/target/ext/target_description_xml_override.rs
@@ -1,5 +1,5 @@
 //! Override the target description XML specified by `Target::Arch`.
-use crate::target::Target;
+use crate::target::{Target, TargetResult};
 
 /// Target Extension - Override the target description XML specified by
 /// `Target::Arch`.
@@ -8,12 +8,30 @@
 /// runtime-configurable target, it's unlikely that you'll need to implement
 /// this extension.
 pub trait TargetDescriptionXmlOverride: Target {
-    /// Return the target's description XML file (`target.xml`).
+    /// Read a target's description XML file at the specified `annex`.
+    ///
+    /// The "root" `annex` will always be `b"target.xml"`, though advanced
+    /// targets may choose to split `target.xml` into multiple files via the
+    /// the `<xi:include href="other_file.xml"/>` XML tag. If the GDB client
+    /// encounter any such tags, it will re-invoke this handler with `annex`
+    /// specified to point to `b"other_file.xml"`.
     ///
     /// Refer to the
     /// [target_description_xml](crate::arch::Arch::target_description_xml)
     /// docs for more info.
-    fn target_description_xml(&self) -> &str;
+    ///
+    /// Return the number of bytes written into `buf` (which may be less than
+    /// `length`).
+    ///
+    /// If `offset` is greater than the length of the underlying data, return
+    /// `Ok(0)`.
+    fn target_description_xml(
+        &self,
+        annex: &[u8],
+        offset: u64,
+        length: usize,
+        buf: &mut [u8],
+    ) -> TargetResult<usize, Self>;
 }
 
 define_ext!(
diff --git a/src/target/mod.rs b/src/target/mod.rs
index d0e2483..eaed427 100644
--- a/src/target/mod.rs
+++ b/src/target/mod.rs
@@ -47,42 +47,76 @@
 //! becomes `fn resume(&mut self, ...) -> Result<_, MyEmuError>`, which makes it
 //! possible to preserve the target-specific error while using `gdbstub`!
 //!
-//! > _Aside:_ What's with all the `<Self::Arch as Arch>::` syntax?
-//!
-//! > As you explore `Target` and its many extension traits, you'll enounter
-//! many method signatures that use this pretty gnarly bit of Rust 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`, but 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.
-//!
-//! > To improve the readability and maintainability of your own implementation,
-//! it'd be best to swap out the fully qualified syntax with whatever concrete
-//! type is being used. e.g: on a 32-bit target, instead of cluttering up a
-//! method implementation with a parameter passed as `(addr: <Self::Arch as
-//! Arch>::Usize)`, just write `(addr: u32)` directly.
-//!
 //! ## Required Methods (Base Protocol)
 //!
 //! A minimal `Target` implementation only needs to implement a single method:
 //! [`Target::base_ops`](trait.Target.html#tymethod.base_ops). This method is
 //! used to select which set of [`base`](crate::target::ext::base)
 //! debugging operations will be used to control the target. These are
-//! fundamental operations such as starting/stopping execution, reading/writing
-//! memory, etc...
+//! fundamental operations such as reading/writing memory, etc...
 //!
 //! All other methods are entirely optional! Check out the
-//! [`ext`] module for a full list of currently supported protocol extensions.
+//! [`ext`](ext#modules) module for a full list of currently supported protocol
+//! extensions.
 //!
-//! ### Example: A Bare-Minimum Single Threaded `Target`
+//! ## Optional Protocol Extensions
+//!
+//! The GDB protocol is _massive_, and there are plenty of optional protocol
+//! extensions that targets can implement to enhance the base debugging
+//! experience.
+//!
+//! These protocol extensions range from relatively mundane things such as
+//! setting/removing breakpoints or reading/writing individual registers, but
+//! also include fancy things such as support for time travel debugging, running
+//! shell commands remotely, or even performing file IO on the target!
+//!
+//! `gdbstub` uses a somewhat unique approach to exposing these many features,
+//! called **Inlinable Dyn Extension Traits (IDETs)**. While this might sound a
+//! bit daunting, the API is actually quite straightforward, and described in
+//! great detail under the [`ext` module's documentation](ext).
+//!
+//! After getting the base protocol up and running, do take a moment to skim
+//! through and familiarize yourself with the [many different protocol
+//! extensions](ext# modules) that `gdbstub` implements. There are some really
+//! nifty ones that you might not even realize you need!
+//!
+//! As a suggestion on where to start, consider implementing some of the
+//! breakpoint related extensions under
+//! [`breakpoints`](crate::target::ext::breakpoints). While setting/removing
+//! breakpoints is technically an "optional" part of the GDB protocol, I'm sure
+//! you'd be hard pressed to find a debugger that doesn't support breakpoints.
+//!
+//! ### Note: Missing Protocol Extensions
+//!
+//! `gdbstub`'s development is guided by the needs of its contributors, with
+//! new features being added on an "as-needed" basis.
+//!
+//! If there's a GDB protocol extensions you're interested in that hasn't been
+//! implemented in `gdbstub` yet, (e.g: remote filesystem access, tracepoint
+//! support, etc...), consider opening an issue / filing a PR on the
+//! [`gdbstub` GitHub repo](https://github.com/daniel5151/gdbstub/).
+//!
+//! 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.
+//!
+//! ### Example: A fairly minimal Single Threaded `Target`
+//!
+//! This example includes a handful of required and optional target features,
+//! and shows off the basics of how to work with IDETs.
 //!
 //! ```rust
+//! use gdbstub::common::Signal;
 //! use gdbstub::target::{Target, TargetResult};
 //! use gdbstub::target::ext::base::BaseOps;
-//! use gdbstub::target::ext::base::singlethread::SingleThreadOps;
-//! use gdbstub::target::ext::base::singlethread::{ResumeAction, GdbInterrupt, StopReason};
+//! use gdbstub::target::ext::base::singlethread::{
+//!     SingleThreadResumeOps, SingleThreadSingleStepOps
+//! };
+//! use gdbstub::target::ext::base::singlethread::{
+//!     SingleThreadBase, SingleThreadResume, SingleThreadSingleStep
+//! };
+//! use gdbstub::target::ext::breakpoints::{Breakpoints, SwBreakpoint};
+//! use gdbstub::target::ext::breakpoints::{BreakpointsOps, SwBreakpointOps};
 //!
 //! struct MyTarget;
 //!
@@ -90,18 +124,19 @@
 //!     type Error = ();
 //!     type Arch = gdbstub_arch::arm::Armv4t; // as an example
 //!
+//!     #[inline(always)]
 //!     fn base_ops(&mut self) -> BaseOps<Self::Arch, Self::Error> {
 //!         BaseOps::SingleThread(self)
 //!     }
+//!
+//!     // opt-in to support for setting/removing breakpoints
+//!     #[inline(always)]
+//!     fn support_breakpoints(&mut self) -> Option<BreakpointsOps<Self>> {
+//!         Some(self)
+//!     }
 //! }
 //!
-//! impl SingleThreadOps for MyTarget {
-//!     fn resume(
-//!         &mut self,
-//!         action: ResumeAction,
-//!         gdb_interrupt: GdbInterrupt<'_>,
-//!     ) -> Result<StopReason<u32>, ()> { todo!() }
-//!
+//! impl SingleThreadBase for MyTarget {
 //!     fn read_registers(
 //!         &mut self,
 //!         regs: &mut gdbstub_arch::arm::reg::ArmCoreRegs,
@@ -123,60 +158,100 @@
 //!         start_addr: u32,
 //!         data: &[u8],
 //!     ) -> TargetResult<(), Self> { todo!() }
+//!
+//!     // most targets will want to support at resumption as well...
+//!
+//!     #[inline(always)]
+//!     fn support_resume(&mut self) -> Option<SingleThreadResumeOps<Self>> {
+//!         Some(self)
+//!     }
+//! }
+//!
+//! impl SingleThreadResume for MyTarget {
+//!     fn resume(
+//!         &mut self,
+//!         signal: Option<Signal>,
+//!     ) -> Result<(), Self::Error> { todo!() }
+//!
+//!     // ...and if the target supports resumption, it'll likely want to support
+//!     // single-step resume as well
+//!
+//!     #[inline(always)]
+//!     fn support_single_step(
+//!         &mut self
+//!     ) -> Option<SingleThreadSingleStepOps<'_, Self>> {
+//!         Some(self)
+//!     }
+//! }
+//!
+//! impl SingleThreadSingleStep for MyTarget {
+//!     fn step(
+//!         &mut self,
+//!         signal: Option<Signal>,
+//!     ) -> Result<(), Self::Error> { todo!() }
+//! }
+//!
+//! impl Breakpoints for MyTarget {
+//!     // there are several kinds of breakpoints - this target uses software breakpoints
+//!     #[inline(always)]
+//!     fn support_sw_breakpoint(&mut self) -> Option<SwBreakpointOps<Self>> {
+//!         Some(self)
+//!     }
+//! }
+//!
+//! impl SwBreakpoint for MyTarget {
+//!     fn add_sw_breakpoint(
+//!         &mut self,
+//!         addr: u32,
+//!         kind: gdbstub_arch::arm::ArmBreakpointKind,
+//!     ) -> TargetResult<bool, Self> { todo!() }
+//!
+//!     fn remove_sw_breakpoint(
+//!         &mut self,
+//!         addr: u32,
+//!         kind: gdbstub_arch::arm::ArmBreakpointKind,
+//!     ) -> TargetResult<bool, Self> { todo!() }
 //! }
 //! ```
 //!
-//! ## Optional Methods (Protocol Extensions)
-//!
-//! The GDB protocol is _massive_, and there are plenty of optional protocol
-//! extensions that targets can implement to enhance the base debugging
-//! experience. These protocol extensions range from relatively mundane things
-//! such as setting/removing breakpoints or reading/writing individual
-//! registers, but also include fancy things such as  support for time travel
-//! debugging, running shell commands remotely, or even performing file IO on
-//! the target!
-//!
-//! As a starting point, consider implementing some of the breakpoint related
-//! extensions under [`breakpoints`](crate::target::ext::breakpoints). While
-//! setting/removing breakpoints is technically an "optional" part of the GDB
-//! protocol, I'm sure you'd be hard pressed to find a debugger that doesn't
-//! support breakpoints.
-//!
-//! Please make sure to read and understand [the documentation](ext) regarding
-//! how IDETs work!
-//!
-//! ### Note: Missing Protocol Extensions
-//!
-//! `gdbstub`'s development is guided by the needs of its contributors, with
-//! new features being added on an "as-needed" basis.
-//!
-//! If there's a GDB protocol extensions you're interested in that hasn't been
-//! implemented in `gdbstub` 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.
-//!
 //! ## A note on error handling
 //!
 //! As you explore the various protocol extension traits, you'll often find that
 //! functions don't return a typical [`Result<T, Self::Error>`],
 //! and will instead return a [`TargetResult<T, Self>`].
 //!
-//! At first glance, this might look a bit strange, since it might look as
-//! though the `Err` variant of `TargetResult` is actually `Self` instead of
-//! `Self::Error`! Thankfully, there's a good reason for why that's the case,
-//! which you can read about as part of the [`TargetError`] documentation.
+//! At first glance this might look a bit strange, since it looks like the `Err`
+//! variant of `TargetResult` is `Self` instead of `Self::Error`!
 //!
-//! In a nutshell, `TargetResult` wraps a typical `Result<T, Self::Error>` with
-//! a few additional error types which can be reported back to the GDB client
-//! via the GDB RSP. For example, if the GDB client tried to read memory from
-//! invalid memory, instead of immediately terminating the entire debugging
-//! session, it's possible to simply return a `Err(TargetError::Errno(14)) //
-//! EFAULT`, which will notify the GDB client that the operation has failed.
-
-use crate::arch::Arch;
+//! Thankfully, there's a good reason for why that's the case. In a nutshell,
+//! `TargetResult` wraps a typical `Result<T, Self::Error>` with a few
+//! additional error types which can be reported back to the GDB client via the
+//! GDB RSP.
+//!
+//! For example, if the GDB client tried to read memory from invalid memory,
+//! instead of immediately terminating the entire debugging session, it's
+//! possible to simply return a `Err(TargetError::Errno(14)) // EFAULT`, which
+//! will notify the GDB client that the operation has failed.
+//!
+//! See the [`TargetError`] docs for more details.
+//!
+//! ## A note on all the `<Self::Arch as Arch>::` syntax
+//!
+//! As you explore `Target` and its many extension traits, you'll enounter
+//! many method signatures that use this pretty gnarly bit of Rust 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`, but 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.
+//!
+//! To improve the readability and maintainability of your own implementation,
+//! it'd be best to swap out the fully qualified syntax with whatever concrete
+//! type is being used. e.g: on a 32-bit target, instead of cluttering up a
+//! method implementation with a parameter passed as `(addr: <Self::Arch as
+//! Arch>::Usize)`, just write `(addr: u32)` directly.
+use crate::arch::{Arch, SingleStepGdbBehavior};
 
 pub mod ext;
 
@@ -187,7 +262,7 @@
 ///
 /// 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).
+/// [`errno`-style error codes](https://chromium.googlesource.com/chromiumos/docs/+/HEAD/constants/errnos.md).
 /// Moreover, often times the GDB client will simply _ignore_ the specific error
 /// code returned by the stub, and print a generic failure message instead.
 ///
@@ -206,8 +281,14 @@
 /// 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
+/// ```rust
+/// use gdbstub::target::TargetError;
+///
+/// /// Target-specific Fatal Error
+/// enum MyTargetFatalError {
+///     // ...
+/// }
+///
 /// impl From<MyTargetFatalError> for TargetError<MyTargetFatalError> {
 ///     fn from(e: MyTargetFatalError) -> Self {
 ///         TargetError::Fatal(e)
@@ -223,7 +304,7 @@
 pub enum TargetError<E> {
     /// A non-specific, non-fatal error has occurred.
     NonFatal,
-    /// I/O Error. Only available when the `std` feature is enabled.
+    /// Non-fatal I/O Error. Only available when the `std` feature is enabled.
     ///
     /// At the moment, this is just shorthand for
     /// `TargetError::NonFatal(e.raw_os_err().unwrap_or(121))`. Error code `121`
@@ -239,12 +320,8 @@
     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.
+    /// **WARNING:** Returning this error will immediately terminate the GDB
+    /// debugging session, and return a top-level `GdbStubError::TargetError`!
     Fatal(E),
 }
 
@@ -263,7 +340,10 @@
     }
 }
 
-/// A specialized `Result` type for `Target` operations.
+/// A specialized `Result` type for `Target` operations. Supports reporting
+/// non-fatal errors back to the GDB client.
+///
+/// See [`TargetError`] for more details.
 ///
 /// _Note:_ While it's typically parameterized as `TargetResult<T, Self>`, the
 /// error value is in-fact `TargetError<Self::Error>` (not `Self`).
@@ -295,59 +375,300 @@
     ///
     /// For example, on a single-threaded target:
     ///
-    /// ```rust,ignore
+    /// ```rust
     /// use gdbstub::target::Target;
-    /// use gdbstub::target::base::singlethread::SingleThreadOps;
-    ///
-    /// impl SingleThreadOps for MyTarget {
-    ///     // ...
-    /// }
+    /// use gdbstub::target::ext::base::BaseOps;
+    /// use gdbstub::target::ext::base::singlethread::SingleThreadBase;
+    /// # use gdbstub::target::TargetResult;
+    /// # struct MyTarget;
     ///
     /// impl Target for MyTarget {
-    ///     fn base_ops(&mut self) -> base::BaseOps<Self::Arch, Self::Error> {
-    ///         base::BaseOps::SingleThread(self)
+    ///     // ...
+    ///     # type Arch = gdbstub_arch::arm::Armv4t;
+    ///     # type Error = ();
+    ///
+    ///     fn base_ops(&mut self) -> BaseOps<Self::Arch, Self::Error> {
+    ///         BaseOps::SingleThread(self)
     ///     }
     /// }
+    ///
+    /// // ...and then implement the associated base IDET
+    /// impl SingleThreadBase for MyTarget {
+    ///     // ...
+    /// #   fn read_registers(
+    /// #       &mut self,
+    /// #       regs: &mut gdbstub_arch::arm::reg::ArmCoreRegs,
+    /// #   ) -> TargetResult<(), Self> { todo!() }
+    /// #
+    /// #   fn write_registers(
+    /// #       &mut self,
+    /// #       regs: &gdbstub_arch::arm::reg::ArmCoreRegs
+    /// #   ) -> TargetResult<(), Self> { todo!() }
+    /// #
+    /// #   fn read_addrs(
+    /// #       &mut self,
+    /// #       start_addr: u32,
+    /// #       data: &mut [u8],
+    /// #   ) -> TargetResult<(), Self> { todo!() }
+    /// #
+    /// #   fn write_addrs(
+    /// #       &mut self,
+    /// #       start_addr: u32,
+    /// #       data: &[u8],
+    /// #   ) -> TargetResult<(), Self> { todo!() }
+    /// }
     /// ```
-    fn base_ops(&mut self) -> ext::base::BaseOps<Self::Arch, Self::Error>;
+    fn base_ops(&mut self) -> ext::base::BaseOps<'_, Self::Arch, Self::Error>;
 
-    /// Set/Remove software breakpoints.
+    /// If the target supports resumption, but hasn't implemented explicit
+    /// support for software breakpoints (via
+    /// [`SwBreakpoints`](ext::breakpoints::SwBreakpoint)), notify the user
+    /// that the GDB client may set "implicit" software breakpoints by
+    /// rewriting the target's instruction stream.
+    ///
+    /// Targets that wish to use the GDB client's implicit software breakpoint
+    /// handler must explicitly **opt-in** to this somewhat surprising GDB
+    /// feature by overriding this method to return `true`.
+    ///
+    /// If you are reading these docs after having encountered a
+    /// [`GdbStubError::ImplicitSwBreakpoints`] error, it's quite likely that
+    /// you'll want to implement explicit support for software breakpoints.
+    ///
+    /// # Context
+    ///
+    /// An "implicit" software breakpoint is set by the GDB client by manually
+    /// writing a software breakpoint instruction into target memory via the
+    /// target's `write_addrs` implementation. i.e: the GDB client will
+    /// overwrite the target's instruction stream with a software breakpoint
+    /// instruction, with the expectation that the target has a implemented a
+    /// breakpoint exception handler.
+    ///
+    /// # Implications
+    ///
+    /// While this is a reasonable (and useful!) bit of behavior when targeting
+    /// many classes of remote stub (e.g: bare-metal, separate process), there
+    /// are many `gdbstub` implementations that do _not_ implement "software
+    /// breakpoints" by naively rewriting the target's instruction stream.
+    ///
+    /// - e.g: a `gdbstub` implemented in an emulator is unlikely to implement
+    ///   "software breakpoints" by hooking into the emulated hardware's
+    ///   breakpoint handler, and would likely implement "breakpoints" by
+    ///   maintaining a list of addresses to stop at as part of its core
+    ///   interpreter loop.
+    /// - e.g: a `gdbstub` implemented in a hypervisor would require special
+    ///   coordination with the guest kernel to support software breakpoints, as
+    ///   there would need to be some way to distinguish between "in-guest"
+    ///   debugging, and "hypervisor" debugging.
+    ///
+    /// As such, `gdbstub` includes this `guard_rail_implicit_sw_breakpoints`
+    /// method.
+    ///
+    /// As the name suggests, this method acts as a "guard rail" that
+    /// warns users from accidentally opting into this "implicit" breakpoint
+    /// functionality, and being exceptionally confused as to why their
+    /// target is acting weird.
+    ///
+    /// If `gdbstub` detects that the target has not implemented a software
+    /// breakpoint handler, it will check if
+    /// `guard_rail_implicit_sw_breakpoints()` has been enabled, and if it
+    /// has not, it will trigger a runtime error that points the user at this
+    /// very documentation.
+    ///
+    /// # A note on breakpoints
+    ///
+    /// Aside from setting breakpoints at the explicit behest of the user (e.g:
+    /// when setting breakpoints via the `b` command in GDB), the GDB client may
+    /// also set/remove _temporary breakpoints_ as part of other commands.
+    ///
+    /// e.g: On targets without native support for hardware single-stepping,
+    /// calling `stepi` in GDB will result in the GDB client setting a temporary
+    /// breakpoint on the next instruction + resuming via `continue` instead.
+    ///
+    /// [`GdbStubError::ImplicitSwBreakpoints`]:
+    /// crate::stub::GdbStubError::ImplicitSwBreakpoints
     #[inline(always)]
-    fn breakpoints(&mut self) -> Option<ext::breakpoints::BreakpointsOps<Self>> {
+    fn guard_rail_implicit_sw_breakpoints(&self) -> bool {
+        false
+    }
+
+    /// Override the arch-level value for [`Arch::single_step_gdb_behavior`].
+    ///
+    /// If you are reading these docs after having encountered a
+    /// [`GdbStubError::SingleStepGdbBehavior`] error, you may need to either:
+    ///
+    /// - implement support for single-step
+    /// - disable existing support for single step
+    /// - be a Good Citizen and perform a quick test to see what kind of
+    ///   behavior your Arch exhibits.
+    ///
+    /// # WARNING
+    ///
+    /// Unless you _really_ know what you're doing (e.g: working on a dynamic
+    /// target implementation, attempting to fix the underlying bug, etc...),
+    /// you should **not** override this method, and instead follow the advice
+    /// the error gives you.
+    ///
+    /// Incorrectly setting this method may lead to "unexpected packet" runtime
+    /// errors!
+    ///
+    /// # Details
+    ///
+    /// This method provides an "escape hatch" for disabling a workaround for a
+    /// bug in the mainline GDB client implementation.
+    ///
+    /// To squelch all errors, this method can be set to return
+    /// [`SingleStepGdbBehavior::Optional`] (though as mentioned above - you
+    /// should only do so if you're sure that's the right behavior).
+    ///
+    /// For more information, see the documentation for
+    /// [`Arch::single_step_gdb_behavior`].
+    ///
+    /// [`GdbStubError::SingleStepGdbBehavior`]:
+    /// crate::stub::GdbStubError::SingleStepGdbBehavior
+    #[inline(always)]
+    fn guard_rail_single_step_gdb_behavior(&self) -> SingleStepGdbBehavior {
+        <Self::Arch as Arch>::single_step_gdb_behavior()
+    }
+
+    /// Enable/disable using the more efficient `X` packet to write to target
+    /// memory (as opposed to the basic `M` packet).
+    ///
+    /// By default, this method returns `true`.
+    ///
+    /// _Author's note:_ Unless you're _really_ trying to squeeze `gdbstub` onto
+    /// a particularly resource-constrained platform, you may as well leave this
+    /// optimization enabled.
+    #[inline(always)]
+    fn use_x_upcase_packet(&self) -> bool {
+        true
+    }
+
+    /// Whether `gdbstub` should provide a "stub" `resume` implementation on
+    /// targets without support for resumption.
+    ///
+    /// At the time of writing, the mainline GDB client does not gracefully
+    /// handle targets that do not support support resumption, and will hang
+    /// indefinitely if a user inadvertently attempts to `continue` or `step`
+    /// such a target.
+    ///
+    /// To make the `gdbstub` user experience a bit better, the library includes
+    /// bit of "stub" code to gracefully handle these cases.
+    ///
+    /// If a user attempts to resume a target that hasn't implemented support
+    /// for resumption, `gdbstub` will write a brief message back to the GDB
+    /// client console, and will immediately return a "stopped with TRAP" stop
+    /// reason.
+    ///
+    /// This method controls whether or not this bt of behavior is enabled.
+    ///
+    /// _Author's note:_ Unless you're _really_ trying to squeeze `gdbstub` onto
+    /// a particularly resource-constrained platform, you may as well leave this
+    /// enabled. The resulting stub code is entirely optimized out on targets
+    /// that implement support for resumption.
+    #[inline(always)]
+    fn use_resume_stub(&self) -> bool {
+        true
+    }
+
+    /// Enable/Disable the use of run-length encoding on outgoing packets.
+    ///
+    /// This is enabled by default, as RLE can save substantial amounts of
+    /// bandwidth down the wire.
+    ///
+    /// _Author's note:_ There are essentially no reasons to disable RLE, unless
+    /// you happen to be using a custom GDB client that doesn't support RLE.
+    #[inline(always)]
+    fn use_rle(&self) -> bool {
+        true
+    }
+
+    /// Whether to send a target description XML to the client.
+    ///
+    /// Setting this to `false` will override both
+    /// [`Target::support_target_description_xml_override`] and the associated
+    /// [`Arch::target_description_xml`].
+    ///
+    /// _Author's note:_ Having the GDB client autodetect your target's
+    /// architecture and register set is really useful, so unless you're
+    /// _really_ trying to squeeze `gdbstub` onto a particularly
+    /// resource-constrained platform, you may as well leave this enabled.
+    #[inline(always)]
+    fn use_target_description_xml(&self) -> bool {
+        true
+    }
+
+    /// Support for setting / removing breakpoints.
+    #[inline(always)]
+    fn support_breakpoints(&mut self) -> Option<ext::breakpoints::BreakpointsOps<'_, Self>> {
         None
     }
 
-    /// Handle custom GDB `monitor` commands.
+    /// Support for handling custom GDB `monitor` commands.
     #[inline(always)]
-    fn monitor_cmd(&mut self) -> Option<ext::monitor_cmd::MonitorCmdOps<Self>> {
+    fn support_monitor_cmd(&mut self) -> Option<ext::monitor_cmd::MonitorCmdOps<'_, Self>> {
         None
     }
 
     /// Support for Extended Mode operations.
     #[inline(always)]
-    fn extended_mode(&mut self) -> Option<ext::extended_mode::ExtendedModeOps<Self>> {
+    fn support_extended_mode(&mut self) -> Option<ext::extended_mode::ExtendedModeOps<'_, Self>> {
         None
     }
 
-    /// Handle requests to get the target's current section (or segment)
-    /// offsets.
+    /// Support for handling requests to get the target's current section (or
+    /// segment) offsets.
     #[inline(always)]
-    fn section_offsets(&mut self) -> Option<ext::section_offsets::SectionOffsetsOps<Self>> {
-        None
-    }
-
-    /// Override the target description XML specified by `Target::Arch`.
-    #[inline(always)]
-    fn target_description_xml_override(
+    fn support_section_offsets(
         &mut self,
-    ) -> Option<ext::target_description_xml_override::TargetDescriptionXmlOverrideOps<Self>> {
+    ) -> Option<ext::section_offsets::SectionOffsetsOps<'_, Self>> {
+        None
+    }
+
+    /// Support for overriding the target description XML specified by
+    /// `Target::Arch`.
+    #[inline(always)]
+    fn support_target_description_xml_override(
+        &mut self,
+    ) -> Option<ext::target_description_xml_override::TargetDescriptionXmlOverrideOps<'_, Self>>
+    {
+        None
+    }
+
+    /// Support for reading the target's memory map.
+    #[inline(always)]
+    fn support_memory_map(&mut self) -> Option<ext::memory_map::MemoryMapOps<'_, Self>> {
+        None
+    }
+
+    /// Support for setting / removing syscall catchpoints.
+    #[inline(always)]
+    fn support_catch_syscalls(
+        &mut self,
+    ) -> Option<ext::catch_syscalls::CatchSyscallsOps<'_, Self>> {
+        None
+    }
+
+    /// Support for Host I/O operations.
+    #[inline(always)]
+    fn support_host_io(&mut self) -> Option<ext::host_io::HostIoOps<'_, Self>> {
+        None
+    }
+
+    /// Support for reading the current exec-file.
+    #[inline(always)]
+    fn support_exec_file(&mut self) -> Option<ext::exec_file::ExecFileOps<'_, Self>> {
+        None
+    }
+
+    /// Support for reading the target's Auxillary Vector.
+    #[inline(always)]
+    fn support_auxv(&mut self) -> Option<ext::auxv::AuxvOps<'_, Self>> {
         None
     }
 }
 
 macro_rules! impl_dyn_target {
     ($type:ty) => {
-        #[allow(clippy::type_complexity)]
         impl<A, E> Target for $type
         where
             A: Arch,
@@ -355,37 +676,84 @@
             type Arch = A;
             type Error = E;
 
-            #[inline(always)]
-            fn base_ops(&mut self) -> ext::base::BaseOps<Self::Arch, Self::Error> {
+            fn base_ops(&mut self) -> ext::base::BaseOps<'_, Self::Arch, Self::Error> {
                 (**self).base_ops()
             }
 
-            #[inline(always)]
-            fn breakpoints(&mut self) -> Option<ext::breakpoints::BreakpointsOps<Self>> {
-                (**self).breakpoints()
+            fn guard_rail_implicit_sw_breakpoints(&self) -> bool {
+                (**self).guard_rail_implicit_sw_breakpoints()
             }
 
-            #[inline(always)]
-            fn monitor_cmd(&mut self) -> Option<ext::monitor_cmd::MonitorCmdOps<Self>> {
-                (**self).monitor_cmd()
+            fn guard_rail_single_step_gdb_behavior(&self) -> SingleStepGdbBehavior {
+                (**self).guard_rail_single_step_gdb_behavior()
             }
 
-            #[inline(always)]
-            fn extended_mode(&mut self) -> Option<ext::extended_mode::ExtendedModeOps<Self>> {
-                (**self).extended_mode()
+            fn use_x_upcase_packet(&self) -> bool {
+                (**self).use_x_upcase_packet()
             }
 
-            #[inline(always)]
-            fn section_offsets(&mut self) -> Option<ext::section_offsets::SectionOffsetsOps<Self>> {
-                (**self).section_offsets()
+            fn use_resume_stub(&self) -> bool {
+                (**self).use_resume_stub()
             }
 
-            #[inline(always)]
-            fn target_description_xml_override(
+            fn use_rle(&self) -> bool {
+                (**self).use_rle()
+            }
+
+            fn use_target_description_xml(&self) -> bool {
+                (**self).use_target_description_xml()
+            }
+
+            fn support_breakpoints(
                 &mut self,
-            ) -> Option<ext::target_description_xml_override::TargetDescriptionXmlOverrideOps<Self>>
-            {
-                (**self).target_description_xml_override()
+            ) -> Option<ext::breakpoints::BreakpointsOps<'_, Self>> {
+                (**self).support_breakpoints()
+            }
+
+            fn support_monitor_cmd(&mut self) -> Option<ext::monitor_cmd::MonitorCmdOps<'_, Self>> {
+                (**self).support_monitor_cmd()
+            }
+
+            fn support_extended_mode(
+                &mut self,
+            ) -> Option<ext::extended_mode::ExtendedModeOps<'_, Self>> {
+                (**self).support_extended_mode()
+            }
+
+            fn support_section_offsets(
+                &mut self,
+            ) -> Option<ext::section_offsets::SectionOffsetsOps<'_, Self>> {
+                (**self).support_section_offsets()
+            }
+
+            fn support_target_description_xml_override(
+                &mut self,
+            ) -> Option<
+                ext::target_description_xml_override::TargetDescriptionXmlOverrideOps<'_, Self>,
+            > {
+                (**self).support_target_description_xml_override()
+            }
+
+            fn support_memory_map(&mut self) -> Option<ext::memory_map::MemoryMapOps<'_, Self>> {
+                (**self).support_memory_map()
+            }
+
+            fn support_catch_syscalls(
+                &mut self,
+            ) -> Option<ext::catch_syscalls::CatchSyscallsOps<'_, Self>> {
+                (**self).support_catch_syscalls()
+            }
+
+            fn support_host_io(&mut self) -> Option<ext::host_io::HostIoOps<'_, Self>> {
+                (**self).support_host_io()
+            }
+
+            fn support_exec_file(&mut self) -> Option<ext::exec_file::ExecFileOps<'_, Self>> {
+                (**self).support_exec_file()
+            }
+
+            fn support_auxv(&mut self) -> Option<ext::auxv::AuxvOps<'_, Self>> {
+                (**self).support_auxv()
             }
         }
     };
diff --git a/src/internal/dead_code_marker.rs b/src/util/dead_code_marker.rs
similarity index 93%
rename from src/internal/dead_code_marker.rs
rename to src/util/dead_code_marker.rs
index a91ed34..2e6c6e7 100644
--- a/src/internal/dead_code_marker.rs
+++ b/src/util/dead_code_marker.rs
@@ -32,6 +32,6 @@
 macro_rules! __dead_code_marker {
     ($feature:literal, $ctx:literal) => {
         #[cfg(feature = "__dead_code_marker")]
-        crate::internal::dead_code_marker::black_box(concat!("<", $feature, ",", $ctx, ">"));
+        crate::util::dead_code_marker::black_box(concat!("<", $feature, ",", $ctx, ">"));
     };
 }
diff --git a/src/util/managed_vec.rs b/src/util/managed_vec.rs
index fd8b8a4..5f3cf58 100644
--- a/src/util/managed_vec.rs
+++ b/src/util/managed_vec.rs
@@ -5,21 +5,14 @@
 pub struct CapacityError<Element>(pub Element);
 
 /// Wraps a ManagedSlice in a vec-like interface.
-pub struct ManagedVec<'a, 'b, T: 'a> {
+pub struct ManagedVec<'a, 'b, T> {
     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) {
-        // While it's very tempting to just call `Vec::clear` in the `Owned` case, doing
-        // so would modify the length of the underlying `ManagedSlice`, which isn't
-        // desirable.
-        self.len = 0;
+    pub fn new_with_idx(buf: &'b mut ManagedSlice<'a, T>, len: usize) -> Self {
+        ManagedVec { buf, len }
     }
 
     pub fn push(&mut self, value: T) -> Result<(), CapacityError<T>> {
@@ -39,6 +32,7 @@
         }
     }
 
+    #[cfg(feature = "trace-pkt")]
     pub fn as_slice<'c: 'b>(&'c self) -> &'b [T] {
         &self.buf[..self.len]
     }
diff --git a/src/util/mod.rs b/src/util/mod.rs
index a0b1e45..7539bbb 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -1 +1,9 @@
+//! Private utility types used internally within `gdbstub`.
+//!
+//! These are all bits of functionality that _could_ exist as their own crates /
+//! libraries, and do not rely on any `gdbstub` specific infrastructure.
+
 pub mod managed_vec;
+pub mod no_panic_iter;
+
+pub(crate) mod dead_code_marker;
diff --git a/src/util/no_panic_iter.rs b/src/util/no_panic_iter.rs
new file mode 100644
index 0000000..af1fb45
--- /dev/null
+++ b/src/util/no_panic_iter.rs
@@ -0,0 +1,126 @@
+/// Slice extension trait that provides non-panicing variants of several
+/// standard library iterators.
+pub trait SliceExt<T> {
+    /// Variant of [`core::slice::split_mut`] that elides bound checks.
+    fn split_mut_no_panic<F>(&mut self, pred: F) -> SplitMut<'_, T, F>
+    where
+        F: FnMut(&T) -> bool;
+
+    /// Variant of [`core::slice::splitn_mut`] that elides bound checks.
+    fn splitn_mut_no_panic<F>(&mut self, n: usize, pred: F) -> SplitNMut<'_, T, F>
+    where
+        F: FnMut(&T) -> bool;
+}
+
+impl<T> SliceExt<T> for [T] {
+    fn split_mut_no_panic<F>(&mut self, pred: F) -> SplitMut<'_, T, F>
+    where
+        F: FnMut(&T) -> bool,
+    {
+        SplitMut::new(self, pred)
+    }
+
+    fn splitn_mut_no_panic<F>(&mut self, n: usize, pred: F) -> SplitNMut<'_, T, F>
+    where
+        F: FnMut(&T) -> bool,
+    {
+        SplitNMut {
+            iter: SplitMut::new(self, pred),
+            count: n,
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct SplitMut<'a, T, P>
+where
+    P: FnMut(&T) -> bool,
+{
+    v: &'a mut [T],
+    pred: P,
+    finished: bool,
+}
+
+impl<'a, T: 'a, P: FnMut(&T) -> bool> SplitMut<'a, T, P> {
+    #[inline]
+    pub fn new(slice: &'a mut [T], pred: P) -> Self {
+        Self {
+            v: slice,
+            pred,
+            finished: false,
+        }
+    }
+
+    #[inline]
+    fn finish(&mut self) -> Option<&'a mut [T]> {
+        if self.finished {
+            None
+        } else {
+            self.finished = true;
+            Some(core::mem::take(&mut self.v))
+        }
+    }
+}
+
+impl<'a, T, P> Iterator for SplitMut<'a, T, P>
+where
+    P: FnMut(&T) -> bool,
+{
+    type Item = &'a mut [T];
+
+    #[inline]
+    fn next(&mut self) -> Option<&'a mut [T]> {
+        if self.finished {
+            return None;
+        }
+
+        let idx_opt = {
+            // work around borrowck limitations
+            let pred = &mut self.pred;
+            self.v.iter().position(|x| (*pred)(x))
+        };
+        match idx_opt {
+            None => self.finish(),
+            Some(idx) => {
+                let tmp = core::mem::take(&mut self.v);
+                let (head, tail) = tmp.split_at_mut(idx);
+                self.v = tail.get_mut(1..)?; // will never fail
+                Some(head)
+            }
+        }
+    }
+}
+
+/// An private iterator over subslices separated by elements that
+/// match a predicate function, splitting at most a fixed number of
+/// times.
+#[derive(Debug)]
+pub struct SplitNMut<'a, T, P>
+where
+    P: FnMut(&T) -> bool,
+{
+    iter: SplitMut<'a, T, P>,
+    count: usize,
+}
+
+impl<'a, T, P> Iterator for SplitNMut<'a, T, P>
+where
+    P: FnMut(&T) -> bool,
+{
+    type Item = &'a mut [T];
+
+    #[inline]
+    fn next(&mut self) -> Option<&'a mut [T]> {
+        match self.count {
+            0 => None,
+            1 => {
+                self.count -= 1;
+                self.iter.finish()
+            }
+            _ => {
+                self.count -= 1;
+                self.iter.next()
+            }
+        }
+    }
+}