Skip to content

Commit 67c060e

Browse files
committed
Wasmtime: implement debug instrumentation and basic host API to examine runtime state.
This PR implements ideas from the [recent RFC] to serve as the basis for Wasm (guest) debugging: it adds a stackslot to each function translated from Wasm, stores to replicate Wasm VM state in the stackslot as the program runs, and metadata to describe the format of that state and allow reading it out at runtime. As an initial user of this state, this PR adds a basic "stack view" API that, from host code that has been called from Wasm, can examine Wasm frames currently on the stack and read out all of their locals and stack slots. Note in particular that this PR does not include breakpoints, watchpoints, stepped execution, or any sort of user interface for any of this; it is only a foundation. This PR still has a few unsatisfying bits that I intend to address: - The "stack view" performs some O(n) work when the view is initially taken, computing some internal data per frame. This is forced by the current design of `Backtrace`, which takes a closure and walks that closure over stack frames eagerly (rather than work as an iterator). It's got some impressive iterator-chain stuff going on internally, so refactoring it to the latter approach might not be *too* bad, but I haven't tackled it yet. A O(1) stack view, that is, one that does work only for frames as the host API is used to walk up the stack, is desirable because some use-cases may want to quickly examine e.g. only the deepest frame (say, running with a breakpoint condition that needs to read a particular local's value after each step). - It includes a new `Config::compiler_force_inlining()` option that is used only for testing that we get the correct frames after inlining. I couldn't get the existing flags to work on a Wasmtime config level and suspect there may be an existing bug there; I will try to split out a fix for it. This PR renames the existing `debug` option to `native_debug`, to distinguish it from the new approach. [recent RFC]: bytecodealliance/rfcs#44
1 parent 9409a84 commit 67c060e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2201
-220
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -918,7 +918,7 @@ jobs:
918918
sudo mkdir -p /usr/lib/local/lib/python3.10/dist-packages/lldb
919919
sudo ln -s /usr/lib/llvm-15/lib/python3.10/dist-packages/lldb/* /usr/lib/python3/dist-packages/lldb/
920920
# Only testing release since it is more likely to expose issues with our low-level symbol handling.
921-
cargo test --release --test all -- --ignored --test-threads 1 debug::
921+
cargo test --release --test all -- --ignored --test-threads 1 native_debug::
922922
env:
923923
LLDB: lldb-18
924924
WASI_SDK_PATH: /tmp/wasi-sdk

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ default = [
469469
"stack-switching",
470470
"winch",
471471
"pulley",
472+
"debug",
472473

473474
# Enable some nice features of clap by default, but they come at a binary size
474475
# cost, so allow disabling this through disabling of our own `default`
@@ -531,6 +532,7 @@ gc-drc = ["gc", "wasmtime/gc-drc", "wasmtime-cli-flags/gc-drc"]
531532
gc-null = ["gc", "wasmtime/gc-null", "wasmtime-cli-flags/gc-null"]
532533
pulley = ["wasmtime-cli-flags/pulley"]
533534
stack-switching = ["wasmtime/stack-switching", "wasmtime-cli-flags/stack-switching"]
535+
debug = ["wasmtime-cli-flags/debug", "wasmtime/debug"]
534536

535537
# CLI subcommands for the `wasmtime` executable. See `wasmtime $cmd --help`
536538
# for more information on each subcommand.

crates/c-api/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ pub extern "C" fn wasm_config_new() -> Box<wasm_config_t> {
5555

5656
#[unsafe(no_mangle)]
5757
pub extern "C" fn wasmtime_config_debug_info_set(c: &mut wasm_config_t, enable: bool) {
58-
c.config.debug_info(enable);
58+
c.config.native_debug_info(enable);
5959
}
6060

6161
#[unsafe(no_mangle)]

crates/cli-flags/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ threads = ["wasmtime/threads"]
4040
memory-protection-keys = ["wasmtime/memory-protection-keys"]
4141
pulley = ["wasmtime/pulley"]
4242
stack-switching = ["wasmtime/stack-switching"]
43+
debug = ["wasmtime/debug"]

crates/cli-flags/src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,9 @@ wasmtime_option_group! {
263263
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
264264
pub struct DebugOptions {
265265
/// Enable generation of DWARF debug information in compiled code.
266-
pub debug_info: Option<bool>,
266+
pub native_debug_info: Option<bool>,
267+
/// Enable debug instrumentation for perfect value reconstruction.
268+
pub debug_instrumentation: Option<bool>,
267269
/// Configure whether compiled code can map native addresses to wasm.
268270
pub address_map: Option<bool>,
269271
/// Configure whether logging is enabled.
@@ -701,8 +703,13 @@ impl CommonOptions {
701703
enable => config.cranelift_debug_verifier(enable),
702704
true => err,
703705
}
704-
if let Some(enable) = self.debug.debug_info {
705-
config.debug_info(enable);
706+
if let Some(enable) = self.debug.native_debug_info {
707+
config.native_debug_info(enable);
708+
}
709+
match_feature! {
710+
["debug" : self.debug.debug_instrumentation]
711+
enable => config.debug_instrumentation(enable),
712+
_ => err,
706713
}
707714
if self.debug.coredump.is_some() {
708715
#[cfg(feature = "coredump")]

crates/cranelift/src/compiled_function.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{Relocation, mach_reloc_to_reloc, mach_trap_to_trap};
22
use cranelift_codegen::{
3-
Final, MachBufferFinalized, MachSrcLoc, ValueLabelsRanges, ir, isa::unwind::CfaUnwindInfo,
4-
isa::unwind::UnwindInfo,
3+
Final, MachBufferFinalized, MachBufferFrameLayout, MachSrcLoc, ValueLabelsRanges, ir,
4+
isa::unwind::CfaUnwindInfo, isa::unwind::UnwindInfo,
55
};
66
use wasmtime_environ::{FilePos, InstructionAddressMap, PrimaryMap, TrapInformation};
77

@@ -44,8 +44,6 @@ pub struct CompiledFunctionMetadata {
4444
pub cfa_unwind_info: Option<CfaUnwindInfo>,
4545
/// Mapping of value labels and their locations.
4646
pub value_labels_ranges: ValueLabelsRanges,
47-
/// Allocated stack slots.
48-
pub sized_stack_slots: ir::StackSlots,
4947
/// Start source location.
5048
pub start_srcloc: FilePos,
5149
/// End source location.
@@ -155,9 +153,11 @@ impl CompiledFunction {
155153
self.metadata.cfa_unwind_info = Some(unwind);
156154
}
157155

158-
/// Set the sized stack slots.
159-
pub fn set_sized_stack_slots(&mut self, slots: ir::StackSlots) {
160-
self.metadata.sized_stack_slots = slots;
156+
/// Returns the frame-layout metadata for this function.
157+
pub fn frame_layout(&self) -> &MachBufferFrameLayout {
158+
self.buffer
159+
.frame_layout()
160+
.expect("Single-function MachBuffer must have frame layout information")
161161
}
162162
}
163163

crates/cranelift/src/compiler.rs

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ use cranelift_codegen::isa::{
1414
unwind::{UnwindInfo, UnwindInfoKind},
1515
};
1616
use cranelift_codegen::print_errors::pretty_error;
17-
use cranelift_codegen::{CompiledCode, Context, FinalizedMachCallSite};
17+
use cranelift_codegen::{
18+
CompiledCode, Context, FinalizedMachCallSite, MachBufferDebugTagList, MachBufferFrameLayout,
19+
MachDebugTagPos,
20+
};
1821
use cranelift_entity::PrimaryMap;
1922
use cranelift_frontend::FunctionBuilder;
2023
use object::write::{Object, StandardSegment, SymbolId};
@@ -28,13 +31,13 @@ use std::ops::Range;
2831
use std::path;
2932
use std::sync::{Arc, Mutex};
3033
use wasmparser::{FuncValidatorAllocations, FunctionBody};
31-
use wasmtime_environ::obj::ELF_WASMTIME_EXCEPTIONS;
34+
use wasmtime_environ::obj::{ELF_WASMTIME_EXCEPTIONS, ELF_WASMTIME_FRAMES};
3235
use wasmtime_environ::{
3336
Abi, AddressMapSection, BuiltinFunctionIndex, CacheStore, CompileError, CompiledFunctionBody,
34-
DefinedFuncIndex, FlagValue, FuncKey, FunctionBodyData, FunctionLoc, HostCall,
35-
InliningCompiler, ModuleTranslation, ModuleTypesBuilder, PtrSize, StackMapSection,
36-
StaticModuleIndex, TrapEncodingBuilder, TrapSentinel, TripleExt, Tunables, VMOffsets,
37-
WasmFuncType, WasmValType,
37+
DefinedFuncIndex, FlagValue, FrameInstPos, FrameStackShape, FrameTableBuilder, FuncKey,
38+
FunctionBodyData, FunctionLoc, HostCall, InliningCompiler, ModuleTranslation,
39+
ModuleTypesBuilder, PtrSize, StackMapSection, StaticModuleIndex, TrapEncodingBuilder,
40+
TrapSentinel, TripleExt, Tunables, VMOffsets, WasmFuncType, WasmValType,
3841
};
3942
use wasmtime_unwinder::ExceptionTableBuilder;
4043

@@ -252,7 +255,7 @@ impl wasmtime_environ::Compiler for Compiler {
252255
context.func.collect_debug_info();
253256
}
254257

255-
let mut func_env = FuncEnvironment::new(self, translation, types, wasm_func_ty);
258+
let mut func_env = FuncEnvironment::new(self, translation, types, wasm_func_ty, key);
256259

257260
// The `stack_limit` global value below is the implementation of stack
258261
// overflow checks in Wasmtime.
@@ -575,6 +578,7 @@ impl wasmtime_environ::Compiler for Compiler {
575578
let mut traps = TrapEncodingBuilder::default();
576579
let mut stack_maps = StackMapSection::default();
577580
let mut exception_tables = ExceptionTableBuilder::default();
581+
let mut frame_tables = FrameTableBuilder::default();
578582

579583
let mut ret = Vec::with_capacity(funcs.len());
580584
for (i, (sym, func)) in funcs.iter().enumerate() {
@@ -602,6 +606,16 @@ impl wasmtime_environ::Compiler for Compiler {
602606
range.clone(),
603607
func.buffer.call_sites(),
604608
)?;
609+
if self.tunables.debug_instrumentation
610+
&& let Some(frame_layout) = func.buffer.frame_layout()
611+
{
612+
clif_to_env_frame_tables(
613+
&mut frame_tables,
614+
range.clone(),
615+
func.buffer.debug_tags(),
616+
frame_layout,
617+
)?;
618+
}
605619
builder.append_padding(self.linkopts.padding_between_functions);
606620

607621
let info = FunctionLoc {
@@ -628,6 +642,17 @@ impl wasmtime_environ::Compiler for Compiler {
628642
obj.append_section_data(exception_section, bytes, 1);
629643
});
630644

645+
if self.tunables.debug_instrumentation {
646+
let frame_table_section = obj.add_section(
647+
obj.segment_name(StandardSegment::Data).to_vec(),
648+
ELF_WASMTIME_FRAMES.as_bytes().to_vec(),
649+
SectionKind::ReadOnlyData,
650+
);
651+
frame_tables.serialize(|bytes| {
652+
obj.append_section_data(frame_table_section, bytes, 1);
653+
});
654+
}
655+
631656
Ok(ret)
632657
}
633658

@@ -1402,8 +1427,6 @@ impl FunctionCompiler<'_> {
14021427
}
14031428
}
14041429

1405-
compiled_function
1406-
.set_sized_stack_slots(std::mem::take(&mut context.func.sized_stack_slots));
14071430
self.compiler.contexts.lock().unwrap().push(self.cx);
14081431

14091432
Ok(compiled_function)
@@ -1448,6 +1471,56 @@ fn clif_to_env_exception_tables<'a>(
14481471
builder.add_func(CodeOffset::try_from(range.start).unwrap(), call_sites)
14491472
}
14501473

1474+
/// Convert from Cranelift's representation of frame state slots and
1475+
/// debug tags to Wasmtime's serialized metadata.
1476+
fn clif_to_env_frame_tables<'a>(
1477+
builder: &mut FrameTableBuilder,
1478+
range: Range<u64>,
1479+
tag_sites: impl Iterator<Item = MachBufferDebugTagList<'a>>,
1480+
frame_layout: &MachBufferFrameLayout,
1481+
) -> anyhow::Result<()> {
1482+
let mut frame_descriptors = HashMap::new();
1483+
for tag_site in tag_sites {
1484+
// Split into frames; each has three debug tags.
1485+
let mut frames = vec![];
1486+
for frame_tags in tag_site.tags.chunks_exact(3) {
1487+
let &[
1488+
ir::DebugTag::StackSlot(slot),
1489+
ir::DebugTag::User(wasm_pc),
1490+
ir::DebugTag::User(stack_shape),
1491+
] = frame_tags
1492+
else {
1493+
panic!("Invalid tags");
1494+
};
1495+
1496+
let frame_descriptor = *frame_descriptors.entry(slot).or_insert_with(|| {
1497+
let slot_to_fp_offset =
1498+
frame_layout.frame_to_fp_offset - frame_layout.stackslots[slot].offset;
1499+
let descriptor = frame_layout.stackslots[slot].descriptor.clone();
1500+
builder.add_frame_descriptor(slot_to_fp_offset, descriptor)
1501+
});
1502+
1503+
frames.push((
1504+
wasm_pc,
1505+
frame_descriptor,
1506+
FrameStackShape::from_raw(stack_shape),
1507+
));
1508+
}
1509+
1510+
let native_pc_in_code_section = u32::try_from(range.start)
1511+
.unwrap()
1512+
.checked_add(tag_site.offset)
1513+
.unwrap();
1514+
let pos = match tag_site.pos {
1515+
MachDebugTagPos::Post => FrameInstPos::Post,
1516+
MachDebugTagPos::Pre => FrameInstPos::Pre,
1517+
};
1518+
builder.add_program_point(native_pc_in_code_section, pos, &frames);
1519+
}
1520+
1521+
Ok(())
1522+
}
1523+
14511524
fn save_last_wasm_entry_context(
14521525
builder: &mut FunctionBuilder,
14531526
pointer_type: ir::Type,

0 commit comments

Comments
 (0)