use crate::error::Result;
use crate::fas::{
DumpRowId, EmittedSpan, FasFile, MacroFrame, PrepLine, ProvenanceDiagnostic, SymbolId,
};
use std::collections::{BTreeMap, BTreeSet};
use std::fs::File;
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Confidence {
Exact,
Strong,
Heuristic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolKind {
Constant,
Label,
Code,
Data,
External,
Marker,
Anonymous,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceExtent {
Emitted(u32),
ZeroByte,
Virtual,
NotInOutput,
Ambiguous,
}
#[derive(Debug, Clone)]
pub struct DebugSymbol {
pub id: SymbolId,
pub original_name: Option<String>,
pub alias: Option<String>,
pub value: u64,
pub output_offset: Option<u32>,
pub flags: u16,
pub size: u8,
pub value_type: u8,
pub extended_sib: u32,
pub relocation: u32,
pub section_index: Option<u32>,
pub external_name: Option<String>,
pub definition_line: u32,
pub defined_pass: u16,
pub used_pass: u16,
pub kind: SymbolKind,
pub confidence: Confidence,
pub debugger_visible: bool,
}
#[derive(Debug, Clone)]
pub struct SourceLocation {
pub row: DumpRowId,
pub address: u64,
pub output_offset: u32,
pub extent: SourceExtent,
pub file: String,
pub line: u32,
pub text: Option<String>,
pub preprocessed_text: Option<String>,
pub macro_frames: Vec<MacroFrame>,
pub provenance_diagnostic: Option<ProvenanceDiagnostic>,
pub code_type: u8,
pub address_type: u8,
pub relocation: u32,
pub primary: bool,
}
#[derive(Debug, Clone)]
pub struct DebugReference {
pub id: usize,
pub symbol: SymbolId,
pub row: DumpRowId,
pub address: u64,
pub symbol_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Diagnostic {
SymbolAlias {
symbol: SymbolId,
original: String,
alias: String,
},
MissingSourceLine {
row: DumpRowId,
offset: u32,
},
Provenance {
row: DumpRowId,
problem: ProvenanceDiagnostic,
},
}
#[derive(Debug, Clone)]
pub struct Label {
pub name: String,
pub addr: u64,
pub size: u8,
pub is_code: bool,
}
#[derive(Debug, Clone)]
pub struct AddrLine {
pub addr: u64,
pub file: String,
pub line: u32,
pub text: Option<String>,
pub macro_name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DebugInfo {
pub fas_path: PathBuf,
pub source_dir: PathBuf,
pub input_name: String,
pub output_name: String,
pub symbols: Vec<DebugSymbol>,
pub source_locations: Vec<SourceLocation>,
pub references: Vec<DebugReference>,
pub sections: Option<Vec<String>>,
pub diagnostics: Vec<Diagnostic>,
pub labels: Vec<Label>,
pub lines: Vec<AddrLine>,
pub original_baddr: u64,
}
impl DebugInfo {
pub fn from_parsed(fas_path: &Path, data: &[u8], fas: &FasFile) -> Result<Self> {
let source_dir = fas_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let input_name = fas.header.input_name(data)?.to_owned();
let output_name = fas.header.output_name(data)?.to_owned();
let original_baddr = fas.dump.inferred_baddr().unwrap_or(0);
let mut diagnostics = Vec::new();
let symbols = collect_symbols(fas, data, &mut diagnostics)?;
let source_locations =
collect_source_locations(fas, data, &source_dir, &input_name, &mut diagnostics)?;
let references = collect_references(fas, &symbols);
let sections = fas.sections.as_ref().map(|sections| {
sections
.iter()
.map(|section| section.name.clone())
.collect()
});
let labels = symbols
.iter()
.filter(|symbol| symbol.debugger_visible)
.filter_map(|symbol| {
Some(Label {
name: symbol.alias.clone()?,
addr: symbol.value,
size: symbol.size,
is_code: symbol.kind == SymbolKind::Code,
})
})
.collect();
let lines = source_locations
.iter()
.filter(|location| location.primary)
.map(|location| AddrLine {
addr: location.address,
file: location.file.clone(),
line: location.line,
text: location.text.clone(),
macro_name: location
.macro_frames
.first()
.and_then(|frame| frame.macro_name.clone()),
})
.collect();
Ok(Self {
fas_path: fas_path.to_path_buf(),
source_dir,
input_name,
output_name,
symbols,
source_locations,
references,
sections,
diagnostics,
labels,
lines,
original_baddr,
})
}
pub fn from_path(path: &Path) -> Result<Self> {
let (bytes, fas) = FasFile::from_path(path)?;
Self::from_parsed(path, &bytes, &fas)
}
pub fn rebased(&self, current_baddr: u64) -> Self {
let delta = current_baddr.wrapping_sub(self.original_baddr);
if delta == 0 {
return self.clone();
}
let mut output = self.clone();
for symbol in &mut output.symbols {
if symbol.debugger_visible {
symbol.value = symbol.value.wrapping_add(delta);
}
}
for location in &mut output.source_locations {
location.address = location.address.wrapping_add(delta);
}
for reference in &mut output.references {
reference.address = reference.address.wrapping_add(delta);
}
for label in &mut output.labels {
label.addr = label.addr.wrapping_add(delta);
}
for line in &mut output.lines {
line.addr = line.addr.wrapping_add(delta);
}
output
}
}
fn collect_symbols(
fas: &FasFile,
data: &[u8],
diagnostics: &mut Vec<Diagnostic>,
) -> Result<Vec<DebugSymbol>> {
let emitted_by_address = fas
.dump
.rows
.iter()
.filter(|row| fas.dump.emitted(row))
.fold(BTreeMap::<u64, u32>::new(), |mut rows, row| {
rows.entry(row.address()).or_insert(row.file_offset);
rows
});
let mut used_aliases = BTreeSet::new();
let mut output = Vec::with_capacity(fas.symbols.len());
for symbol in &fas.symbols {
let original_name = symbol.name(&fas.header, data)?.map(str::to_owned);
let external_name = symbol.external_name(&fas.header, data)?.map(str::to_owned);
let output_offset = emitted_by_address.get(&symbol.value).copied();
let (kind, confidence) = classify_symbol(symbol, output_offset.is_some());
let debugger_visible = symbol.is_defined()
&& !symbol.is_variable()
&& !symbol.is_marker()
&& !symbol.is_external()
&& original_name
.as_deref()
.is_some_and(|name| !name.is_empty() && !name.contains('?'))
&& (symbol.val_type != 0 || output_offset.is_some());
let alias = if debugger_visible {
let original = original_name.as_deref().unwrap_or_default();
let alias = unique_alias(original, symbol.id, &mut used_aliases);
if alias != original {
diagnostics.push(Diagnostic::SymbolAlias {
symbol: SymbolId(symbol.id),
original: original.to_owned(),
alias: alias.clone(),
});
}
Some(alias)
} else {
None
};
output.push(DebugSymbol {
id: SymbolId(symbol.id),
original_name,
alias,
value: symbol.value,
output_offset,
flags: symbol.flags,
size: symbol.size,
value_type: symbol.val_type,
extended_sib: symbol.extended_sib,
relocation: symbol.reloc,
section_index: symbol.section_index(),
external_name,
definition_line: symbol.def_line_off,
defined_pass: symbol.defined_pass,
used_pass: symbol.used_pass,
kind,
confidence,
debugger_visible,
});
}
output.sort_by_key(|symbol| symbol.id);
Ok(output)
}
fn classify_symbol(symbol: &crate::fas::Symbol, emitted: bool) -> (SymbolKind, Confidence) {
if symbol.is_anonymous() {
(SymbolKind::Anonymous, Confidence::Exact)
} else if symbol.is_marker() {
(SymbolKind::Marker, Confidence::Exact)
} else if symbol.is_external() {
(SymbolKind::External, Confidence::Exact)
} else if symbol.is_variable() || (symbol.val_type == 0 && !emitted) {
(SymbolKind::Constant, Confidence::Strong)
} else if symbol.size > 0 {
(SymbolKind::Data, Confidence::Exact)
} else if emitted {
(SymbolKind::Code, Confidence::Heuristic)
} else {
(SymbolKind::Label, Confidence::Strong)
}
}
fn unique_alias(original: &str, id: usize, used: &mut BTreeSet<String>) -> String {
let mut base = sanitize_alias(original);
if base.is_empty() {
base = format!("fas_symbol_{id}");
}
if used.insert(base.clone()) {
return base;
}
let mut candidate = format!("{base}__fas_{id}");
let mut suffix = 1usize;
while !used.insert(candidate.clone()) {
candidate = format!("{base}__fas_{id}_{suffix}");
suffix += 1;
}
candidate
}
fn sanitize_alias(name: &str) -> String {
let mut alias = name
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() || matches!(character, '_' | '.' | ':') {
character
} else {
'_'
}
})
.collect::<String>();
if alias
.chars()
.next()
.is_some_and(|character| character.is_ascii_digit())
{
alias.insert(0, '_');
}
alias
}
fn collect_source_locations(
fas: &FasFile,
data: &[u8],
source_dir: &Path,
input_name: &str,
diagnostics: &mut Vec<Diagnostic>,
) -> Result<Vec<SourceLocation>> {
let prep_bytes = fas.header.preprocessed(data)?;
let mut locations = Vec::with_capacity(fas.dump.rows.len());
for row in &fas.dump.rows {
let fallback;
let raw_line = if let Some(line) = fas.prep.get(row.line_off) {
line
} else if let Ok((line, _)) = PrepLine::parse_at(prep_bytes, row.line_off) {
fallback = line;
&fallback
} else {
diagnostics.push(Diagnostic::MissingSourceLine {
row: DumpRowId(row.id),
offset: row.line_off,
});
continue;
};
let provenance = fas.prep.provenance(raw_line, prep_bytes);
if let Some(problem) = provenance.diagnostic.clone() {
diagnostics.push(Diagnostic::Provenance {
row: DumpRowId(row.id),
problem,
});
}
let origin = fas.prep.get(provenance.origin_offset).unwrap_or(raw_line);
let file = fas
.prep
.file_name(origin, prep_bytes, input_name)
.unwrap_or(input_name)
.to_owned();
let preprocessed_text = raw_line
.detokenize(prep_bytes)
.ok()
.filter(|text| !text.is_empty());
let fallback_text = origin
.detokenize(prep_bytes)
.ok()
.filter(|text| !text.is_empty());
let text = read_source_text(
&source_dir.join(&file),
origin.source_pos_or_invoke,
origin.line_number,
)
.or(fallback_text);
let extent = if row.is_virtual() {
SourceExtent::Virtual
} else if row.not_in_output() {
SourceExtent::NotInOutput
} else {
match fas.dump.span(row) {
EmittedSpan::Bytes(length) if length > 0 => SourceExtent::Emitted(length),
EmittedSpan::Bytes(_) | EmittedSpan::None => SourceExtent::ZeroByte,
EmittedSpan::Ambiguous => SourceExtent::Ambiguous,
}
};
locations.push(SourceLocation {
row: DumpRowId(row.id),
address: row.address(),
output_offset: row.file_offset,
extent,
file,
line: origin.line_number,
text,
preprocessed_text,
macro_frames: provenance.frames,
provenance_diagnostic: provenance.diagnostic,
code_type: row.code_type,
address_type: row.addr_type,
relocation: row.reloc,
primary: false,
});
}
let mut primary_by_address = BTreeMap::<u64, usize>::new();
for (index, location) in locations.iter().enumerate() {
if matches!(location.extent, SourceExtent::Emitted(_)) && location.address != 0 {
primary_by_address.insert(location.address, index);
}
}
for index in primary_by_address.into_values() {
locations[index].primary = true;
}
Ok(locations)
}
fn collect_references(fas: &FasFile, symbols: &[DebugSymbol]) -> Vec<DebugReference> {
fas.references
.as_deref()
.unwrap_or_default()
.iter()
.filter_map(|reference| {
let symbol = symbols.get(reference.symbol.0)?;
let row = fas.dump.rows.get(reference.row.0)?;
Some(DebugReference {
id: reference.id,
symbol: reference.symbol,
row: reference.row,
address: row.address(),
symbol_name: symbol.original_name.clone(),
})
})
.collect()
}
fn read_source_text(path: &Path, byte_offset: u32, line_number: u32) -> Option<String> {
if !path.is_file() {
return None;
}
read_at_offset(path, byte_offset).or_else(|| read_line_number(path, line_number))
}
fn read_at_offset(path: &Path, byte_offset: u32) -> Option<String> {
let mut file = File::open(path).ok()?;
file.seek(SeekFrom::Start(u64::from(byte_offset))).ok()?;
let mut bytes = Vec::new();
let mut byte = [0u8; 1];
while file.read(&mut byte).ok()? == 1 {
if matches!(byte[0], b'\n' | b'\r') {
break;
}
bytes.push(byte[0]);
if bytes.len() > 512 {
break;
}
}
let text = String::from_utf8_lossy(&bytes).trim().to_owned();
(!text.is_empty()).then_some(text)
}
fn read_line_number(path: &Path, line_number: u32) -> Option<String> {
if line_number == 0 {
return None;
}
let file = File::open(path).ok()?;
let wanted = line_number as usize;
for (index, line) in BufReader::new(file).lines().enumerate() {
if index + 1 == wanted {
let text = line.ok()?.trim().to_owned();
return (!text.is_empty()).then_some(text);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture(relative: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(relative)
}
fn all_fas() -> Vec<PathBuf> {
let mut files = Vec::new();
visit(
&PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures"),
&mut files,
);
files.sort();
files
}
fn visit(directory: &Path, files: &mut Vec<PathBuf>) {
for entry in std::fs::read_dir(directory).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
visit(&path, files);
} else if path.extension().and_then(|extension| extension.to_str()) == Some("fas") {
files.push(path);
}
}
}
#[test]
fn every_fixture_builds_complete_debug_info() {
let files = all_fas();
assert!(files.len() >= 8, "missing FASM example dumps: {files:?}");
for path in files {
let info = DebugInfo::from_path(&path)
.unwrap_or_else(|error| panic!("{}: {error}", path.display()));
assert!(!info.input_name.is_empty(), "{}", path.display());
assert!(!info.symbols.is_empty(), "{}", path.display());
assert!(!info.source_locations.is_empty(), "{}", path.display());
assert!(!info.labels.is_empty(), "{}", path.display());
assert!(!info.lines.is_empty(), "{}", path.display());
}
}
#[test]
fn hello_preserves_symbols_references_and_emitted_lines() {
let info = DebugInfo::from_path(&fixture("elfexe/hello.fas")).unwrap();
assert_eq!(info.output_name, "hello");
assert!(
info.labels
.iter()
.any(|label| label.name == "start" && label.is_code)
);
assert!(
info.labels
.iter()
.any(|label| label.name == "msg" && !label.is_code)
);
assert!(!info.labels.iter().any(|label| label.name == "msg_size"));
assert!(
info.symbols
.iter()
.any(|symbol| symbol.original_name.as_deref() == Some("msg_size"))
);
assert!(!info.references.is_empty());
let first_reference = &info.references[0];
assert!(first_reference.symbol.0 < info.symbols.len());
assert!(
info.source_locations
.iter()
.any(|location| location.row == first_reference.row
&& location.address == first_reference.address)
);
assert!(first_reference.symbol_name.is_some());
assert!(info.lines.iter().all(|line| {
info.source_locations
.iter()
.any(|location| location.primary && location.address == line.addr)
}));
assert!(
info.lines
.iter()
.any(|line| line.file.contains("hello.asm"))
);
assert!(info.lines.iter().any(|line| {
line.text
.as_deref()
.is_some_and(|text| text.contains("int"))
}));
assert_eq!(info.original_baddr, 0x0804_8000);
}
#[test]
fn object_sections_and_external_symbols_are_retained() {
let info = DebugInfo::from_path(&fixture("elfobj/msgdemo.fas")).unwrap();
assert_eq!(
info.sections.as_deref(),
Some([".text".into(), ".data".into()].as_slice())
);
assert!(info.labels.iter().any(|label| label.name == "_start"));
assert!(
info.symbols
.iter()
.any(|symbol| symbol.original_name.as_deref() == Some("writemsg")
&& symbol.kind == SymbolKind::External)
);
}
#[test]
fn generated_names_are_preserved_but_not_applied() {
let info = DebugInfo::from_path(&fixture("gtk-button/button.fas")).unwrap();
assert!(info.symbols.iter().any(|symbol| {
symbol
.original_name
.as_deref()
.is_some_and(|name| name.contains('?'))
}));
assert!(!info.labels.iter().any(|label| label.name.contains('?')));
assert!(info.labels.iter().any(|label| label.name == "main"));
}
#[test]
fn rebasing_updates_every_address_view() {
let info = DebugInfo::from_path(&fixture("elfexe/hello64.fas")).unwrap();
let rebased = info.rebased(0x500000);
let delta = 0x100000;
assert_eq!(rebased.labels[0].addr, info.labels[0].addr + delta);
assert_eq!(rebased.lines[0].addr, info.lines[0].addr + delta);
assert_eq!(
rebased.source_locations[0].address,
info.source_locations[0].address + delta
);
assert_eq!(
rebased.references[0].address,
info.references[0].address + delta
);
assert_eq!(
rebased
.symbols
.iter()
.find(|symbol| symbol.debugger_visible)
.unwrap()
.value,
info.symbols
.iter()
.find(|symbol| symbol.debugger_visible)
.unwrap()
.value
+ delta
);
}
#[test]
fn aliases_are_deterministic_and_collision_free() {
let mut used = BTreeSet::new();
assert_eq!(unique_alias("one two", 1, &mut used), "one_two");
assert_eq!(unique_alias("one-two", 2, &mut used), "one_two__fas_2");
assert_eq!(unique_alias("123", 3, &mut used), "_123");
}
}