Skip to content

Commit fc1f75a

Browse files
committed
Debugging: add builtin gdbstub component.
This adds a debug component that makes use of the debug-main world defined in bytecodealliance#12756 and serves the gdbstub protocol, with Wasm extensions, compatible with LLDB. This component is built and included inside the Wasmtime binary, and is loaded using the lower-level `-D debugger=...` debug-main option; the user doesn't need to specify the `.wasm` adapter component. Instead, the user simply runs `wasmtime run -g <PORT> program.wasm ...` and Wasmtime will load and prepare to run `program.wasm` as the debuggee, waiting for a gdbstub connection on the given TCP port before continuing. The workflow is: ``` $ wasmtime run -g 1234 program.wasm [ wasmtime starts and waits for connection ] $ /opt/wasi-sdk/bin/lldb # use LLDB from wasi-sdk release 32 or later (lldb) process connect --plugin wasm connect://localhost:1234 Process 1 stopped * thread #1, stop reason = signal SIGTRAP frame #0: 0x40000000000001cc -> 0x40000000000001cc: unreachable 0x40000000000001cd: end 0x40000000000001ce: local.get 0 0x40000000000001d0: call 13 (lldb) si Process 1 stopped * thread #1, stop reason = instruction step into frame #0: 0x4000000000000184 -> 0x4000000000000184: block 0x4000000000000186: block 0x4000000000000188: global.get 1 0x400000000000018e: i32.const 3664 [ ... ] ``` This makes use of the `gdbstub` third-party crate, into which I've upstreamed support for the Wasm extensions in daniel5151/gdbstub#188, daniel5151/gdbstub#189, daniel5151/gdbstub#190, and daniel5151/gdbstub#192. (I'll add vets as part of this PR.)
1 parent c9e59a8 commit fc1f75a

14 files changed

Lines changed: 1452 additions & 62 deletions

File tree

Cargo.lock

