Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 271 additions & 18 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ wasmtime-wasi-http = { workspace = true, optional = true }
wasmtime-unwinder = { workspace = true }
wasmtime-wizer = { workspace = true, optional = true, features = ['clap', 'wasmtime'] }
wasmtime-debugger = { workspace = true, optional = true }
gdbstub-component-artifact = { workspace = true, optional = true }
clap = { workspace = true }
clap_complete = { workspace = true, optional = true }
target-lexicon = { workspace = true }
Expand Down Expand Up @@ -166,6 +167,8 @@ members = [
"crates/wasi-preview1-component-adapter",
"crates/wasi-preview1-component-adapter/verify",
"crates/debugger",
"crates/gdbstub-component",
"crates/gdbstub-component/artifact",
"crates/wizer/fuzz",
"crates/wizer/tests/regex-test",
"crates/wizer/benches/regex-bench",
Expand Down Expand Up @@ -276,6 +279,7 @@ wasmtime-jit-icache-coherence = { path = "crates/jit-icache-coherence", version
wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=44.0.0", package = 'wasmtime-internal-wit-bindgen' }
wasmtime-unwinder = { path = "crates/unwinder", version = "=44.0.0", package = 'wasmtime-internal-unwinder' }
wasmtime-debugger = { path = "crates/debugger", version = "=44.0.0", package = "wasmtime-internal-debugger" }
gdbstub-component-artifact = { path = "crates/gdbstub-component/artifact", package = "wasmtime-internal-gdbstub-component-artifact" }
wasmtime-wizer = { path = "crates/wizer", version = "44.0.0" }

# Miscellaneous crates without a `wasmtime-*` prefix in their name but still
Expand Down Expand Up @@ -354,6 +358,9 @@ wasm-wave = "0.245.0"
wasm-compose = "0.245.0"
json-from-wast = "0.245.0"

wstd = "0.6.5"
wasip2 = "1.0"

# Non-Bytecode Alliance maintained dependencies:
# --------------------------
arbitrary = "1.4.2"
Expand Down Expand Up @@ -437,6 +444,9 @@ rayon = "1.5.3"
regex = "1.9.1"
pin-project-lite = "0.2.14"
sha2 = { version = "0.10.2", default-features = false }
structopt = "0.3.26"
gdbstub = "0.7.10"
gdbstub_arch = "0.3.3"

# =============================================================================
#
Expand Down Expand Up @@ -562,7 +572,7 @@ gc-drc = ["gc", "wasmtime/gc-drc", "wasmtime-cli-flags/gc-drc"]
gc-null = ["gc", "wasmtime/gc-null", "wasmtime-cli-flags/gc-null"]
pulley = ["wasmtime-cli-flags/pulley"]
stack-switching = ["wasmtime/stack-switching", "wasmtime-cli-flags/stack-switching"]
debug = ["wasmtime-cli-flags/debug", "wasmtime/debug", "component-model"]
debug = ["wasmtime-cli-flags/debug", "wasmtime/debug", "component-model", "dep:gdbstub-component-artifact"]

# CLI subcommands for the `wasmtime` executable. See `wasmtime $cmd --help`
# for more information on each subcommand.
Expand Down
26 changes: 26 additions & 0 deletions crates/gdbstub-component/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "wasmtime-internal-gdbstub-component"
version.workspace = true
authors.workspace = true
edition.workspace = true
license = "Apache-2.0 WITH LLVM-exception"
description = "gdbstub debug-component adapter"
publish = false

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = { workspace = true, features = ["macros"] }
anyhow = { workspace = true }
structopt = { workspace = true }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of pulling in structopt, can you use clap? They should be pretty similar and easy to transition between, but my impression is that structopt is more-or-less deprecated in favor of clap

wstd = { workspace = true }
wasip2 = { workspace = true }
futures = { workspace = true, default-features = true }
gdbstub = { workspace = true }
gdbstub_arch = { workspace = true }
log = { workspace = true }
env_logger = { workspace = true }

[lints]
workspace = true
10 changes: 10 additions & 0 deletions crates/gdbstub-component/artifact/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "wasmtime-internal-gdbstub-component-artifact"
version.workspace = true
authors.workspace = true
edition.workspace = true
license = "Apache-2.0 WITH LLVM-exception"
publish = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm ok here's a sticking point I didn't realize: this makes the wasmtime-cli crate non-publishable because there's a dependency on something that doesn't exist on crates.io. I'm also realizing now that it's not as simple as publishing this crate since it's fundamentally not publishable, it relies on being part of this workspace to build a sibling crate, which isn't present when built as a dep from crates.io.

The "true fix" for this is https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies, an unstable Cargo feature. Unfortunately we can't rely on that even in a nightly-conditional context I believe, if we tried to use that it would mean that Wasmtime would always require nightly.

Some possible ideas:

  • Check in the gdbstub binary and verify it's built in CI. I suspect it's a bit large and will receive many changes, so not my first choice.
  • Download gdbstub from github releases for released wasmtime-cli artifacts. We don't currently have HTTP/network dependencies in the CLI outside of WASI impls, so that'll be hard.
  • Publish this crate to crates.io, but handle failures in the build.rs script. That'd mean that the gdbstub binary would have to be an off-by-default optional feature which we enable for our release builds but would be off-by-default for crates.io.

Well, "some possible ideas" aka I think randomly in real time until something semi-reasonable pops out... Maybe that last one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, hmm, I definitely didn't foresee this one being a problem either!

Question on the last option: what do you mean by "handle failures in build.rs"; in other words, why would such a failure be any different than some other build failure for a crate on crates.io (some of which are e.g. crates that wrap C code and use cc, or do other build-time shenanigans)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort of two things:

  1. I forget what cwd is used for build.rs so I don't know what effect cargo build -p thing will have when something is published to crates.io. If that tries to build other random crates in a workspace, for example, I think that'd be bad.
  2. I think this would ideally have a better error message than "package thing not found" when built from crates.io, aka something like "you can't enable the gdbstub component when wasmtime is built from crates.io" or similar.

Basically, yeah, build-time weirdness is expected, but I'd like to ideally tame it. Another example would be printing a better error if wasm32-wasip2 weren't installed, but rustc does a decent job of this already.


[lints]
workspace = true
63 changes: 63 additions & 0 deletions crates/gdbstub-component/artifact/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use std::env;
use std::path::PathBuf;
use std::process::Command;

fn main() {
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

let mut cmd = cargo();
cmd.arg("build")
.arg("--release")
.arg("--target=wasm32-wasip2")
.arg("--package=wasmtime-internal-gdbstub-component")
.env("CARGO_TARGET_DIR", &out_dir)
.env("RUSTFLAGS", rustflags())
.env_remove("CARGO_ENCODED_RUSTFLAGS");
eprintln!("running: {cmd:?}");
let status = cmd.status().unwrap();
assert!(status.success());

let wasm = out_dir
.join("wasm32-wasip2")
.join("release")
.join("wasmtime_internal_gdbstub_component.wasm");
Comment on lines +20 to +23
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To gauge, how big is this binary? Are we talking ~30M or something more like ~1M? Given that this is included in the CLI uncompressed it might be reasonable to try to apply simple size optimizations where possible if it's extra large.


// Read dep-info to get proper rerun-if-changed directives.
let deps_file = wasm.with_extension("d");
if let Ok(contents) = std::fs::read_to_string(&deps_file) {
for line in contents.lines() {
let Some(pos) = line.find(": ") else {
continue;
};
let line = &line[pos + 2..];
let mut parts = line.split_whitespace();
while let Some(part) = parts.next() {
let mut file = part.to_string();
while file.ends_with('\\') {
file.pop();
file.push(' ');
file.push_str(parts.next().unwrap());
}
println!("cargo:rerun-if-changed={file}");
}
}
}

let generated = format!("pub const GDBSTUB_COMPONENT: &[u8] = include_bytes!({wasm:?});\n");
std::fs::write(out_dir.join("gen.rs"), generated).unwrap();
}

fn cargo() -> Command {
let mut cargo = Command::new("cargo");
if std::env::var("CARGO_CFG_MIRI").is_ok() {
cargo.env_remove("RUSTC").env_remove("RUSTC_WRAPPER");
}
cargo
}

fn rustflags() -> &'static str {
match option_env!("RUSTFLAGS") {
Some(s) if s.contains("-D warnings") => "-D warnings",
_ => "",
}
}
1 change: 1 addition & 0 deletions crates/gdbstub-component/artifact/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/gen.rs"));
186 changes: 186 additions & 0 deletions crates/gdbstub-component/src/addr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
//! Synthetic Wasm address space expected by the gdbstub Wasm
//! extensions.

use crate::api::{Debuggee, Frame, Memory, Module};
use anyhow::Result;
use gdbstub_arch::wasm::addr::{WasmAddr, WasmAddrType};
use std::collections::{HashMap, hash_map::Entry};

/// Representation of the synthesized Wasm address space.
pub struct AddrSpace {
module_ids: HashMap<u64, u32>,
memory_ids: HashMap<u64, u32>,
modules: Vec<Module>,
module_bytecode: Vec<Vec<u8>>,
memories: Vec<Memory>,
}

/// The result of a lookup in the address space.
pub enum AddrSpaceLookup<'a> {
Module {
module: &'a Module,
bytecode: &'a [u8],
offset: u32,
},
Memory {
memory: &'a Memory,
offset: u32,
},
Empty,
}

