//! Implementation of emitting DWARF debugging information for `*.wat` files. //! //! This is intended to be relatively simple but the goal is to enable emission //! of DWARF sections which point back to the original `*.wat` file itself. This //! enables debuggers like LLDB to debug `*.wat` files without necessarily //! built-in knowledge of WebAssembly itself. //! //! Overall I was curious on weekend and decided to implement this. It's an //! off-by-default crate feature and an off-by-default runtime feature of this //! crate. Hopefully doesn't carry too much complexity with it while still being //! easy/fun to play around with.
usecrate::core::binary::{EncodeOptions, Encoder, GenerateDwarf, Names, RecOrType}; usecrate::core::{InnerTypeKind, Local, ValType}; usecrate::token::Span; use gimli::write::{ self, Address, AttributeValue, DwarfUnit, Expression, FileId, LineProgram, LineString,
Sections, UnitEntryId, Writer,
}; use gimli::{Encoding, Format, LineEncoding, LittleEndian}; use std::cmp::Ordering; use std::collections::HashMap; use std::path::Path;
impl<'a> Dwarf<'a> { /// Creates a new `Dwarf` from the specified configuration. /// /// * `func_imports` - the number of imported functions in this module, or /// the index of the first defined function. /// * `opts` - encoding options, namely where whether DWARF is to be emitted /// is configured. /// * `names` - the `name` custom section for this module, used to name /// functions in DWARF. pubfn new(
func_imports: u32,
opts: &EncodeOptions<'a>,
names: &Names<'a>,
types: &'a [RecOrType<'a>],
) -> Option<Dwarf<'a>> { // This is a load-bearing `?` which notably short-circuits all DWARF // machinery entirely if this was not enabled at runtime. let (file, contents, style) = opts.dwarf_info?; let file = file.to_str()?;
// Configure some initial `gimli::write` context. let encoding = Encoding {
address_size: 4,
format: Format::Dwarf32,
version: 5,
}; letmut dwarf = DwarfUnit::new(encoding); let (comp_dir, comp_file) = match (
Path::new(file).parent().and_then(|s| s.to_str()),
Path::new(file).file_name().and_then(|s| s.to_str()),
) {
(Some(parent), Some(file_name)) if !parent.is_empty() => (parent, file_name),
_ => (".", file),
}; let comp_dir_ref = dwarf.strings.add(comp_dir); let comp_file_ref = dwarf.strings.add(comp_file);
dwarf.unit.line_program = LineProgram::new(
encoding,
LineEncoding::default(),
LineString::StringRef(comp_dir_ref),
LineString::StringRef(comp_file_ref),
None,
); let dir_id = dwarf.unit.line_program.default_directory(); let file_id =
dwarf
.unit
.line_program
.add_file(LineString::StringRef(comp_file_ref), dir_id, None);
// Configure a single compilation unit which encompasses the entire code // section. The code section isn't fully known at this point so only a // "low pc" is emitted here. let root = dwarf.unit.root(); let cu = dwarf.unit.get_mut(root);
cu.set(
gimli::DW_AT_producer,
AttributeValue::String(format!("wast {}", env!("CARGO_PKG_VERSION")).into_bytes()),
);
cu.set(
gimli::DW_AT_language, // Technically this should be something like wasm or wat but that // doesn't exist so fill in something here.
AttributeValue::Language(gimli::DW_LANG_C),
);
cu.set(gimli::DW_AT_name, AttributeValue::StringRef(comp_file_ref));
cu.set(
gimli::DW_AT_comp_dir,
AttributeValue::StringRef(comp_dir_ref),
);
cu.set(gimli::DW_AT_low_pc, AttributeValue::Data4(0));
// Build a lookup table for defined function index to its name. letmut func_names = HashMap::new(); for (idx, name) in names.funcs.iter() {
func_names.insert(*idx, *name);
}
// Offsets pointing to newlines are considered internally as the 0th // column of the next line, so handle the case that the contents start // with a newline. let (line, column) = if contents.starts_with("\n") {
(2, 0)
} else {
(1, 1)
};
Some(Dwarf {
dwarf,
style,
next_func: func_imports,
func_imports,
sym_offsets: Vec::new(),
contents,
line,
column,
last_offset: 0,
file_id,
cur_subprogram: None,
cur_subprogram_instrs: 0,
func_names,
i32_ty: None,
i64_ty: None,
f32_ty: None,
f64_ty: None,
types,
})
}
/// Start emitting a new function defined at `span`. /// /// This will start a new line program for this function and additionally /// configure a new `DW_TAG_subprogram` for this new function. pubfn start_func(&mutself, span: Span, ty: u32, locals: &[Local<'_>]) { self.change_linecol(span); let addr = Address::Symbol {
symbol: (self.next_func - self.func_imports) as usize,
addend: 0,
}; self.dwarf.unit.line_program.begin_sequence(Some(addr));
let root = self.dwarf.unit.root(); let subprogram = self.dwarf.unit.add(root, gimli::DW_TAG_subprogram); let entry = self.dwarf.unit.get_mut(subprogram); let fallback = format!("wasm-function[{}]", self.next_func); let func_name = self
.func_names
.get(&self.next_func)
.copied()
.unwrap_or(&fallback);
entry.set(gimli::DW_AT_name, AttributeValue::String(func_name.into()));
entry.set(
gimli::DW_AT_decl_file,
AttributeValue::FileIndex(Some(self.file_id)),
);
entry.set(gimli::DW_AT_decl_line, AttributeValue::Udata(self.line));
entry.set(gimli::DW_AT_decl_column, AttributeValue::Udata(self.column));
entry.set(gimli::DW_AT_external, AttributeValue::FlagPresent);
entry.set(gimli::DW_AT_low_pc, AttributeValue::Address(addr));
/// Adds `DW_TAG_formal_parameter` and `DW_TAG_variable` for all locals /// (which are both params and function-defined locals). /// /// This is pretty simple in that the expression for the location of /// these variables is constant, it's just "it's the local", and it spans /// the entire function. fn add_func_params_and_locals(
&mutself,
subprogram: UnitEntryId,
ty: u32,
locals: &[Local<'_>],
) { // Iterate through `self.types` which is what was encoded into the // module and find the function type which gives access to the // parameters which gives access to their types. let ty = self
.types
.iter()
.flat_map(|t| match t {
RecOrType::Type(t) => std::slice::from_ref(*t),
RecOrType::Rec(r) => &r.types,
})
.nth(ty as usize); let ty = match ty.map(|t| &t.def.kind) {
Some(InnerTypeKind::Func(ty)) => ty,
_ => return,
};
letmut local_idx = 0; for (_, _, ty) in ty.params.iter() { self.local(local_idx, subprogram, gimli::DW_TAG_formal_parameter, ty);
local_idx += 1;
}
for local in locals { self.local(local_idx, subprogram, gimli::DW_TAG_variable, &local.ty);
local_idx += 1;
}
}
/// Attempts to define a local variable within `subprogram` with the `ty` /// given. /// /// This does nothing if `ty` can't be represented in DWARF. fn local(&mutself, local: u32, subprogram: UnitEntryId, tag: gimli::DwTag, ty: &ValType<'_>) { let ty = matchself.val_type_to_dwarf_type(ty) {
Some(ty) => ty,
None => return,
};
let param = self.dwarf.unit.add(subprogram, tag); let entry = self.dwarf.unit.get_mut(param);
entry.set(
gimli::DW_AT_name,
AttributeValue::String(format!("local{local}").into()),
);
fn val_type_to_dwarf_type(&mutself, ty: &ValType<'_>) -> Option<UnitEntryId> { match ty {
ValType::I32 => Some(self.i32_ty()),
ValType::I64 => Some(self.i64_ty()),
ValType::F32 => Some(self.f32_ty()),
ValType::F64 => Some(self.f64_ty()), // TODO: make a C union of sorts or something like that to // represent v128 as an array-of-lanes or u128 or something like // that.
ValType::V128 => None, // Not much that can be done about reference types without actually // knowing what the engine does, this probably needs an addition to // DWARF itself to represent this.
ValType::Ref(_) => None,
}
}
/// Emit an instruction which starts at `offset` and is defined at `span`. /// /// Note that `offset` is code-section-relative. pubfn instr(&mutself, offset: usize, span: Span) { self.change_linecol(span); let offset = u64::try_from(offset).unwrap();
/// Change `self.line` and `self.column` to be appropriate for the offset /// in `span`. /// /// This will incrementally move from `self.last_offset` to `span.offset()` /// and update line/column information as we go. It's assumed that this is /// more efficient than a precalculate-all-the-positions-for-each-byte /// approach since that would require a great deal of memory to store a /// line/col for all bytes in the input string. It's also assumed that most /// `span` adjustments are minor as it's between instructions in a function /// which are frequently close together. Whether or not this assumption /// pans out is yet to be seen. fn change_linecol(&mutself, span: Span) { let offset = span.offset();
/// Completes emission of the latest function. /// /// The latest function took `func_size` bytes to encode and the current end /// of the code section, after the function was appended, is /// `code_section_end`. pubfn end_func(&mutself, func_size: usize, code_section_end: usize) { // Add a final row corresponding to the final `end` instruction in the // function to ensure there's something for all bytecodes. let row = self.dwarf.unit.line_program.row();
row.address_offset = (func_size - 1) as u64; self.dwarf.unit.line_program.generate_row();
// This function's symbol is relative to the start of the function // itself. Functions are encoded as a leb-size-of-function then the // function itself, so to account for the size of the // leb-size-of-function we calculate the function start as the current // end of the code section minus the size of the function's bytes. self.sym_offsets.push(code_section_end - func_size);
// The line program is relative to the start address, so only the // function's size is used here. self.dwarf
.unit
.line_program
.end_sequence(u64::try_from(func_size).unwrap());
// The high PC value here is relative to `DW_AT_low_pc`, so it's the // size of the function. let entry = self.dwarf.unit.get_mut(self.cur_subprogram.take().unwrap());
entry.set(
gimli::DW_AT_high_pc,
AttributeValue::Data4(func_size as u32),
);
}
pubfn set_code_section_size(&mutself, size: usize) { let root = self.dwarf.unit.root(); let entry = self.dwarf.unit.get_mut(root);
entry.set(gimli::DW_AT_high_pc, AttributeValue::Data4(size as u32));
}
#[cfg(test)] mod tests { usesuper::{Dwarf, EncodeOptions, GenerateDwarf}; usecrate::token::Span; use rand::rngs::SmallRng; use rand::{Rng, SeedableRng};
// Print some debugging information in case someone's debugging this // test letmut offset = 0; for (i, line) in contents.lines().enumerate() {
println!( "line {:2} is at {:2} .. {:2}",
i + 1,
offset,
offset + line.len()
);
offset += line.len() + 1;
}
println!("");
// Precalculate (line, col) for all characters, assumed to all be one // byte here. letmut precalculated_linecols = Vec::new(); letmut line = 1; letmut col = 1; for c in contents.chars() { if c == '\n' {
line += 1;
col = 0;
}
precalculated_linecols.push((line, col));
col += 1;
}
// Traverse randomly throughout this string and assert that the // incremental update matches the precalculated position. letmut rand = SmallRng::seed_from_u64(102); for _ in0..1000 { let pos = rand.gen_range(0..contents.len());
dwarf.change_linecol(Span::from_offset(pos)); let (line, col) = precalculated_linecols[pos];
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.