-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Debugging: add builtin gdbstub component. #12771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| 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 } | ||
| 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 | ||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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:
Well, "some possible ideas" aka I think randomly in real time until something semi-reasonable pops out... Maybe that last one?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sort of two things:
Basically, yeah, build-time weirdness is expected, but I'd like to ideally tame it. Another example would be printing a better error if |
||
|
|
||
| [lints] | ||
| workspace = true | ||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
| _ => "", | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| include!(concat!(env!("OUT_DIR"), "/gen.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(), | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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 useclap? They should be pretty similar and easy to transition between, but my impression is thatstructoptis more-or-less deprecated in favor ofclap