impl AddrSpace {
pub fn new() -> Self {
AddrSpace {
module_ids: HashMap::new(),
modules: vec![],
module_bytecode: vec![],
memory_ids: HashMap::new(),
memories: vec![],
}
}

fn module_id(&mut self, m: &Module) -> u32 {
match self.module_ids.entry(m.unique_id()) {
Entry::Occupied(o) => *o.get(),
Entry::Vacant(v) => {
let id = u32::try_from(self.modules.len()).unwrap();
let bytecode = m.bytecode().unwrap_or(vec![]);
self.module_bytecode.push(bytecode);
self.modules.push(m.clone());
*v.insert(id)
}
}
}

fn memory_id(&mut self, m: &Memory) -> u32 {
match self.memory_ids.entry(m.unique_id()) {
Entry::Occupied(o) => *o.get(),
Entry::Vacant(v) => {
let id = u32::try_from(self.memories.len()).unwrap();
self.memories.push(m.clone());
*v.insert(id)
}
}
}

/// Update/create new mappings so that all modules and instances'
/// memories in the debuggee have mappings.
pub fn update(&mut self, d: &Debuggee) -> Result<()> {
for module in d.all_modules() {
let _ = self.module_id(&module);
}
for instance in d.all_instances() {
let mut idx = 0;
loop {
if let Ok(m) = instance.get_memory(d, idx) {
let _ = self.memory_id(&m);
idx += 1;
} else {
break;
}
}
}
Ok(())
}

/// Iterate over the base `WasmAddr` of every registered module.
pub fn module_base_addrs(&self) -> impl Iterator<Item = WasmAddr> + '_ {
(0..self.modules.len())
.map(|idx| WasmAddr::new(WasmAddrType::Object, u32::try_from(idx).unwrap(), 0).unwrap())
}

