diff --git a/artifacts/verified-codegen-roadmap.yaml b/artifacts/verified-codegen-roadmap.yaml index 9f6f1909..f29a698e 100644 --- a/artifacts/verified-codegen-roadmap.yaml +++ b/artifacts/verified-codegen-roadmap.yaml @@ -1455,9 +1455,42 @@ artifacts: map is never serialized, so `.text` is byte-identical (the existing differential fixtures + `test_line_map_is_wellformed_dbg001` assert monotonic in-bounds offsets and deterministic, unperturbed code). ARM - only; RISC-V carries an empty map (follow-up). step (5b) = gimli prod-dep - + input `.debug_line` read + emit behind `--debug-line` + the additivity - byte-diff oracle. + only; RISC-V carries an empty map (follow-up). + - step (5b) EMIT landed (PR B #429, a89ef28, v0.12.0 in progress): + gimli is now a PRODUCTION dep of synth-core (Cargo.toml + the + hand-written `crates/BUILD.bazel` synth-core `deps` — from_cargo makes + `@crates//:gimli` available but the manual rust_library must list it; + cargo passed locally while Bazel reddened E0433 until fix 63c510f). + `--debug-line` (clap flag, EXPERIMENTAL caveat) gates emission: when set + AND the input wasm carries `.debug_line`, synth reads the input rows + (`read_input_dwarf_line`), composes addr→source via the captured + `line_map`, and emits a FULL DWARF unit (DW_TAG_compile_unit with + DW_AT_stmt_list → `.debug_line`, so a normal `dwarf.units()` walk reads + it — not just a bare `DebugLine` program). Three oracles + (dwarf_debug_line_emit_394.rs): additive-on-dwarf-input (byte-compares + .text/.data/.bss, asserts `.debug_*` flag-gated), no-op-on-nodwarf-input + (whole-.o identical), and resolves-arm-addr-to-source (walks the normal + unit path, 110 in-range rows → non-zero lines). Frozen fixtures + untouched. KNOWN GAP closed by step 5c: emitted `.debug_*` addresses are + `.text`-base-0 with NO relocations, so they are CORRECT in the object + but WRONG on a linked image — a `--debug-line` consumer (jess, actively + Renode/silicon-debugging gust, the plausible first consumer) would get + silently-wrong source lines after host-link. The v0.12.0 TAG is HELD for + step 5c (advisor-confirmed: PR B in main is default-off + frozen-safe, so + it banks the work at zero risk; the tag waits for relocations). + - step (5c) RELOCATIONS — PR C, REQUIRED before the v0.12.0 tag: + C1 = generalize elf_builder's `.rel.text`-ONLY relocation machinery to + PER-SECTION relocations, **REL form (not RELA)** — synth/ARM32 uses REL + with the addend stored in-place (`.rel.debug_*`, matching the existing + `.rel.text` convention); frozen-safe, gated on the existing `.rel.text` + fixtures (cabi_arena_realloc_linkability_418) staying byte-identical. + C2 = emit DWARF addresses via gimli `Address::Symbol{text_sym, addend}`, + extract the relocations, emit `.rel.debug_line` / `.rel.debug_info` + against the `.text` symbol. The C2 ORACLE must APPLY the relocations at a + NONZERO `.text` base and verify the address resolves to the CORRECT + source line (not merely that a reloc record exists). Advisor caution: + "do NOT rush the addend math into a debug-info path — careful (even next + session) beats fast." status: approved tags: [toolchain, dwarf, debuggability, elf, meld-coordination, feature-loop, release-v0.12.0, synth-394] links: diff --git a/crates/synth-backend/src/elf_builder.rs b/crates/synth-backend/src/elf_builder.rs index e25c916e..c868a385 100644 --- a/crates/synth-backend/src/elf_builder.rs +++ b/crates/synth-backend/src/elf_builder.rs @@ -338,6 +338,16 @@ pub enum ArmRelocationType { MovtAbs = 44, } +/// A built `.rel.` section: its name offset in `.shstrtab`, the target +/// section index it relocates (`sh_info`), its file offset, and the encoded +/// REL entries. Internal to [`ElfBuilder::build`]. +struct ExtraRelSection { + name_offset: usize, + target_idx: u32, + offset: usize, + data: Vec, +} + /// ELF relocation entry (REL format, no addend) #[derive(Debug, Clone)] pub struct Relocation { @@ -378,6 +388,12 @@ pub struct ElfBuilder { program_headers: Vec, /// Relocations for .text section relocations: Vec, + /// Extra per-section relocation tables, keyed by the target section's name + /// (e.g. `.debug_line`). Each produces a `.rel.` section. Kept separate + /// from `relocations` (the `.text` set) so the existing `.rel.text` byte + /// layout is untouched: when this is empty the build is byte-identical to the + /// pre-generalization output (VCR-DBG-001 PR C, #394). + extra_relocations: Vec<(String, Vec)>, } impl ElfBuilder { @@ -394,6 +410,7 @@ impl ElfBuilder { symbols: Vec::new(), program_headers: Vec::new(), relocations: Vec::new(), + extra_relocations: Vec::new(), } } @@ -431,6 +448,16 @@ impl ElfBuilder { self.symbols.push(symbol); } + /// Add a symbol and return its 1-based index in `.symtab` (index 0 is the + /// reserved null symbol). Use when a later relocation must reference this + /// symbol — e.g. the `.text` base symbol the DWARF `.rel.debug_*` records + /// resolve against (VCR-DBG-001). + pub fn add_symbol_indexed(&mut self, symbol: Symbol) -> u32 { + let index = self.symbols.len() as u32 + 1; + self.symbols.push(symbol); + index + } + /// Add a program header (segment) pub fn add_program_header(&mut self, ph: ProgramHeader) { self.program_headers.push(ph); @@ -441,6 +468,20 @@ impl ElfBuilder { self.relocations.push(reloc); } + /// Add a relocation table targeting a non-`.text` section by name (e.g. + /// `.debug_line`). Produces a separate `.rel.` section whose `sh_info` + /// points at the named section. The section must already have been added via + /// [`add_section`]; if no matching section exists at build time the table is + /// silently dropped. Used by VCR-DBG-001 to relocate the DWARF `.text` + /// references so a host linker fixes them up alongside `.text`. + pub fn add_section_relocations(&mut self, target_section: &str, relocs: Vec) { + if relocs.is_empty() { + return; + } + self.extra_relocations + .push((target_section.to_string(), relocs)); + } + /// Add an undefined external symbol (e.g., __meld_dispatch_import) /// Returns the symbol index (1-based, accounting for null symbol) pub fn add_undefined_symbol(&mut self, name: &str) -> u32 { @@ -471,7 +512,8 @@ impl ElfBuilder { output.resize(header_size + ph_table_size, 0); // Build string table for section names - let (shstrtab_data, section_name_offsets) = self.build_section_string_table(); + let (shstrtab_data, section_name_offsets, extra_rel_name_offsets) = + self.build_section_string_table(); // Build symbol string table let (strtab_data, symbol_name_offsets) = self.build_symbol_string_table(); @@ -504,6 +546,26 @@ impl ElfBuilder { let rel_offset = current_offset; current_offset += rel_data.len(); + // Extra per-section relocation tables (.rel.), laid out after + // .rel.text. Each entry resolves its target section index by name; a + // table whose target section is absent is dropped. Empty when no + // --debug-line ⇒ byte-identical to the pre-generalization layout. + let mut extra_rel: Vec = Vec::new(); + for (i, (target, relocs)) in self.extra_relocations.iter().enumerate() { + let Some(target_idx) = self.section_index_by_name(target) else { + continue; + }; + let data = Self::encode_rel_entries(relocs); + let name_offset = extra_rel_name_offsets.get(i).copied().unwrap_or(0); + extra_rel.push(ExtraRelSection { + name_offset, + target_idx, + offset: current_offset, + data, + }); + current_offset += extra_rel.last().unwrap().data.len(); + } + // Section header table comes at the end let sh_offset = current_offset; @@ -517,6 +579,9 @@ impl ElfBuilder { output.extend_from_slice(&symtab_data); output.extend_from_slice(&rel_data); + for er in &extra_rel { + output.extend_from_slice(&er.data); + } // Write section headers let section_headers = self.build_section_headers_with_rel( @@ -530,6 +595,7 @@ impl ElfBuilder { §ion_offsets, rel_offset, &rel_data, + &extra_rel, ); output.extend_from_slice(§ion_headers); @@ -555,7 +621,7 @@ impl ElfBuilder { // Now write the actual ELF header at the beginning let has_rel = !self.relocations.is_empty(); - let num_sections = 4 + self.sections.len() + if has_rel { 1 } else { 0 }; + let num_sections = 4 + self.sections.len() + if has_rel { 1 } else { 0 } + extra_rel.len(); let ph_offset = if ph_count > 0 { header_size as u32 } else { 0 }; self.write_elf_header_with_phdrs( &mut output[0..header_size], @@ -700,8 +766,12 @@ impl ElfBuilder { Ok(()) } - /// Build section name string table - fn build_section_string_table(&self) -> (Vec, Vec) { + /// Build section name string table. Returns the bytes, the per-user-section + /// name offsets, and the per-extra-relocation `.rel.` name offsets + /// (parallel to `self.extra_relocations`). The extra-rel names are appended + /// AFTER `.rel.text`, so when `extra_relocations` is empty the table is + /// byte-identical to the pre-generalization layout. + fn build_section_string_table(&self) -> (Vec, Vec, Vec) { let mut strtab = vec![0]; // null string at offset 0 let mut offsets = Vec::new(); @@ -723,7 +793,15 @@ impl ElfBuilder { strtab.extend_from_slice(b".rel.text\0"); } - (strtab, offsets) + // .rel. for each extra per-section relocation table. + let mut extra_rel_offsets = Vec::new(); + for (target, _) in &self.extra_relocations { + let offset = strtab.len(); + extra_rel_offsets.push(offset); + strtab.extend_from_slice(format!(".rel{target}\0").as_bytes()); + } + + (strtab, offsets, extra_rel_offsets) } /// Build symbol name string table @@ -743,12 +821,14 @@ impl ElfBuilder { /// Build relocation table (ELF32 REL entries: 8 bytes each) fn build_relocation_table(&self) -> Vec { - if self.relocations.is_empty() { - return Vec::new(); - } + Self::encode_rel_entries(&self.relocations) + } + /// Encode a slice of relocations as ELF32 REL entries (8 bytes each). Shared + /// by `.rel.text` and the per-section `.rel.` tables. + fn encode_rel_entries(relocs: &[Relocation]) -> Vec { let mut rel_data = Vec::new(); - for reloc in &self.relocations { + for reloc in relocs { // r_offset (4 bytes) rel_data.extend_from_slice(&reloc.offset.to_le_bytes()); // r_info (4 bytes) = (sym_index << 8) | type @@ -758,6 +838,16 @@ impl ElfBuilder { rel_data } + /// Resolve a target section name to its ELF section index. User sections + /// begin at index 4 (null=0, shstrtab=1, strtab=2, symtab=3). Returns `None` + /// if no user section has that name. + fn section_index_by_name(&self, name: &str) -> Option { + self.sections + .iter() + .position(|s| s.name == name) + .map(|pos| 4 + pos as u32) + } + /// Build symbol table fn build_symbol_table(&self, name_offsets: &[usize]) -> Vec { let mut symtab = Vec::new(); @@ -817,6 +907,7 @@ impl ElfBuilder { section_offsets: &[usize], rel_offset: usize, rel_data: &[u8], + extra_rel: &[ExtraRelSection], ) -> Vec { let mut headers = Vec::new(); @@ -920,6 +1011,24 @@ impl ElfBuilder { ); } + // Extra .rel. sections (e.g. .rel.debug_line). Same shape as + // .rel.text but sh_info points at the named target section. + for er in extra_rel { + self.write_section_header( + &mut headers, + er.name_offset as u32, + SectionType::Rel as u32, + 0, + 0, + er.offset as u32, + er.data.len() as u32, + 3, // sh_link = .symtab section index + er.target_idx, // sh_info = relocated section + 4, + 8, // Each REL entry is 8 bytes + ); + } + headers } @@ -1233,7 +1342,7 @@ mod tests { builder.add_section(Section::new(".text", SectionType::ProgBits)); builder.add_section(Section::new(".data", SectionType::ProgBits)); - let (strtab, offsets) = builder.build_section_string_table(); + let (strtab, offsets, _extra_rel_offsets) = builder.build_section_string_table(); // Should have null byte at start assert_eq!(strtab[0], 0); diff --git a/crates/synth-cli/src/main.rs b/crates/synth-cli/src/main.rs index 0c804411..bc8bbd70 100644 --- a/crates/synth-cli/src/main.rs +++ b/crates/synth-cli/src/main.rs @@ -21,7 +21,7 @@ use synth_core::wasm_decoder::ImportEntry; use synth_synthesis::{ FunctionOps, WasmGlobal, WasmMemory, WasmOp, decode_wasm_functions, decode_wasm_module, }; -use tracing::{Level, info}; +use tracing::{Level, info, warn}; use wast::parser::{self, ParseBuffer}; use wast::{Wast, WastDirective}; @@ -258,11 +258,15 @@ enum Commands { /// addresses back to the input wasm's source lines. Requires the input to /// carry DWARF (`.debug_line` custom section) and the ARM backend (RISC-V /// carries no line_map). Purely additive: `.text`/`.data`/`.bss` stay - /// byte-identical; off by default. Wired on the relocatable-object - /// (host-link) path. EXPERIMENTAL: addresses are object-relative (`.text` - /// base 0) and carry no relocations yet, so they are correct for the - /// unlinked object but shift by the load base once linked — linked-binary - /// debugging needs the `.rela.debug_*` follow-up (VCR-DBG-002). + /// byte-identical; off by default. Emitted only on the relocatable-object + /// (host-link) path — on a self-contained image or RISC-V it is a no-op + /// and warns. The `.text` addresses carry `.rel.debug_*` relocations + /// against a `__synth_text_base` symbol, so a host linker fixes them up to + /// the final load address (verified end-to-end with `arm-none-eabi-ld`). + /// EXPERIMENTAL: `__synth_text_base` is a global symbol, so linking more + /// than one synth `--debug-line` object into a single image collides + /// (`multiple definition`) — compile such modules as one object or link + /// separately until the local-section-symbol follow-up lands. #[arg(long)] debug_line: bool, }, @@ -2232,6 +2236,26 @@ fn compile_all_exports( None }; + // VCR-DBG-001 PR C (#394): DWARF is emitted (with `.rel.debug_*` so a host + // linker fixes up the `.text` addresses) ONLY on the ARM relocatable-object + // path. On a self-contained ET_EXEC image or the RISC-V path there is no + // relocatable text symbol to anchor the addresses, so `--debug-line` would + // silently drop. Warn LOUDLY rather than mislead a consumer (jess) into + // expecting source lines that aren't there — the #383 honest-fail rule. + let dwarf_effective = !is_riscv && (has_external_relocations || relocatable); + if debug_line && !dwarf_effective { + warn!( + "--debug-line has no effect on this output: DWARF line tables are emitted only on \ + the ARM relocatable-object path (link via --relocatable, then `ld`). \ + {} produces no .debug_* sections.", + if is_riscv { + "The RISC-V backend" + } else { + "A self-contained executable image" + } + ); + } + let elf_data = if is_riscv { info!("Building RISC-V multi-function relocatable object (EM_RISCV)"); build_multi_func_riscv_elf(&compiled_funcs)? @@ -3166,20 +3190,53 @@ fn build_relocatable_elf( table.sort_by_key(|&(a, _)| a); table.dedup_by_key(|&mut (a, _)| a); - let dwarf_sections = synth_core::dwarf_line::emit_debug_sections(&table); + // A dedicated GLOBAL symbol at `.text + 0` that the DWARF `.rel.debug_*` + // records resolve against (R_ARM_ABS32, REL: S=`.text` base + in-place + // addend 0). Global + appended last so no existing symbol index shifts + // and `.symtab`'s `sh_info` is untouched; present only under + // `--debug-line`, so the no-DWARF symtab stays byte-identical. A local + // STT_SECTION symbol would have to precede all globals (ELF orders + // locals first) and bump `sh_info`, shifting every `.rel.text` index. + let text_base_sym = Symbol::new("__synth_text_base") + .with_value(0) + .with_binding(SymbolBinding::Global) + .with_type(SymbolType::NoType) + .with_section(4); // .text is section index 4 + let text_sym_idx = elf_builder.add_symbol_indexed(text_base_sym); + + let dwarf_sections = + synth_core::dwarf_line::emit_debug_sections(&table, text_sym_idx as usize); if !dwarf_sections.is_empty() { - let names: Vec<&str> = dwarf_sections.iter().map(|(n, _)| *n).collect(); - for (name, bytes) in &dwarf_sections { - let dbg_section = Section::new(name, ElfSectionType::ProgBits) + let names: Vec<&str> = dwarf_sections.iter().map(|s| s.name).collect(); + let mut total_relocs = 0usize; + for sec in &dwarf_sections { + let dbg_section = Section::new(sec.name, ElfSectionType::ProgBits) .with_align(1) - .with_data(bytes.clone()); + .with_data(sec.bytes.clone()); elf_builder.add_section(dbg_section); + // Register the section's `.text` relocations as `.rel.` + // (R_ARM_ABS32 against the `.text` base symbol). REL form: the + // addend already sits in-place in the section bytes. + if !sec.text_relocs.is_empty() { + let relocs: Vec = sec + .text_relocs + .iter() + .map(|r| Relocation { + offset: r.offset, + symbol_index: text_sym_idx, + reloc_type: ArmRelocationType::Abs32, + }) + .collect(); + total_relocs += relocs.len(); + elf_builder.add_section_relocations(sec.name, relocs); + } } info!( - "DWARF: emitted {} sections {:?} ({} address rows, --debug-line)", + "DWARF: emitted {} sections {:?} ({} address rows, {} .text relocations, --debug-line)", dwarf_sections.len(), names, - table.len() + table.len(), + total_relocs ); } } diff --git a/crates/synth-cli/tests/dwarf_debug_line_emit_394.rs b/crates/synth-cli/tests/dwarf_debug_line_emit_394.rs index 0df58977..4581cce4 100644 --- a/crates/synth-cli/tests/dwarf_debug_line_emit_394.rs +++ b/crates/synth-cli/tests/dwarf_debug_line_emit_394.rs @@ -27,7 +27,7 @@ use std::process::Command; use gimli::{EndianSlice, LittleEndian, SectionId}; use object::read::elf::ElfFile32; -use object::{Object, ObjectSection}; +use object::{Object, ObjectSection, ObjectSymbol}; fn synth() -> &'static str { env!("CARGO_BIN_EXE_synth") @@ -220,3 +220,148 @@ fn emitted_debug_line_resolves_arm_addr_to_source_394() { eprintln!(" .text+0x{addr:04x} -> line {line}"); } } + +/// R_ARM_ABS32 — the relocation type the DWARF `.text` references use. +const R_ARM_ABS32: u32 = 2; + +/// Walk the NORMAL debugger path (`dwarf.units()` → CU `DW_AT_stmt_list` line +/// program) over a section-name → bytes map, returning every `(address, line)` +/// row. Shared by the un-relocated and relocated parses in Oracle C. +fn line_rows(secs: &HashMap>) -> Vec<(u64, u64)> { + let empty: &[u8] = &[]; + let load = |id: SectionId| -> Result, 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 +} + +/// ORACLE C — the `.rel.debug_*` relocations are real and CORRECT under linking +/// (VCR-DBG-001 PR C). The emitted object's `.debug_*` addresses are `.text`-base +/// 0; a host linker that places `.text` at a non-zero address must shift them via +/// `.rel.debug_*`. This oracle proves the records do exactly that: +/// +/// 1. `.debug_line` carries EXACTLY ONE relocation — `R_ARM_ABS32` against +/// `__synth_text_base` (the single `DW_LNE_set_address` anchor) — and +/// `.debug_info` one (the CU `DW_AT_low_pc`). REL form ⇒ the in-place addend +/// bytes are 0. +/// 2. APPLY the `.debug_line` relocation at a NON-ZERO base (`S + A`, A=0 ⇒ +/// the base itself), then re-walk the line program. EVERY row's address must +/// shift by exactly the base and its source LINE must be preserved — i.e. on +/// a linked image each `.text` address resolves to the SAME, CORRECT source +/// line it did object-relative. A missing/wrong relocation would leave the +/// addresses at base 0 (silently wrong on silicon — the #383 shape). +#[test] +fn rel_debug_relocations_shift_addresses_to_correct_line_394() { + const LINK_BASE: u64 = 0x0800_0000; // a realistic non-zero flash `.text` base + + let wasm = repro("msgq_put_359.wasm"); + let dbg = compile(&wasm, "/tmp/dbg394_msgq_oraclec.o", true); + let obj = ElfFile32::::parse(&*dbg).expect("parse ELF"); + + // (1) Inspect the `.rel.debug_line` / `.rel.debug_info` records. object maps + // each `.rel.` (sh_info → target) onto the target section, so + // `section.relocations()` yields them. + 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; + // R_ARM_ABS32 against __synth_text_base. + 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" + ); + // REL form: the in-place addend at the reloc site is 0. + 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"); + + // (2) Un-relocated rows (object-relative, base 0) vs rows after applying the + // `.debug_line` relocation at a non-zero 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"); + // S + A with A = the in-place value (0) ⇒ write the link base itself. + 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() + ); +} diff --git a/crates/synth-core/src/dwarf_line.rs b/crates/synth-core/src/dwarf_line.rs index 47726cf8..7c8a8b00 100644 --- a/crates/synth-core/src/dwarf_line.rs +++ b/crates/synth-core/src/dwarf_line.rs @@ -188,10 +188,8 @@ fn parse_debug_line_rows( /// /// Ports `tests/dwarf_emit_roundtrip_step4.rs::emit_dwarf` (which emits the same /// full unit and round-trips through `Dwarf::units()`). -pub fn emit_debug_sections(table: &[(u64, u32)]) -> Vec<(&'static str, Vec)> { - use gimli::write::{ - Address, AttributeValue, DwarfUnit, EndianVec, LineProgram, LineString, Sections, - }; +pub fn emit_debug_sections(table: &[(u64, u32)], text_sym: usize) -> Vec { + use gimli::write::{Address, AttributeValue, DwarfUnit, LineProgram, LineString, Sections}; if table.is_empty() { return Vec::new(); @@ -204,8 +202,8 @@ pub fn emit_debug_sections(table: &[(u64, u32)]) -> Vec<(&'static str, Vec)> }; let mut dwarf = DwarfUnit::new(encoding); - // The span of emitted text the unit describes: low_pc=0 (text base), high_pc - // one past the last mapped address. + // The span of emitted text the unit describes: low_pc=`.text`+0 (text base), + // high_pc one past the last mapped address. let high_pc = table.iter().map(|&(a, _)| a).max().unwrap_or(0) + 1; let mut program = LineProgram::new( @@ -218,7 +216,16 @@ pub fn emit_debug_sections(table: &[(u64, u32)]) -> Vec<(&'static str, Vec)> let dir = program.default_directory(); let fid = program.add_file(LineString::String(b"synth.wasm".to_vec()), dir, None); - program.begin_sequence(Some(Address::Constant(0))); + // The sequence base is `.text + 0` as a RELOCATABLE address (one + // `DW_LNE_set_address` against the `.text` symbol, addend 0); each row's + // `address_offset` stays a text-relative DELTA, so only this single site + // needs a relocation per section. Addend 0 ⇒ the in-place bytes are + // byte-identical to the previous `Address::Constant(0)` form. + let text_base = Address::Symbol { + symbol: text_sym, + addend: 0, + }; + program.begin_sequence(Some(text_base)); for &(addr, line) in table { let row = program.row(); row.address_offset = addr; @@ -236,31 +243,121 @@ pub fn emit_debug_sections(table: &[(u64, u32)]) -> Vec<(&'static str, Vec)> 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(Address::Constant(0)), - ); + 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 mut sections = Sections::new(EndianVec::new(LittleEndian)); + 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<(&'static str, Vec)> = Vec::new(); - let _ = sections.for_each(|id, data| -> Result<(), ()> { - let bytes = data.slice(); + let mut out: Vec = 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) { - out.push((name, bytes.to_vec())); + 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 } +/// A relocation a `.debug_*` section needs against the `.text` symbol so a host +/// linker fixes up the embedded `.text` address when `.text` is placed. REL +/// form: the in-place bytes already hold the addend (always `0` for our +/// text-base references), so only the site (`offset`) and `size` travel here. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DwarfTextReloc { + /// Byte offset within the section where the relocated address word sits. + pub offset: u32, + /// Size of the relocated value (always 4 for DWARF32 addresses). + pub size: u8, +} + +/// One emitted `.debug_*` section: its ELF name, bytes, and the `.text`-symbol +/// relocations it needs (empty for address-free sections like `.debug_str`). +#[derive(Debug, Clone)] +pub struct EmittedDwarfSection { + /// `'static` ELF section name (e.g. `.debug_line`). + pub name: &'static str, + /// Section payload bytes. + pub bytes: Vec, + /// `.text`-symbol relocations within this section (REL, in-place addend 0). + pub text_relocs: Vec, +} + +/// A gimli `write::Writer` that delegates to an inner `EndianVec` but records +/// every `Address::Symbol` write as a relocation. ONLY `write_address` is +/// overridden — `write_offset` (gimli's internal section-to-section references, +/// e.g. `.debug_info` → `.debug_str`/`.debug_abbrev` and `DW_AT_stmt_list` → +/// `.debug_line`) keeps the default, so those stay CONCRETE intra-file offsets +/// and need no section symbols. The only relocations captured are the two +/// `.text` references (the line program's `DW_LNE_set_address` and the CU's +/// `DW_AT_low_pc`). `Clone` so `Sections::new` can seed each section writer. +#[derive(Clone)] +struct RelocWriter { + inner: gimli::write::EndianVec, + /// (offset within section, addend, size) for each `Address::Symbol` write. + 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 } => { + // REL: record the site and write the addend in place (0 ⇒ the + // bytes match the old `Address::Constant(0)` exactly). + let offset = self.inner.len(); + self.relocs.push((offset, addend, size)); + self.inner.write_udata(addend as u64, size) + } + } + } +} + /// `'static` ELF section name for the `.debug_*` sections the emitter can /// produce. Returns `None` for any section id we do not wire (none are expected /// for this minimal unit, but the match keeps the names `'static`).