diff --git a/crates/api/src/runtime.rs b/crates/api/src/runtime.rs index 369f767d3d..33f1a46cf5 100644 --- a/crates/api/src/runtime.rs +++ b/crates/api/src/runtime.rs @@ -13,7 +13,7 @@ use wasmtime_environ::CacheConfig; use wasmtime_environ::Tunables; use wasmtime_jit::{native, CompilationStrategy, Compiler}; use wasmtime_profiling::{JitDumpAgent, NullProfilerAgent, ProfilingAgent, VTuneAgent}; -use wasmtime_runtime::RuntimeMemoryCreator; +use wasmtime_runtime::{debug_builtins, RuntimeMemoryCreator}; // Runtime Environment @@ -465,6 +465,7 @@ impl Engine { /// Creates a new [`Engine`] with the specified compilation and /// configuration settings. pub fn new(config: &Config) -> Engine { + debug_builtins::ensure_exported(); Engine { config: Arc::new(config.clone()), } diff --git a/crates/debug/src/transform/unit.rs b/crates/debug/src/transform/unit.rs index ca49a78aac..68b3f209ca 100644 --- a/crates/debug/src/transform/unit.rs +++ b/crates/debug/src/transform/unit.rs @@ -82,11 +82,23 @@ where Ok(String::from("??")) } +/// Replaces WebAssembly pointer type DIE with the wrapper +/// which natively represented by offset in a Wasm memory. +/// +/// `pointer_type_entry` is an DW_TAG_pointer_type entry (e.g. `T*`), +/// which refers its base type (e.g. `T`). +/// +/// The generated wrapper is a structure that contains only the +/// `__ptr` field. The utility operators overloads is added to +/// provide better debugging experience. +/// +/// Notice that "resolve_vmctx_memory_ptr" is external/builtin +/// subprogram that is not part of Wasm code. fn replace_pointer_type( parent_id: write::UnitEntryId, comp_unit: &mut write::Unit, wp_die_id: write::UnitEntryId, - entry: &DebuggingInformationEntry, + pointer_type_entry: &DebuggingInformationEntry, unit: &Unit, context: &DebugInputContext, out_strings: &mut write::StringTable, @@ -95,48 +107,121 @@ fn replace_pointer_type( where R: Reader, { - let die_id = comp_unit.add(parent_id, gimli::DW_TAG_structure_type); - let die = comp_unit.get_mut(die_id); + const WASM_PTR_LEN: u8 = 4; - let name = format!( - "WebAssemblyPtrWrapper<{}>", - get_base_type_name(entry, unit, context)? - ); - die.set( - gimli::DW_AT_name, - write::AttributeValue::StringRef(out_strings.add(name.as_str())), - ); - die.set(gimli::DW_AT_byte_size, write::AttributeValue::Data1(4)); - - let p_die_id = comp_unit.add(die_id, gimli::DW_TAG_template_type_parameter); - let p_die = comp_unit.get_mut(p_die_id); - p_die.set( - gimli::DW_AT_name, - write::AttributeValue::StringRef(out_strings.add("T")), - ); - p_die.set( - gimli::DW_AT_type, - write::AttributeValue::ThisUnitEntryRef(wp_die_id), - ); - if let Some(AttributeValue::UnitRef(ref offset)) = entry.attr_value(gimli::DW_AT_type)? { - pending_die_refs.insert(p_die_id, gimli::DW_AT_type, *offset); + macro_rules! add_tag { + ($parent_id:ident, $tag:expr => $die:ident as $die_id:ident { $($a:path = $v:expr),* }) => { + let $die_id = comp_unit.add($parent_id, $tag); + #[allow(unused_variables)] + let $die = comp_unit.get_mut($die_id); + $( $die.set($a, $v); )* + }; } - let m_die_id = comp_unit.add(die_id, gimli::DW_TAG_member); - let m_die = comp_unit.get_mut(m_die_id); - m_die.set( - gimli::DW_AT_name, - write::AttributeValue::StringRef(out_strings.add("__ptr")), - ); - m_die.set( - gimli::DW_AT_type, - write::AttributeValue::ThisUnitEntryRef(wp_die_id), - ); - m_die.set( - gimli::DW_AT_data_member_location, - write::AttributeValue::Data1(0), - ); - Ok(die_id) + // Build DW_TAG_structure_type for the wrapper: + // .. DW_AT_name = "WebAssemblyPtrWrapper", + // .. DW_AT_byte_size = 4, + add_tag!(parent_id, gimli::DW_TAG_structure_type => wrapper_die as wrapper_die_id { + gimli::DW_AT_name = write::AttributeValue::StringRef(out_strings.add(format!( + "WebAssemblyPtrWrapper<{}>", + get_base_type_name(pointer_type_entry, unit, context)? + ).as_str())), + gimli::DW_AT_byte_size = write::AttributeValue::Data1(WASM_PTR_LEN) + }); + + // Build DW_TAG_pointer_type for `WebAssemblyPtrWrapper*`: + // .. DW_AT_type = + add_tag!(parent_id, gimli::DW_TAG_pointer_type => wrapper_ptr_type as wrapper_ptr_type_id { + gimli::DW_AT_type = write::AttributeValue::ThisUnitEntryRef(wrapper_die_id) + }); + + let base_type_id = pointer_type_entry.attr_value(gimli::DW_AT_type)?; + // Build DW_TAG_reference_type for `T&`: + // .. DW_AT_type = + add_tag!(parent_id, gimli::DW_TAG_reference_type => ref_type as ref_type_id {}); + if let Some(AttributeValue::UnitRef(ref offset)) = base_type_id { + pending_die_refs.insert(ref_type_id, gimli::DW_AT_type, *offset); + } + + // Build DW_TAG_pointer_type for `T*`: + // .. DW_AT_type = + add_tag!(parent_id, gimli::DW_TAG_pointer_type => ptr_type as ptr_type_id {}); + if let Some(AttributeValue::UnitRef(ref offset)) = base_type_id { + pending_die_refs.insert(ptr_type_id, gimli::DW_AT_type, *offset); + } + + // Build wrapper_die's DW_TAG_template_type_parameter: + // .. DW_AT_name = "T" + // .. DW_AT_type = + add_tag!(wrapper_die_id, gimli::DW_TAG_template_type_parameter => t_param_die as t_param_die_id { + gimli::DW_AT_name = write::AttributeValue::StringRef(out_strings.add("T")) + }); + if let Some(AttributeValue::UnitRef(ref offset)) = base_type_id { + pending_die_refs.insert(t_param_die_id, gimli::DW_AT_type, *offset); + } + + // Build wrapper_die's DW_TAG_member for `__ptr`: + // .. DW_AT_name = "__ptr" + // .. DW_AT_type = + // .. DW_AT_location = 0 + add_tag!(wrapper_die_id, gimli::DW_TAG_member => m_die as m_die_id { + gimli::DW_AT_name = write::AttributeValue::StringRef(out_strings.add("__ptr")), + gimli::DW_AT_type = write::AttributeValue::ThisUnitEntryRef(wp_die_id), + gimli::DW_AT_data_member_location = write::AttributeValue::Data1(0) + }); + + // Build wrapper_die's DW_TAG_subprogram for `ptr()`: + // .. DW_AT_linkage_name = "resolve_vmctx_memory_ptr" + // .. DW_AT_name = "ptr" + // .. DW_AT_type = + // .. DW_TAG_formal_parameter + // .. .. DW_AT_type = + // .. .. DW_AT_artificial = 1 + add_tag!(wrapper_die_id, gimli::DW_TAG_subprogram => deref_op_die as deref_op_die_id { + gimli::DW_AT_linkage_name = write::AttributeValue::StringRef(out_strings.add("resolve_vmctx_memory_ptr")), + gimli::DW_AT_name = write::AttributeValue::StringRef(out_strings.add("ptr")), + gimli::DW_AT_type = write::AttributeValue::ThisUnitEntryRef(ptr_type_id) + }); + add_tag!(deref_op_die_id, gimli::DW_TAG_formal_parameter => deref_op_this_param as deref_op_this_param_id { + gimli::DW_AT_type = write::AttributeValue::ThisUnitEntryRef(wrapper_ptr_type_id), + gimli::DW_AT_artificial = write::AttributeValue::Flag(true) + }); + + // Build wrapper_die's DW_TAG_subprogram for `operator*`: + // .. DW_AT_linkage_name = "resolve_vmctx_memory_ptr" + // .. DW_AT_name = "operator*" + // .. DW_AT_type = + // .. DW_TAG_formal_parameter + // .. .. DW_AT_type = + // .. .. DW_AT_artificial = 1 + add_tag!(wrapper_die_id, gimli::DW_TAG_subprogram => deref_op_die as deref_op_die_id { + gimli::DW_AT_linkage_name = write::AttributeValue::StringRef(out_strings.add("resolve_vmctx_memory_ptr")), + gimli::DW_AT_name = write::AttributeValue::StringRef(out_strings.add("operator*")), + gimli::DW_AT_type = write::AttributeValue::ThisUnitEntryRef(ref_type_id) + }); + add_tag!(deref_op_die_id, gimli::DW_TAG_formal_parameter => deref_op_this_param as deref_op_this_param_id { + gimli::DW_AT_type = write::AttributeValue::ThisUnitEntryRef(wrapper_ptr_type_id), + gimli::DW_AT_artificial = write::AttributeValue::Flag(true) + }); + + // Build wrapper_die's DW_TAG_subprogram for `operator->`: + // .. DW_AT_linkage_name = "resolve_vmctx_memory_ptr" + // .. DW_AT_name = "operator->" + // .. DW_AT_type = + // .. DW_TAG_formal_parameter + // .. .. DW_AT_type = + // .. .. DW_AT_artificial = 1 + add_tag!(wrapper_die_id, gimli::DW_TAG_subprogram => deref_op_die as deref_op_die_id { + gimli::DW_AT_linkage_name = write::AttributeValue::StringRef(out_strings.add("resolve_vmctx_memory_ptr")), + gimli::DW_AT_name = write::AttributeValue::StringRef(out_strings.add("operator->")), + gimli::DW_AT_type = write::AttributeValue::ThisUnitEntryRef(ptr_type_id) + }); + add_tag!(deref_op_die_id, gimli::DW_TAG_formal_parameter => deref_op_this_param as deref_op_this_param_id { + gimli::DW_AT_type = write::AttributeValue::ThisUnitEntryRef(wrapper_ptr_type_id), + gimli::DW_AT_artificial = write::AttributeValue::Flag(true) + }); + + Ok(wrapper_die_id) } pub(crate) fn clone_unit<'a, R>( diff --git a/crates/debug/src/transform/utils.rs b/crates/debug/src/transform/utils.rs index cf4885f94c..506b6b71c0 100644 --- a/crates/debug/src/transform/utils.rs +++ b/crates/debug/src/transform/utils.rs @@ -6,49 +6,62 @@ use wasmtime_environ::isa::TargetIsa; use wasmtime_environ::wasm::DefinedFuncIndex; use wasmtime_environ::{ModuleMemoryOffset, ModuleVmctxInfo, ValueLabelsRanges}; +/// Adds internal Wasm utility types DIEs such as WebAssemblyPtr and +/// WasmtimeVMContext. +/// +/// For unwrapping Wasm pointer, the WasmtimeVMContext has the `set()` method +/// that allows to contol current Wasm memory to inspect. +/// Notice that "set_vmctx_memory" is an external/builtin subprogram that +/// is not part of Wasm code. pub(crate) fn add_internal_types( comp_unit: &mut write::Unit, root_id: write::UnitEntryId, out_strings: &mut write::StringTable, module_info: &ModuleVmctxInfo, ) -> (write::UnitEntryId, write::UnitEntryId) { - let wp_die_id = comp_unit.add(root_id, gimli::DW_TAG_base_type); - let wp_die = comp_unit.get_mut(wp_die_id); - wp_die.set( - gimli::DW_AT_name, - write::AttributeValue::StringRef(out_strings.add("WebAssemblyPtr")), - ); - wp_die.set(gimli::DW_AT_byte_size, write::AttributeValue::Data1(4)); - wp_die.set( - gimli::DW_AT_encoding, - write::AttributeValue::Encoding(gimli::DW_ATE_unsigned), - ); + const WASM_PTR_LEN: u8 = 4; - let memory_byte_die_id = comp_unit.add(root_id, gimli::DW_TAG_base_type); - let memory_byte_die = comp_unit.get_mut(memory_byte_die_id); - memory_byte_die.set( - gimli::DW_AT_name, - write::AttributeValue::StringRef(out_strings.add("u8")), - ); - memory_byte_die.set( - gimli::DW_AT_encoding, - write::AttributeValue::Encoding(gimli::DW_ATE_unsigned), - ); - memory_byte_die.set(gimli::DW_AT_byte_size, write::AttributeValue::Data1(1)); + macro_rules! add_tag { + ($parent_id:ident, $tag:expr => $die:ident as $die_id:ident { $($a:path = $v:expr),* }) => { + let $die_id = comp_unit.add($parent_id, $tag); + let $die = comp_unit.get_mut($die_id); + $( $die.set($a, $v); )* + }; + } - let memory_bytes_die_id = comp_unit.add(root_id, gimli::DW_TAG_pointer_type); - let memory_bytes_die = comp_unit.get_mut(memory_bytes_die_id); - memory_bytes_die.set( - gimli::DW_AT_name, - write::AttributeValue::StringRef(out_strings.add("u8*")), - ); - memory_bytes_die.set( - gimli::DW_AT_type, - write::AttributeValue::ThisUnitEntryRef(memory_byte_die_id), - ); + // Build DW_TAG_base_type for generic `WebAssemblyPtr`. + // .. DW_AT_name = "WebAssemblyPtr" + // .. DW_AT_byte_size = 4 + // .. DW_AT_encoding = DW_ATE_unsigned + // let wp_die_id = comp_unit.add(root_id, gimli::DW_TAG_base_type); + // let wp_die = comp_unit.get_mut(wp_die_id); + add_tag!(root_id, gimli::DW_TAG_base_type => wp_die as wp_die_id { + gimli::DW_AT_name = write::AttributeValue::StringRef(out_strings.add("WebAssemblyPtr")), + gimli::DW_AT_byte_size = write::AttributeValue::Data1(WASM_PTR_LEN), + gimli::DW_AT_encoding = write::AttributeValue::Encoding(gimli::DW_ATE_unsigned) + }); + + // Build DW_TAG_base_type for Wasm byte: + // .. DW_AT_name = u8 + // .. DW_AT_encoding = DW_ATE_unsigned + // .. DW_AT_byte_size = 1 + add_tag!(root_id, gimli::DW_TAG_base_type => memory_byte_die as memory_byte_die_id { + gimli::DW_AT_name = write::AttributeValue::StringRef(out_strings.add("u8")), + gimli::DW_AT_encoding = write::AttributeValue::Encoding(gimli::DW_ATE_unsigned), + gimli::DW_AT_byte_size = write::AttributeValue::Data1(1) + }); + + // Build DW_TAG_pointer_type that references Wasm bytes: + // .. DW_AT_name = "u8*" + // .. DW_AT_type = + add_tag!(root_id, gimli::DW_TAG_pointer_type => memory_bytes_die as memory_bytes_die_id { + gimli::DW_AT_name = write::AttributeValue::StringRef(out_strings.add("u8*")), + gimli::DW_AT_type = write::AttributeValue::ThisUnitEntryRef(memory_byte_die_id) + }); // Create artificial VMContext type and its reference for convinience viewing - // its fields (such as memory ref) in a debugger. + // its fields (such as memory ref) in a debugger. Build DW_TAG_structure_type: + // .. DW_AT_name = "WasmtimeVMContext" let vmctx_die_id = comp_unit.add(root_id, gimli::DW_TAG_structure_type); let vmctx_die = comp_unit.get_mut(vmctx_die_id); vmctx_die.set( @@ -56,6 +69,7 @@ pub(crate) fn add_internal_types( write::AttributeValue::StringRef(out_strings.add("WasmtimeVMContext")), ); + // TODO multiple memories match module_info.memory_offset { ModuleMemoryOffset::Defined(memory_offset) => { // The context has defined memory: extend the WasmtimeVMContext size @@ -67,20 +81,15 @@ pub(crate) fn add_internal_types( ); // Define the "memory" field which is a direct pointer to allocated Wasm memory. - let m_die_id = comp_unit.add(vmctx_die_id, gimli::DW_TAG_member); - let m_die = comp_unit.get_mut(m_die_id); - m_die.set( - gimli::DW_AT_name, - write::AttributeValue::StringRef(out_strings.add("memory")), - ); - m_die.set( - gimli::DW_AT_type, - write::AttributeValue::ThisUnitEntryRef(memory_bytes_die_id), - ); - m_die.set( - gimli::DW_AT_data_member_location, - write::AttributeValue::Udata(memory_offset as u64), - ); + // Build DW_TAG_member: + // .. DW_AT_name = "memory" + // .. DW_AT_type = + // .. DW_AT_data_member_location = `memory_offset` + add_tag!(vmctx_die_id, gimli::DW_TAG_member => m_die as m_die_id { + gimli::DW_AT_name = write::AttributeValue::StringRef(out_strings.add("memory")), + gimli::DW_AT_type = write::AttributeValue::ThisUnitEntryRef(memory_bytes_die_id), + gimli::DW_AT_data_member_location = write::AttributeValue::Udata(memory_offset as u64) + }); } ModuleMemoryOffset::Imported(_) => { // TODO implement convinience pointer to and additional types for VMMemoryImport. @@ -88,16 +97,28 @@ pub(crate) fn add_internal_types( ModuleMemoryOffset::None => (), } - let vmctx_ptr_die_id = comp_unit.add(root_id, gimli::DW_TAG_pointer_type); - let vmctx_ptr_die = comp_unit.get_mut(vmctx_ptr_die_id); - vmctx_ptr_die.set( - gimli::DW_AT_name, - write::AttributeValue::StringRef(out_strings.add("WasmtimeVMContext*")), - ); - vmctx_ptr_die.set( - gimli::DW_AT_type, - write::AttributeValue::ThisUnitEntryRef(vmctx_die_id), - ); + // Build DW_TAG_pointer_type for `WasmtimeVMContext*`: + // .. DW_AT_name = "WasmtimeVMContext*" + // .. DW_AT_type = + add_tag!(root_id, gimli::DW_TAG_pointer_type => vmctx_ptr_die as vmctx_ptr_die_id { + gimli::DW_AT_name = write::AttributeValue::StringRef(out_strings.add("WasmtimeVMContext*")), + gimli::DW_AT_type = write::AttributeValue::ThisUnitEntryRef(vmctx_die_id) + }); + + // Build vmctx_die's DW_TAG_subprogram for `set` method: + // .. DW_AT_linkage_name = "set_vmctx_memory" + // .. DW_AT_name = "set" + // .. DW_TAG_formal_parameter + // .. .. DW_AT_type = + // .. .. DW_AT_artificial = 1 + add_tag!(vmctx_die_id, gimli::DW_TAG_subprogram => vmctx_set as vmctx_set_id { + gimli::DW_AT_linkage_name = write::AttributeValue::StringRef(out_strings.add("set_vmctx_memory")), + gimli::DW_AT_name = write::AttributeValue::StringRef(out_strings.add("set")) + }); + add_tag!(vmctx_set_id, gimli::DW_TAG_formal_parameter => vmctx_set_this_param as vmctx_set_this_param_id { + gimli::DW_AT_type = write::AttributeValue::ThisUnitEntryRef(vmctx_ptr_die_id), + gimli::DW_AT_artificial = write::AttributeValue::Flag(true) + }); (wp_die_id, vmctx_ptr_die_id) } diff --git a/crates/runtime/src/debug_builtins.rs b/crates/runtime/src/debug_builtins.rs new file mode 100644 index 0000000000..831bbdd3a8 --- /dev/null +++ b/crates/runtime/src/debug_builtins.rs @@ -0,0 +1,41 @@ +#![doc(hidden)] + +use crate::instance::InstanceHandle; +use crate::vmcontext::VMContext; +use wasmtime_environ::entity::EntityRef; +use wasmtime_environ::wasm::MemoryIndex; + +static mut VMCTX_AND_MEMORY: (*mut VMContext, usize) = (std::ptr::null_mut(), 0); + +#[no_mangle] +pub unsafe extern "C" fn resolve_vmctx_memory_ptr(p: *const u32) -> *const u8 { + let ptr = std::ptr::read(p); + assert!( + !VMCTX_AND_MEMORY.0.is_null(), + "must call `__vmctx->set()` before resolving Wasm pointers" + ); + let handle = InstanceHandle::from_vmctx(VMCTX_AND_MEMORY.0); + assert!( + VMCTX_AND_MEMORY.1 < handle.instance().module().local.memory_plans.len(), + "memory index for debugger is out of bounds" + ); + let index = MemoryIndex::new(VMCTX_AND_MEMORY.1); + let mem = handle.instance().get_memory(index); + mem.base.add(ptr as usize) +} + +#[no_mangle] +pub unsafe extern "C" fn set_vmctx_memory(vmctx_ptr: *mut VMContext) { + // TODO multi-memory + VMCTX_AND_MEMORY = (vmctx_ptr, 0); +} + +// Ensures that set_vmctx_memory and resolve_vmctx_memory_ptr are linked and +// exported as symbols. It is a workaround: the executable normally ignores +// `pub extern "C"`, see rust-lang/rust#25057. +pub fn ensure_exported() { + unsafe { + std::ptr::read_volatile(resolve_vmctx_memory_ptr as *const u8); + std::ptr::read_volatile(set_vmctx_memory as *const u8); + } +} diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 8adda16f02..7cdd331acf 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -33,6 +33,7 @@ mod trap_registry; mod traphandlers; mod vmcontext; +pub mod debug_builtins; pub mod libcalls; pub use crate::export::*; diff --git a/tests/debug/lldb.rs b/tests/debug/lldb.rs index 5c7ba9f978..abf14f353e 100644 --- a/tests/debug/lldb.rs +++ b/tests/debug/lldb.rs @@ -92,3 +92,37 @@ check: exited with status )?; Ok(()) } + +#[test] +#[ignore] +#[cfg(all( + any(target_os = "linux", target_os = "macos"), + target_pointer_width = "64" +))] +pub fn test_debug_dwarf_ptr() -> Result<()> { + let output = lldb_with_script( + &[ + "-g", + "--opt-level", + "0", + "tests/debug/testsuite/reverse-str.wasm", + ], + r#"b reverse-str.c:9 +r +p __vmctx->set(),&*s +c"#, + )?; + + check_lldb_output( + &output, + r#" +check: Breakpoint 1: no locations (pending) +check: stop reason = breakpoint 1.1 +check: frame #0 +sameln: reverse(s=(__ptr = +check: "Hello, world." +check: resuming +"#, + )?; + Ok(()) +} diff --git a/tests/debug/testsuite/reverse-str.c b/tests/debug/testsuite/reverse-str.c new file mode 100644 index 0000000000..0cad1b2f0f --- /dev/null +++ b/tests/debug/testsuite/reverse-str.c @@ -0,0 +1,21 @@ +// Compile with: +// clang --target=wasm32 reverse-str.c -o reverse-str.wasm -g \ +// -O0 -nostdlib -fdebug-prefix-map=$PWD=. +#include + +void reverse(char *s, size_t len) +{ + if (!len) return; + size_t i = 0, j = len - 1; + while (i < j) { + char t = s[i]; + s[i++] = s[j]; + s[j--] = t; + } +} + +void _start() +{ + char hello[] = "Hello, world."; + reverse(hello, 13); +} diff --git a/tests/debug/testsuite/reverse-str.wasm b/tests/debug/testsuite/reverse-str.wasm new file mode 100755 index 0000000000..cfbd253855 Binary files /dev/null and b/tests/debug/testsuite/reverse-str.wasm differ