#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LineRow {
pub addr: u32,
pub line: u32,
pub file: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceLoc {
pub line: u32,
pub file: u32,
}
pub fn op_offsets_to_source(
op_offsets: &[u32],
code_base: u32,
rows: &[LineRow],
) -> Vec<Option<SourceLoc>> {
let mut sorted: Vec<LineRow> = rows.to_vec();
sorted.sort_by_key(|r| r.addr);
op_offsets
.iter()
.map(|&off| {
let a = off.checked_sub(code_base)?;
sorted
.iter()
.rev()
.find(|r| r.addr <= a)
.map(|r| SourceLoc {
line: r.line,
file: r.file,
})
})
.collect()
}
use std::collections::HashMap;
use gimli::{Dwarf, EndianSlice, LittleEndian, SectionId};
use wasmparser::{Parser, Payload};
#[derive(Debug, Default, Clone)]
pub struct InputDwarfLine {
pub rows: Vec<LineRow>,
pub code_base: u32,
pub files: Vec<(String, String)>,
}
pub fn read_input_dwarf_line(wasm: &[u8]) -> InputDwarfLine {
let mut sections: HashMap<String, Vec<u8>> = HashMap::new();
let mut code_base = 0u32;
for payload in Parser::new(0).parse_all(wasm) {
match payload {
Ok(Payload::CustomSection(c)) if c.name().starts_with(".debug_") => {
sections.insert(c.name().to_string(), c.data().to_vec());
}
Ok(Payload::CodeSectionStart { range, .. }) => {
code_base = range.start as u32;
}
_ => {}
}
}
if !sections.contains_key(".debug_line") {
return InputDwarfLine {
rows: Vec::new(),
code_base,
files: Vec::new(),
};
}
let (rows, files) = parse_debug_line_rows(§ions).unwrap_or_default();
InputDwarfLine {
rows,
code_base,
files,
}
}
type LineTable = (Vec<LineRow>, Vec<(String, String)>);
fn parse_debug_line_rows(sections: &HashMap<String, Vec<u8>>) -> Result<LineTable, gimli::Error> {
let empty: &[u8] = &[];
let load = |id: SectionId| -> Result<EndianSlice<'_, LittleEndian>, gimli::Error> {
let data = sections.get(id.name()).map_or(empty, |v| v.as_slice());
Ok(EndianSlice::new(data, LittleEndian))
};
let dwarf = Dwarf::load(load)?;
let mut rows = Vec::new();
let mut files: Vec<(String, String)> = Vec::new();
let mut units = dwarf.units();
while let Some(header) = units.next()? {
let unit = dwarf.unit(header)?;
let Some(program) = unit.line_program.clone() else {
continue;
};
let line_header = program.header().clone();
let mut state = program.rows();
while let Some((_, row)) = state.next_row()? {
if row.end_sequence() {
continue;
}
let file = resolve_file(&dwarf, &unit, &line_header, row.file_index())
.map(|entry| intern_file(&mut files, entry))
.unwrap_or(0);
rows.push(LineRow {
addr: row.address() as u32,
line: row.line().map(|l| l.get() as u32).unwrap_or(0),
file,
});
}
}
Ok((rows, files))
}
fn resolve_file(
dwarf: &Dwarf<EndianSlice<'_, LittleEndian>>,
unit: &gimli::Unit<EndianSlice<'_, LittleEndian>>,
header: &gimli::LineProgramHeader<EndianSlice<'_, LittleEndian>>,
file_index: u64,
) -> Option<(String, String)> {
let file = header.file(file_index)?;
let name = dwarf
.attr_string(unit, file.path_name())
.ok()?
.to_string_lossy()
.into_owned();
let dir = match header.directory(file.directory_index()) {
Some(av) => dwarf
.attr_string(unit, av)
.ok()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default(),
None => String::new(),
};
Some((dir, name))
}
fn intern_file(files: &mut Vec<(String, String)>, entry: (String, String)) -> u32 {
if let Some(i) = files.iter().position(|f| *f == entry) {
return i as u32;
}
files.push(entry);
(files.len() - 1) as u32
}
#[derive(Debug, Clone)]
pub struct SubprogramInfo {
pub name: String,
pub low_pc: u64,
pub high_pc: u64,
}
pub fn emit_debug_sections(
table: &[(u64, u32, u32)],
text_sym: usize,
files: &[(String, String)],
subprograms: &[SubprogramInfo],
) -> Vec<EmittedDwarfSection> {
use gimli::write::{Address, AttributeValue, DwarfUnit, LineProgram, LineString, Sections};
if table.is_empty() {
return Vec::new();
}
let encoding = gimli::Encoding {
format: gimli::Format::Dwarf32,
version: 4,
address_size: 4,
};
let mut dwarf = DwarfUnit::new(encoding);
let high_pc = table.iter().map(|&(a, _, _)| a).max().unwrap_or(0) + 1;
let (primary_dir, primary_name) = files
.first()
.map(|(d, n)| (d.clone().into_bytes(), n.clone().into_bytes()))
.unwrap_or_else(|| (b"/synth".to_vec(), b"synth.wasm".to_vec()));
let mut program = LineProgram::new(
encoding,
gimli::LineEncoding::default(),
LineString::String(primary_dir),
None,
LineString::String(primary_name.clone()),
None,
);
let mut file_ids = Vec::with_capacity(files.len().max(1));
if files.is_empty() {
let dir = program.default_directory();
file_ids.push(program.add_file(LineString::String(b"synth.wasm".to_vec()), dir, None));
} else {
for (dir, name) in files {
let dir_id = program.add_directory(LineString::String(dir.clone().into_bytes()));
file_ids.push(program.add_file(
LineString::String(name.clone().into_bytes()),
dir_id,
None,
));
}
}
let text_base = Address::Symbol {
symbol: text_sym,
addend: 0,
};
program.begin_sequence(Some(text_base));
for &(addr, line, file) in table {
let row = program.row();
row.address_offset = addr;
row.file = *file_ids.get(file as usize).unwrap_or(&file_ids[0]);
row.line = line as u64;
program.generate_row();
}
program.end_sequence(high_pc);
dwarf.unit.line_program = program;
{
let cu_name = files
.first()
.map(|(_, n)| n.clone())
.unwrap_or_else(|| "synth.wasm".to_string());
let name_id = dwarf.strings.add(cu_name);
let root = dwarf.unit.root();
let root_die = dwarf.unit.get_mut(root);
root_die.set(gimli::DW_AT_name, AttributeValue::StringRef(name_id));
root_die.set(gimli::DW_AT_low_pc, AttributeValue::Address(text_base));
root_die.set(gimli::DW_AT_high_pc, AttributeValue::Udata(high_pc));
}
{
let root = dwarf.unit.root();
for sp in subprograms {
let name_id = dwarf.strings.add(sp.name.clone());
let die_id = dwarf.unit.add(root, gimli::DW_TAG_subprogram);
let die = dwarf.unit.get_mut(die_id);
die.set(gimli::DW_AT_name, AttributeValue::StringRef(name_id));
die.set(
gimli::DW_AT_low_pc,
AttributeValue::Address(Address::Symbol {
symbol: text_sym,
addend: sp.low_pc as i64,
}),
);
die.set(
gimli::DW_AT_high_pc,
AttributeValue::Udata(sp.high_pc.saturating_sub(sp.low_pc)),
);
}
}
let seed = RelocWriter {
inner: gimli::write::EndianVec::new(LittleEndian),
relocs: Vec::new(),
};
let mut sections = Sections::new(seed);
if dwarf.write(&mut sections).is_err() {
return Vec::new();
}
let mut out: Vec<EmittedDwarfSection> = Vec::new();
let _ = sections.for_each(|id, w: &RelocWriter| -> Result<(), ()> {
let bytes = w.inner.slice();
if !bytes.is_empty()
&& let Some(name) = section_name(id)
{
let text_relocs = w
.relocs
.iter()
.map(|&(offset, _addend, size)| DwarfTextReloc {
offset: offset as u32,
size,
})
.collect();
out.push(EmittedDwarfSection {
name,
bytes: bytes.to_vec(),
text_relocs,
});
}
Ok(())
});
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DwarfTextReloc {
pub offset: u32,
pub size: u8,
}
#[derive(Debug, Clone)]
pub struct EmittedDwarfSection {
pub name: &'static str,
pub bytes: Vec<u8>,
pub text_relocs: Vec<DwarfTextReloc>,
}
#[derive(Clone)]
struct RelocWriter {
inner: gimli::write::EndianVec<LittleEndian>,
relocs: Vec<(usize, i64, u8)>,
}
impl gimli::write::Writer for RelocWriter {
type Endian = LittleEndian;
fn endian(&self) -> Self::Endian {
self.inner.endian()
}
fn len(&self) -> usize {
self.inner.len()
}
fn write(&mut self, bytes: &[u8]) -> gimli::write::Result<()> {
self.inner.write(bytes)
}
fn write_at(&mut self, offset: usize, bytes: &[u8]) -> gimli::write::Result<()> {
self.inner.write_at(offset, bytes)
}
fn write_address(
&mut self,
address: gimli::write::Address,
size: u8,
) -> gimli::write::Result<()> {
use gimli::write::Address;
match address {
Address::Constant(val) => self.inner.write_udata(val, size),
Address::Symbol { symbol: _, addend } => {
let offset = self.inner.len();
self.relocs.push((offset, addend, size));
self.inner.write_udata(addend as u64, size)
}
}
}
}
fn section_name(id: SectionId) -> Option<&'static str> {
Some(match id {
SectionId::DebugInfo => ".debug_info",
SectionId::DebugAbbrev => ".debug_abbrev",
SectionId::DebugStr => ".debug_str",
SectionId::DebugLine => ".debug_line",
SectionId::DebugLineStr => ".debug_line_str",
SectionId::DebugRanges => ".debug_ranges",
SectionId::DebugRngLists => ".debug_rnglists",
SectionId::DebugStrOffsets => ".debug_str_offsets",
SectionId::DebugAddr => ".debug_addr",
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn covering_row_lookup() {
let rows = [
LineRow {
addr: 0,
line: 10,
file: 1,
},
LineRow {
addr: 8,
line: 11,
file: 1,
},
LineRow {
addr: 20,
line: 12,
file: 1,
},
];
let got = op_offsets_to_source(&[100, 104, 108, 130], 100, &rows);
assert_eq!(got[0].map(|s| s.line), Some(10)); assert_eq!(got[1].map(|s| s.line), Some(10)); assert_eq!(got[2].map(|s| s.line), Some(11)); assert_eq!(got[3].map(|s| s.line), Some(12)); }
#[test]
fn op_before_first_row_is_none() {
let rows = [LineRow {
addr: 8,
line: 11,
file: 1,
}];
let got = op_offsets_to_source(&[100], 100, &rows);
assert_eq!(got[0], None);
}
#[test]
fn op_before_code_base_is_none() {
let rows = [LineRow {
addr: 0,
line: 1,
file: 1,
}];
let got = op_offsets_to_source(&[50], 100, &rows);
assert_eq!(got[0], None);
}
}