/// Build the GDB memory-map XML describing all known regions.
///
/// Module bytecode regions are reported as `rom` (read-only), and
/// linear memories as `ram` (read-write).
pub fn memory_map_xml(&self, debuggee: &Debuggee) -> String {
use std::fmt::Write;
let mut xml = String::from(
"<?xml version=\"1.0\"?><!DOCTYPE memory-map SYSTEM \"memory-map.dtd\"><memory-map>",
);
for (idx, bc) in self.module_bytecode.iter().enumerate() {
let start =
WasmAddr::new(WasmAddrType::Object, u32::try_from(idx).unwrap(), 0).unwrap();
let len = bc.len();
if len > 0 {
write!(
xml,
"<memory type=\"rom\" start=\"0x{:x}\" length=\"0x{:x}\"/>",
start.as_raw(),
len
)
.unwrap();
}
}
for (idx, mem) in self.memories.iter().enumerate() {
let start =
WasmAddr::new(WasmAddrType::Memory, u32::try_from(idx).unwrap(), 0).unwrap();
let len = mem.size_bytes(debuggee);
if len > 0 {
write!(
xml,
"<memory type=\"ram\" start=\"0x{:x}\" length=\"0x{:x}\"/>",
start.as_raw(),
len
)
.unwrap();
}
}
xml.push_str("</memory-map>");
xml
}

