diff --git a/docs/coff.md b/docs/coff.md new file mode 100644 index 00000000..9277450b --- /dev/null +++ b/docs/coff.md @@ -0,0 +1,94 @@ +# COFF / PE support (prototype) + +Notes on behavior specific to the `dtk coff` subcommand family (x86 PE +executables split into MSVC-style COFF objects). + +## Object file layout + +By default (`function_sections: true` in `config.yml`), each function in a +unit is emitted as its own `.text` section, mirroring MSVC `/Gy` +(function-level linking). This gives tooling (objdiff, `dumpbin`, +`llvm-readobj`) exact per-function section sizes instead of sizes inferred +from symbol gaps, which would include inter-function padding. + +Details: + +- COFF permits multiple sections with the same name in one object; lld-link + concatenates them in section-table order, so relinked output is unchanged. +- Inter-function padding bytes (`int3`/`nop`) and trailing jump tables stay + attached to the preceding function's section. Non-first sections have + alignment 1, so the linker inserts no padding between them and the byte + stream is preserved. +- Sections are not COMDAT: dtk deduplicates symbol names globally at split + time, so COMDAT selection semantics are unnecessary. + +Set `function_sections: false` to emit one section per split range instead. + +## Symbol visibility + +- `scope:local` in `symbols.txt` is honored even with `export_all: true` + (the default): the symbol is emitted as `IMAGE_SYM_CLASS_STATIC`. +- If a `scope:local` symbol is referenced from another unit and + `globalize_symbols` is enabled (the default), it is renamed to + `_` and promoted to external automatically. +- With `globalize_symbols: false`, a `scope:local` symbol referenced + cross-unit will fail to link (undefined symbol) — same contract as the + ELF pipeline. +- `noexport` still forces a symbol static regardless of scope. + +## symbols.txt / splits.txt changes + +Symbol names, scopes, and splits are baked into the emitted objects at +`dtk coff split` time. After editing `symbols.txt` or `splits.txt`, re-run +`dtk coff split` to regenerate the objects. + +## Non-contiguous units + +A unit may be listed in `splits.txt` with multiple, non-adjacent ranges of +the same section (e.g. two `.text` ranges). This is supported for diffing: +the unit's object receives one section per range. + +Limitations: + +- Link order is resolved from address adjacency; interleaved units create + cycles that are broken heuristically. A warning names any unit with + non-contiguous ranges. +- A relinked image cannot be byte-identical to the original in this case, + since the linker emits one object's sections adjacently. Multi-section + ranges (`.text` + `.data` + `.bss` for one unit) are unaffected — that is + the normal case and fully supported. + +## Header-slack comment strings (`comment_regions`) + +MSVC's `#pragma comment(exestr, …)` / Intel `-?comment:"…"` directives cause the +original linker to embed comment strings into the PE header slack (between the +section table and the first raw section). These strings survive in the linked +image but the `.drectve` directives that produced them are gone. + +Declare the byte ranges holding them per image (base exe or a DLL module): + +```yaml +comment_regions: +- offset: 912 # file offset in the source PE + size: 882 # byte length +``` + +At split time dtk lifts those bytes, splits them on NUL, and re-emits each run +as a `-?comment:"…"` directive in a `.drectve` section of a generated +`auto_comments` object (appended to the link order / `objs.rsp`). Bytes are +emitted verbatim — a run truncated in the source image stays truncated. + +Notes: + +- Default is empty (feature off): no comment object is emitted and output is + byte-identical to a build without the field. +- Requires a linker that accepts `-?comment` in `.drectve`. Stock `lld-link` + rejects it (`is not allowed in .drectve`); reproducing the strings needs a + linker with comment-embedding support. + +## Known limitations + +- objdiff may display function signatures as `void(void)`: COFF symbols + carry no parameter or return type information (only the DTYPE_FUNCTION + bit). Real signatures live in CodeView debug data, which dtk does not + emit. This is cosmetic and does not affect diffing. diff --git a/src/analysis/x86.rs b/src/analysis/x86.rs index 992bb884..4998ff49 100644 --- a/src/analysis/x86.rs +++ b/src/analysis/x86.rs @@ -345,8 +345,7 @@ pub fn analyze_x86_functions(obj: &mut ObjInfo) -> Result { ObjSymbolKind::Function, )? .is_some(); - if is_tail_call && inside_known_function(obj, tgt_sec, target) - { + if is_tail_call && inside_known_function(obj, tgt_sec, target) { // Tail JMP into the body of a known function: // emit the reference (resolves to the containing // function + addend) but don't mint a sub-entry @@ -589,14 +588,7 @@ pub fn analyze_x86_functions(obj: &mut ObjInfo) -> Result { .kind_at_section_address(sec_idx, operand_va, ObjSymbolKind::Function)? .is_some(); if !already && emit && !on_symbol_start { - add_rel32( - obj, - sec_idx, - operand_va, - tgt_sec, - target, - &mut sweep_count, - )?; + add_rel32(obj, sec_idx, operand_va, tgt_sec, target, &mut sweep_count)?; } } } @@ -807,14 +799,12 @@ fn is_within_decoded_span( /// its declared size. The reference itself is still emitted as a relocation /// against the containing function (with an addend). fn inside_known_function(obj: &ObjInfo, sec: SectionIndex, va: u32) -> bool { - obj.symbols - .for_section_range(sec, ..=va) - .any(|(_, s)| { - s.kind == ObjSymbolKind::Function - && s.size_known - && (s.address as u32) < va - && va < (s.address as u32).wrapping_add(s.size as u32) - }) + obj.symbols.for_section_range(sec, ..=va).any(|(_, s)| { + s.kind == ObjSymbolKind::Function + && s.size_known + && (s.address as u32) < va + && va < (s.address as u32).wrapping_add(s.size as u32) + }) } /// Returns `true` if `va` decodes as a valid (non-invalid) instruction. diff --git a/src/cmd/coff.rs b/src/cmd/coff.rs index b32db0ef..b497bfac 100644 --- a/src/cmd/coff.rs +++ b/src/cmd/coff.rs @@ -30,14 +30,17 @@ use crate::{ shasum::file_sha1_string, }, obj::{ - ObjInfo, ObjKind, ObjRelocKind, ObjSymbol, ObjSymbolFlags, ObjSymbolFlagSet, - ObjSymbolKind, ObjSymbolScope, SectionIndex, SymbolIndex, best_match_for_reloc, + ObjInfo, ObjKind, ObjRelocKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind, + ObjSymbolScope, SectionIndex, SymbolIndex, best_match_for_reloc, }, util::{ - coff::{apply_base_relocations, create_function_splits, process_coff, write_coff}, + coff::{ + apply_base_relocations, create_function_splits, extract_comment_directives, + process_coff, write_coff, + }, config::{ apply_splits_file, apply_symbols_file, create_auto_symbol_name, is_auto_symbol, - write_splits_file, write_symbols_file, + read_comment_regions, write_splits_file, write_symbols_file, }, dep::DepFile, file::{FileReadInfo, buf_writer, process_rsp, touch, verify_hash}, @@ -555,11 +558,31 @@ fn load_coff_module( let pe_header = PeHeaderInfo::parse(data); let (mut obj, image_base) = process_coff(data, config.name())?; detect_pe_symbols(&mut obj, data)?; + // Extract exestr comments declared as `type:comment` ranges in the + // splits file's Sections block, re-emitted later as `.drectve` directives. + if let Some(splits) = &config.splits { + let regions = comment_regions_from_splits(&splits.with_encoding(), image_base)?; + extract_comment_directives(data, ®ions, &mut obj, config.name())?; + } (obj, image_base, pe_header) }; Ok((obj, image_base, pe_header, object_path)) } +/// Resolve `type:comment` entries in the splits file's Sections block to +/// `(file_offset, size)` ranges. Header padding maps 1:1, so the file offset is +/// `vaddr - image_base`. +fn comment_regions_from_splits( + splits_path: &Utf8NativePath, + image_base: Option, +) -> Result)>> { + let base = image_base.unwrap_or(0); + Ok(read_comment_regions(splits_path)? + .into_iter() + .map(|(start, end, unit)| (start - base, end - start, unit)) + .collect()) +} + /// Import authoritative symbol sizes from one prebuilt verbatim library object. /// /// The object's symbol layout (sizes inferred as the gap to the next symbol in @@ -868,7 +891,8 @@ fn load_analyze_coff( Some(s) => s, None => continue, }; - if unit_at(&obj, sec_idx, reloc_addr) != unit_at(&obj, tgt_sec, sym.address as u32) { + if unit_at(&obj, sec_idx, reloc_addr) != unit_at(&obj, tgt_sec, sym.address as u32) + { to_remove.push((sec_idx, reloc_addr)); } } @@ -878,7 +902,10 @@ fn load_analyze_coff( obj.sections[sec_idx].relocations.remove(addr); } if dropped > 0 { - info!("{}: dropped {dropped} cross-unit relocation(s) to non-exportable targets", obj.name); + info!( + "{}: dropped {dropped} cross-unit relocation(s) to non-exportable targets", + obj.name + ); } } @@ -1102,8 +1129,10 @@ fn split_write_coff( }; // Serialize all split objects in parallel (CPU-bound), then write serially. - let serialized: Vec>> = - split_objs.par_iter().map(|split_obj| write_coff(split_obj, config.export_all)).collect(); + let serialized: Vec>> = split_objs + .par_iter() + .map(|split_obj| write_coff(split_obj, config.export_all, config.function_sections)) + .collect(); // Serial bookkeeping (path dedup, unit order, directory creation), then write // the objects in parallel — writing 18k+ files dominates the split otherwise. @@ -1139,6 +1168,48 @@ fn split_write_coff( } pending_writes.push((out_path, out_obj)); } + + // Comments attributed to a unit are emitted into that unit's object by + // write_coff. Unattributed comments go in a shared `.drectve`-only object, + // appended to the link order so it lands in objs.rsp. + let unattributed: Vec<&[u8]> = module + .obj + .pe_comment_directives + .iter() + .filter(|(unit, _)| unit.is_none()) + .map(|(_, bytes)| bytes.as_slice()) + .collect(); + if let Some(comment_obj) = crate::util::coff::write_coff_comments(&unattributed)? { + let unit_name = "auto_comments".to_string(); + let obj_path = obj_path_for_unit(&unit_name); + let out_path = obj_dir.join(&obj_path); + if object_paths.contains_key(&obj_path) { + bail!("Comment object path {} collides with an existing unit", out_path); + } + if let Some(parent) = out_path.parent() { + if created_dirs.insert(parent.to_owned()) { + DirBuilder::new().recursive(true).create(parent)?; + } + } + // Link the shared comment object first: its comments sit at the lowest + // header addresses (before any code), so a comment-aware linker embeds + // them ahead of any per-unit `.drectve`, preserving header address order. + out_config.units.insert(0, OutputUnit { + object: out_path.with_unix_encoding(), + name: unit_name.clone(), + autogenerated: true, + code_size: 0, + data_size: 0, + }); + module.obj.link_order.insert(0, crate::obj::ObjUnit { + name: unit_name, + autogenerated: true, + comment_version: None, + order: None, + }); + pending_writes.push((out_path, comment_obj)); + } + pending_writes.par_iter().try_for_each(|(path, data)| write_if_changed(path, data))?; // Generate args.rsp (flags) and objs.rsp (object file list) diff --git a/src/cmd/dol.rs b/src/cmd/dol.rs index 793e159b..f3043751 100644 --- a/src/cmd/dol.rs +++ b/src/cmd/dol.rs @@ -248,6 +248,10 @@ pub struct ProjectConfig { /// Promotes local symbols referenced by other units to global. #[serde(default = "bool_true", skip_serializing_if = "is_true")] pub globalize_symbols: bool, + /// Emit each function as its own section in COFF objects (MSVC /Gy style), + /// so tooling (objdiff, dumpbin) sees exact per-function sizes. + #[serde(default = "bool_true", skip_serializing_if = "is_true")] + pub function_sections: bool, /// Optional base path for all object files. #[serde(with = "unix_path_serde_option", default, skip_serializing_if = "is_default")] pub object_base: Option, @@ -286,6 +290,7 @@ impl Default for ProjectConfig { dead_strip: false, export_all: true, globalize_symbols: true, + function_sections: true, object_base: None, extract_objects: true, x86_signatures: None, diff --git a/src/obj/mod.rs b/src/obj/mod.rs index 833fab7c..0910f74e 100644 --- a/src/obj/mod.rs +++ b/src/obj/mod.rs @@ -12,11 +12,11 @@ use std::{ use anyhow::{Result, anyhow, bail, ensure}; use objdiff_core::obj::split_meta::SplitMeta; -use serde::{Deserialize, Serialize}; pub use relocations::{ObjReloc, ObjRelocKind, ObjRelocations}; pub use sections::{ ObjSection, ObjSectionKind, ObjSections, ObjSubRegion, SectionIndex, section_kind_for_section, }; +use serde::{Deserialize, Serialize}; pub use splits::{ObjSplit, ObjSplits}; pub use symbols::{ ObjDataKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind, ObjSymbolScope, @@ -111,6 +111,18 @@ pub struct ObjInfo { /// absolute relocations. Not emitted as a section; the linker regenerates it. pub pe_reloc_data: Vec, + /// Compiler comment strings extracted from the PE header slack via + /// `type:comment` splits. Each entry is `(owning unit, literal bytes)`; + /// emitted as an `-?comment:"..."` directive in a `.drectve` section of the + /// owning unit's object (or a shared object when the unit is `None`). Not a + /// real section. + pub pe_comment_directives: Vec<(Option, Vec)>, + + /// `type:comment` split declarations `(name, start_va, end_va, owning unit)`, + /// retained so `dtk coff split` round-trips them into the regenerated splits + /// file. A `None` unit is a shared `Sections:` entry; `Some` is under a unit. + pub pe_comment_sections: Vec<(String, u32, u32, Option)>, + /// Original PE header metadata, retained so the post-link patch can reproduce /// fields the relinker doesn't. Only set for COFF/PE images. pub pe_metadata: Option, @@ -147,6 +159,8 @@ impl ObjInfo { module_id: 0, unresolved_relocations: vec![], pe_reloc_data: Vec::new(), + pe_comment_directives: Vec::new(), + pe_comment_sections: Vec::new(), pe_metadata: None, } } diff --git a/src/util/coff.rs b/src/util/coff.rs index 6264ace2..0439e3be 100644 --- a/src/util/coff.rs +++ b/src/util/coff.rs @@ -4,14 +4,13 @@ use anyhow::{Context, Result, bail}; use cwdemangle::demangle; use flagset::Flags; use object::{ - Architecture, BinaryFormat, Endianness, Object, ObjectKind, ObjectSection, ObjectSymbol, - RelocationEncoding, RelocationKind, RelocationTarget, SectionKind, SymbolFlags, SymbolKind, - SymbolScope, + Architecture, BinaryFormat, ComdatKind, Endianness, Object, ObjectKind, ObjectSection, + ObjectSymbol, RelocationEncoding, RelocationKind, RelocationTarget, SectionKind, SymbolFlags, + SymbolKind, SymbolScope, write::{ Comdat, Mangling, Object as WriteObject, Relocation, RelocationFlags, SectionId, Symbol, SymbolId, SymbolSection as WriteSymbolSection, }, - ComdatKind, }; use crate::{ @@ -23,6 +22,47 @@ use crate::{ }, }; +/// Extract exestr comment strings from the given `(file_offset, size, unit)` +/// ranges of the PE header padding onto `ObjInfo::pe_comment_directives`. Each +/// range may hold several NUL-separated runs; each non-empty run is kept +/// verbatim (a run truncated in the source is emitted as-is) and tagged with the +/// range's owning unit. Ranges come from `type:comment` entries in the splits +/// file (see `read_comment_regions`). +pub fn extract_comment_directives( + data: &[u8], + regions: &[(u32, u32, Option)], + obj: &mut ObjInfo, + name: &str, +) -> Result<()> { + for (offset, size, unit) in regions { + let (start, end) = (*offset as usize, *offset as usize + *size as usize); + let Some(region) = data.get(start..end) else { + bail!("Comment region {offset:#x}..+{size:#x} is out of bounds for '{name}'"); + }; + for run in region.split(|&b| b == 0) { + if !run.is_empty() { + obj.pe_comment_directives.push((unit.clone(), run.to_vec())); + } + } + } + Ok(()) +} + +/// Encode exestr comment payloads as whitespace-separated `-?comment:"..."` +/// linker directives for a `.drectve` section. +fn encode_drectve<'a>(payloads: impl Iterator) -> Vec { + let mut data = Vec::new(); + for (i, payload) in payloads.enumerate() { + if i > 0 { + data.push(b' '); + } + data.extend_from_slice(b"-?comment:\""); + data.extend_from_slice(payload); + data.push(b'"'); + } + data +} + /// Returns the parsed `ObjInfo` and, for PE executables, the ImageBase. pub fn process_coff(data: &[u8], name: &str) -> Result<(ObjInfo, Option)> { let obj_file = object::File::parse(data).context("Failed to parse COFF/PE file")?; @@ -267,7 +307,24 @@ pub fn process_coff(data: &[u8], name: &str) -> Result<(ObjInfo, Option)> { Ok((obj, image_base)) } -pub fn write_coff(obj: &ObjInfo, export_all: bool) -> Result> { +/// A contiguous byte range of an input section emitted as its own COFF +/// section (MSVC /Gy-style function-level sections). `start`/`end` are +/// section-relative offsets. +struct SectionChunk { + start: u64, + end: u64, + id: SectionId, +} + +/// Find the chunk containing `offset`. An offset at or past the end of the +/// last chunk clamps to the last chunk (defensive; split_obj filters symbols +/// at the section end address). +fn chunk_for(chunks: &[SectionChunk], offset: u64) -> Option<&SectionChunk> { + let idx = chunks.partition_point(|c| c.start <= offset); + if idx == 0 { None } else { Some(&chunks[idx - 1]) } +} + +pub fn write_coff(obj: &ObjInfo, export_all: bool, function_sections: bool) -> Result> { let mut out = WriteObject::new(BinaryFormat::Coff, Architecture::I386, Endianness::Little); // Disable object crate's auto leading-underscore mangling: it blindly // prepends '_' to every Text/Data symbol, which corrupts MSVC C++ @@ -277,8 +334,16 @@ pub fn write_coff(obj: &ObjInfo, export_all: bool) -> Result> { // so we write them verbatim. out.set_mangling(Mangling::None); - // Add sections and build section id map (indexed by ObjSectionIndex) - let mut section_ids: Vec> = vec![None; obj.sections.len() as usize]; + // Add sections and build chunk table (indexed by ObjSectionIndex). + // With function_sections enabled, each code section is cut at function + // symbol boundaries so every function gets its own `.text` section (MSVC + // /Gy style). COFF allows multiple sections with the same name, and + // lld-link concatenates them in section-table order, so the emitted byte + // stream is unchanged: inter-function padding and trailing jump tables + // stay attached to the preceding function's chunk, and non-first chunks + // use alignment 1 so the linker inserts nothing between them. + let mut section_chunks: Vec> = Vec::new(); + section_chunks.resize_with(obj.sections.len() as usize, Vec::new); for (idx, section) in obj.sections.iter() { let kind = match section.kind { ObjSectionKind::Code => SectionKind::Text, @@ -286,21 +351,50 @@ pub fn write_coff(obj: &ObjInfo, export_all: bool) -> Result> { ObjSectionKind::ReadOnlyData => SectionKind::ReadOnlyData, ObjSectionKind::Bss => SectionKind::UninitializedData, }; - let sid = out.add_section(vec![], section.name.as_bytes().to_vec(), kind); if section.kind == ObjSectionKind::Bss { + let sid = out.add_section(vec![], section.name.as_bytes().to_vec(), kind); out.append_section_bss(sid, section.size, section.align.max(1)); - } else { + section_chunks[idx as usize].push(SectionChunk { + start: 0, + end: section.size, + id: sid, + }); + continue; + } + // Section-relative offsets where a new chunk begins. Labels (Unknown) + // and Object symbols (e.g. jump tables) do not cut. + let mut starts = vec![0u64]; + if function_sections && section.kind == ObjSectionKind::Code { + let mut cuts: Vec = obj + .symbols + .for_section(idx) + .filter(|(_, s)| s.kind == ObjSymbolKind::Function) + .map(|(_, s)| s.address.saturating_sub(section.address)) + .filter(|&a| a > 0 && a < section.size) + .collect(); + cuts.sort_unstable(); + cuts.dedup(); + starts.extend(cuts); + } + for (i, &start) in starts.iter().enumerate() { + let end = starts.get(i + 1).copied().unwrap_or(section.size); + let sid = out.add_section(vec![], section.name.as_bytes().to_vec(), kind); + let data_end = (end as usize).min(section.data.len()); + let mut data = section.data[(start as usize).min(data_end)..data_end].to_vec(); // Zero out bytes at relocation sites; addend is carried by the relocation record - let mut data = section.data.clone(); for (addr, _) in section.relocations.iter() { - let off = (addr as u64 - section.address) as usize; - if off + 4 <= data.len() { - data[off..off + 4].fill(0); + let off = (addr as u64).saturating_sub(section.address); + if off >= start && off < end { + let rel = (off - start) as usize; + if rel + 4 <= data.len() { + data[rel..rel + 4].fill(0); + } } } - out.set_section_data(sid, data, section.align.max(1)); + let align = if start == 0 { section.align.max(1) } else { 1 }; + out.set_section_data(sid, data, align); + section_chunks[idx as usize].push(SectionChunk { start, end, id: sid }); } - section_ids[idx as usize] = Some(sid); } // Add symbols and build symbol id map (indexed by SymbolIndex) @@ -316,22 +410,39 @@ pub fn write_coff(obj: &ObjInfo, export_all: bool) -> Result> { let mut defined_by_name: std::collections::HashMap, SymbolId> = std::collections::HashMap::new(); for (_, sym) in obj.symbols.iter() { - let sym_section = match sym.section { - Some(sec_idx) => match section_ids.get(sec_idx as usize).copied().flatten() { - Some(sid) => WriteSymbolSection::Section(sid), - None => WriteSymbolSection::Undefined, - }, - None => WriteSymbolSection::Undefined, + // Resolve the symbol's chunk and chunk-relative value. Function + // symbols that start a chunk land at value 0 in their own section; + // mid-function labels get chunk-relative values. + let (sym_section, sym_value) = match sym.section { + Some(sec_idx) => { + let sec_addr = obj.sections.get(sec_idx).map_or(0, |s| s.address); + let offset = sym.address.saturating_sub(sec_addr); + match section_chunks + .get(sec_idx as usize) + .and_then(|chunks| chunk_for(chunks, offset)) + { + Some(chunk) => (WriteSymbolSection::Section(chunk.id), offset - chunk.start), + None => (WriteSymbolSection::Undefined, sym.address), + } + } + None => (WriteSymbolSection::Undefined, sym.address), }; + // scope:local in symbols.txt is honored even with export_all: the + // globalize pass in split_obj has already cleared the Local flag on + // any local symbol referenced cross-unit, so remaining locals are + // genuinely unit-private and emit as IMAGE_SYM_CLASS_STATIC. + let is_local = sym.flags.0.contains(ObjSymbolFlags::Local); let is_exported = sym.flags.0.contains(ObjSymbolFlags::Exported) || sym.flags.0.contains(ObjSymbolFlags::Global) || (export_all && !sym.flags.0.contains(ObjSymbolFlags::NoExport) + && !is_local && matches!(sym.kind, ObjSymbolKind::Function | ObjSymbolKind::Object)); // Label (Unknown) symbols with a defined section are code labels that // may be referenced cross-object by DISP32 relocations. Promote them // to Linkage scope so lld can resolve the cross-object reference. - let is_defined_label = sym.kind == ObjSymbolKind::Unknown && sym.section.is_some(); + let is_defined_label = + sym.kind == ObjSymbolKind::Unknown && sym.section.is_some() && !is_local; let scope = if sym.flags.0.contains(ObjSymbolFlags::Weak) || is_exported || is_defined_label { SymbolScope::Linkage @@ -370,7 +481,7 @@ pub fn write_coff(obj: &ObjInfo, export_all: bool) -> Result> { } let sid = out.add_symbol(Symbol { name: sym.name.as_bytes().to_vec(), - value: sym.address, + value: sym_value, size: sym.size, kind, scope, @@ -397,10 +508,10 @@ pub fn write_coff(obj: &ObjInfo, export_all: bool) -> Result> { // Add relocations for (sec_idx, section) in obj.sections.iter() { - let sid = match section_ids[sec_idx as usize] { - Some(id) => id, - None => continue, - }; + let chunks = §ion_chunks[sec_idx as usize]; + if chunks.is_empty() { + continue; + } for (addr, reloc) in section.relocations.iter() { let (kind, encoding, size) = match reloc.kind { ObjRelocKind::X86Abs32 => { @@ -411,19 +522,22 @@ pub fn write_coff(obj: &ObjInfo, export_all: bool) -> Result> { } _ => continue, }; - let offset = (addr as u64).saturating_sub(section.address) as usize; + let offset = (addr as u64).saturating_sub(section.address); + let Some(chunk) = chunk_for(chunks, offset) else { continue }; + let chunk_off = (offset - chunk.start) as usize; + let chunk_len = ((chunk.end.min(section.data.len() as u64)) - chunk.start) as usize; // Skip relocations whose 4-byte field extends past the end of the - // section data. This can happen when a false-positive function split + // chunk data. This can happen when a false-positive function split // cuts a boundary mid-instruction; the relocation belongs to the // correctly-sized split of the enclosing function. - if offset + 4 > section.data.len() { + if chunk_off + 4 > chunk_len { log::warn!( "Skipping relocation at {:#010X} in section {} (offset {} + 4 > {} bytes): \ split boundary may be mid-instruction", addr, section.name, - offset, - section.data.len() + chunk_off, + chunk_len ); continue; } @@ -438,9 +552,9 @@ pub fn write_coff(obj: &ObjInfo, export_all: bool) -> Result> { let coff_addend = if reloc.kind == ObjRelocKind::X86Rel32 { reloc.addend - 4 } else { reloc.addend }; out.add_relocation( - sid, + chunk.id, Relocation { - offset: offset as u64, + offset: chunk_off as u64, symbol: sym_id, addend: coff_addend, flags: RelocationFlags::Generic { kind, encoding, size }, @@ -452,9 +566,32 @@ pub fn write_coff(obj: &ObjInfo, export_all: bool) -> Result> { } } + // Emit this unit's exestr comments as a `.drectve` section + // (IMAGE_SCN_LNK_INFO | IMAGE_SCN_LNK_REMOVE), which a comment-aware linker + // re-embeds and others ignore. + if !obj.pe_comment_directives.is_empty() { + let data = encode_drectve(obj.pe_comment_directives.iter().map(|(_, p)| p.as_slice())); + let sid = out.add_section(vec![], b".drectve".to_vec(), SectionKind::Linker); + out.set_section_data(sid, data, 1); + } + out.write().map_err(|e| anyhow::anyhow!("{e:?}")) } +/// Build a COFF object whose sole content is a `.drectve` section carrying one +/// `-?comment:"..."` directive per payload (used for comments not attributed to +/// any unit). Returns `None` if there are no directives. +pub fn write_coff_comments(directives: &[&[u8]]) -> Result>> { + if directives.is_empty() { + return Ok(None); + } + let mut out = WriteObject::new(BinaryFormat::Coff, Architecture::I386, Endianness::Little); + out.set_mangling(Mangling::None); + let sid = out.add_section(vec![], b".drectve".to_vec(), SectionKind::Linker); + out.set_section_data(sid, encode_drectve(directives.iter().copied()), 1); + out.write().map(Some).map_err(|e| anyhow::anyhow!("{e:?}")) +} + const IMAGE_REL_BASED_HIGHLOW: u16 = 3; /// Parse the PE `.reloc` section and add [`ObjRelocKind::X86Abs32`] relocations. @@ -500,7 +637,11 @@ pub fn apply_base_relocations(obj: &mut ObjInfo, image_base: u32) -> Result<()> } // Skip IAT slots: the linker regenerates the import address table and // its base relocations from the import directory. - if obj.symbols.at_section_address(src_idx, reloc_va).any(|(_, s)| s.name.starts_with("__imp_")) { + if obj + .symbols + .at_section_address(src_idx, reloc_va) + .any(|(_, s)| s.name.starts_with("__imp_")) + { continue; } let off = (reloc_va as u64 - src_sec.address) as usize; @@ -619,12 +760,7 @@ fn reconstruct_abs32_relocations_by_scan(obj: &mut ObjInfo) -> Result<()> { .relocations .insert( reloc_va, - ObjReloc { - kind: ObjRelocKind::X86Abs32, - target_symbol, - addend, - module: None, - }, + ObjReloc { kind: ObjRelocKind::X86Abs32, target_symbol, addend, module: None }, ) .ok(); count += 1; @@ -699,27 +835,27 @@ pub fn create_function_splits(obj: &mut ObjInfo) -> Result<()> { generated } else { match unit_name_to_addr.entry(name.clone()) { - std::collections::hash_map::Entry::Vacant(e) => { - e.insert(*addr); - name.clone() - } - std::collections::hash_map::Entry::Occupied(e) if *e.get() == *addr => { - // Same address — already handled (split exists check above - // should have caught this, but be safe). - name.clone() - } - std::collections::hash_map::Entry::Occupied(_) => { - // Duplicate: generate an address-unique unit name. - let generated = format!("fn_{:#010x}", addr); - log::warn!( - "Duplicate split unit name '{}' at {:#010X}; using '{}'", - name, - addr, + std::collections::hash_map::Entry::Vacant(e) => { + e.insert(*addr); + name.clone() + } + std::collections::hash_map::Entry::Occupied(e) if *e.get() == *addr => { + // Same address — already handled (split exists check above + // should have caught this, but be safe). + name.clone() + } + std::collections::hash_map::Entry::Occupied(_) => { + // Duplicate: generate an address-unique unit name. + let generated = format!("fn_{:#010x}", addr); + log::warn!( + "Duplicate split unit name '{}' at {:#010X}; using '{}'", + name, + addr, + generated + ); + unit_name_to_addr.insert(generated.clone(), *addr); generated - ); - unit_name_to_addr.insert(generated.clone(), *addr); - generated - } + } } }; @@ -742,3 +878,295 @@ pub fn create_function_splits(obj: &mut ObjInfo) -> Result<()> { log::info!("Created {total} function splits"); Ok(()) } + +#[cfg(test)] +mod tests { + use object::ObjectSymbol; + + use super::*; + use crate::obj::{ObjRelocations, ObjSplits}; + + fn section( + name: &str, + kind: ObjSectionKind, + elf_index: ObjSectionIndex, + data: Vec, + relocations: Vec<(u32, ObjReloc)>, + ) -> ObjSection { + ObjSection { + name: name.to_string(), + kind, + address: 0, + size: data.len() as u64, + data, + align: 16, + elf_index, + relocations: ObjRelocations::new(relocations).unwrap(), + virtual_address: None, + file_offset: 0, + section_known: true, + splits: ObjSplits::default(), + sub_regions: vec![], + } + } + + fn symbol( + name: &str, + address: u64, + section: Option, + size: u64, + kind: ObjSymbolKind, + flags: ObjSymbolFlagSet, + ) -> ObjSymbol { + ObjSymbol { + name: name.to_string(), + address, + section, + size, + size_known: true, + flags, + kind, + ..Default::default() + } + } + + /// .text: fnA = 8 bytes (Rel32 at +2), 2 bytes 0xCC padding, fnB = 6 bytes + /// (Abs32 at +1, i.e. section offset 11); label mid-fnB at +12. Plus .data + /// with one Local object symbol. + fn test_obj() -> ObjInfo { + let mut text: Vec = (1..=16u8).collect(); + text[8] = 0xCC; + text[9] = 0xCC; + let text_sec = section( + ".text", + ObjSectionKind::Code, + 0, + text, + vec![ + ( + 2, + ObjReloc { + kind: ObjRelocKind::X86Rel32, + target_symbol: 1, + addend: 0, + module: None, + }, + ), + ( + 11, + ObjReloc { + kind: ObjRelocKind::X86Abs32, + target_symbol: 3, + addend: 0, + module: None, + }, + ), + ], + ); + let data_sec = section(".data", ObjSectionKind::Data, 1, vec![1, 2, 3, 4], vec![]); + let symbols = vec![ + symbol( + "fnA", + 0, + Some(0), + 8, + ObjSymbolKind::Function, + ObjSymbolFlagSet(ObjSymbolFlags::Global.into()), + ), + symbol( + "fnB", + 10, + Some(0), + 6, + ObjSymbolKind::Function, + ObjSymbolFlagSet(ObjSymbolFlags::Global.into()), + ), + symbol("lbl_12", 12, Some(0), 0, ObjSymbolKind::Unknown, Default::default()), + symbol( + "dat", + 0, + Some(1), + 4, + ObjSymbolKind::Object, + ObjSymbolFlagSet(ObjSymbolFlags::Local.into()), + ), + ]; + ObjInfo::new( + ObjKind::Relocatable, + ObjArchitecture::X86, + "test".to_string(), + symbols, + vec![text_sec, data_sec], + ) + } + + #[test] + fn test_function_sections() { + let obj = test_obj(); + let out = write_coff(&obj, true, true).unwrap(); + let file = object::File::parse(&*out).unwrap(); + + // fnA chunk [0,10) with padding attached, fnB chunk [10,16), .data + let sections: Vec<_> = file.sections().collect(); + assert_eq!(sections.len(), 3); + assert_eq!(sections[0].name().unwrap(), ".text"); + assert_eq!(sections[1].name().unwrap(), ".text"); + assert_eq!(sections[2].name().unwrap(), ".data"); + assert_eq!(sections[0].size(), 10); + assert_eq!(sections[1].size(), 6); + + let d0 = sections[0].data().unwrap(); + assert_eq!(&d0[8..10], &[0xCC, 0xCC], "padding attached to preceding function"); + assert_eq!(&d0[6..8], &[7, 8], "non-reloc bytes preserved"); + let d1 = sections[1].data().unwrap(); + assert_eq!(d1[0], 11, "fnB data starts at section offset 10"); + assert_eq!(&d1[1..5], &[0, 0, 0, 0], "Abs32 reloc site zeroed chunk-relative"); + + // Relocations rebased into their chunks + let relocs0: Vec<_> = sections[0].relocations().collect(); + assert_eq!(relocs0.len(), 1); + assert_eq!(relocs0[0].0, 2); + let relocs1: Vec<_> = sections[1].relocations().collect(); + assert_eq!(relocs1.len(), 1); + assert_eq!(relocs1[0].0, 1); + + // Symbols land in their chunks with chunk-relative values + let find = |name: &str| file.symbols().find(|s| s.name() == Ok(name)).unwrap(); + let fna = find("fnA"); + assert_eq!(fna.section_index(), Some(sections[0].index())); + assert_eq!(fna.address(), 0); + let fnb = find("fnB"); + assert_eq!(fnb.section_index(), Some(sections[1].index())); + assert_eq!(fnb.address(), 0); + let lbl = find("lbl_12"); + assert_eq!(lbl.section_index(), Some(sections[1].index())); + assert_eq!(lbl.address(), 2); + } + + #[test] + fn test_no_function_sections() { + let obj = test_obj(); + let out = write_coff(&obj, true, false).unwrap(); + let file = object::File::parse(&*out).unwrap(); + let sections: Vec<_> = file.sections().collect(); + assert_eq!(sections.len(), 2); + assert_eq!(sections[0].name().unwrap(), ".text"); + assert_eq!(sections[0].size(), 16); + let relocs: Vec<_> = sections[0].relocations().collect(); + assert_eq!(relocs.len(), 2); + assert_eq!(relocs[0].0, 2); + assert_eq!(relocs[1].0, 11); + let fnb = file.symbols().find(|s| s.name() == Ok("fnB")).unwrap(); + assert_eq!(fnb.address(), 10); + } + + #[test] + fn test_scope_local_static() { + let obj = test_obj(); + let out = write_coff(&obj, true, true).unwrap(); + let file = object::File::parse(&*out).unwrap(); + let find = |name: &str| file.symbols().find(|s| s.name() == Ok(name)).unwrap(); + // scope:local honored despite export_all + assert!(find("dat").is_local(), "Local flag must emit IMAGE_SYM_CLASS_STATIC"); + // globalized/global symbols stay external + assert!(find("fnA").is_global()); + assert!(find("fnB").is_global()); + } + + #[test] + fn test_multi_range_unit_chunks_per_section() { + // Two same-named code sections (non-contiguous unit): chunked + // independently, no index collisions. + let text_a = section(".text", ObjSectionKind::Code, 0, vec![0x90; 8], vec![]); + let text_b = section(".text", ObjSectionKind::Code, 1, vec![0x90; 8], vec![]); + let symbols = vec![ + symbol( + "fn1", + 0, + Some(0), + 8, + ObjSymbolKind::Function, + ObjSymbolFlagSet(ObjSymbolFlags::Global.into()), + ), + symbol( + "fn2", + 0, + Some(1), + 4, + ObjSymbolKind::Function, + ObjSymbolFlagSet(ObjSymbolFlags::Global.into()), + ), + symbol( + "fn3", + 4, + Some(1), + 4, + ObjSymbolKind::Function, + ObjSymbolFlagSet(ObjSymbolFlags::Global.into()), + ), + ]; + let obj = ObjInfo::new( + ObjKind::Relocatable, + ObjArchitecture::X86, + "test".to_string(), + symbols, + vec![text_a, text_b], + ); + let out = write_coff(&obj, true, true).unwrap(); + let file = object::File::parse(&*out).unwrap(); + let sections: Vec<_> = file.sections().collect(); + // section A: 1 chunk; section B: 2 chunks + assert_eq!(sections.len(), 3); + assert!(sections.iter().all(|s| s.name() == Ok(".text"))); + assert_eq!(sections[0].size(), 8); + assert_eq!(sections[1].size(), 4); + assert_eq!(sections[2].size(), 4); + let find = |name: &str| file.symbols().find(|s| s.name() == Ok(name)).unwrap(); + assert_eq!(find("fn1").section_index(), Some(sections[0].index())); + assert_eq!(find("fn2").section_index(), Some(sections[1].index())); + let fn3 = find("fn3"); + assert_eq!(fn3.section_index(), Some(sections[2].index())); + assert_eq!(fn3.address(), 0); + } + + #[test] + fn test_extract_comment_directives() { + // Runs inside the region: "abc" \0 "def"; a trailing NUL yields no empty + // run. Bytes outside the declared region must be ignored. + let mut data = Vec::new(); + let region_off = data.len() as u32; + data.extend_from_slice(b"abc\0def\0"); + data.extend_from_slice(b"JUNK_OUTSIDE"); // outside the declared size + let region_size = 8u32; // covers "abc\0def\0" only + + let (mut obj, _) = + process_coff(&write_coff(&test_obj(), true, false).unwrap(), "test").unwrap(); + obj.pe_comment_directives.clear(); + let unit = Some("amaths".to_string()); + extract_comment_directives( + &data, + &[(region_off, region_size, unit.clone())], + &mut obj, + "t", + ) + .unwrap(); + assert_eq!( + obj.pe_comment_directives, + vec![(unit.clone(), b"abc".to_vec()), (unit, b"def".to_vec()),] + ); + } + + #[test] + fn test_write_coff_comments() { + assert!(write_coff_comments(&[]).unwrap().is_none()); + + let out = + write_coff_comments(&[b"Intel(R) foo".as_slice(), b"bar".as_slice()]).unwrap().unwrap(); + let file = object::File::parse(&*out).unwrap(); + let sections: Vec<_> = file.sections().collect(); + assert_eq!(sections.len(), 1); + assert_eq!(sections[0].name().unwrap(), ".drectve"); + assert_eq!(sections[0].kind(), object::SectionKind::Linker); + assert_eq!(sections[0].data().unwrap(), br#"-?comment:"Intel(R) foo" -?comment:"bar""#); + } +} diff --git a/src/util/config.rs b/src/util/config.rs index 30379b91..6364d28b 100644 --- a/src/util/config.rs +++ b/src/util/config.rs @@ -509,6 +509,11 @@ where )?; } } + for (name, start, end, unit) in &obj.pe_comment_sections { + if unit.is_none() { + writeln!(w, "\t{name:<11} type:comment vaddr:{start:#010X} end:{end:#010X}")?; + } + } for unit in obj.link_order.iter().filter(|unit| all || !unit.autogenerated) { write!(w, "\n{}:", unit.name)?; if let Some(comment_version) = unit.comment_version { @@ -553,6 +558,11 @@ where } writeln!(w)?; } + for (name, start, end, cunit) in &obj.pe_comment_sections { + if cunit.as_deref() == Some(unit.name.as_str()) { + writeln!(w, "\t{name:<11} type:comment start:{start:#010X} end:{end:#010X}")?; + } + } } Ok(()) } @@ -566,6 +576,9 @@ struct SplitSection { common: bool, rename: Option, skip: bool, + /// `type:comment` — a [start, end) range of exestr comment bytes in the PE + /// header padding, emitted as `.drectve` directives in this unit's object. + comment: bool, } struct SplitUnit { @@ -585,6 +598,14 @@ pub struct SectionDef { /// logical sub-section nested inside an existing physical section (e.g. /// MSVC COFF `.rdata$r` mapped onto a PE `.rdata`). pub end: Option, + /// `type:comment` — a [vaddr, end) range of exestr comment bytes in the PE + /// header padding (outside any physical section). Read via + /// [`read_splits_sections`] and re-emitted as `.drectve` directives; skipped + /// by [`apply_splits`]. + pub comment: bool, + /// `unit:` — owning unit for a `type:comment` range; its `.drectve` is + /// emitted into that unit's object (else a shared object). + pub unit: Option, } enum SplitLine { @@ -639,12 +660,21 @@ fn parse_unit_line(captures: Captures) -> Result { fn parse_section_line(captures: Captures, state: &SplitState) -> Result { if matches!(state, SplitState::Sections(_)) { let name = &captures["name"]; - let mut section = - SectionDef { name: name.to_string(), kind: None, align: None, vaddr: None, end: None }; + let mut section = SectionDef { + name: name.to_string(), + kind: None, + align: None, + vaddr: None, + end: None, + comment: false, + unit: None, + }; for attr in captures["attrs"].split(' ').filter(|&s| !s.is_empty()) { if let Some((attr, value)) = attr.split_once(':') { match attr { + "type" if value == "comment" => section.comment = true, + "unit" => section.unit = Some(value.to_string()), "type" => { section.kind = Some( section_kind_from_str(value) @@ -680,6 +710,7 @@ fn parse_section_line(captures: Captures, state: &SplitState) -> Result Result end = Some(parse_u32(value)?), "align" => section.align = Some(parse_u32(value)?), "rename" => section.rename = Some(value.to_string()), + "type" if value == "comment" => section.comment = true, _ => bail!("Unknown split attribute '{attr}'"), } } else { @@ -759,8 +791,17 @@ where } ( SplitState::Sections(index), - SplitLine::Section(SectionDef { name, kind, align, vaddr, end }), + SplitLine::Section(SectionDef { name, kind, align, vaddr, end, comment, unit }), ) => { + // Comment ranges live in the header padding, outside any physical + // section; the COFF loader reads them. Retain the declaration so + // it round-trips through a splits-file rewrite. + if comment { + if let (Some(start), Some(stop)) = (vaddr, end) { + obj.pe_comment_sections.push((name, start, stop, unit)); + } + continue; + } // Logical sub-section: declared with `vaddr:X end:Y`. Mapped // onto whichever physical section contains [X, Y) — no new // physical section is consumed. @@ -815,8 +856,16 @@ where common, rename, skip, + comment, }), ) => { + // Comment ranges live in the header padding, outside any physical + // section; the COFF loader reads them. Retain the declaration so + // it round-trips through a splits-file rewrite. + if comment { + obj.pe_comment_sections.push((name, start, end, Some(unit.clone()))); + continue; + } ensure!(end >= start, "Invalid split range {:#X}..{:#X}", start, end); let (section_index, fallback_rename) = match obj.sections.by_name(&name)? { Some(v) => Ok((v.0, None)), @@ -882,6 +931,40 @@ where Ok(()) } +/// Read `type:comment` ranges from a splits file as `(start_va, end_va, unit)`. +/// A range in the `Sections:` block is shared (`unit = None` unless it carries a +/// `unit:` attribute); a range under a unit belongs to that unit. +pub fn read_comment_regions(path: &Utf8NativePath) -> Result)>> { + if !fs::metadata(path).is_ok_and(|m| m.is_file()) { + return Ok(vec![]); + } + let file = open_file(path, true)?; + let mut regions = Vec::new(); + let mut state = SplitState::None; + for result in file.lines() { + let line = result?; + match parse_split_line(&line, &state)? { + SplitLine::SectionsStart => state = SplitState::Sections(0), + SplitLine::Unit(unit) => state = SplitState::Unit(unit.name), + SplitLine::Section(def) if def.comment => { + let (Some(start), Some(end)) = (def.vaddr, def.end) else { + bail!("Comment section '{}' requires vaddr and end", def.name); + }; + regions.push((start, end, def.unit)); + } + SplitLine::UnitSection(sec) if sec.comment => { + let unit = match &state { + SplitState::Unit(name) => name.clone(), + _ => bail!("Comment section '{}' outside a unit", sec.name), + }; + regions.push((sec.start, sec.end, Some(unit))); + } + _ => {} + } + } + Ok(regions) +} + pub fn read_splits_sections(path: &Utf8NativePath) -> Result>> { if !fs::metadata(path).is_ok_and(|m| m.is_file()) { return Ok(None); diff --git a/src/util/dol.rs b/src/util/dol.rs index bb116cfc..8be1bf00 100644 --- a/src/util/dol.rs +++ b/src/util/dol.rs @@ -383,7 +383,7 @@ pub fn process_dol(buf: &[u8], name: &str) -> Result { file_offset: file_offset as u64, section_known: known, splits: Default::default(), - sub_regions: Vec::new(), + sub_regions: Vec::new(), }); } } else { @@ -437,7 +437,7 @@ pub fn process_dol(buf: &[u8], name: &str) -> Result { file_offset: dol_section.file_offset as u64, section_known: known, splits: Default::default(), - sub_regions: Vec::new(), + sub_regions: Vec::new(), }); } } @@ -470,7 +470,7 @@ pub fn process_dol(buf: &[u8], name: &str) -> Result { file_offset: 0, section_known: false, splits: Default::default(), - sub_regions: Vec::new(), + sub_regions: Vec::new(), }); } @@ -491,7 +491,7 @@ pub fn process_dol(buf: &[u8], name: &str) -> Result { file_offset: 0, section_known: false, splits: Default::default(), - sub_regions: Vec::new(), + sub_regions: Vec::new(), }); let mut obj = ObjInfo::new( ObjKind::Executable, @@ -519,7 +519,7 @@ pub fn process_dol(buf: &[u8], name: &str) -> Result { file_offset: 0, section_known: false, splits: Default::default(), - sub_regions: Vec::new(), + sub_regions: Vec::new(), }); sections.push(ObjSection { name: ".sbss".to_string(), @@ -534,7 +534,7 @@ pub fn process_dol(buf: &[u8], name: &str) -> Result { file_offset: 0, section_known: false, splits: Default::default(), - sub_regions: Vec::new(), + sub_regions: Vec::new(), }); } n => bail!("Invalid number of BSS sections: {}", n), diff --git a/src/util/lcf.rs b/src/util/lcf.rs index fa0a5794..a8d115ec 100644 --- a/src/util/lcf.rs +++ b/src/util/lcf.rs @@ -180,6 +180,11 @@ mod tests { #[test] fn preserves_short_unit_names() { - assert_eq!(obj_path_for_unit("lib/amaths/AMaths").as_str(), "lib/amaths/AMaths.o"); + // obj_path_for_unit returns a native path (backslash-separated on + // Windows); normalize before comparing. + assert_eq!( + obj_path_for_unit("lib/amaths/AMaths").as_str().replace('\\', "/"), + "lib/amaths/AMaths.o" + ); } } diff --git a/src/util/map.rs b/src/util/map.rs index 4469229b..07adbd75 100644 --- a/src/util/map.rs +++ b/src/util/map.rs @@ -1092,7 +1092,8 @@ pub fn apply_map(mut result: MapInfo, obj: &mut ObjInfo) -> Result<()> { } let section_name = normalize_section_name(section_name); let lookup_name = pe_section_base_name(section_name); - let rename = if section_name != lookup_name { Some(section_name.to_string()) } else { None }; + let rename = + if section_name != lookup_name { Some(section_name.to_string()) } else { None }; let (_, section) = obj .sections .iter_mut() @@ -1158,7 +1159,7 @@ pub fn create_obj(result: &MapInfo) -> Result { file_offset, section_known: true, splits: Default::default(), - sub_regions: Vec::new(), + sub_regions: Vec::new(), } }) .collect(); @@ -1185,6 +1186,8 @@ pub fn create_obj(result: &MapInfo) -> Result { module_id: 0, unresolved_relocations: vec![], pe_reloc_data: Vec::new(), + pe_comment_directives: Vec::new(), + pe_comment_sections: Vec::new(), pe_metadata: None, }; diff --git a/src/util/rsp.rs b/src/util/rsp.rs index a7b41a8b..da8d5590 100644 --- a/src/util/rsp.rs +++ b/src/util/rsp.rs @@ -160,10 +160,7 @@ pub fn generate_args_rsp( force_includes: &[String], dead_strip: bool, ) -> Result { - let mut lines: Vec = vec![ - "/errorlimit:0".to_string(), - "/demangle:no".to_string(), - ]; + let mut lines: Vec = vec!["/errorlimit:0".to_string(), "/demangle:no".to_string()]; if dead_strip && !pe.is_dll { // Reproduce the original linker's dead-code elimination: drop // unreferenced functions (e.g. the parts of a verbatim library object diff --git a/src/util/split.rs b/src/util/split.rs index b9253dd8..fd092065 100644 --- a/src/util/split.rs +++ b/src/util/split.rs @@ -753,8 +753,7 @@ fn create_gap_splits(obj: &mut ObjInfo) -> Result<()> { symbol.name, symbol.address, ); - new_split_end.address = - current_address.address + symbol.size as u32; + new_split_end.address = current_address.address + symbol.size as u32; } else { new_split_end.address = symbol.address as u32; } @@ -815,8 +814,7 @@ fn create_gap_splits(obj: &mut ObjInfo) -> Result<()> { .then(|| r.name.clone()) }); let effective_rename = region_rename.or_else(|| prev_rename.clone()); - let effective_section = - effective_rename.as_deref().unwrap_or(§ion.name); + let effective_section = effective_rename.as_deref().unwrap_or(§ion.name); let unit = format!( "auto_{:02}_{:08X}_{}", current_address.section, @@ -1438,6 +1436,33 @@ fn resolve_link_order(obj: &ObjInfo) -> Result> { .min() .unwrap_or(section.address + section.data.len() as u64); let has_tail = init_end < section.address + section.size; + // Diagnose non-contiguous units: a unit whose splits reappear in this + // section after an intervening unit. This is legal (the unit's object + // gets multiple same-named sections, which is fine for objdiff), but + // the link order below becomes heuristic and a relinked image cannot + // be byte-identical, since the linker emits the unit's ranges + // adjacently. + let mut prev_unit: Option<&str> = None; + let mut ended_units = HashSet::<&str>::new(); + for (_, split) in section.splits.iter() { + if split.common { + continue; + } + if let Some(prev) = prev_unit + && prev != split.unit + { + ended_units.insert(prev); + if ended_units.contains(split.unit.as_str()) { + log::warn!( + "Unit '{}' has non-contiguous ranges in section {}: link order is \ + heuristic and a relinked image will not be byte-identical", + split.unit, + section.name + ); + } + } + prev_unit = Some(split.unit.as_str()); + } let mut iter = section.splits.iter().peekable(); if section.name == ".ctors" || section.name == ".dtors" { // Skip __init_cpp_exceptions.o @@ -1616,6 +1641,16 @@ pub fn split_obj( objects.push(split_obj); } + // Route exestr comments to their owning units; unattributed ones stay on the + // module object for a shared `.drectve` object. + for (unit, bytes) in &obj.pe_comment_directives { + if let Some(unit) = unit + && let Some(&idx) = name_to_obj.get(unit) + { + objects[idx].pe_comment_directives.push((None, bytes.clone())); + } + } + for (section_index, section) in obj.sections.iter() { let mut current_address = SectionAddress::new(section_index, section.address as u32); let section_end = end_for_section(obj, section_index)?; @@ -1809,7 +1844,9 @@ pub fn split_obj( _ => section.data[start_off..end_off.min(phys_len)].to_vec(), }; let split_kind = if in_bss_subregion - || (data.is_empty() && section.kind != ObjSectionKind::Bss && end_off > phys_len) + || (data.is_empty() + && section.kind != ObjSectionKind::Bss + && end_off > phys_len) { ObjSectionKind::Bss } else { @@ -1829,7 +1866,7 @@ pub fn split_obj( + (current_address.address as u64 - section.address), section_known: true, splits: Default::default(), - sub_regions: Vec::new(), + sub_regions: Vec::new(), }); }