use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use gimli::{EndianSlice, LittleEndian, SectionId};
use object::read::elf::ElfFile32;
use object::{Object, ObjectSection, ObjectSymbol};
fn synth() -> &'static str {
env!("CARGO_BIN_EXE_synth")
}
fn repro(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("scripts/repro")
.join(name)
}
fn compile(wasm: &Path, out: &str, debug_line: bool) -> Vec<u8> {
let mut args = vec![
"compile",
wasm.to_str().unwrap(),
"--target",
"cortex-m4",
"--all-exports",
"--relocatable",
"-o",
out,
];
if debug_line {
args.push("--debug-line");
}
let r = Command::new(synth())
.args(&args)
.output()
.expect("run synth");
assert!(
r.status.success(),
"compile failed (debug_line={debug_line}): {}",
String::from_utf8_lossy(&r.stderr)
);
std::fs::read(out).expect("read .o")
}
fn section_data(elf: &[u8]) -> HashMap<String, Vec<u8>> {
let obj = ElfFile32::<object::Endianness>::parse(elf).expect("parse ELF");
let mut out = HashMap::new();
for sec in obj.sections() {
if let Ok(name) = sec.name()
&& !name.is_empty()
{
out.insert(name.to_string(), sec.data().unwrap_or(&[]).to_vec());
}
}
out
}
#[test]
fn debug_line_is_additive_on_dwarf_input_394() {
let wasm = repro("msgq_put_359.wasm");
let plain = compile(&wasm, "/tmp/dbg394_msgq_plain.o", false);
let dbg = compile(&wasm, "/tmp/dbg394_msgq_dbg.o", true);
let ps = section_data(&plain);
let ds = section_data(&dbg);
for name in [".text", ".data", ".bss"] {
match (ps.get(name), ds.get(name)) {
(Some(a), Some(b)) => assert_eq!(
a, b,
"section {name} differs between plain and --debug-line builds"
),
(None, None) => { }
(a, b) => panic!(
"section {name} presence mismatch: plain={} dbg={}",
a.is_some(),
b.is_some()
),
}
}
for name in [".debug_info", ".debug_abbrev", ".debug_str", ".debug_line"] {
assert!(!ps.contains_key(name), "plain build must NOT carry {name}");
assert!(
ds.get(name).is_some_and(|d| !d.is_empty()),
"--debug-line build must carry a non-empty {name}"
);
}
}
#[test]
fn debug_line_is_noop_on_nodwarf_input_394() {
let wasm = repro("gust_kernel.wasm");
let plain = compile(&wasm, "/tmp/dbg394_gust_plain.o", false);
let dbg = compile(&wasm, "/tmp/dbg394_gust_dbg.o", true);
assert_eq!(
plain, dbg,
"no-DWARF input: --debug-line must produce a byte-identical object"
);
}
#[test]
fn emitted_debug_line_resolves_arm_addr_to_source_394() {
let wasm = repro("msgq_put_359.wasm");
let dbg = compile(&wasm, "/tmp/dbg394_msgq_oracleb.o", true);
let obj = ElfFile32::<object::Endianness>::parse(&*dbg).expect("parse ELF");
let text_len = obj.section_by_name(".text").expect(".text present").size();
assert!(text_len > 0, ".text must be non-empty");
let secs = section_data(&dbg);
let empty: &[u8] = &[];
let load = |id: SectionId| -> Result<EndianSlice<'_, LittleEndian>, gimli::Error> {
let data = secs.get(id.name()).map_or(empty, |v| v.as_slice());
Ok(EndianSlice::new(data, LittleEndian))
};
let dwarf = gimli::Dwarf::load(load).expect("load emitted .debug_* sections");
let mut rows: Vec<(u64, u64)> = Vec::new();
let mut unit_count = 0usize;
let mut units = dwarf.units();
while let Some(header) = units.next().expect("unit header") {
unit_count += 1;
let unit = dwarf.unit(header).expect("unit");
let Some(program) = unit.line_program.clone() else {
continue; };
let mut state = program.rows();
while let Some((_, row)) = state.next_row().expect("row") {
if row.end_sequence() {
continue;
}
if let Some(line) = row.line() {
rows.push((row.address(), line.get()));
}
}
}
assert!(
unit_count > 0,
"emitted DWARF has NO compilation unit — `.debug_info` is missing or empty, \
so a debugger cannot reach `.debug_line` via DW_AT_stmt_list"
);
assert!(
!rows.is_empty(),
"the normal `dwarf.units()` → line-program walk decoded ZERO rows from \
{unit_count} unit(s); the CU's DW_AT_stmt_list does not reach the line table"
);
let good = rows
.iter()
.filter(|&&(addr, line)| line > 0 && addr < text_len)
.count();
assert!(
good > 0,
"expected ≥1 .debug_line row with addr in .text (<0x{text_len:x}) and \
non-zero line; got {} rows, sample: {:?}",
rows.len(),
rows.iter().take(5).collect::<Vec<_>>()
);
eprintln!(
"[dbg394-oracleB] {unit_count} CU(s) via dwarf.units(); {} line rows, {good} \
map an in-range .text addr to a non-zero source line. sample:",
rows.len()
);
for (addr, line) in rows.iter().filter(|&&(a, l)| l > 0 && a < text_len).take(5) {
eprintln!(" .text+0x{addr:04x} -> line {line}");
}
}
const R_ARM_ABS32: u32 = 2;
fn line_rows(secs: &HashMap<String, Vec<u8>>) -> Vec<(u64, u64)> {
let empty: &[u8] = &[];
let load = |id: SectionId| -> Result<EndianSlice<'_, LittleEndian>, gimli::Error> {
let data = secs.get(id.name()).map_or(empty, |v| v.as_slice());
Ok(EndianSlice::new(data, LittleEndian))
};
let dwarf = gimli::Dwarf::load(load).expect("load .debug_* sections");
let mut rows = Vec::new();
let mut units = dwarf.units();
while let Some(header) = units.next().expect("unit header") {
let unit = dwarf.unit(header).expect("unit");
let Some(program) = unit.line_program.clone() else {
continue;
};
let mut state = program.rows();
while let Some((_, row)) = state.next_row().expect("row") {
if row.end_sequence() {
continue;
}
if let Some(line) = row.line() {
rows.push((row.address(), line.get()));
}
}
}
rows
}
#[test]
fn rel_debug_relocations_shift_addresses_to_correct_line_394() {
const LINK_BASE: u64 = 0x0800_0000;
let wasm = repro("msgq_put_359.wasm");
let dbg = compile(&wasm, "/tmp/dbg394_msgq_oraclec.o", true);
let obj = ElfFile32::<object::Endianness>::parse(&*dbg).expect("parse ELF");
let check_one_text_reloc = |sec_name: &str| -> u64 {
let sec = obj
.section_by_name(sec_name)
.expect("debug section present");
let relocs: Vec<_> = sec.relocations().collect();
assert_eq!(
relocs.len(),
1,
"{sec_name} must carry exactly one `.text` relocation (the single \
relocatable address anchor); got {}",
relocs.len()
);
let (offset, reloc) = &relocs[0];
let offset = *offset;
match reloc.flags() {
object::RelocationFlags::Elf { r_type } => assert_eq!(
r_type, R_ARM_ABS32,
"{sec_name} reloc must be R_ARM_ABS32, got r_type={r_type}"
),
other => panic!("{sec_name} reloc has non-ELF flags: {other:?}"),
}
let object::RelocationTarget::Symbol(sym_idx) = reloc.target() else {
panic!("{sec_name} reloc target is not a symbol");
};
let sym = obj.symbol_by_index(sym_idx).expect("reloc symbol");
assert_eq!(
sym.name().expect("sym name"),
"__synth_text_base",
"{sec_name} reloc must resolve against the .text base symbol"
);
let data = sec.data().expect("section data");
let off = offset as usize;
let in_place = u32::from_le_bytes(data[off..off + 4].try_into().unwrap());
assert_eq!(
in_place, 0,
"{sec_name} REL addend must be 0 in-place (S + A, A=0)"
);
offset
};
let line_reloc_off = check_one_text_reloc(".debug_line") as usize;
let _ = check_one_text_reloc(".debug_info");
let secs = section_data(&dbg);
let before = line_rows(&secs);
assert!(!before.is_empty(), "expected line rows object-relative");
let mut relocated = secs.clone();
{
let dl = relocated.get_mut(".debug_line").expect(".debug_line");
let patched = (LINK_BASE as u32).to_le_bytes();
dl[line_reloc_off..line_reloc_off + 4].copy_from_slice(&patched);
}
let after = line_rows(&relocated);
assert_eq!(
before.len(),
after.len(),
"relocation changed the ROW COUNT — it must only shift addresses"
);
for ((a0, l0), (a1, l1)) in before.iter().zip(after.iter()) {
assert_eq!(
*l1, *l0,
"relocation changed a source LINE ({l0} -> {l1}); it must preserve the mapping"
);
assert_eq!(
*a1,
a0 + LINK_BASE,
"row at object-relative .text+0x{a0:x} did not shift to the linked \
base 0x{LINK_BASE:x}+0x{a0:x} (got 0x{a1:x}); the `.rel.debug_line` \
record is wrong"
);
}
eprintln!(
"[dbg394-oracleC] applied .rel.debug_line at base 0x{LINK_BASE:x}: {} rows \
shifted by exactly the base, all source lines preserved.",
after.len()
);
}