use std::collections::{BTreeSet, 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")
}
#[test]
fn debug_line_is_a_loud_noop_on_self_contained_build_394() {
let wasm = repro("gust_kernel.wasm");
let out = "/tmp/dbg394_selfcontained.elf";
let r = Command::new(synth())
.args([
"compile",
wasm.to_str().unwrap(),
"--target",
"cortex-m4",
"--all-exports",
"--debug-line",
"-o",
out,
])
.output()
.expect("run synth");
assert!(
r.status.success(),
"self-contained --debug-line must still compile: {}",
String::from_utf8_lossy(&r.stderr)
);
let mut output = String::from_utf8_lossy(&r.stdout).into_owned();
output.push_str(&String::from_utf8_lossy(&r.stderr));
assert!(
output.contains("--debug-line has no effect") && output.contains("relocatable"),
"expected a loud no-effect warning naming the relocatable path; output:\n{output}"
);
let elf = std::fs::read(out).expect("read elf");
let obj = ElfFile32::<object::Endianness>::parse(&*elf).expect("parse ELF");
assert_eq!(
obj.kind(),
object::ObjectKind::Executable,
"self-contained build must be an executable image (ET_EXEC), not ET_REL"
);
let debug_sections: Vec<String> = obj
.sections()
.filter_map(|s| s.name().ok().map(str::to_string))
.filter(|n| n.starts_with(".debug_"))
.collect();
assert!(
debug_sections.is_empty(),
"self-contained --debug-line must emit NO .debug_* sections (would be \
unrelocated, silently-wrong addresses); found {debug_sections:?}"
);
}
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 subs = collect_subprograms(§ion_data(&dbg));
assert!(
!subs.is_empty(),
".debug_info reloc check needs ≥1 subprogram DIE"
);
let sec = obj
.section_by_name(".debug_info")
.expect(".debug_info present");
let data = sec.data().expect(".debug_info data");
let relocs: Vec<_> = sec.relocations().collect();
assert_eq!(
relocs.len(),
1 + subs.len(),
".debug_info must carry 1 (CU low_pc) + {} (subprogram low_pc) `.text` \
relocations; got {}",
subs.len(),
relocs.len()
);
let mut got_addends: Vec<u64> = Vec::new();
for (offset, reloc) in &relocs {
match reloc.flags() {
object::RelocationFlags::Elf { r_type } => assert_eq!(
r_type, R_ARM_ABS32,
".debug_info reloc must be R_ARM_ABS32, got r_type={r_type}"
),
other => panic!(".debug_info reloc has non-ELF flags: {other:?}"),
}
let object::RelocationTarget::Symbol(sym_idx) = reloc.target() else {
panic!(".debug_info 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",
".debug_info reloc must resolve against the .text base symbol"
);
let off = *offset as usize;
got_addends.push(u32::from_le_bytes(data[off..off + 4].try_into().unwrap()) as u64);
}
got_addends.sort_unstable();
let mut expected: Vec<u64> = std::iter::once(0u64)
.chain(subs.iter().map(|s| s.low_pc))
.collect();
expected.sort_unstable();
assert_eq!(
got_addends, expected,
".debug_info in-place addends must be {{0 (CU)}} ∪ subprogram low_pcs; \
each subprogram low_pc must relocate by the base"
);
}
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()
);
}
fn basename(name: &str) -> String {
name.rsplit(['/', '\\']).next().unwrap_or(name).to_string()
}
fn wasm_debug_sections(wasm: &[u8]) -> HashMap<String, Vec<u8>> {
use wasmparser::{Parser, Payload};
let mut out = HashMap::new();
for payload in Parser::new(0).parse_all(wasm) {
if let Ok(Payload::CustomSection(c)) = payload
&& c.name().starts_with(".debug_")
{
out.insert(c.name().to_string(), c.data().to_vec());
}
}
out
}
fn row_referenced_basenames(secs: &HashMap<String, Vec<u8>>) -> BTreeSet<String> {
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 out = BTreeSet::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 h = program.header().clone();
let mut file_indices = Vec::new();
let mut state = program.rows();
while let Some((_, row)) = state.next_row().expect("row") {
if row.end_sequence() {
continue;
}
file_indices.push(row.file_index());
}
for fi in file_indices {
if let Some(file) = h.file(fi)
&& let Ok(name) = dwarf.attr_string(&unit, file.path_name())
{
out.insert(basename(&name.to_string_lossy()));
}
}
}
out
}
fn emitted_file_basenames(secs: &HashMap<String, Vec<u8>>) -> BTreeSet<String> {
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 out = BTreeSet::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 h = program.header();
for file in h.file_names() {
if let Ok(name) = dwarf.attr_string(&unit, file.path_name()) {
out.insert(basename(&name.to_string_lossy()));
}
}
}
out
}
#[test]
fn emitted_debug_line_carries_real_source_filenames_394() {
let wasm = repro("msgq_put_359.wasm");
let wasm_bytes = std::fs::read(&wasm).expect("read fixture wasm");
let expected = row_referenced_basenames(&wasm_debug_sections(&wasm_bytes));
assert!(
!expected.is_empty(),
"fixture must have `.debug_line` rows referencing ≥1 source file"
);
let dbg = compile(&wasm, "/tmp/dbg394_msgq_oraclee.o", true);
let emitted = emitted_file_basenames(§ion_data(&dbg));
assert!(
!emitted.contains("synth.wasm"),
"emitted line-program file table must NOT contain the fabricated \
`synth.wasm`; got {emitted:?}"
);
for want in &expected {
assert!(
emitted.contains(want),
"emitted file table missing real source file {want:?}; \
expected {expected:?}, got {emitted:?}"
);
}
eprintln!(
"[dbg394-oracleE] input rows reference {expected:?}; emitted file table {emitted:?} \
(real names propagated, synth.wasm gone)"
);
}
#[derive(Debug, Clone)]
struct SubprogramDie {
name: Option<String>,
low_pc: u64,
high_pc: u64,
}
fn collect_subprograms(secs: &HashMap<String, Vec<u8>>) -> Vec<SubprogramDie> {
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 out = Vec::new();
let mut units = dwarf.units();
while let Some(header) = units.next().expect("unit header") {
let unit = dwarf.unit(header).expect("unit");
let mut entries = unit.entries();
while let Some(entry) = entries.next_dfs().expect("dfs") {
if entry.tag() != gimli::DW_TAG_subprogram {
continue;
}
let name = entry
.attr_value(gimli::DW_AT_name)
.and_then(|v| dwarf.attr_string(&unit, v).ok())
.map(|s| s.to_string_lossy().into_owned());
let low_pc = match entry.attr_value(gimli::DW_AT_low_pc) {
Some(gimli::AttributeValue::Addr(a)) => a,
other => panic!("subprogram DW_AT_low_pc must be an Addr, got {other:?}"),
};
let size = match entry.attr_value(gimli::DW_AT_high_pc) {
Some(gimli::AttributeValue::Udata(n)) => n,
other => {
panic!("subprogram DW_AT_high_pc must be Udata (offset form), got {other:?}")
}
};
out.push(SubprogramDie {
name,
low_pc,
high_pc: low_pc + size,
});
}
}
out
}
fn wasm_exported_func_names(wasm: &[u8]) -> BTreeSet<String> {
use wasmparser::{ExternalKind, Parser, Payload};
let mut out = BTreeSet::new();
for payload in Parser::new(0).parse_all(wasm) {
if let Ok(Payload::ExportSection(reader)) = payload {
for export in reader.into_iter().flatten() {
if export.kind == ExternalKind::Func {
out.insert(export.name.to_string());
}
}
}
}
out
}
#[test]
fn emitted_debug_info_has_subprogram_dies_per_function_394() {
let wasm = repro("msgq_put_359.wasm");
let wasm_bytes = std::fs::read(&wasm).expect("read fixture wasm");
let expected_names = wasm_exported_func_names(&wasm_bytes);
assert!(
!expected_names.is_empty(),
"fixture must export ≥1 function to anchor the name check"
);
let dbg = compile(&wasm, "/tmp/dbg394_msgq_oraclef.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 func_sym_count = obj
.symbols()
.filter_map(|s| s.name().ok().map(str::to_string))
.filter(|n| {
n.strip_prefix("func_")
.is_some_and(|d| !d.is_empty() && d.bytes().all(|b| b.is_ascii_digit()))
})
.count();
assert!(func_sym_count > 0, "object must define ≥1 func_N symbol");
let subs = collect_subprograms(§ion_data(&dbg));
assert!(
!subs.is_empty(),
"emitted CU has NO DW_TAG_subprogram children — a debugger backtrace \
would show addresses, not function names"
);
assert_eq!(
subs.len(),
func_sym_count,
"expected one DW_TAG_subprogram per compiled function ({func_sym_count} \
func_N symbols); got {} subprograms: {:?}",
subs.len(),
subs.iter().map(|s| &s.name).collect::<Vec<_>>()
);
for s in &subs {
assert!(
s.low_pc < s.high_pc && s.high_pc <= text_len,
"subprogram {:?} range [0x{:x},0x{:x}) is not within .text (<0x{text_len:x})",
s.name,
s.low_pc,
s.high_pc
);
}
let got_names: BTreeSet<String> = subs.iter().filter_map(|s| s.name.clone()).collect();
for want in &expected_names {
assert!(
got_names.contains(want),
"no DW_TAG_subprogram named {want:?} (an exported function); \
got {got_names:?}"
);
}
eprintln!(
"[dbg394-oracleF] {} subprogram DIEs ({} func_N syms); exports {expected_names:?} \
all present. sample: {:?}",
subs.len(),
func_sym_count,
subs.iter()
.take(5)
.map(|s| format!("{:?} [0x{:x},0x{:x})", s.name, s.low_pc, s.high_pc))
.collect::<Vec<_>>()
);
}
fn wasm_name_section_func_names(wasm: &[u8]) -> HashMap<u32, String> {
use wasmparser::{KnownCustom, Parser, Payload};
let mut out = HashMap::new();
for payload in Parser::new(0).parse_all(wasm) {
if let Ok(Payload::CustomSection(c)) = payload
&& let KnownCustom::Name(reader) = c.as_known()
{
for subsection in reader.into_iter().flatten() {
if let wasmparser::Name::Function(map) = subsection {
for naming in map.into_iter().flatten() {
out.insert(naming.index, naming.name.to_string());
}
}
}
}
}
out
}
#[test]
fn emitted_subprogram_names_use_name_section_for_internal_functions_394() {
let wasm = repro("msgq_put_359.wasm");
let wasm_bytes = std::fs::read(&wasm).expect("read fixture wasm");
let name_map = wasm_name_section_func_names(&wasm_bytes);
assert!(
!name_map.is_empty(),
"fixture must carry a `name` custom section with function names \
(otherwise this oracle tests nothing — pick a different fixture)"
);
let exported = wasm_exported_func_names(&wasm_bytes);
let internal_names: BTreeSet<String> = name_map
.values()
.filter(|n| !exported.contains(*n))
.cloned()
.collect();
assert!(
!internal_names.is_empty(),
"fixture must have ≥1 internal function named by its `name` section"
);
let dbg = compile(&wasm, "/tmp/dbg394_msgq_oracleg.o", true);
let subs = collect_subprograms(§ion_data(&dbg));
let got_names: BTreeSet<String> = subs.iter().filter_map(|s| s.name.clone()).collect();
let internal_present: Vec<&String> = internal_names.intersection(&got_names).collect();
assert!(
!internal_present.is_empty(),
"no DW_TAG_subprogram carries an internal function's `name`-section \
name; internal names in fixture: {internal_names:?}, subprogram names: \
{got_names:?} — internal functions are still falling back to `func_N`"
);
let synthetic: Vec<&String> = got_names
.iter()
.filter(|n| {
n.strip_prefix("func_")
.is_some_and(|d| !d.is_empty() && d.bytes().all(|b| b.is_ascii_digit()))
})
.collect();
assert!(
synthetic.is_empty(),
"the fixture's `name` section names every function, yet {} subprogram \
DIE(s) still carry the synthetic fallback: {synthetic:?}",
synthetic.len()
);
eprintln!(
"[dbg394-oracleG] {} subprogram DIEs; internal `name`-section names \
present: {internal_present:?}; zero synthetic func_N names remain.",
subs.len()
);
}
fn compile_unit_range(secs: &HashMap<String, Vec<u8>>) -> (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 emitted .debug_* sections");
let mut units = dwarf.units();
while let Some(header) = units.next().expect("unit header") {
let unit = dwarf.unit(header).expect("unit");
let mut entries = unit.entries();
while let Some(entry) = entries.next_dfs().expect("dfs") {
if entry.tag() != gimli::DW_TAG_compile_unit {
continue;
}
let low_pc = match entry.attr_value(gimli::DW_AT_low_pc) {
Some(gimli::AttributeValue::Addr(a)) => a,
other => panic!("CU DW_AT_low_pc must be an Addr, got {other:?}"),
};
let size = match entry.attr_value(gimli::DW_AT_high_pc) {
Some(gimli::AttributeValue::Udata(n)) => n,
other => panic!("CU DW_AT_high_pc must be Udata (offset form), got {other:?}"),
};
return (low_pc, low_pc + size);
}
}
panic!("no DW_TAG_compile_unit DIE in emitted .debug_info");
}
#[test]
fn compile_unit_range_contains_every_subprogram_564() {
let wasm = repro("msgq_put_359.wasm");
let dbg = compile(&wasm, "/tmp/dbg394_msgq_oracleh.o", true);
let secs = section_data(&dbg);
let (cu_low, cu_high) = compile_unit_range(&secs);
let subs = collect_subprograms(&secs);
assert!(
!subs.is_empty(),
"fixture must emit ≥1 DW_TAG_subprogram to anchor the containment check"
);
let line_extent = line_rows(&secs)
.iter()
.map(|&(a, _)| a)
.max()
.expect("line table must have rows")
+ 1;
let code_extent = subs.iter().map(|s| s.high_pc).max().unwrap();
assert!(
code_extent > line_extent,
"fixture no longer exercises the #564 shape: max subprogram high_pc \
0x{code_extent:x} must exceed the line-table extent 0x{line_extent:x}"
);
for s in &subs {
assert!(
cu_low <= s.low_pc && s.high_pc <= cu_high,
"subprogram {:?} [0x{:x},0x{:x}) is NOT contained in its parent \
compile_unit's range [0x{cu_low:x},0x{cu_high:x}) — the CU \
DW_AT_high_pc is the line-table extent, not the code extent \
(#564); llvm-dwarfdump --verify fails this post-link",
s.name,
s.low_pc,
s.high_pc
);
}
eprintln!(
"[dbg394-oracleH] CU [0x{cu_low:x},0x{cu_high:x}) contains all {} \
subprograms (code extent 0x{code_extent:x} > line extent 0x{line_extent:x})",
subs.len()
);
}