Lines changed: 196 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ wasmtime-wasi-http = { workspace = true, optional = true }
6161
wasmtime-unwinder = { workspace = true }
6262
wasmtime-wizer = { workspace = true, optional = true, features = ['clap', 'wasmtime'] }
6363
wasmtime-debugger = { workspace = true, optional = true }
64+
gdbstub-component-artifact = { workspace = true, optional = true }
6465
clap = { workspace = true }
6566
clap_complete = { workspace = true, optional = true }
6667
target-lexicon = { workspace = true }
@@ -168,6 +169,8 @@ members = [
168169
"crates/wasi-tls-nativetls",
169170
"crates/wasi-tls-openssl",
170171
"crates/debugger",
172+
"crates/gdbstub-component",
173+
"crates/gdbstub-component/artifact",
171174
"crates/wizer/fuzz",
172175
"crates/wizer/tests/regex-test",
173176
"crates/wizer/benches/regex-bench",
@@ -280,6 +283,7 @@ wasmtime-jit-icache-coherence = { path = "crates/jit-icache-coherence", version
280283
wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=44.0.0", package = 'wasmtime-internal-wit-bindgen' }
281284
wasmtime-unwinder = { path = "crates/unwinder", version = "=44.0.0", package = 'wasmtime-internal-unwinder' }
282285
wasmtime-debugger = { path = "crates/debugger", version = "=44.0.0", package = "wasmtime-internal-debugger" }
286+
gdbstub-component-artifact = { path = "crates/gdbstub-component/artifact", package = "wasmtime-internal-gdbstub-component-artifact" }
283287
wasmtime-wizer = { path = "crates/wizer", version = "44.0.0" }
284288

285289
# Miscellaneous crates without a `wasmtime-*` prefix in their name but still
@@ -358,6 +362,9 @@ wasm-wave = "0.245.0"
358362
wasm-compose = "0.245.0"
359363
json-from-wast = "0.245.0"
360364

365+
wstd = "0.6.5"
366+
wasip2 = "1.0"
367+
361368
# Non-Bytecode Alliance maintained dependencies:
362369
# --------------------------
363370
arbitrary = "1.4.2"
@@ -441,6 +448,9 @@ rayon = "1.5.3"
441448
regex = "1.9.1"
442449
pin-project-lite = "0.2.14"
443450
sha2 = { version = "0.10.2", default-features = false }
451+
structopt = "0.3.26"
452+
gdbstub = "0.7.10"
453+
gdbstub_arch = "0.3.3"
444454

445455
# =============================================================================
446456
#
@@ -566,7 +576,7 @@ gc-drc = ["gc", "wasmtime/gc-drc", "wasmtime-cli-flags/gc-drc"]
566576
gc-null = ["gc", "wasmtime/gc-null", "wasmtime-cli-flags/gc-null"]
567577
pulley = ["wasmtime-cli-flags/pulley"]
568578
stack-switching = ["wasmtime/stack-switching", "wasmtime-cli-flags/stack-switching"]
569-
debug = ["wasmtime-cli-flags/debug", "wasmtime/debug", "component-model"]
579+
debug = ["wasmtime-cli-flags/debug", "wasmtime/debug", "component-model", "dep:gdbstub-component-artifact"]
570580

571581
# CLI subcommands for the `wasmtime` executable. See `wasmtime $cmd --help`
572582
# for more information on each subcommand.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "wasmtime-internal-gdbstub-component"
3+
version.workspace = true
4+
authors.workspace = true
5+
edition.workspace = true
6+
description = "gdbstub debug-component adapter"
7+
publish = false
8+
9+
[lib]
10+
crate-type = ["cdylib"]
11+
12+
[dependencies]
13+
wit-bindgen = { workspace = true, features = ["macros"] }
14+
anyhow = { workspace = true }
15+
structopt = { workspace = true }
16+
wstd = { workspace = true }
17+
wasip2 = { workspace = true }
18+
futures = { workspace = true, default-features = true }
19+
gdbstub = { workspace = true }
20+
gdbstub_arch = { workspace = true }
21+
log = { workspace = true }
22+
env_logger = { workspace = true }
23+
24+
[lints]
25+
workspace = true
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "wasmtime-internal-gdbstub-component-artifact"
3+
version.workspace = true
4+
authors.workspace = true
5+
edition.workspace = true
6+
publish = false
7+
8+
[lints]
9+
workspace = true
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use std::env;
2+
use std::path::PathBuf;
3+
use std::process::Command;
4+
5+
fn main() {
6+
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
7+
8+
let mut cmd = cargo();
9+
cmd.arg("build")
10+
.arg("--release")
11+
.arg("--target=wasm32-wasip2")
12+
.arg("--package=wasmtime-internal-gdbstub-component")
13+
.env("CARGO_TARGET_DIR", &out_dir)
14+
.env("RUSTFLAGS", rustflags())
15+
.env_remove("CARGO_ENCODED_RUSTFLAGS");
16+
eprintln!("running: {cmd:?}");
17+
let status = cmd.status().unwrap();
18+
assert!(status.success());
19+
20+
let wasm = out_dir
21+
.join("wasm32-wasip2")
22+
.join("release")
23+
.join("wasmtime_internal_gdbstub_component.wasm");
24+
25+
// Read dep-info to get proper rerun-if-changed directives.
26+
let deps_file = wasm.with_extension("d");
27+
if let Ok(contents) = std::fs::read_to_string(&deps_file) {
28+
for line in contents.lines() {
29+
let Some(pos) = line.find(": ") else {
30+
continue;
31+
};
32+
let line = &line[pos + 2..];
33+
let mut parts = line.split_whitespace();
34+
while let Some(part) = parts.next() {
35+
let mut file = part.to_string();
36+
while file.ends_with('\\') {
37+
file.pop();
38+
file.push(' ');
39+
file.push_str(parts.next().unwrap());
40+
}
41+
println!("cargo:rerun-if-changed={file}");
42+
}
43+
}
44+
}
45+
46+
let generated = format!("pub const GDBSTUB_COMPONENT: &[u8] = include_bytes!({wasm:?});\n");
47+
std::fs::write(out_dir.join("gen.rs"), generated).unwrap();
48+
}
49+
50+
fn cargo() -> Command {
51+
let mut cargo = Command::new("cargo");
52+
if std::env::var("CARGO_CFG_MIRI").is_ok() {
53+
cargo.env_remove("RUSTC").env_remove("RUSTC_WRAPPER");
54+
}
55+
cargo
56+
}
57+
58+
fn rustflags() -> &'static str {
59+
match option_env!("RUSTFLAGS") {
60+
Some(s) if s.contains("-D warnings") => "-D warnings",
61+
_ => "",
62+
}
63+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include!(concat!(env!("OUT_DIR"), "/gen.rs"));
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
//! Synthetic Wasm address space expected by the gdbstub Wasm
2+
//! extensions.
3+
4+
use crate::api::{Debuggee, Frame, Memory, Module};
5+
use anyhow::Result;
6+
use gdbstub_arch::wasm::addr::{WasmAddr, WasmAddrType};
7+
use std::collections::{HashMap, hash_map::Entry};
8+
9+
/// Representation of the synthesized Wasm address space.
10+
pub struct AddrSpace {
11+
module_ids: HashMap<u64, u32>,
12+
memory_ids: HashMap<u64, u32>,
13+
modules: Vec<Module>,
14+
module_bytecode: Vec<Vec<u8>>,
15+
memories: Vec<Memory>,
16+
}
17+
18+
/// The result of a lookup in the address space.
19+
pub enum AddrSpaceLookup<'a> {
20+
Module {
21+
module: &'a Module,
22+
bytecode: &'a [u8],
23+
offset: u32,
24+
},
25+
Memory {
26+
memory: &'a Memory,
27+
offset: u32,
28+
},
29+
Empty,
30+
}
31+
32+
impl AddrSpace {
33+
pub fn new() -> Self {
34+
AddrSpace {
35+
module_ids: HashMap::new(),
36+
modules: vec![],
37+
module_bytecode: vec![],
38+
memory_ids: HashMap::new(),
39+
memories: vec![],
40+
}
41+
}
42+
43+
fn module_id(&mut self, m: &Module) -> u32 {
44+
match self.module_ids.entry(m.unique_id()) {
45+
Entry::Occupied(o) => *o.get(),
46+
Entry::Vacant(v) => {
47+
let id = u32::try_from(self.modules.len()).unwrap();
48+
let bytecode = m.bytecode().unwrap_or(vec![]);
49+
self.module_bytecode.push(bytecode);
50+
self.modules.push(m.clone());
51+
*v.insert(id)
52+
}
53+
}
54+
}
55+
56+
fn memory_id(&mut self, m: &Memory) -> u32 {
57+
match self.memory_ids.entry(m.unique_id()) {
58+
Entry::Occupied(o) => *o.get(),
59+
Entry::Vacant(v) => {
60+
let id = u32::try_from(self.memories.len()).unwrap();
61+
self.memories.push(m.clone());
62+
*v.insert(id)
63+
}
64+
}
65+
}
66+
67+
/// Update/create new mappings so that all modules and instances'
68+
/// memories in the debuggee have mappings.
69+
pub fn update(&mut self, d: &Debuggee) -> Result<()> {
70+
for module in d.all_modules() {
71+
let _ = self.module_id(&module);
72+
}
73+
for instance in d.all_instances() {
74+
let mut idx = 0;
75+
loop {
76+
if let Ok(m) = instance.get_memory(d, idx) {
77+
let _ = self.memory_id(&m);
78+
idx += 1;
79+
} else {
80+
break;
81+
}
82+
}
83+
}
84+
Ok(())
85+
}
86+
87+
/// Iterate over the base `WasmAddr` of every registered module.
88+
pub fn module_base_addrs(&self) -> impl Iterator<Item = WasmAddr> + '_ {
89+
(0..self.modules.len())
90+
.map(|idx| WasmAddr::new(WasmAddrType::Object, u32::try_from(idx).unwrap(), 0).unwrap())
91+
}
92+
93+
/// Build the GDB memory-map XML describing all known regions.
94+
///
95+
/// Module bytecode regions are reported as `rom` (read-only), and
96+
/// linear memories as `ram` (read-write).
97+
pub fn memory_map_xml(&self, debuggee: &Debuggee) -> String {
98+
use std::fmt::Write;
99+
let mut xml = String::from(
100+
"<?xml version=\"1.0\"?><!DOCTYPE memory-map SYSTEM \"memory-map.dtd\"><memory-map>",
101+
);
102+
for (idx, bc) in self.module_bytecode.iter().enumerate() {
103+
let start =
104+
WasmAddr::new(WasmAddrType::Object, u32::try_from(idx).unwrap(), 0).unwrap();
105+
let len = bc.len();
106+
if len > 0 {
107+
write!(
108+
xml,
109+
"<memory type=\"rom\" start=\"0x{:x}\" length=\"0x{:x}\"/>",
110+
start.as_raw(),
111+
len
112+
)
113+
.unwrap();
114+
}
115+
}
116+
for (idx, mem) in self.memories.iter().enumerate() {
117+
let start =
118+
WasmAddr::new(WasmAddrType::Memory, u32::try_from(idx).unwrap(), 0).unwrap();
119+
let len = mem.size_bytes(debuggee);
120+
if len > 0 {
121+
write!(
122+
xml,
123+
"<memory type=\"ram\" start=\"0x{:x}\" length=\"0x{:x}\"/>",
124+
start.as_raw(),
125+
len
126+
)
127+
.unwrap();
128+
}
129+
}
130+
xml.push_str("</memory-map>");
131+
xml
132+
}
133+
134+
pub fn frame_to_pc(&self, frame: &Frame, debuggee: &Debuggee) -> WasmAddr {
135+
let module = frame.get_instance(debuggee).unwrap().get_module(debuggee);
136+
let &module_id = self
137+
.module_ids
138+
.get(&module.unique_id())
139+
.expect("module not found in addr space");
140+
let pc = frame.get_pc(debuggee).unwrap();
141+
WasmAddr::new(WasmAddrType::Object, module_id, pc).unwrap()
142+
}
143+
144+
pub fn frame_to_return_addr(&self, frame: &Frame, debuggee: &Debuggee) -> Option<WasmAddr> {
145+
let module = frame.get_instance(debuggee).unwrap().get_module(debuggee);
146+
let &module_id = self
147+
.module_ids
148+
.get(&module.unique_id())
149+
.expect("module not found in addr space");
150+
let ret_pc = frame.get_pc(debuggee).ok()?;
151+
Some(WasmAddr::new(WasmAddrType::Object, module_id, ret_pc).unwrap())
152+
}
153+
154+
pub fn lookup(&self, addr: WasmAddr, d: &Debuggee) -> AddrSpaceLookup<'_> {
155+
let index = usize::try_from(addr.module_index()).unwrap();
156+
match addr.addr_type() {
157+
WasmAddrType::Object => {
158+
if index >= self.modules.len() {
159+
return AddrSpaceLookup::Empty;
160+
}
161+
let bytecode = &self.module_bytecode[index];
162+
if addr.offset() >= u32::try_from(bytecode.len()).unwrap() {
163+
return AddrSpaceLookup::Empty;
164+
}
165+
AddrSpaceLookup::Module {
166+
module: &self.modules[index],
167+
bytecode,
168+
offset: addr.offset(),
169+
}
170+
}
171+
WasmAddrType::Memory => {
172+
if index >= self.memories.len() {
173+
return AddrSpaceLookup::Empty;
174+
}
175+
let size = self.memories[index].size_bytes(d);
176+
if u64::from(addr.offset()) >= size {
177+
return AddrSpaceLookup::Empty;
178+
}
179+
AddrSpaceLookup::Memory {
180+
memory: &self.memories[index],
181+
offset: addr.offset(),
182+
}
183+
}
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)