From 4739dc58124c8c92834f0ccca88e39ea0b22dd1f Mon Sep 17 00:00:00 2001 From: John Stiles Date: Sun, 30 Aug 2026 17:35:57 -0400 Subject: [PATCH 1/4] split: join unclaimed data ranges into their sole referencing unit create_gap_splits() currently buckets every address range not covered by an explicit split entry into a generic auto_XX_ADDR_section unit, even when every relocation inside that range points into exactly one already-split function (e.g. an anonymous jump table living in its own .rodata gap, entirely referenced by one .text function). Since that jump table's home object is a different translation unit than the function's, its entries can't use a GAS local (.L_) label to address something in another object - the only cross-object-visible name available is the enclosing function's own symbol, so write_asm's existing label synthesis (util/asm.rs) falls back to plain `funcName+offset` for every entry, including ones that alias another label's exact address. That's what breaks m2c's switch/jump-table handling downstream (matt-kempster/m2c#360) - m2c has to special-case parsing `symbol+offset` jtbl entries because dtk's own asm output never had a real label to give it in the first place. Add single_referencing_unit(), which returns the split unit already proven to be the sole address-owner of a candidate range (skipping relocations that don't yet resolve to a known split, and refusing to propose a unit that already owns a non-adjacent piece of this same section - ObjInfo::add_split merges same-unit/same-section splits by taking their min..max span, which is only correct for genuinely adjacent pieces). A generic gap can be large and contain many unrelated anonymous blobs, not just one function's data, so the whole-gap check alone rarely fires on real projects. ownership_run_end() finds the largest symbol-aligned prefix of a gap that agrees on one owner, evaluating ownership per symbol (a small, independent range) rather than re-probing single_referencing_unit over a growing prefix - the latter would hard-fail permanently on the first unresolvable relocation anywhere in the gap, however far it sits from the actual data in question. create_gap_splits() now uses this to both narrow a gap's boundary and name the resulting split in one step, instead of always minting a fresh auto_ unit. Verified against a real GameCube retail DOL (a community decomp project's full ~8MB main.dol, 3694 functions, 679 objects at baseline): output is unchanged except for 4 previously-generic auto_XX units being absorbed into their real owning units (679 -> 675 objects; identical total .fn/.obj symbol count; no new "Unsplit data" or other split_obj/validate_splits errors on a full run). The motivating jump table - previously 21 entries of `.4byte fn_800EB828+0x38` etc., all in a separate auto data unit - now lives in the same unit as the function and emits real `.rel fn_800EB828, .L_800EB860`-style relocations against real local labels, including for entries that alias another entry's exact target address. --- src/util/split.rs | 116 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/src/util/split.rs b/src/util/split.rs index 14650e69..284dc4a9 100644 --- a/src/util/split.rs +++ b/src/util/split.rs @@ -740,17 +740,42 @@ fn create_gap_splits(obj: &mut ObjInfo) -> Result<()> { .filter(|(_, s)| s.address == current_address.address as u64) .collect_vec(), ); + // Narrow the split further if a prefix of it is solely owned by one + // already-known unit (e.g. a jump table referenced by one function). + let owned = ownership_run_end( + obj, + section, + &symbols, + current_address.address, + new_split_end.address, + ); + if let Some((owned_end, _)) = &owned { + if *owned_end < new_split_end.address { + new_split_end.address = *owned_end; + } + } + log::debug!( "Creating split from {:#010X}..{:#010X}", current_address, new_split_end ); - let unit = format!( - "auto_{:02}_{:08X}_{}", - current_address.section, - current_address.address, - section.name.trim_start_matches('.') - ); + let unit = owned + .map(|(_, unit)| unit) + // Don't reuse a unit this same pass already joined (add_split() merge risk). + .filter(|unit| { + !new_splits.iter().any(|(addr, s)| { + addr.section == current_address.section && &s.unit == unit + }) + }) + .unwrap_or_else(|| { + format!( + "auto_{:02}_{:08X}_{}", + current_address.section, + current_address.address, + section.name.trim_start_matches('.') + ) + }); new_splits.insert(current_address, ObjSplit { unit: unit.clone(), end: new_split_end.address, @@ -1772,6 +1797,85 @@ pub fn end_for_section(obj: &ObjInfo, section_index: SectionIndex) -> Result Option { + let mut found: Option<&str> = None; + for (_, reloc) in section.relocations.range(start..end) { + let target = &obj.symbols[reloc.target_symbol]; + let target_section_idx = target.section?; + let target_section = obj.sections.get(target_section_idx)?; + let (_, split) = target_section.splits.for_address(target.address as u32)?; + match found { + None => found = Some(split.unit.as_str()), + Some(unit) if unit == split.unit => {} + // Referenced by 2+ distinct already-known units: ambiguous, don't guess. + Some(_) => return None, + } + } + let unit = found?; + if section.splits.for_unit(unit).ok()?.is_some() { + return None; + } + Some(unit.to_string()) +} + +/// Finds the largest symbol-aligned prefix of `[start, limit)` owned by exactly one known unit, +/// so a jump table etc. buried in an otherwise-mixed gap can still be joined without requiring +/// the whole (possibly huge) gap to agree. `symbols` is every symbol in range, address order. +/// +/// Evaluates ownership per symbol rather than re-probing [`single_referencing_unit`] over a +/// growing prefix: that function hard-fails a whole range on its first unresolved relocation, +/// which would permanently poison every later, cleanly-owned symbol too. A symbol with no +/// resolvable owner is just "no evidence" and doesn't break an already-established run. +fn ownership_run_end( + obj: &ObjInfo, + section: &ObjSection, + symbols: &[(SymbolIndex, &ObjSymbol)], + start: u32, + limit: u32, +) -> Option<(u32, String)> { + let mut owner: Option = None; + let mut end: Option = None; + for (i, &(_, symbol)) in symbols.iter().enumerate() { + let sym_start = symbol.address as u32; + if sym_start < start { + continue; + } + let sym_end = symbols.get(i + 1).map(|&(_, s)| s.address as u32).unwrap_or(limit); + match single_referencing_unit(obj, section, sym_start, sym_end) { + Some(unit) => match &owner { + None => { + owner = Some(unit); + end = Some(sym_end); + } + Some(o) if *o == unit => end = Some(sym_end), + // A different already-known owner: stop before this symbol. + Some(_) => break, + }, + None => { + // No evidence either way; extend an already-started run over it, but don't + // start a run on a neutral symbol alone. + if owner.is_some() { + end = Some(sym_end); + } + } + } + } + end.zip(owner) +} + /// Generates a unit name for an autogenerated split. /// The name is based on the symbol name and section name. /// If the name is not unique, a number is appended to the end. From 02a57c27aebf8e09cec1b9c17f4e7adfdd05b0a1 Mon Sep 17 00:00:00 2001 From: John Stiles Date: Fri, 4 Sep 2026 14:24:21 -0400 Subject: [PATCH 2/4] split: tighten comments in create_gap_splits/single_referencing_unit/ownership_run_end Trim inline and doc comments to be more concise, and move the add_split-merge rationale out of single_referencing_unit's doc comment since it's an implementation detail rather than part of the function's contract. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018n9Wm7wVbnqadSbbThVc7c --- src/util/split.rs | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/src/util/split.rs b/src/util/split.rs index 284dc4a9..ff154813 100644 --- a/src/util/split.rs +++ b/src/util/split.rs @@ -740,8 +740,7 @@ fn create_gap_splits(obj: &mut ObjInfo) -> Result<()> { .filter(|(_, s)| s.address == current_address.address as u64) .collect_vec(), ); - // Narrow the split further if a prefix of it is solely owned by one - // already-known unit (e.g. a jump table referenced by one function). + // Identify and claim prefixes that have a single owner. let owned = ownership_run_end( obj, section, @@ -762,7 +761,7 @@ fn create_gap_splits(obj: &mut ObjInfo) -> Result<()> { ); let unit = owned .map(|(_, unit)| unit) - // Don't reuse a unit this same pass already joined (add_split() merge risk). + // Skip units already claimed in this section, to prevent add_split from merging them. .filter(|unit| { !new_splits.iter().any(|(addr, s)| { addr.section == current_address.section && &s.unit == unit @@ -1798,13 +1797,8 @@ pub fn end_for_section(obj: &ObjInfo, section_index: SectionIndex) -> Result found = Some(split.unit.as_str()), Some(unit) if unit == split.unit => {} - // Referenced by 2+ distinct already-known units: ambiguous, don't guess. + // Referenced by multiple distinct units; there's no single owner. Some(_) => return None, } } @@ -1831,14 +1825,8 @@ fn single_referencing_unit( Some(unit.to_string()) } -/// Finds the largest symbol-aligned prefix of `[start, limit)` owned by exactly one known unit, -/// so a jump table etc. buried in an otherwise-mixed gap can still be joined without requiring -/// the whole (possibly huge) gap to agree. `symbols` is every symbol in range, address order. -/// -/// Evaluates ownership per symbol rather than re-probing [`single_referencing_unit`] over a -/// growing prefix: that function hard-fails a whole range on its first unresolved relocation, -/// which would permanently poison every later, cleanly-owned symbol too. A symbol with no -/// resolvable owner is just "no evidence" and doesn't break an already-established run. +/// Finds the largest possible prefix of `[start, limit)` that is owned by exactly one known unit. +/// This lets us identify a jump table that starts at `start`. fn ownership_run_end( obj: &ObjInfo, section: &ObjSection, @@ -1861,12 +1849,11 @@ fn ownership_run_end( end = Some(sym_end); } Some(o) if *o == unit => end = Some(sym_end), - // A different already-known owner: stop before this symbol. + // A different owner; stop the search here. Some(_) => break, }, None => { - // No evidence either way; extend an already-started run over it, but don't - // start a run on a neutral symbol alone. + // Unknown provenance: only allowed if we've already started a run. if owner.is_some() { end = Some(sym_end); } From aa790c2770ac8f81d9fd6871324ecaf72615dd42 Mon Sep 17 00:00:00 2001 From: John Stiles Date: Fri, 4 Sep 2026 18:05:54 -0400 Subject: [PATCH 3/4] to be rewritten: - create_gap_splits() joined an unclaimed range into its referencing unit on relocation evidence alone, never checking link order - resolve_link_order() needs a unit's chunks in every section to agree on one global position; 4 of 37 joins on a real ~1300-object project contradicted that and made it cyclic - moved the graph construction into link_order_graph(), which can also take not-yet-applied splits plus a candidate - create_gap_splits() now rejects a join that would make the order cyclic and falls back to a plain auto_ split with no boundary trim Apologies for the Claude-written prose in the previous version of this message on a human-facing PR; that was uncalled for. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_018n9Wm7wVbnqadSbbThVc7c --- src/util/split.rs | 141 +++++++++++++++++++++++++++++++--------------- 1 file changed, 96 insertions(+), 45 deletions(-) diff --git a/src/util/split.rs b/src/util/split.rs index ff154813..fb5ba891 100644 --- a/src/util/split.rs +++ b/src/util/split.rs @@ -747,7 +747,18 @@ fn create_gap_splits(obj: &mut ObjInfo) -> Result<()> { &symbols, current_address.address, new_split_end.address, - ); + ) + // Skip units already claimed in this section, to prevent add_split from merging them. + .filter(|(_, unit)| { + !new_splits + .iter() + .any(|(addr, s)| addr.section == current_address.section && &s.unit == unit) + }) + // A unit's chunks in other sections already fix its place in the link order; + // claiming a range here that contradicts that would make the order cyclic. + .filter(|(_, unit)| { + link_order_is_acyclic(obj, &new_splits, Some((current_address, unit.as_str()))) + }); if let Some((owned_end, _)) = &owned { if *owned_end < new_split_end.address { new_split_end.address = *owned_end; @@ -759,22 +770,14 @@ fn create_gap_splits(obj: &mut ObjInfo) -> Result<()> { current_address, new_split_end ); - let unit = owned - .map(|(_, unit)| unit) - // Skip units already claimed in this section, to prevent add_split from merging them. - .filter(|unit| { - !new_splits.iter().any(|(addr, s)| { - addr.section == current_address.section && &s.unit == unit - }) - }) - .unwrap_or_else(|| { - format!( - "auto_{:02}_{:08X}_{}", - current_address.section, - current_address.address, - section.name.trim_start_matches('.') - ) - }); + let unit = owned.map(|(_, unit)| unit).unwrap_or_else(|| { + format!( + "auto_{:02}_{:08X}_{}", + current_address.section, + current_address.address, + section.name.trim_start_matches('.') + ) + }); new_splits.insert(current_address, ObjSplit { unit: unit.clone(), end: new_split_end.address, @@ -1227,54 +1230,74 @@ pub fn update_splits(obj: &mut ObjInfo, common_start: Option, fill_gaps: bo Ok(()) } -/// The ordering of TUs inside of each section represents a directed edge in a DAG. -/// We can use a topological sort to determine a valid global TU order. -/// There can be ambiguities, but any solution that satisfies the link order -/// constraints is considered valid. -#[instrument(level = "debug", skip(obj))] -fn resolve_link_order(obj: &ObjInfo) -> Result> { - #[allow(dead_code)] - #[derive(Debug, Copy, Clone)] - struct SplitEdge { - from: i64, - to: i64, +/// Builds the link order dependency graph from every split in `obj`, plus `extra` splits not yet +/// applied to `obj` and an optional `candidate` (address, unit) split. Returns the adjacency +/// list and the unit name for each node index. +fn link_order_graph<'a>( + obj: &'a ObjInfo, + extra: &'a BTreeMap, + candidate: Option<(SectionAddress, &'a str)>, +) -> Result<(Vec>, Vec<&'a str>)> { + // Per section: (address, unit, common), merged and sorted by address + let mut sections = vec![]; + for (section_index, section) in obj.sections.iter() { + let mut entries = section + .splits + .iter() + .map(|(addr, split)| (addr, split.unit.as_str(), split.common)) + .chain( + extra + .iter() + .filter(|(addr, _)| addr.section == section_index) + .map(|(addr, split)| (addr.address, split.unit.as_str(), split.common)), + ) + .chain( + candidate + .filter(|(addr, _)| addr.section == section_index) + .map(|(addr, unit)| (addr.address, unit, false)), + ) + .collect_vec(); + entries.sort_by_key(|&(addr, _, _)| addr); + sections.push((section.name.as_str(), entries)); } let mut unit_to_index_map = BTreeMap::<&str, usize>::new(); let mut index_to_unit = vec![]; - for (_, _, _, split) in obj.sections.all_splits() { - unit_to_index_map.entry(split.unit.as_str()).or_insert_with(|| { - let idx = index_to_unit.len(); - index_to_unit.push(split.unit.as_str()); - idx - }); + for (_, entries) in §ions { + for &(_, unit, _) in entries { + unit_to_index_map.entry(unit).or_insert_with(|| { + let idx = index_to_unit.len(); + index_to_unit.push(unit); + idx + }); + } } let mut graph = vec![vec![]; index_to_unit.len()]; - for (_section_index, section) in obj.sections.iter() { - let mut iter = section.splits.iter().peekable(); - if section.name == ".ctors" || section.name == ".dtors" { + for (section_name, entries) in §ions { + let mut iter = entries.iter().peekable(); + if *section_name == ".ctors" || *section_name == ".dtors" { // Skip __init_cpp_exceptions.o let skipped = iter.next(); log::debug!("Skipping split {:?} (next: {:?})", skipped, iter.peek()); } - while let (Some((a_addr, a)), Some(&(b_addr, b))) = (iter.next(), iter.peek()) { - if !a.common && b.common { + while let (Some(&(a_addr, a_unit, a_common)), Some(&&(b_addr, b_unit, b_common))) = + (iter.next(), iter.peek()) + { + if !a_common && b_common { // This marks the beginning of the common BSS section. continue; } - if a.unit != b.unit { + if a_unit != b_unit { log::debug!( "Adding dependency {} ({:#010X}) -> {} ({:#010X})", - a.unit, + a_unit, a_addr, - b.unit, + b_unit, b_addr ); - let a_index = *unit_to_index_map.get(a.unit.as_str()).unwrap(); - let b_index = *unit_to_index_map.get(b.unit.as_str()).unwrap(); - graph[a_index].push(b_index); + graph[unit_to_index_map[a_unit]].push(unit_to_index_map[b_unit]); } } } @@ -1299,6 +1322,34 @@ fn resolve_link_order(obj: &ObjInfo) -> Result> { graph[a_index].push(b_index); } + Ok((graph, index_to_unit)) +} + +/// Whether the link order would still be resolvable with `extra` splits and `candidate` added. +fn link_order_is_acyclic( + obj: &ObjInfo, + extra: &BTreeMap, + candidate: Option<(SectionAddress, &str)>, +) -> bool { + link_order_graph(obj, extra, candidate).is_ok_and(|(graph, _)| toposort(&graph).is_ok()) +} + +/// The ordering of TUs inside of each section represents a directed edge in a DAG. +/// We can use a topological sort to determine a valid global TU order. +/// There can be ambiguities, but any solution that satisfies the link order +/// constraints is considered valid. +#[instrument(level = "debug", skip(obj))] +fn resolve_link_order(obj: &ObjInfo) -> Result> { + #[allow(dead_code)] + #[derive(Debug, Copy, Clone)] + struct SplitEdge { + from: i64, + to: i64, + } + + let no_extra = BTreeMap::new(); + let (graph, index_to_unit) = link_order_graph(obj, &no_extra, None)?; + match toposort(&graph) { Ok(vec) => Ok(vec .iter() From 12e735b90be64f18b6bca611e28f62c1912147c6 Mon Sep 17 00:00:00 2001 From: John Stiles Date: Fri, 4 Sep 2026 22:23:25 -0400 Subject: [PATCH 4/4] split: only join ranges shaped like jump tables Require that a joined range contain relocations, that all of them target the owning unit, and that every reference to the range comes from that unit's code. Data that is only referenced from one unit but points nowhere is no longer claimed, since a Matching unit's source may merely extern it. Skip code sections, keep run boundaries 4-byte aligned, and when a gap does not begin with an owned symbol, end the auto split at the first one that is so the next pass can claim it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_018n9Wm7wVbnqadSbbThVc7c --- src/util/split.rs | 254 +++++++++++++++++++++++++++++++++------------- 1 file changed, 181 insertions(+), 73 deletions(-) diff --git a/src/util/split.rs b/src/util/split.rs index fb5ba891..2499ece6 100644 --- a/src/util/split.rs +++ b/src/util/split.rs @@ -637,6 +637,7 @@ fn split_extabindex(obj: &mut ObjInfo, start: SectionAddress) -> Result<()> { /// Create splits for gaps between existing splits. fn create_gap_splits(obj: &mut ObjInfo) -> Result<()> { let mut new_splits = BTreeMap::::new(); + let referencers = incoming_references(obj); for (section_index, section) in obj.sections.iter() { let mut current_address = SectionAddress::new(section_index, section.address as u32); @@ -740,28 +741,27 @@ fn create_gap_splits(obj: &mut ObjInfo) -> Result<()> { .filter(|(_, s)| s.address == current_address.address as u64) .collect_vec(), ); - // Identify and claim prefixes that have a single owner. - let owned = ownership_run_end( - obj, - section, - &symbols, - current_address.address, - new_split_end.address, - ) - // Skip units already claimed in this section, to prevent add_split from merging them. - .filter(|(_, unit)| { - !new_splits - .iter() - .any(|(addr, s)| addr.section == current_address.section && &s.unit == unit) - }) - // A unit's chunks in other sections already fix its place in the link order; - // claiming a range here that contradicts that would make the order cyclic. - .filter(|(_, unit)| { - link_order_is_acyclic(obj, &new_splits, Some((current_address, unit.as_str()))) - }); - if let Some((owned_end, _)) = &owned { - if *owned_end < new_split_end.address { - new_split_end.address = *owned_end; + // Identify and claim data-only prefixes with a single owner. + let mut owner = None; + if section.kind != ObjSectionKind::Code { + match ownership_run( + obj, + section_index, + section, + &referencers, + &symbols, + current_address.address, + new_split_end.address, + ) { + OwnershipRun::Owned { end, unit } => { + new_split_end.address = min(new_split_end.address, end); + owner = Some(unit); + } + // A run could plausibly start at `next`; end this split there. + OwnershipRun::Unowned { next: Some(next) } => { + new_split_end.address = min(new_split_end.address, next); + } + OwnershipRun::Unowned { next: None } => {} } } @@ -770,14 +770,29 @@ fn create_gap_splits(obj: &mut ObjInfo) -> Result<()> { current_address, new_split_end ); - let unit = owned.map(|(_, unit)| unit).unwrap_or_else(|| { - format!( - "auto_{:02}_{:08X}_{}", - current_address.section, - current_address.address, - section.name.trim_start_matches('.') - ) - }); + let unit = owner + // Skip units already claimed in this section, to prevent add_split from merging them. + .filter(|unit| { + !new_splits.iter().any(|(addr, s)| { + addr.section == current_address.section && &s.unit == unit + }) + }) + // Prevent any cycles in the link order. + .filter(|unit| { + link_order_is_acyclic( + obj, + &new_splits, + Some((current_address, unit.as_str())), + ) + }) + .unwrap_or_else(|| { + format!( + "auto_{:02}_{:08X}_{}", + current_address.section, + current_address.address, + section.name.trim_start_matches('.') + ) + }); new_splits.insert(current_address, ObjSplit { unit: unit.clone(), end: new_split_end.address, @@ -1847,71 +1862,164 @@ pub fn end_for_section(obj: &ObjInfo, section_index: SectionIndex) -> Result>; + +/// Indexes every relocation in `obj` by the address it targets. +fn incoming_references(obj: &ObjInfo) -> Referencers { + let mut referencers = Referencers::new(); + for (section_index, section) in obj.sections.iter() { + for (addr, reloc) in section.relocations.iter() { + let target = &obj.symbols[reloc.target_symbol]; + let Some(target_section) = target.section else { + continue; + }; + let target_address = (target.address as i64 + reloc.addend) as u32; + referencers + .entry(SectionAddress::new(target_section, target_address)) + .or_default() + .push(SectionAddress::new(section_index, addr)); + } + } + referencers +} + +/// How `[start, end)` relates to the already-declared splits. +#[derive(Debug)] +enum Ownership { + /// The range points into this one unit, and only that unit's code points at the range. + Owned(String), + /// No relocations into or out of the range; nothing to go on either way. + Neutral, + /// Anything else: shared, referenced from data, or nothing that ties it to one unit. + Unowned, +} + +/// The unit whose declared split contains `addr`, if any. +fn split_owner(obj: &ObjInfo, addr: SectionAddress) -> Option<&str> { + let (_, split) = obj.sections.get(addr.section)?.splits.for_address(addr.address)?; + Some(split.unit.as_str()) +} + +/// Determines which unit, if any, exclusively owns `start..end`. Only ranges that appear to +/// be jump tables are considered: it must exclusively contain addresses into the target. +fn range_ownership( obj: &ObjInfo, + section_index: SectionIndex, section: &ObjSection, + referencers: &Referencers, start: u32, end: u32, -) -> Option { +) -> Ownership { + /// Tracks the range's owner: the first call establishes it. Subsequent calls return true + /// only if `unit` is that same owner. + fn consider<'a>(found: &mut Option<&'a str>, unit: Option<&'a str>) -> bool { + match (*found, unit) { + // An address nobody has claimed yet; we can't tell who it belongs to. + (_, None) => false, + (None, Some(unit)) => { + *found = Some(unit); + true + } + // More than one distinct unit is involved; there's no single owner. + (Some(existing), Some(unit)) => existing == unit, + } + } let mut found: Option<&str> = None; + + // Every address the range points to must belong to the same unit. + let mut outgoing = false; for (_, reloc) in section.relocations.range(start..end) { + outgoing = true; let target = &obj.symbols[reloc.target_symbol]; - let target_section_idx = target.section?; - let target_section = obj.sections.get(target_section_idx)?; - let (_, split) = target_section.splits.for_address(target.address as u32)?; - match found { - None => found = Some(split.unit.as_str()), - Some(unit) if unit == split.unit => {} - // Referenced by multiple distinct units; there's no single owner. - Some(_) => return None, + let target_address = target.section.map(|target_section| { + SectionAddress::new(target_section, (target.address as i64 + reloc.addend) as u32) + }); + if !consider(&mut found, target_address.and_then(|addr| split_owner(obj, addr))) { + return Ownership::Unowned; + } + } + // Every reference to the range must come from that unit's code. + let range = SectionAddress::new(section_index, start)..SectionAddress::new(section_index, end); + for &source in referencers.range(range).flat_map(|(_, sources)| sources) { + if obj.sections[source.section].kind != ObjSectionKind::Code + || !consider(&mut found, split_owner(obj, source)) + { + return Ownership::Unowned; } } - let unit = found?; - if section.splits.for_unit(unit).ok()?.is_some() { - return None; + + match found { + None => Ownership::Neutral, + // Referenced but pointing nowhere: an ordinary variable, which may live anywhere. + Some(_) if !outgoing => Ownership::Unowned, + // A unit can't have more than one chunk per section. + Some(unit) if section.splits.for_unit(unit).ok().flatten().is_some() => Ownership::Unowned, + Some(unit) => Ownership::Owned(unit.to_string()), } - Some(unit.to_string()) +} + +/// Result of scanning a gap for a prefix with a single owner. +#[derive(Debug)] +enum OwnershipRun { + /// `[start, end)` is owned by `unit`. + Owned { end: u32, unit: String }, + /// The prefix has no single owner. A run might begin at `next` instead. + Unowned { next: Option }, } /// Finds the largest possible prefix of `[start, limit)` that is owned by exactly one known unit. /// This lets us identify a jump table that starts at `start`. -fn ownership_run_end( +fn ownership_run( obj: &ObjInfo, + section_index: SectionIndex, section: &ObjSection, + referencers: &Referencers, symbols: &[(SymbolIndex, &ObjSymbol)], start: u32, limit: u32, -) -> Option<(u32, String)> { - let mut owner: Option = None; - let mut end: Option = None; - for (i, &(_, symbol)) in symbols.iter().enumerate() { - let sym_start = symbol.address as u32; - if sym_start < start { - continue; +) -> OwnershipRun { + let mut ranges = symbols + .iter() + .enumerate() + .map(|(i, &(_, symbol))| { + let sym_end = symbols.get(i + 1).map(|&(_, s)| s.address as u32).unwrap_or(limit); + (symbol.address as u32, sym_end) + }) + .filter(|&(sym_start, _)| sym_start >= start && sym_start < limit) + .map(|(sym_start, sym_end)| { + let ownership = + range_ownership(obj, section_index, section, referencers, sym_start, sym_end); + (sym_start, sym_end, ownership) + }); + + let Some((_, first_end, Ownership::Owned(unit))) = ranges.next() else { + // A run must begin with a symbol owned by exactly one unit. Find the next one, so + // the following pass can start a split there. + let next = ranges + .find(|&(sym_start, _, ref ownership)| { + sym_start & 3 == 0 && matches!(ownership, Ownership::Owned(_)) + }) + .map(|(sym_start, _, _)| sym_start); + return OwnershipRun::Unowned { next }; + }; + let mut end = Some(first_end).filter(|end| end & 3 == 0); + for (_, sym_end, ownership) in ranges { + match ownership { + Ownership::Owned(other) if other == unit => {} + // Unknown provenance: only allowed if we've already started a run. + Ownership::Neutral => {} + // A different owner, or someone else involved; stop the search here. + _ => break, } - let sym_end = symbols.get(i + 1).map(|&(_, s)| s.address as u32).unwrap_or(limit); - match single_referencing_unit(obj, section, sym_start, sym_end) { - Some(unit) => match &owner { - None => { - owner = Some(unit); - end = Some(sym_end); - } - Some(o) if *o == unit => end = Some(sym_end), - // A different owner; stop the search here. - Some(_) => break, - }, - None => { - // Unknown provenance: only allowed if we've already started a run. - if owner.is_some() { - end = Some(sym_end); - } - } + if sym_end & 3 == 0 { + end = Some(sym_end); } } - end.zip(owner) + match end { + Some(end) => OwnershipRun::Owned { end, unit }, + None => OwnershipRun::Unowned { next: None }, + } } /// Generates a unit name for an autogenerated split.