pub fn frame_to_pc(&self, frame: &Frame, debuggee: &Debuggee) -> WasmAddr {
let module = frame.get_instance(debuggee).unwrap().get_module(debuggee);
let &module_id = self
.module_ids
.get(&module.unique_id())
.expect("module not found in addr space");
let pc = frame.get_pc(debuggee).unwrap();
WasmAddr::new(WasmAddrType::Object, module_id, pc).unwrap()
}

pub fn frame_to_return_addr(&self, frame: &Frame, debuggee: &Debuggee) -> Option<WasmAddr> {
let module = frame.get_instance(debuggee).unwrap().get_module(debuggee);
let &module_id = self
.module_ids
.get(&module.unique_id())
.expect("module not found in addr space");
let ret_pc = frame.get_pc(debuggee).ok()?;
Some(WasmAddr::new(WasmAddrType::Object, module_id, ret_pc).unwrap())
}

pub fn lookup(&self, addr: WasmAddr, d: &Debuggee) -> AddrSpaceLookup<'_> {
let index = usize::try_from(addr.module_index()).unwrap();
match addr.addr_type() {
WasmAddrType::Object => {
if index >= self.modules.len() {
return AddrSpaceLookup::Empty;
}
let bytecode = &self.module_bytecode[index];
if addr.offset() >= u32::try_from(bytecode.len()).unwrap() {
return AddrSpaceLookup::Empty;
}
AddrSpaceLookup::Module {
module: &self.modules[index],
bytecode,
offset: addr.offset(),
}
}
WasmAddrType::Memory => {
if index >= self.memories.len() {
return AddrSpaceLookup::Empty;
}
let size = self.memories[index].size_bytes(d);
if u64::from(addr.offset()) >= size {
return AddrSpaceLookup::Empty;
}
AddrSpaceLookup::Memory {
memory: &self.memories[index],
offset: addr.offset(),
}
}
}
}
}
44 changes: 44 additions & 0 deletions crates/gdbstub-component/src/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! Bindings for Wasmtime's debugger API.

use wstd::runtime::AsyncPollable;

wit_bindgen::generate!({
world: "bytecodealliance:wasmtime/debug-main",
path: "../debugger/wit",
with: {
"wasi:io/poll@0.2.6": wasip2::io::poll,
}
});
pub(crate) use bytecodealliance::wasmtime::debuggee::*;

/// One "resumption", or period of execution, in the debuggee.
pub struct Resumption {
future: EventFuture,
pollable: Option<AsyncPollable>,
}

impl Resumption {
pub fn continue_(d: &Debuggee, r: ResumptionValue) -> Self {
let future = d.continue_(r);
let pollable = Some(AsyncPollable::new(future.subscribe()));
Resumption { future, pollable }
}

pub fn single_step(d: &Debuggee, r: ResumptionValue) -> Self {
let future = d.single_step(r);
let pollable = Some(AsyncPollable::new(future.subscribe()));
Resumption { future, pollable }
}

pub async fn wait(&mut self) {
if let Some(pollable) = self.pollable.as_mut() {
pollable.wait_for().await;
}
}

pub fn result(mut self, d: &Debuggee) -> std::result::Result<Event, Error> {
// Drop the pollable first, since it's a child resource.
let _ = self.pollable.take();
EventFuture::finish(self.future, d)
}
}
Loading
Loading