diff --git a/src/protocol/commands.rs b/src/protocol/commands.rs index 13056cd..455bd73 100644 --- a/src/protocol/commands.rs +++ b/src/protocol/commands.rs @@ -366,4 +366,11 @@ commands! { "qTfV" => _qTfV::qTfV, "qTsV" => _qTsV::qTsV, } + + wasm use 'a { + "qWasmCallStack" => _qWasmCallStack::qWasmCallStack, + "qWasmLocal" => _qWasmLocal::qWasmLocal<'a>, + "qWasmGlobal" => _qWasmGlobal::qWasmGlobal<'a>, + "qWasmStackValue" => _qWasmStackValue::qWasmStackValue<'a>, + } } diff --git a/src/protocol/commands/_qWasmCallStack.rs b/src/protocol/commands/_qWasmCallStack.rs new file mode 100644 index 0000000..f84a8dc --- /dev/null +++ b/src/protocol/commands/_qWasmCallStack.rs @@ -0,0 +1,21 @@ +use super::prelude::*; +use crate::protocol::common::thread_id::ThreadId; +use crate::protocol::ConcreteThreadId; + +#[derive(Debug)] +pub struct qWasmCallStack { + pub tid: ConcreteThreadId, +} + +impl<'a> ParseCommand<'a> for qWasmCallStack { + #[inline(always)] + fn from_packet(buf: PacketBuf<'a>) -> Option { + let body = buf.into_body(); + if body.is_empty() || body[0] != b':' { + return None; + } + let tid = &body[1..]; + let tid = ConcreteThreadId::try_from(ThreadId::try_from(tid).ok()?).ok()?; + Some(qWasmCallStack { tid }) + } +} diff --git a/src/protocol/commands/_qWasmGlobal.rs b/src/protocol/commands/_qWasmGlobal.rs new file mode 100644 index 0000000..52f38ee --- /dev/null +++ b/src/protocol/commands/_qWasmGlobal.rs @@ -0,0 +1,35 @@ +use super::prelude::*; + +#[derive(Debug)] +pub struct qWasmGlobal<'a> { + pub frame: usize, + pub global: usize, + pub buf: &'a mut [u8], +} + +impl<'a> ParseCommand<'a> for qWasmGlobal<'a> { + #[inline(always)] + fn from_packet(buf: PacketBuf<'a>) -> Option { + let (buf, body_range) = buf.into_raw_buf(); + let body = buf.get(body_range.start..body_range.end)?; + + if body.is_empty() || body[0] != b':' { + return None; + } + let mut parts = body[1..].split(|b| *b == b';'); + let frame = parts.next()?; + let frame = str::from_utf8(frame).ok()?.parse::().ok()?; + let global = parts.next()?; + let global = str::from_utf8(global).ok()?.parse::().ok()?; + if parts.next().is_some() { + // Too many parameters. + return None; + } + + Some(qWasmGlobal { + frame, + global, + buf, + }) + } +} diff --git a/src/protocol/commands/_qWasmLocal.rs b/src/protocol/commands/_qWasmLocal.rs new file mode 100644 index 0000000..b09e45c --- /dev/null +++ b/src/protocol/commands/_qWasmLocal.rs @@ -0,0 +1,35 @@ +use super::prelude::*; + +#[derive(Debug)] +pub struct qWasmLocal<'a> { + pub frame: usize, + pub local: usize, + pub buf: &'a mut [u8], +} + +impl<'a> ParseCommand<'a> for qWasmLocal<'a> { + #[inline(always)] + fn from_packet(buf: PacketBuf<'a>) -> Option { + let (buf, body_range) = buf.into_raw_buf(); + let body = buf.get(body_range.start..body_range.end)?; + + if body.is_empty() || body[0] != b':' { + return None; + } + let mut parts = body[1..].split(|b| *b == b';'); + let frame = parts.next()?; + let frame = str::from_utf8(frame).ok()?.parse::().ok()?; + let local = parts.next()?; + let local = str::from_utf8(local).ok()?.parse::().ok()?; + if parts.next().is_some() { + // Too many parameters. + return None; + } + + Some(qWasmLocal { + frame, + local, + buf, + }) + } +} diff --git a/src/protocol/commands/_qWasmStackValue.rs b/src/protocol/commands/_qWasmStackValue.rs new file mode 100644 index 0000000..8c3f9ec --- /dev/null +++ b/src/protocol/commands/_qWasmStackValue.rs @@ -0,0 +1,35 @@ +use super::prelude::*; + +#[derive(Debug)] +pub struct qWasmStackValue<'a> { + pub frame: usize, + pub index: usize, + pub buf: &'a mut [u8], +} + +impl<'a> ParseCommand<'a> for qWasmStackValue<'a> { + #[inline(always)] + fn from_packet(buf: PacketBuf<'a>) -> Option { + let (buf, body_range) = buf.into_raw_buf(); + let body = buf.get(body_range.start..body_range.end)?; + + if body.is_empty() || body[0] != b':' { + return None; + } + let mut parts = body[1..].split(|b| *b == b';'); + let frame = parts.next()?; + let frame = str::from_utf8(frame).ok()?.parse::().ok()?; + let index = parts.next()?; + let index = str::from_utf8(index).ok()?.parse::().ok()?; + if parts.next().is_some() { + // Too many parameters. + return None; + } + + Some(qWasmStackValue { + frame, + index, + buf, + }) + } +} diff --git a/src/stub/core_impl.rs b/src/stub/core_impl.rs index e815e54..172d3c8 100644 --- a/src/stub/core_impl.rs +++ b/src/stub/core_impl.rs @@ -44,6 +44,7 @@ mod single_register_access; mod target_xml; mod thread_extra_info; mod tracepoints; +mod wasm; mod x_upcase_packet; pub(crate) use resume::FinishExecStatus; @@ -223,6 +224,7 @@ impl GdbStubImpl { Command::LibrariesSvr4(cmd) => self.handle_libraries_svr4(res, target, cmd), Command::Libraries(cmd) => self.handle_libraries(res, target, cmd), Command::Tracepoints(cmd) => self.handle_tracepoints(res, target, cmd), + Command::Wasm(cmd) => self.handle_wasm(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 diff --git a/src/stub/core_impl/wasm.rs b/src/stub/core_impl/wasm.rs new file mode 100644 index 0000000..e278183 --- /dev/null +++ b/src/stub/core_impl/wasm.rs @@ -0,0 +1,51 @@ +use super::prelude::*; +use crate::protocol::commands::ext::Wasm; + +impl GdbStubImpl { + pub(crate) fn handle_wasm( + &mut self, + res: &mut ResponseWriter<'_, C>, + target: &mut T, + command: Wasm<'_>, + ) -> Result> { + let ops = match target.support_wasm() { + Some(ops) => ops, + None => return Ok(HandlerStatus::Handled), + }; + + crate::__dead_code_marker!("wasm", "impl"); + + match command { + Wasm::qWasmCallStack(cmd) => { + let mut error: Result<(), Error> = Ok(()); + ops.wasm_call_stack(cmd.tid.tid, &mut |pc| { + if let Err(e) = res.write_hex_buf(&pc.to_le_bytes()) { + error = Err(e.into()); + } + }) + .map_err(Error::TargetError)?; + error?; + } + Wasm::qWasmLocal(cmd) => { + let len = ops + .read_wasm_local(self.current_mem_tid, cmd.frame, cmd.local, cmd.buf) + .map_err(Error::TargetError)?; + res.write_hex_buf(&cmd.buf[0..len])?; + } + Wasm::qWasmGlobal(cmd) => { + let len = ops + .read_wasm_global(self.current_mem_tid, cmd.frame, cmd.global, cmd.buf) + .map_err(Error::TargetError)?; + res.write_hex_buf(&cmd.buf[0..len])?; + } + Wasm::qWasmStackValue(cmd) => { + let len = ops + .read_wasm_stack(self.current_mem_tid, cmd.frame, cmd.index, cmd.buf) + .map_err(Error::TargetError)?; + res.write_hex_buf(&cmd.buf[0..len])?; + } + }; + + Ok(HandlerStatus::Handled) + } +} diff --git a/src/target/ext/mod.rs b/src/target/ext/mod.rs index 3720bc2..8b3adfc 100644 --- a/src/target/ext/mod.rs +++ b/src/target/ext/mod.rs @@ -274,3 +274,4 @@ pub mod section_offsets; pub mod target_description_xml_override; pub mod thread_extra_info; pub mod tracepoints; +pub mod wasm; diff --git a/src/target/ext/wasm.rs b/src/target/ext/wasm.rs new file mode 100644 index 0000000..55956ee --- /dev/null +++ b/src/target/ext/wasm.rs @@ -0,0 +1,102 @@ +//! (LLDB extension) Provide Wasm-specific actions for the target. +//! +//! ### Address Encoding +//! +//! The LLDB Wasm extension to the GDB RSP uses a +//! specific encoding for addresses, both for commands in this +//! extension trait and for commands in the base protocol (e.g., for +//! reading and writing memory and setting breakpoints). The need for +//! this scheme arises from the fact that Wasm is natively +//! "multimemory": there can be many code modules, and many linear +//! memories, and each is a native entity (rather than mapped into a +//! larger single address space) in the VM definition. The gdbstub +//! protocol extensions map these native entities into an address +//! space where the upper 32 bits encode the index of a particular +//! Wasm code module or linear (data) memory and the lower 32 bits +//! encode an offset. +//! +//! See the [LLDB source code] (particularly `WasmAddressType` and +//! `wasm_addr_t`) for a description of the encoding of the PC values. +//! +//! [LLDB souce code]: https://github.com/llvm/llvm-project/blob/main/lldb/source/Plugins/Process/wasm/ProcessWasm.h +use crate::common::Tid; +use crate::target::Target; + +/// (LLDB extension) Target Extension - perform Wasm-specific actions. +pub trait Wasm: Target { + /// Get the Wasm call stack for a given thread. + /// + /// The addresses provided for the PC at each frame shouuld be + /// encoded as per the [Wasm address encoding]. + /// + /// To avoid allocation, the call stack PCs should be returned to + /// the caller by calling the given callback, in order from + /// innermost (most recently called) frame to outermost. + /// + /// [Wasm address encoding]: `self#Address_Encoding` + fn wasm_call_stack(&self, tid: Tid, next_pc: &mut dyn FnMut(u64)) -> Result<(), Self::Error>; + + /// Get the Wasm local for a given thread, frame index, and local + /// index. + /// + /// The Wasm local's value should be placed into `buf`, and the + /// length should be returned. If the Wasm local or frame does not + /// exist, this method should return `0`. + /// + /// `buf` will be long enough to allow for the larget possible + /// supported Wasm value (i.e., at least a `v128` SIMD + /// value). Values should be encoded in little-endian format with + /// their native length (e.g., 4 bytes for a Wasm `i32` or `f32` + /// type, or 8 bytes for a Wasm `i64` or `f64` type). + fn read_wasm_local( + &self, + tid: Tid, + frame: usize, + local: usize, + buf: &mut [u8], + ) -> Result; + + /// Get the Wasm operand-stack value for a given thread, frame + /// index, and stack index. Top-of-stack is index 0, and values + /// below that have incrementing indices. + /// + /// The Wasm operand's value should be placed into `buf`, and the + /// length should be returned. If the Wasm local or frame does not + /// exist, this method should return `0`. + /// + /// `buf` will be long enough to allow for the larget possible + /// supported Wasm value (i.e., at least a `v128` SIMD + /// value). Values should be encoded in little-endian format with + /// their native length (e.g., 4 bytes for a Wasm `i32` or `f32` + /// type, or 8 bytes for a Wasm `i64` or `f64` type). + fn read_wasm_stack( + &self, + tid: Tid, + frame: usize, + index: usize, + buf: &mut [u8], + ) -> Result; + + /// Get the Wasm global value for a given thread, frame, and + /// global index. The global index is relative to the module whose + /// function corresponds to that frame. + /// + /// The Wasm global's value should be placed into `buf`, and the + /// length should be returned. If the Wasm local or frame does not + /// exist, this method should return `0`. + /// + /// `buf` will be long enough to allow for the larget possible + /// supported Wasm value (i.e., at least a `v128` SIMD + /// value). Values should be encoded in little-endian format with + /// their native length (e.g., 4 bytes for a Wasm `i32` or `f32` + /// type, or 8 bytes for a Wasm `i64` or `f64` type). + fn read_wasm_global( + &self, + tid: Tid, + frame: usize, + global: usize, + buf: &mut [u8], + ) -> Result; +} + +define_ext!(WasmOps, Wasm); diff --git a/src/target/mod.rs b/src/target/mod.rs index d3eac88..be34a61 100644 --- a/src/target/mod.rs +++ b/src/target/mod.rs @@ -746,6 +746,12 @@ pub trait Target { fn support_libraries(&mut self) -> Option> { None } + + /// (LLDB extension) Support for WebAssembly (Wasm)-specific commands. + #[inline(always)] + fn support_wasm(&mut self) -> Option> { + None + } } macro_rules! __delegate {